Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -964,13 +964,36 @@ borrow of a parameter's field across calls to `total` and `M.first`. On Windows,
rejected, because `shrink` removes an element and is not listed. And the same program is rejected
against the library built under rc, which lists nothing: that is the fact's teeth.

Until the fix below, test-runner's `-shared` mode passed `-mm=` to the library only, and the
program was built under gc. Every `-shared -mm=rc`, `-mm=none` and `-mm=own` test ran a mixed
link. So, before it, the two runner tests above showed only that the pair works; the Windows
script was the one checking the fact. The flag now goes to every file, and all 302 `-shared`
tests pass with it.

**Objects of an imported class.** `new H()` of a class from a library builds its object with the
library's `H..new` and runs `H.constructor` through a method bound to it:
`ts.CreateBoundFunction(ts.Cast(h to !ts.opaque), ctor)`, split back into `ts.GetThis` and
`ts.GetMethod` for the call. The cast read as taking `h`, so every later use of `h` was "used after
its value was moved". The cast is now a borrow when it has only that use, and when the object comes
out again only as a call's argument, as `ts.ThisSymbolRef` is for a method of this module.

The object is also fresh, and owned here, when the library was built under own. Its `..new` is
listed among the library's `__own_no_drops`, and the signature pass marks such a call
`__own_fresh_result`, which `isFresh` reads. Nothing else makes an imported call's result fresh.
A library built under rc lists nothing, and its blocks carry a count their own module holds. There,
a borrow under the object still ends at any unknown call. `import_own_class.ts` and
`export_own_class.ts` run as a `-shared` pair under every model, AOT and JIT. The Windows script
checks that the program is rejected against the rc library.

### 15.7 Known limits (the input to phase 5 and later)

- An exported function, or a method of an exported class, gets no owned or borrowed facts (§15.8
says why they cannot be exported), and a virtual call on one gets no `__own_no_drops`. That is
sound and restrictive for `-shared` modules.
- An importer under own cannot yet build an object of an imported class: `new H()` through the
library reports `'this value' is used here after its value was moved`.
- A value an importer under own receives from a library built under rc (a call's result, now an
object it builds with `new` too) is destroyed at the end of its owner's scope. Own's release
skips only immortal blocks. So if the library kept a reference of its own, that is a
use-after-free, where the mixed-link policy promises a leak.
- A parameter kept on some paths only (needs drop elaboration: a release on the others).
- A borrowed result's places are a wildcard, so any field overwrite between the call and the use
drops it, even of an unrelated object.
Expand Down
15 changes: 13 additions & 2 deletions tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ namespace mlir_ts = mlir::typescript;
// On a function whose body relies on a fact its callers cannot all know (a parameter it keeps, a
// result that borrows): why, for the error the body gets instead.
#define OWN_FACTS_LOST_ATTR_NAME "__own_facts_lost"
// A call of another module's `<class>..new`, from a library built under own: it allocates the
// object and hands it over, so the result is fresh (isFresh). Only such a library says so - it lists
// the function among its `__own_no_drops` - and only its blocks are ones this module may destroy:
// a block made under rc still carries a count its own module holds.
#define OWN_FRESH_RESULT_ATTR_NAME "__own_fresh_result"

// The arguments of a call, without the callee value of an indirect one.
inline mlir::OperandRange callArgs(mlir::Operation *op)
Expand Down Expand Up @@ -166,8 +171,9 @@ inline bool isLiteral(mlir::Operation *def)
// Made here and held by nobody else: an allocation, a literal cast to its value type, an
// operation rc marks as arriving with a reference, or a direct call of a function this module
// defines (every function returns its result retained, rc 9.24; the call's own mark does not
// survive the affine lowering). A declared callee, an indirect call, a parameter, a load, a
// value merged from branches, or a result that borrows an argument is not fresh.
// survive the affine lowering), or another own module's `..new` (OWN_FRESH_RESULT_ATTR_NAME). A
// declared callee, an indirect call, a parameter, a load, a value merged from branches, or a result
// that borrows an argument is not fresh.
inline bool isFresh(mlir::Value value)
{
auto *def = value.getDefiningOp();
Expand All @@ -188,6 +194,11 @@ inline bool isFresh(mlir::Value value)
return callee && !callee.isDeclaration();
}

