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 2d4a94502..4a50e4075 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 @@ -5217,3 +5217,63 @@ the object out of it (`ts.GetValueFromUnion`), casts it to an opaque pointer, an `own_union_narrowed_calls` calls the narrowed name twice with a churn between, under own, rc, gc and none; own emits no counting. Negative: `own_err_union_narrowed_moved`, which stores the narrowed value into a global and then calls through it. + +## 62. An object literal given to a variable with no type, 2026-10-28 + +`const o = { w: (x: number) => x * 2 }; apply(o); o.w(1)` was `'this value' borrows a field and cannot +be stored, returned or captured` at `apply(o)`, and the read after it a use after a move: ยง60 left the +members of a literal with no receiver methods bound to the literal. + +- **A literal a variable declared with no type is given** (MLIRGen marks it + `InternalFlags::UntypedVariableInitializer`) is one more whose this-free arrows are closures + (`markClosureMembers`), as one made for an object type is. A cast of it to an interface copies the + closure into the interface's object field by field (`const a: Adder = o`), as it copies any field. +- **Not an arrow that names a local around it** (any identifier the enclosing scopes' symbol table + holds, its own parameters included when they shadow one): copied out of the literal by such a cast, + a closure's captures would still be the literal's, freed with it (`const o = { area: () => box.w }; + return o;` for a `Shape` result). It stays a method, as before. +- **Not a literal returned or given with no receiver** (`return { id, area: () => box.w }`): MLIRGen + builds it for no type and casts it to the interface, which takes the literal's methods for its + vtable; a closure field of a capture's type does not fit that slot. + +`own_literal_variable_closures` gives such literals to a function-typed field and calls them after, casts +one to an interface after reading its number field, and gives one holding a class object to a +function-typed field and reads the object after, churning between steps, under own, rc, gc and none. +`00interface_type_tag_in_vtable` keeps its directly returned capturing literal. + +Not here: a literal whose arrows capture a local (`const o = { w: (x: number) => x * k }; apply(o)`) +is still rejected under own. Found and fixed (every model): `x` of an `any` that holds an object +literal read its block as the interface's pair and crashed; it now throws, since the box does not say which +fields the literal has (`00any_literal_to_interface`). A boxed null cast so was not null under rc, none and +own; it is the null interface now. + +## 63. A field taken out of a temporary in a branch, 2026-10-28 + +`let result = initial ?? next(xs, 0).value` (DefaultLib's `reduce` shape, `value: any`) was `'this +value' borrows a field and cannot be stored, returned or captured`. The `??` builds a `ts.If`: the +branch that calls `next` reads the field, rc retains the read, the temporary is released at the end of +the branch, and the merge goes to the local. + +- **The take** (`temporaryFieldTakeOf`, checkPlaceRead). A read of a field of a call's own result + (`callResultIsOwn`), with rc's retain of it right after the read, the result released once after + that retain in the same block, and no other read of that field, is a move out of the field: the walk + is made again with the retain and its taker (a local, a store) taken for uses, and nothing may use the + borrow after the taker. rc's retain goes, and right before the release a `ts.Default` of the field's + type is stored into the field, so the release frees the object and nothing of what moved. A string is + moved the same way. +- **rc, every branch retains** (`consumeConditionalResults`, OwnedReturnConsumptionPass). A branch that + yields a field read out of a temporary made in it gave the temporary back before the `ts.If` ended, + and with it what the field held; the receiver's retain after the merge came too late. rc then read + freed memory for a class or a string field (`const item = given ?? pair(n).item` read 0, and with + `let` the program died): only an `any` field, whose other branch boxed a fresh value, was retained in + the branch. Such a branch now retains what it yields, as a branch beside one that carries a fresh + value always did, and the `ts.If` carries the reference. + +`own_temporary_field_take` reduces strings through `initial ?? next(xs, 0).value`, picks with a `?:`, +and gives class objects read out of temporaries in both branches of a `?:` to a `const` and a `let`, +churning between steps, under own, rc, gc and none. Teeth: without the store of the empty value, the +own build fails its count (exit 3). + +Not here: the field of a named local of a class type (`const r = next(xs, 0); let v = initial ?? +r.value`) is still a borrow that cannot be stored, and a parameter on the other branch (`given ?? +pair(n).item`) is kept, which callers cannot give up. diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 1aa029dab..44a8845af 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -2592,6 +2592,9 @@ namespace mlirgen // - any other class of this module can't: no vtable for the interface was made for it, and none // is made here (one made here would be a way to the class's fields that nothing else in the // module shows), so the cast throws. A class object no class answers for throws too; + // - an object literal boxed as it is (`typeof` 'object' or 'tuple') throws: the box does not say + // which fields it has, so nothing can lay out a vtable for it; + // - null and undefined give the null interface; // - anything else - an interface value over an object literal or a class of another module, a // number used as a token - is read as the interface's pair, as before: an `any` holding the // interface it is cast to round-trips. @@ -2680,6 +2683,13 @@ namespace mlirgen } ss << S("}\n"); + + // an object literal, boxed as it is: the box says only that it is one, not what fields it has, so + // no vtable can be found for it here, and its block read as the interface's pair called garbage + ss << S("if (typeof a == 'object' || typeof a == 'tuple') throw \"Can't cast from any: an object literal has no vtable for interface '") + << stows(interfaceInfo->fullName.str()) << S("'\";\n"); + // null and undefined have a box of their own under the counting models, not the null pair + ss << S("if (typeof a == 'null' || typeof a == 'undefined') {\nconst n: TYPE_ANY_INTERFACE_ALIAS | null = null;\nreturn n;\n}\n"); ss << S("return a;\n}\n"); auto src = ss.str(); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index a410adec1..5a545de2c 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -11618,14 +11618,18 @@ class MLIRGenImpl // not the literal's) is a value in a field like any other, made as a closure, not a method bound to the // literal object: a method read out of the literal into a function-typed field (`{ w: x => x + 1 }` // given for `{ w: (x: number) => number }`) hands out the literal, which nothing owns then. Only a literal - // made for an object type is one: a literal with no receiver may become an interface, whose vtable is - // built from the literal's methods, and so may one made for an interface. An arrow whose untyped - // parameters have no function type to take theirs from stays a method too. + // made for an object type is one, or the value of a variable declared with no type (`const o = { w: ... }`, + // given later to `apply(o)`), which a cast to an interface copies field by field. A literal returned or + // given with no receiver may be built for an interface, whose vtable takes the literal's methods, and so + // may one made for an interface. An arrow whose untyped parameters have no function type to take theirs + // from stays a method too. void markClosureMembers(ObjectLiteralInfo &oli, const GenContext &genContext) { oli.closureMembers.clear(); auto receiverType = oli.receiverType ? oli.receiverType : oli.memberReceiverType; - auto objectTypeReceiver = receiverType && isa(MLIRTypeHelper::stripValueType(receiverType)); + auto untypedVariable = !receiverType && !!(oli.objectLiteral->internalFlags & InternalFlags::UntypedVariableInitializer); + auto objectTypeReceiver = receiverType ? isa(MLIRTypeHelper::stripValueType(receiverType)) + : untypedVariable; for (auto &item : oli.objectLiteral->properties) { auto closure = false; @@ -11635,16 +11639,26 @@ class MLIRGenImpl Node initializer = propertyAssignment->initializer; if (initializer == SyntaxKind::ArrowFunction) { - auto saysThis = false; - FilterVisitorAST thisVisitor(SyntaxKind::ThisKeyword, [&](auto) { saysThis = true; }); - FilterVisitorAST superVisitor(SyntaxKind::SuperKeyword, [&](auto) { saysThis = true; }); + auto bound = false; + FilterVisitorAST thisVisitor(SyntaxKind::ThisKeyword, [&](auto) { bound = true; }); + FilterVisitorAST superVisitor(SyntaxKind::SuperKeyword, [&](auto) { bound = true; }); thisVisitor.visit(initializer); superVisitor.visit(initializer); + // ...nor, in a variable's literal, a local around it: a closure that captures one + // is copied out of the literal by a cast to an interface, its captures still the literal's + if (untypedVariable) + { + FilterVisitorAST localVisitor(SyntaxKind::Identifier, [&](auto identifier) { + bound = bound || symbolTable.count(MLIRHelper::getName(identifier.template as())); + }); + localVisitor.visit(initializer); + } + auto arrow = initializer.as(); auto typed = llvm::all_of(arrow->parameters, [](ParameterDeclaration parameter) { return !!parameter->type; }) || getObjectLiteralMemberReceiverFuncType(oli, TupleFieldName(propertyAssignment->name, genContext)); - closure = !saysThis && typed; + closure = !bound && typed; } } diff --git a/tslang/lib/TypeScript/MLIRGenVariables.cpp b/tslang/lib/TypeScript/MLIRGenVariables.cpp index a5833d1fb..c1c2cd5f0 100644 --- a/tslang/lib/TypeScript/MLIRGenVariables.cpp +++ b/tslang/lib/TypeScript/MLIRGenVariables.cpp @@ -841,6 +841,11 @@ namespace mlirgen } #endif + if (!item->type && item->initializer == SyntaxKind::ObjectLiteralExpression) + { + item->initializer->internalFlags |= InternalFlags::UntypedVariableInitializer; + } + auto initFunc = [&](mlir::Location location, const GenContext &genContext) { // A module imported as source only declares what it defines, so a module-level // variable gets its type and no initializer code. Not a local: a function body is only diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index 0513705e6..f91e6a32b 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -1563,7 +1563,32 @@ class OwnedReturnConsumptionPass definingOp->getParentRegion() == resultOp->getParentRegion(); }; - if (llvm::none_of(yields, carriesOwned)) + // ...or some branch yields a field read out of a temporary it made (`given ?? pair(n).item`): + // the temporary is given back before the branch ends, and with it what the field holds, + // so the receiver's retain after the `ts.If` comes too late. The branch retains it first. + auto borrowsBranchTemporary = [&](mlir_ts::ResultOp resultOp) { + auto value = producerOf(resultOp->getOperand(0)); + while (auto loadOp = value.getDefiningOp()) + { + auto propertyRefOp = loadOp.getReference().getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + value = propertyRefOp.getObjectRef(); + auto *definingOp = value.getDefiningOp(); + if (definingOp && ::typescript::mayTakeOverReference(value) && + definingOp->getParentRegion() == resultOp->getParentRegion()) + { + return true; + } + } + + return false; + }; + + if (llvm::none_of(yields, carriesOwned) && llvm::none_of(yields, borrowsBranchTemporary)) { return; } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 096c23913..d390f7640 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -817,6 +817,8 @@ class OwnershipInferencePass mlir::Operation *taker = nullptr; mlir::Value field; mlir_ts::VariableOp slot; + // a field of a temporary (spec 63): the temporary's release, before which the field is emptied + mlir::Operation *release = nullptr; }; // Is `kept`, what keeps the borrow `read` (a read of a field of an owning record local), one @@ -826,6 +828,9 @@ class OwnershipInferencePass // ...made once for each value read, with nothing using the borrow after it, and nothing reading // the field or the record before the local ends? bool fieldTakeHolds(mlir::Operation *read, const FieldTake &take, llvm::ArrayRef uses); + // Is `kept` rc's retain of `read`, a read of a field of a call's own result released as a + // temporary right after, with nothing else reading that field (spec 63)? + bool temporaryFieldTakeOf(mlir::Operation *read, mlir::Operation *kept, FieldTake &take); // Can `call` be given a copy of `value`, a string read out of a place, in its place (callCopies)? static bool copiesInto(mlir::Operation *call, mlir::Value value); // Does `store` put `value`, a string read out of a place, into another place with no retain of diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index b746e2628..7b3ac7eae 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -831,6 +831,39 @@ namespace own_inference } } + // ...or out of a field of a call's own result, released as a temporary right after the read + // (spec 63): `init ?? next(xs, 0).value`. The temporary frees nothing of it then, so nothing + // after the move can see the release. + if (!take.taker && kept && !countedHandle && temporaryFieldTakeOf(read, kept, take)) + { + takenOps.insert(take.retain); + takenOps.insert(take.taker); + kept = walk(); + takenOps.clear(); + auto usedAfter = llvm::any_of(uses, [&](auto &use) { + return use.op != take.taker && use.op != take.retain && reachableAfter(take.taker, use.op, use.kills); + }); + if (kept || letKept || usedAfter) + { + take = {}; + kept = walk(); + } + else + { + if (!dryRun) + { + mlir::OpBuilder builder(take.release); + auto fieldType = mlir::cast(take.field.getType()).getElementType(); + auto empty = builder.create(take.release->getLoc(), fieldType); + builder.create(take.release->getLoc(), empty, take.field); + } + + bookkeeping.push_back(take.retain); + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + } + if (take.taker) { bookkeeping.push_back(take.retain); @@ -1106,6 +1139,66 @@ namespace own_inference return true; } + bool OwnershipInferencePass::temporaryFieldTakeOf(mlir::Operation *read, mlir::Operation *kept, FieldTake &take) + { + // rc's retain of the read itself, right after it: nothing runs between the read and the move. The + // walk may meet the taker first (`r = next(xs, 0).value`). A string too: it moves, as nothing + // else holds it. + auto loadOp = mlir::dyn_cast(read); + auto retainOp = loadOp ? mlir::dyn_cast_or_null(read->getNextNode()) : mlir_ts::RetainOp(); + if (!retainOp || retainOp.getReference() != loadOp.getResult() || + (kept != retainOp && takerOf(retainOp, loadOp.getResult()) != kept)) + { + return false; + } + + kept = retainOp; + + // a field read in place out of a call's own result, not a borrow of what the call was given + auto field = loadOp.getReference().getDefiningOp(); + auto temporary = field ? field.getObjectRef() : mlir::Value(); + auto *call = temporary ? temporary.getDefiningOp() : nullptr; + if (!call || !callResultIsOwn(call) || !field->hasOneUse()) + { + return false; + } + + // released once, as a temporary, after the move: the release is the only end of what the field + // holds, and no other read of the result reaches that field + mlir::Operation *release = nullptr; + for (auto *user : temporary.getUsers()) + { + if (mlir::isa(user) && !release) + { + release = user; + continue; + } + + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp || (propertyRefOp != field && propertyRefOp.getPosition() == field.getPosition())) + { + return false; + } + } + + if (!release || release->getBlock() != kept->getBlock() || !kept->isBeforeInBlock(release)) + { + return false; + } + + auto *taker = takerOf(kept, loadOp.getResult()); + if (!taker) + { + return false; + } + + take.retain = kept; + take.taker = taker; + take.field = field.getResult(); + take.release = release; + return true; + } + bool OwnershipInferencePass::keptParamOutlives(const Chain &chain, llvm::function_ref outlives) { for (auto &root : chain.roots) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index f3e0beabb..78f848e9b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -753,6 +753,7 @@ tslang_add_test(NAME test-compile-00-spread COMMAND test-runner "${PROJECT_SOURC tslang_add_test(NAME test-compile-01-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01spread.ts") tslang_add_test(NAME test-compile-00-types-unknown1 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00types_unknown1.ts") tslang_add_test(NAME test-compile-00-any-to-tuple COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_to_tuple.ts") +tslang_add_test(NAME test-compile-00-any-literal-to-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_literal_to_interface.ts") tslang_add_test(NAME test-compile-00-unknown-round-trip COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00unknown_round_trip.ts") tslang_add_test(NAME test-compile-00-types-indexed-access-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00types_indexedaccesstype.ts") tslang_add_test(NAME test-compile-00-types-mapped-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00types_mappedtype.ts") @@ -1376,6 +1377,7 @@ tslang_add_test(NAME test-jit-00-spread COMMAND test-runner -jit "${PROJECT_SOUR tslang_add_test(NAME test-jit-01-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01spread.ts") tslang_add_test(NAME test-jit-00-types-unknown1 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00types_unknown1.ts") tslang_add_test(NAME test-jit-00-any-to-tuple COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_to_tuple.ts") +tslang_add_test(NAME test-jit-00-any-literal-to-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_literal_to_interface.ts") tslang_add_test(NAME test-jit-00-unknown-round-trip COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00unknown_round_trip.ts") tslang_add_test(NAME test-jit-00-types-indexed-access-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00types_indexedaccesstype.ts") tslang_add_test(NAME test-jit-00-types-mapped-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00types_mappedtype.ts") @@ -2477,6 +2479,7 @@ set(TSLANG_CORPUS 00template_literal_escapes.ts 00types_unknown1.ts 00any_to_tuple.ts + 00any_literal_to_interface.ts 00unknown_round_trip.ts 00types_utility.ts 00types.ts @@ -3978,7 +3981,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_this_param own_copy_within_returned_param own_swap_elements own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array own_generator_take own_rest_array_borrows own_string_copies - own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_union_narrowed_calls + own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_literal_variable_closures own_temporary_field_take own_union_narrowed_calls own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") @@ -4016,7 +4019,7 @@ foreach(keeps_mm rc gc none) own_copy_within_returned_param own_swap_elements own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array own_generator_take own_rest_array_borrows own_string_copies - own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_union_narrowed_calls + own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_literal_variable_closures own_temporary_field_take own_union_narrowed_calls own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot) tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") diff --git a/tslang/test/tester/own/own_literal_variable_closures.ts b/tslang/test/tester/own/own_literal_variable_closures.ts new file mode 100644 index 000000000..57182ed77 --- /dev/null +++ b/tslang/test/tester/own/own_literal_variable_closures.ts @@ -0,0 +1,66 @@ +// An arrow function member of an object literal a variable is declared with, with no type, is a closure +// in a field when it says neither `this` nor a local around it (spec 62): the literal given later to a +// function-typed field (`apply(o)`), and used after, hands out no literal object, and a cast to an +// interface copies the function as it would a method. Each step churns, so anything freed too early reads +// garbage; every model runs it. +class Item { + constructor(public n: number) {} +} + +interface Adder { + add(x: number): number; +} + +let seen = 0; + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item(999)); +} + +function apply(o: { w: (x: number) => number }) { + return o.w(3); +} + +function useAdder(a: Adder) { + return a.add(2); +} + +function test() { + let s: number = 0; + const o = { w: (x: number) => x * 2 }; + s += apply(o); + churn(); + s += o.w(1); + s += apply(o); + + const a = { add: (x: number) => x + 10, n: 4 }; + s += useAdder(a); + churn(); + s += a.n; + const i: Adder = a; + s += i.add(1); + + const h = { w: (x: number) => x + 1, item: new Item(5) }; + s += apply(h); + churn(); + s += h.item.n; + + const g = { w: (x: number) => x + seen }; + seen = 1; + s += apply(g); + churn(); + return s; +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + seen = 0; + if (test() == 6 + 2 + 6 + 12 + 4 + 11 + 4 + 5 + 4) ok++; + churn(); + } + + assert(ok == 300, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_temporary_field_take.ts b/tslang/test/tester/own/own_temporary_field_take.ts new file mode 100644 index 000000000..b9c9c1c92 --- /dev/null +++ b/tslang/test/tester/own/own_temporary_field_take.ts @@ -0,0 +1,75 @@ +// A field read out of a call's own result in one branch of a `??` or a `?:`, kept, and the result +// released right after (spec 63): rc retains it in the branch, before the release, and own takes it out +// of the field, so the result's release frees nothing of it. `initial ?? next(xs, 0).value` of +// DefaultLib's reduce, a string, and a class object given a `const` or a `let`. Each step churns, so a +// value freed with the temporary reads garbage; every model runs it. +class Item { + constructor(public n: number) {} +} + +class Res { + constructor(public value: any, public done: boolean) {} +} + +class Pair { + constructor(public item: Item, public tag: number) {} +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item(999)); +} + +function next(xs: string[], i: number): Res { + return new Res(xs[i], i >= xs.length); +} + +function pair(n: number): Pair { + return new Pair(new Item(n), n); +} + +function red(xs: string[], f: (a: string, b: string) => string, initial?: string): string { + let result = initial ?? next(xs, 0).value; + churn(); + for (let i = initial ? 0 : 1; i < xs.length; i++) { + result = f(result, xs[i]); + } + + return result; +} + +function pick(xs: string[], initial?: string): string { + let result = initial ? initial : next(xs, 0).value; + churn(); + return result; +} + +function itemOf(n: number): number { + const item = n > 0 ? pair(n).item : pair(-n).item; + churn(); + let other = n > 0 ? pair(n + 1).item : pair(0).item; + churn(); + return item.n * 10 + other.n; +} + +function test() { + let s: number = 0; + if (red(["a", "b", "c"], (a, b) => a + b) == "abc") s++; + if (red(["a", "b"], (a, b) => a + b, "z") == "zab") s++; + if (pick(["q"]) == "q") s++; + if (pick(["q"], "p") == "p") s++; + s += itemOf(5); + s += itemOf(-2); + return s; +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + if (test() == 4 + 56 + 20) ok++; + churn(); + } + + assert(ok == 300, "ok"); + print("done."); +} diff --git a/tslang/test/tester/tests/00any_literal_to_interface.ts b/tslang/test/tester/tests/00any_literal_to_interface.ts new file mode 100644 index 000000000..a6b8fc1bc --- /dev/null +++ b/tslang/test/tester/tests/00any_literal_to_interface.ts @@ -0,0 +1,39 @@ +// `any` to an interface of an object literal boxed as it is throws: the box says only that it holds an +// object literal ('object', or 'tuple' for one of fields only), not which fields, so no vtable can be +// laid out for it. Its block was read as the interface's (this, vtable) pair, and the call through it +// crashed, under every memory model. An interface value made of a literal before it was boxed still +// round-trips. A boxed null gives the null interface: under rc, none and own it was a box of its own, +// read as a pair that was not null. +interface Adder { + add(x: number): number; +} + +function asAdder(x: any) { + return x; +} + +function throws(x: any) { + let thrown = false; + try { + asAdder(x); + } catch (e) { + thrown = true; + } + + return thrown; +} + +function main() { + const fields = { add: (x: number) => x + 10 }; + assert(throws(fields), "a literal of fields only"); + + const methods = { add(x: number) { return x + 20; } }; + assert(throws(methods), "a literal with a method"); + + const i: Adder = { add(x: number) { return x + 30; } }; + assert(asAdder(i).add(1) == 31, "an interface value round-trips"); + + assert(asAdder(null) == null, "null"); + + print("done."); +} diff --git a/tslang/ts-new-parser/enums.h b/tslang/ts-new-parser/enums.h index f92036580..418176301 100644 --- a/tslang/ts-new-parser/enums.h +++ b/tslang/ts-new-parser/enums.h @@ -567,7 +567,10 @@ enum class InternalFlags : number GeneratorNext = 1 << 13, // the `undefined` of a finished generator's {value: undefined, done: true}: a `value` type that cannot hold // undefined takes its zero (mlirGenObjectLiteralFields) - GeneratorDoneValue = 1 << 14 + GeneratorDoneValue = 1 << 14, + // an object literal a variable declared with no type is given (`const o = { w: ... }`): its this-free arrows + // are closures (markClosureMembers) + UntypedVariableInitializer = 1 << 15 }; ENUM_OPS(InternalFlags)