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."); +}