if (def->hasAttr(OWN_FRESH_RESULT_ATTR_NAME))
{
return true;
}

if (mlir::isa<mlir_ts::CallOp, mlir_ts::CallIndirectOp, mlir_ts::CallInternalOp, mlir_ts::CallHybridInternalOp>(def))
{
return false;
Expand Down
36 changes: 34 additions & 2 deletions tslang/lib/TypeScript/OwnershipInferencePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ class OwnershipInferencePass : public mlir::PassWrapper<OwnershipInferencePass,
returnedParams.clear();
f.walk([](mlir::Operation *op) {
for (auto *name : {OWN_PARAMS_ATTR_NAME, OWN_RESULT_BORROWS_ATTR_NAME, OWN_NO_DROPS_ATTR_NAME,
OWN_FACTS_LOST_ATTR_NAME})
OWN_FACTS_LOST_ATTR_NAME, OWN_FRESH_RESULT_ATTR_NAME})
{
op->removeAttr(name);
}
Expand Down Expand Up @@ -1466,7 +1466,8 @@ class OwnershipInferencePass : public mlir::PassWrapper<OwnershipInferencePass,

if (auto castOp = mlir::dyn_cast<mlir_ts::CastOp>(user))
{
return mlir::isa<mlir_ts::BooleanType>(castOp.getType()) || castOp.getType().isInteger(1);
return mlir::isa<mlir_ts::BooleanType>(castOp.getType()) || castOp.getType().isInteger(1) ||
isBoundForCall(castOp);
}

// arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee
Expand Down Expand Up @@ -1501,6 +1502,37 @@ class OwnershipInferencePass : public mlir::PassWrapper<OwnershipInferencePass,
return false;
}

// `ts.CreateBoundFunction(ts.Cast(object), method)` split straight back into `ts.GetThis` and
// `ts.GetMethod` for a call: a method of another module's class - its constructor, when it is
// built with `new` - called on an object here. It reads the object as `ts.ThisSymbolRef` does
// for a method of this module; nothing else may use the cast or the bound function, and the
// object comes out again only as a call's argument, which is a borrow of its own.
static bool isBoundForCall(mlir_ts::CastOp castOp)
{
if (!mlir::isa<mlir_ts::OpaqueType>(castOp.getType()) || castOp->use_empty())
{
return false;
}

return llvm::all_of(castOp->getUses(), [](mlir::OpOperand &use) {
auto boundOp = mlir::dyn_cast<mlir_ts::CreateBoundFunctionOp>(use.getOwner());
if (!boundOp || boundOp.getThisVal() != use.get())
{
return false;
}

return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) {
if (mlir::isa<mlir_ts::GetMethodOp>(boundUser))
{
return true;
}

return mlir::isa<mlir_ts::GetThisOp>(boundUser) &&
llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); });
});
});
}

// Is `value` an argument the call's callee keeps?
static bool isKeptArgument(mlir::Operation *call, mlir::Value value)
{
Expand Down
11 changes: 11 additions & 0 deletions tslang/lib/TypeScript/OwnershipSignaturePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
{
call.op->setAttr(OWN_NO_DROPS_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
}

// `new C()` of a class from a library built under own: see OWN_FRESH_RESULT_ATTR_NAME
if (call.callees.imported && call.callees.importedName.ends_with("." NEW_METHOD_NAME) &&
call.op->getNumResults() == 1)
{
call.op->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
}
}

exportNoDrops();
Expand All @@ -108,6 +115,8 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
bool known = false;
bool instanceOf = false;
bool imported = false;
// the name the imported function is exported under
llvm::StringRef importedName;
llvm::SmallVector<mlir_ts::FuncOp, 2> funcs;
};

Expand Down Expand Up @@ -291,6 +300,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
}

callees.imported = true;
callees.importedName = symbol.getValue();
return true;
}

Expand Down Expand Up @@ -333,6 +343,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
}

callees.imported = true;
callees.importedName = name.getValue();
return true;
}

Expand Down
45 changes: 40 additions & 5 deletions tslang/lib/TypeScriptRuntime/MemRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
#include <sys/time.h>
#else
#include "malloc.h"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif // _WIN32

