From 25d7d6fc4d99917c49fd7a45ea79e87736efb061 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 22:13:54 +0100 Subject: [PATCH] -mm=own: a library exports which of its functions destroy nothing A library built under own now writes, beside its memory-model marker __tsmm_own_, an exported string __tsown_: the exported functions that have __own_no_drops, one name per line. The signature pass writes it as a copy of the marker global, so it is exported the way the marker is on every platform and MLIRGen emits the same under own as under rc. The importer reads it beside the marker (from the loaded library or from the file) and keeps the names on its module, ts.own_imported_no_drops, in every model. The signature pass resolves a call to one - through a declaration linked with the import library, or through the global a library loaded at run time fills from SearchForAddressOfSymbol - as known and drop-free, like the .instanceOf slot. A global whose address is used other than to load from it is not trusted. Only this fact crosses. A missing one only makes an importer report more, so a module linking the library statically, which re-parses its source and sees no facts, stays sound. The kept-parameter and borrowed-result facts change the callee's body; an importer that does not know them frees twice, and under own there is no counting to adapt the call with. Tests: a -shared pair under JIT and AOT (a borrow of a parameter's field held across the imported calls), and on Windows a script that checks an unlisted dropping function is still rejected and that the same program is rejected against the library built under rc, which lists nothing. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 55 +++++- tslang/include/TypeScript/Defines.h | 9 + tslang/lib/TypeScript/MLIRGenImpl.h | 2 + tslang/lib/TypeScript/MLIRGenModule.cpp | 65 +++++++ .../lib/TypeScript/OwnershipSignaturePass.cpp | 167 +++++++++++++++++- tslang/test/tester/CMakeLists.txt | 16 ++ tslang/test/tester/own-shared-no-drops.cmake | 60 +++++++ tslang/test/tester/own/export_own_no_drops.ts | 21 +++ .../own/import_own_err_imported_drops.ts | 17 ++ tslang/test/tester/own/import_own_no_drops.ts | 21 +++ 10 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 tslang/test/tester/own-shared-no-drops.cmake create mode 100644 tslang/test/tester/own/export_own_no_drops.ts create mode 100644 tslang/test/tester/own/import_own_err_imported_drops.ts create mode 100644 tslang/test/tester/own/import_own_no_drops.ts diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index e9ba5c466..f86a0d3e7 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -90,7 +90,8 @@ Two Rust rules are ported as *inference*, never as annotation: Both are intra-module facts. A callee without facts - declared, imported from a module that does not export them, a virtual call whose candidates disagree - defaults to borrowed parameters and an owned result, the leak-side default `OwnedReturnConsumptionPass` already uses. A module -built under `own` exports its facts beside the existing `__tsmm_own_*` marker. +built under `own` exports one fact, `__own_no_drops`, beside the existing `__tsmm_own_*` marker +(§15.8); the two above stay intra-module. ### 2.5 Closures @@ -146,9 +147,10 @@ Computes the interprocedural facts of §2.4 and pins them on each `FuncOp` as at - `__own_result_borrows`: the parameter index (or `this`) the result borrows from, if any. Runs to a fixpoint over the call graph (a function that returns what a callee returns depends -on the callee's fact). Unknown callees default as in §2.4. For a `-shared` build the facts are -serialised into an exported symbol beside the marker; the importer reads them during the symbol -enumeration it already does for `__tsmm_` and applies them to the declared `FuncOp`s. +on the callee's fact). Unknown callees default as in §2.4. A library built under own exports +`__own_no_drops` into a symbol beside the marker, and the importer reads it during the symbol +enumeration it already does for `__tsmm_` (§15.8). The other two facts are not exported: a module +linking the library statically sees none, and a caller that does not know them frees twice. ### 3.3 `OwnershipInferencePass` (per `ts.FuncOp`) @@ -296,7 +298,7 @@ Each phase is a PR series leaving `main` green. and reads; `any` moved to phase 4. 4. **Signature pass**: owned-by-callee parameters, borrowed-from-argument results, export beside the marker and import on the other side. As built (§15): the facts, drop-free callees and - `any`; export and import deferred. + `any`; only `__own_no_drops` is exported and imported (§15.8). 5. **Closures**: escaping vs non-escaping, cells. 6. **Corpus report and diagnostics polish**: refine the shapes the histogram says matter. 7. **Generators and async**: the state object as owner. @@ -924,11 +926,48 @@ Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`): 272 of 564 c the new ones throw or catch (the copy thunks); several more cast out of `any`. Release suite: 3226 of 3226. +### 15.8 Export and import of `__own_no_drops` + +A library built under own writes, beside its marker `__tsmm_own_`, a second exported string +`__tsown_`. It lists the exported functions that destroy nothing a caller can reach, one +name per line, by the name they are exported under. The signature pass writes it: it is a copy of +the marker global under another name and value. So it is exported exactly as the marker is on +every platform, and MLIRGen emits the same under own as under rc (`test-own-mlirgen-matches-rc`). + +The importer reads it in `mlirGenImportSharedLib`, beside the marker, from the loaded library or +from the file. It keeps the names on its module as `ts.own_imported_no_drops`. That happens in every +model, for the same reason, and only own's signature pass reads it. A call resolves to such a +function in either of two forms: + +- a declaration linked through the import library; +- `ts.Load(ts.AddressOf @f)`, where the global `f` is `SearchForAddressOfSymbol("")` (a + library loaded at run time). + +The call is then known and drop-free, like the `.instanceOf` slot. A virtual call to a method of +an imported class stays unknown, because an override may be defined in the importer. + +Only this fact crosses, and only in one direction of trust: + +- A missing `__own_no_drops` only makes the importer report more. So a module that links the + library statically is sound without it: it re-parses the library's source and sees no facts. +- `__own_params` and `__own_result_borrows` change the callee's body: its retains are gone. An + importer that does not know them releases a moved argument or frees a borrowed result. The static + importer above is such an importer, and under own there is no counting to adapt a call with. + +Tests: `test-jit-own-shared-no-drops` and `test-compile-own-shared-no-drops` run +`import_own_no_drops.ts` against `export_own_no_drops.ts` as a `-shared` pair. The importer holds a +borrow of a parameter's field across calls to `total` and `M.first`. On Windows, +`own-shared-no-drops.cmake` also checks two things. `import_own_err_imported_drops.ts` is still +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. + ### 15.7 Known limits (the input to phase 5 and later) -- Export and import of the facts (§3.2). An exported function, or a method of an exported class, - gets no owned or borrowed facts, and a virtual call on one gets no `__own_no_drops`. That is sound - and restrictive for `-shared` modules. +- 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 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. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 137578aa7..834818c6d 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -134,6 +134,15 @@ // see docs/reference-counting-evaluation.md section 4. A missing marker means a module built // before this existed, which is always garbage-collected. #define SHARED_LIB_MEMORY_MODEL "__tsmm_" +// A library built under -mm=own also exports "__tsown__": the names of its exported +// functions that destroy nothing a caller can reach (`__own_no_drops`), one per line. The importer +// reads it beside the marker and keeps the names on its module (SHARED_LIB_OWN_NO_DROPS_ATTR_NAME), +// for the ownership signature pass. Only this fact crosses: a missing one just makes an importer +// under own report more, while the facts a callee's body relies on cannot be seen by an importer +// that re-parses source, and would free twice there. +#define SHARED_LIB_OWN_FACTS "__tsown_" +// a module attribute, so it has to carry the dialect's prefix +#define SHARED_LIB_OWN_NO_DROPS_ATTR_NAME "ts.own_imported_no_drops" #define DLL_EXPORT "dllexport" #define DLL_IMPORT "dllimport" #define DLL_NAME "dllname" diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 49362abd8..253e4cea4 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -271,6 +271,8 @@ class MLIRGenImpl mlir::LogicalResult mlirGenImportSharedLib(mlir::Location location, StringRef filePath, bool dynamic, const GenContext &genContext); + void addImportedOwnNoDrops(StringRef factsText); + mlir::LogicalResult mlirGen(ImportDeclaration importDeclarationAST, const GenContext &genContext); mlir::LogicalResult mlirGenImportBindings(ImportClause importClause); diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index 09854b044..6347a661d 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -530,6 +530,38 @@ namespace mlirgen return mlir::success(); } + // The names in a library's SHARED_LIB_OWN_FACTS text, one per line, added to the ones this + // module already has from other libraries. + void MLIRGenImpl::addImportedOwnNoDrops(StringRef factsText) + { + llvm::SmallVector names; + llvm::StringSet<> seen; + if (auto existing = theModule->getAttrOfType(SHARED_LIB_OWN_NO_DROPS_ATTR_NAME)) + { + for (auto name : existing.getAsRange()) + { + seen.insert(name.getValue()); + names.push_back(name); + } + } + + llvm::SmallVector lines; + factsText.split(lines, '\n', -1, false); + for (auto line : lines) + { + line = line.trim(); + if (!line.empty() && seen.insert(line).second) + { + names.push_back(builder.getStringAttr(line)); + } + } + + if (!names.empty()) + { + theModule->setAttr(SHARED_LIB_OWN_NO_DROPS_ATTR_NAME, builder.getArrayAttr(names)); + } + } + mlir::LogicalResult MLIRGenImpl::createGenericClassDeclarationExportGlobalVar(const GenContext &genContext) { if (!genericDeclExports.rdbuf()->in_avail() || !compileOptions.embedExportDeclarations) @@ -1249,6 +1281,8 @@ namespace mlirgen StringRef mlirGctors; // every symbol the library exports SmallVector symbolsAll; + // "__tsown__", one per module in the library built under own + SmallVector ownFactsSymbols; #ifndef GENERATE_IMPORT_INFO_USING_D_TS_FILE // loading Binary to get list of symbols Dump::getSymbols(filePath, symbolsAll, stringAllocator); @@ -1264,6 +1298,10 @@ namespace mlirgen { memoryModelSymbol = symbol; } + else if (symbol.starts_with(SHARED_LIB_OWN_FACTS)) + { + ownFactsSymbols.push_back(symbol); + } else if (symbol == MLIR_GCTORS) { mlirGctors = symbol; @@ -1453,6 +1491,33 @@ namespace mlirgen } } + // What a library built under own says about its functions (SHARED_LIB_OWN_FACTS). Kept in + // every model, so MLIRGen emits the same under own as under rc; only own reads it. + for (auto factsSymbol : ownFactsSymbols) + { + std::optional factsText; + if (loadIntoCompiler) + { + if (auto addrOfFacts = dynLib.getAddressOfSymbol(factsSymbol.str().c_str())) + { + factsText = std::string(*(const char **)addrOfFacts); + } + } + else + { + factsText = Dump::readExportedCString(filePath, factsSymbol); + } + + if (!factsText) + { + emitError(location) << "shared library '" << filePath << "' exports " << factsSymbol + << " but it could not be read from the file"; + return mlir::failure(); + } + + addImportedOwnNoDrops(*factsText); + } + // only now: an import that failed is tried again on the next pass, and must fail again // rather than find itself already done emittedFiles.insert(canonicalPath); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 24bc9ced6..9708e25c6 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -37,6 +37,11 @@ using namespace own_facts; // argument and releases it, or releases a result it does not own, a double free either way. So a // function has those only when every call that can reach it is one this pass resolved (the closed // world, see `open`). +// +// `__own_no_drops` alone crosses a module boundary: a library built under own exports the names of +// its exported functions that have it (SHARED_LIB_OWN_FACTS), and a module importing it calls them +// knowing it. The other two cannot: a module linking the library statically re-parses its source +// and sees no facts at all, and a caller that does not know them frees twice. class OwnershipSignaturePass : public mlir::PassWrapper> { public: @@ -46,6 +51,24 @@ class OwnershipSignaturePass : public mlir::PassWrappergetUsers(), [](mlir::Operation *user) { + return mlir::isa(user); + }); + if (!onlyLoaded) + { + writtenGlobals.insert(addressOfOp.getGlobalName()); + } + }); + if (auto names = module->getAttrOfType(SHARED_LIB_OWN_NO_DROPS_ATTR_NAME)) + { + for (auto name : names.getAsRange()) + { + importedNoDrops.insert(name.getValue()); + } + } + collectClassVTables(module); module.walk([&](mlir::Operation *op) { @@ -66,21 +89,25 @@ class OwnershipSignaturePass : public mlir::PassWrappersetAttr(OWN_NO_DROPS_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } } + + exportNoDrops(); } private: // What a call may reach: known, when every candidate is a function defined in this module (or - // the call goes through the `.instanceOf` slot); else anything. + // the call goes through the `.instanceOf` slot, or reaches a function another module says + // destroys nothing); else anything. struct Callees { bool known = false; bool instanceOf = false; + bool imported = false; llvm::SmallVector funcs; }; @@ -91,8 +118,15 @@ class OwnershipSignaturePass : public mlir::PassWrapper functions; + llvm::StringMap globals; + // Globals whose address is used other than to load from it: what they hold may change. + llvm::StringSet<> writtenGlobals; llvm::SmallVector calls; + // Functions of imported libraries that destroy nothing a caller can reach, by the name they + // are exported under (SHARED_LIB_OWN_NO_DROPS_ATTR_NAME). + llvm::StringSet<> importedNoDrops; + // Class names, from their vtables, to split a method's symbol into class and method name. llvm::StringSet<> classNames; // (vtable position, method name) -> the functions at that position under that name, in every @@ -246,16 +280,74 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp) + { + return false; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return false; + } + + // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` + auto ®ion = globalOp.getInitializerRegion(); + if (!region.hasOneBlock()) + { + return false; + } + + auto resultOp = mlir::dyn_cast(region.front().getTerminator()); + auto castOp = resultOp && resultOp->getNumOperands() == 1 + ? resultOp->getOperand(0).getDefiningOp() + : mlir_ts::CastOp(); + auto searchOp = castOp ? castOp.getIn().getDefiningOp() + : mlir_ts::SearchForAddressOfSymbolOp(); + auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); + auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); + if (!name || !importedNoDrops.contains(name.getValue())) + { + return false; + } + + callees.imported = true; + return true; + } + Callees resolve(mlir::Operation *op) { Callees callees; if (auto callOp = mlir::dyn_cast(op)) { - callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees); + callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees) || + addImported(callOp.getCalleeAttr().getAttr(), callees); } else if (auto callOp = mlir::dyn_cast(op)) { - callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees); + callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees) || + addImported(callOp.getCalleeAttr().getAttr(), callees); } else if (mlir::isa(op)) { @@ -275,6 +367,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) { callee = getMethodOp.getBoundFunc(); @@ -315,7 +413,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper` + // under another name and with another value, so it is exported exactly as the marker is, and + // MLIRGen emits the same under own as under rc. No marker, nothing is exported. + void exportNoDrops() + { + auto markerPrefix = std::string(SHARED_LIB_MEMORY_MODEL) + "own_"; + mlir_ts::GlobalOp marker; + for (auto &entry : globals) + { + if (entry.getKey().starts_with(markerPrefix)) + { + marker = entry.second; + break; + } + } + + if (!marker) + { + return; + } + + llvm::SmallVector names; + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (!funcOp.isDeclaration() && !funcOp.isPrivate() && noDrops.contains(funcOp)) + { + names.push_back(funcOp.getSymName()); + } + } + + llvm::sort(names); + std::string text; + for (auto name : names) + { + text += name.str(); + text += '\n'; + } + + auto suffix = marker.getSymName().drop_front(markerPrefix.size()); + auto name = (llvm::Twine(SHARED_LIB_OWN_FACTS) + suffix).str(); + + mlir::OpBuilder builder(marker); + builder.setInsertionPointAfter(marker); + auto factsOp = mlir::cast(builder.clone(*marker.getOperation())); + factsOp.setSymName(name); + factsOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { + if (mlir::isa(constantOp.getValue())) + { + constantOp.setValueAttr(builder.getStringAttr(text)); + } + }); + } + // Does the body itself destroy something a caller may reach: overwrite a field or an element // of a block it did not make, assign a global, remove elements from an array it did not make, // or `delete`? A local's own releases are not drops, nor is the constructor's filling of the diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1fb3a54b1..01724487c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2421,6 +2421,22 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o FAIL_REGULAR_EXPRESSION "__tslang_inc_ref|__tslang_dec_ref|error|Stack dump") endforeach() +# Across a library boundary: the library exports its functions' `__own_no_drops`, and a borrow held +# across a call to one survives. Run as a -shared pair (the library loaded at run time) and, on +# 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") +if (WIN32) + add_test(NAME test-own-shared-no-drops-listed + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}/test/tester/own" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/own-shared-no-drops" + "-DLLVM_LIB=${LLVM_LIBRARY_DIR}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + -P "${CMAKE_CURRENT_SOURCE_DIR}/own-shared-no-drops.cmake") +endif() + # The ownership verifier runs under own too, on the IR before inference erases its birth takes. add_test(NAME test-own-verify-ownership COMMAND $ --emit=mlir-affine --no-default-lib -mm=own --verify-ownership diff --git a/tslang/test/tester/own-shared-no-drops.cmake b/tslang/test/tester/own-shared-no-drops.cmake new file mode 100644 index 000000000..1827ce590 --- /dev/null +++ b/tslang/test/tester/own-shared-no-drops.cmake @@ -0,0 +1,60 @@ +# -mm=own across a DLL boundary: the library exports which of its functions destroy nothing +# (`__tsown_`), and an importer under own relies on it. test-runner's -shared mode runs the +# pair; this checks what it cannot - that an imported function the library does not list still +# drops, and that the listed ones are accepted because of the list: against the same library +# built under rc, which says nothing, the program that passes above is an error. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG SOURCE_DIR WORK_DIR LLVM_LIB TSLANG_LIB) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +set(ENV{GC_LIB_PATH} "") +set(ENV{TSLANG_LIB_PATH} "") + +set(borrow_ended "borrows (a field|an element) but is used here after it may be released or overwritten") + +# compile( ) - is "ok" or a regex the output must match +function(compile dir what expect) + execute_process(COMMAND ${ARGN} + WORKING_DIRECTORY "${dir}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if("${out}${err}" MATCHES "Stack dump|Assertion failed") + message(FATAL_ERROR "${what}: crashed\n${out}\n${err}") + endif() + if(expect STREQUAL "ok") + if(NOT status EQUAL 0 OR "${out}${err}" MATCHES "error:") + message(FATAL_ERROR "${what}: exit ${status}\n${out}\n${err}") + endif() + elseif(status EQUAL 0 OR NOT "${out}${err}" MATCHES "${expect}") + message(FATAL_ERROR "${what}: expected '${expect}', exit ${status}\n${out}\n${err}") + endif() +endfunction() + +foreach(library_model own rc) + set(dir "${WORK_DIR}/${library_model}") + file(REMOVE_RECURSE "${dir}") + file(MAKE_DIRECTORY "${dir}") + + compile("${dir}" "--emit=dll -mm=${library_model} export_own_no_drops" ok + "${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) +endforeach() + +# `import './export_own_no_drops'` finds the DLL in the working directory before the source +compile("${WORK_DIR}/own" "import_own_no_drops against the own library" ok + "${TSLANG}" --emit=obj -mm=own --no-default-lib "${SOURCE_DIR}/import_own_no_drops.ts" -o import.obj) + +compile("${WORK_DIR}/own" "import_own_err_imported_drops against the own library" "${borrow_ended}" + "${TSLANG}" --emit=obj -mm=own --no-default-lib "${SOURCE_DIR}/import_own_err_imported_drops.ts" -o import.obj) + +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") diff --git a/tslang/test/tester/own/export_own_no_drops.ts b/tslang/test/tester/own/export_own_no_drops.ts new file mode 100644 index 000000000..2855ec27c --- /dev/null +++ b/tslang/test/tester/own/export_own_no_drops.ts @@ -0,0 +1,21 @@ +// -mm=own across a module boundary: the library side. `total` and `M.first` destroy nothing +// their caller can reach, and the library says so (`__tsown_`); `shrink` removes an +// element of its argument, so it is not listed. See import_own_no_drops.ts. +export function total(a: number[]) { + let s = 0; + for (let i = 0; i < a.length; i++) { + s += a[i]; + } + + return s; +} + +export namespace M { + export function first(a: number[]) { + return a.length > 0 ? a[0] : -1; + } +} + +export function shrink(a: number[][]) { + a.pop(); +} diff --git a/tslang/test/tester/own/import_own_err_imported_drops.ts b/tslang/test/tester/own/import_own_err_imported_drops.ts new file mode 100644 index 000000000..929b78fd0 --- /dev/null +++ b/tslang/test/tester/own/import_own_err_imported_drops.ts @@ -0,0 +1,17 @@ +// -mm=own across a module boundary rejects: `shrink` removes an element of the array it is +// given, and the library does not list it as destroying nothing, so `w` may be gone. +import './export_own_no_drops' + +class L { + all: number[][] = [[4], [5]]; +} + +function read(l: L) { + const w = l.all[1]; + shrink(l.all); + return w.length; +} + +function main() { + print(read(new L())); +} diff --git a/tslang/test/tester/own/import_own_no_drops.ts b/tslang/test/tester/own/import_own_no_drops.ts new file mode 100644 index 000000000..8c3ee8dfa --- /dev/null +++ b/tslang/test/tester/own/import_own_no_drops.ts @@ -0,0 +1,21 @@ +// -mm=own across a module boundary: calls into a library that exported `__own_no_drops` keep a +// borrow alive. `v` borrows a field of the parameter `l`, and a call that may destroy anything +// reachable from `l` ends it; `total` and `M.first` are known, from the library, to destroy +// nothing. Built against a library that does not say so, `v.length` is an error. +import './export_own_no_drops' + +class L { + v: number[] = [1, 2, 3]; +} + +function read(l: L) { + const v = l.v; + const t = total(v) + M.first(v); + return v.length + t; +} + +function main() { + const l = new L(); + assert(read(l) == 10); + print("done."); +}