#include <cinttypes>
Expand All @@ -32,14 +36,45 @@ namespace mlir
namespace runtime
{

extern "C" void *Alloc(uint64_t size) { return malloc(size); }
// The heap these hand out: the release CRT's, on the process heap, which is also what JIT-compiled
// code gets as `malloc` and `free` (tslang/jit.cpp, jitHeapFunction) - a coroutine frame this
// allocates, the program frees with plain `free`. This DLL's own `malloc` need not be that heap:
// linked against the prebuilt LLVM, LLVMSupport makes it rpmalloc (see
// scripts/llvm_prebuilt_common.ps1), and a block of one freed by the other corrupts the heap. A
// debug build keeps its own, as the JIT does: the debug CRT puts a header in front of each block.
#if defined(_WIN32) && !defined(_DEBUG)
template <typename F> static F crtFunction(const char *name, F ownFunction)
{
static auto ucrt = LoadLibraryW(L"ucrtbase.dll");
auto function = ucrt ? reinterpret_cast<F>(GetProcAddress(ucrt, name)) : nullptr;
return function ? function : ownFunction;
}

static void *heapMalloc(size_t size)
{
static auto function = crtFunction<void *(*)(size_t)>("malloc", &malloc);
return function(size);
}

static void heapFree(void *ptr)
{
static auto function = crtFunction<void (*)(void *)>("free", &free);
function(ptr);
}
#else
static void *heapMalloc(size_t size) { return malloc(size); }

static void heapFree(void *ptr) { free(ptr); }
#endif

extern "C" void *Alloc(uint64_t size) { return heapMalloc(size); }

// What MSVC's `malloc` guarantees: enough for any fundamental type, 16 bytes on x64.
static constexpr uint64_t kMallocAlignment = 2 * sizeof(void *);

extern "C" void *AlignedAlloc(uint64_t alignment, uint64_t size) {
#ifdef _WIN32
// Everything here comes from `malloc`, so the block can go back through either `free` or
// Everything here comes from `heapMalloc`, so the block can go back through either `free` or
// AlignedFree and both are right. `malloc`'s own guarantee covers every request anything
// makes - the coroutine frame asks for 8. A stricter request cannot be served and stay
// `free`-compatible at the same time, and silently handing back under-aligned memory is the
Expand All @@ -50,17 +85,17 @@ extern "C" void *AlignedAlloc(uint64_t alignment, uint64_t size) {
alignment, kMallocAlignment);
}

return malloc(size);
return heapMalloc(size);
#else
void *result = nullptr;
(void)::posix_memalign(&result, alignment, size);
return result;
#endif
}

extern "C" void Free(void *ptr) { free(ptr); }
extern "C" void Free(void *ptr) { heapFree(ptr); }

extern "C" void AlignedFree(void *ptr) { free(ptr); }
extern "C" void AlignedFree(void *ptr) { heapFree(ptr); }

} // namespace runtime
} // namespace mlir
Expand Down
6 changes: 6 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,12 @@ endforeach()
# Windows, checked against a library that lists nothing (own-shared-no-drops.cmake).
tslang_add_test(NAME test-jit-own-shared-no-drops COMMAND test-runner -jit -shared -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/import_own_no_drops.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/export_own_no_drops.ts")
tslang_add_test(NAME test-compile-own-shared-no-drops COMMAND test-runner -shared -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/import_own_no_drops.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/export_own_no_drops.ts")
# An importer builds and uses objects of an own library's classes; under every model, since the pair
# is ordinary TypeScript.
foreach(class_mm own rc none gc)
tslang_add_test(NAME test-jit-${class_mm}-shared-import-class COMMAND test-runner -jit -shared -mm=${class_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/import_own_class.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/export_own_class.ts")
tslang_add_test(NAME test-compile-${class_mm}-shared-import-class COMMAND test-runner -shared -mm=${class_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/import_own_class.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/export_own_class.ts")
endforeach()
if (WIN32)
add_test(NAME test-own-shared-no-drops-listed
COMMAND ${CMAKE_COMMAND}
Expand Down
14 changes: 13 additions & 1 deletion tslang/test/tester/own-shared-no-drops.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ foreach(library_model own rc)
"${TSLANG}" --emit=dll -mm=${library_model} --no-default-lib
"--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}"
"${SOURCE_DIR}/export_own_no_drops.ts" -o export_own_no_drops.dll)
compile("${dir}" "--emit=dll -mm=${library_model} export_own_class" ok
"${TSLANG}" --emit=dll -mm=${library_model} --no-default-lib
"--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}"
"${SOURCE_DIR}/export_own_class.ts" -o export_own_class.dll)
endforeach()

# `import './export_own_no_drops'` finds the DLL in the working directory before the source
Expand All @@ -57,4 +61,12 @@ compile("${WORK_DIR}/own" "import_own_err_imported_drops against the own library
compile("${WORK_DIR}/rc" "import_own_no_drops against the rc library" "${borrow_ended}"
"${TSLANG}" --emit=obj -mm=own --no-default-lib "${SOURCE_DIR}/import_own_no_drops.ts" -o import.obj)

message(STATUS "an own library's __own_no_drops reaches its importer, and only what it lists")
# `new H()` of an own library's class is a fresh object this module owns; of an rc library's, whose
# blocks carry a count their module holds, it is not, and a borrow under it ends at any unknown call
compile("${WORK_DIR}/own" "import_own_class against the own library" ok
"${TSLANG}" --emit=obj -mm=own --no-default-lib "${SOURCE_DIR}/import_own_class.ts" -o import.obj)

compile("${WORK_DIR}/rc" "import_own_class against the rc library" "${borrow_ended}"
"${TSLANG}" --emit=obj -mm=own --no-default-lib "${SOURCE_DIR}/import_own_class.ts" -o import.obj)

message(STATUS "an own library's __own_no_drops reaches its importer, and only what it lists; its classes can be built")
31 changes: 31 additions & 0 deletions tslang/test/tester/own/export_own_class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// -mm=own across a module boundary, the library side: classes an importer builds and uses. See
// import_own_class.ts.
export class C {
v: number[] = [];
constructor(public x: number) {}

twice() {
return this.x * 2;
}
}

export class H {
c: C = new C(0);

get cx() {
return this.c.x;
}

add(n: number) {
this.c.v.push(n);
}
}

export function sumOf(h: H) {
let s = 0;
for (let i = 0; i < h.c.v.length; i++) {
s += h.c.v[i];
}

return s;
}
32 changes: 32 additions & 0 deletions tslang/test/tester/own/import_own_class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// -mm=own across a module boundary: an importer builds objects of the library's classes. `new H()`
// calls the constructor through a method bound to the new object - `ts.CreateBoundFunction` over
// the object cast to `!ts.opaque` - and that was read as taking the object, so every later use of
// it was "used after its value was moved". It reads the object the way calling a method of this
// module does. Each object is read after a churn that would reuse a block freed too early.
import './export_own_class'

function churn() {
const junk: C[] = [];
for (let i = 0; i < 64; i++) {
junk.push(new C(999));
}
}

function main() {
let t = 0;
for (let i = 0; i < 1000; i++) {
const h = new H();
h.c = new C(i % 10);
h.add(1);
h.add(2);
churn();
// a virtual call on an imported class may be to an override that drops anything under
// `h`, so `h.cx` comes before the borrow of `h.c`; `sumOf` is listed as dropping nothing
t += h.cx;
const c = h.c;
t += c.x + c.twice() + sumOf(h) + c.v.length;
}

assert(t == 45 * 100 * 4 + 1000 * 5);
print("done.");
}
7 changes: 5 additions & 2 deletions tslang/test/tester/test-runner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -791,8 +791,11 @@ void readParams(int argc, char **argv, std::vector<std::string> &files)
std::string(argv[index]) == "-mm=none" || std::string(argv[index]) == "-mm=own")
{
memoryModel = std::string(argv[index]).substr(4);
tslang_opt_ext += " ";
tslang_opt_ext += argv[index];
// into tslang_opt, like -x86's triple: every file of a multi-file test is built under
// the model, the program as well as its library. In tslang_opt_ext it reached only
// the library, and every `-shared -mm=...` test built its program under gc.
tslang_opt += " ";
tslang_opt += argv[index];
}
else if (exists(argv[index]))
{
Expand Down
Loading
Loading