From fa62f5e7b4d0cd31ea656f715ac71ff7529aad5b Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 20 Jul 2026 17:32:49 +0100 Subject: [PATCH] Fix object literals silently corrupting data with interleaved field/method type annotations An object literal's own storage always lays out as "all fields, then all methods" (two separate codegen passes over the properties, by design - a method may reference sibling fields that must already be registered). A type-literal annotation on the receiving var/let, however, preserves verbatim interleaved source declaration order. When these orderings diverge - any annotation with 2+ fields and 2+ methods not grouped field-first - method bodies get compiled against the WRONG byte offsets for the annotated variable's actual memory layout: a `this.field` write can silently land on a neighboring method's function-pointer slot instead, corrupting data with no compile error and no crash. Fix: after all fields/methods/captures are processed but before method bodies are generated, if the receiver type's field order differs from the literal's own accumulated order, permute the literal's field list (and parallel value list) to match it. Field access inside method bodies resolves by name against the storage type's live field list, so this reordering alone is sufficient - no per-method index patching needed. Co-Authored-By: Claude Sonnet 5 --- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 53 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 4 ++ .../00object_annotated_method_interleaved.ts | 40 ++++++++++++++ ...ct_literal_structural_typed_interleaved.ts | 33 ++++++++++++ ...ct_literal_structural_typed_interleaved.ts | 20 +++++++ 5 files changed, 150 insertions(+) create mode 100644 tslang/test/tester/tests/00object_annotated_method_interleaved.ts create mode 100644 tslang/test/tester/tests/export_object_literal_structural_typed_interleaved.ts create mode 100644 tslang/test/tester/tests/import_object_literal_structural_typed_interleaved.ts diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index d8f80a56e..5230cf078 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1319,6 +1319,59 @@ namespace mlirgen return mlir::failure(); } + // If the destination has a known field order (e.g. a type-literal + // annotation on the receiving var/let), re-lay-out this literal's own + // storage to match it BEFORE method bodies are generated. Without + // this, the literal's storage stays in "all fields, then all + // methods" order (mlirGenObjectLiteralFields/-MethodPrototypes run as + // two separate passes over the properties, by design - methods may + // need every field already registered), which only coincidentally + // matches a declared type whose members interleave fields and + // methods in a different order. Method bodies compiled against the + // MISMATCHED order silently read/write the wrong byte offset when + // later invoked against the (differently laid out) destination + // memory - no crash, no error, just corrupted data: a `this.field` + // write can land on a neighboring method's function-pointer slot. + // Field position resolution inside method bodies is by NAME against + // objectStorageType's CURRENT field list (TupleFieldType/getIndex, + // MLIRCodeLogic.h), so reordering here - before + // mlirGenObjectLiteralMethodBodies runs - is sufficient; no + // per-method index patching needed. Must run after + // mlirGenObjectLiteralCaptures (methodInfosWithCaptures already holds + // pre-reorder indices consumed there) and before the final + // setFields/method-bodies pass below. + llvm::SmallVector receiverFields; + if (oli.receiverType && mlir::succeeded(mth.getFields(oli.receiverType, receiverFields, true)) && + receiverFields.size() == oli.fieldInfos.size()) + { + llvm::SmallVector reorderedValues; + llvm::SmallVector reorderedFieldInfos; + reorderedValues.reserve(oli.fieldInfos.size()); + reorderedFieldInfos.reserve(oli.fieldInfos.size()); + + auto allMatched = true; + for (auto &receiverField : receiverFields) + { + auto foundIt = llvm::find_if(oli.fieldInfos, + [&](const mlir_ts::FieldInfo &fieldInfo) { return fieldInfo.id == receiverField.id; }); + if (foundIt == oli.fieldInfos.end()) + { + allMatched = false; + break; + } + + auto index = std::distance(oli.fieldInfos.begin(), foundIt); + reorderedFieldInfos.push_back(*foundIt); + reorderedValues.push_back(oli.values[index]); + } + + if (allMatched) + { + oli.fieldInfos = std::move(reorderedFieldInfos); + oli.values = std::move(reorderedValues); + } + } + // final type, update objectStorageType.setFields(oli.fieldInfos); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1b5724478..27a61ddc9 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -289,6 +289,7 @@ add_test(NAME test-compile-00-interface-function-typed-field COMMAND test-runner add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") add_test(NAME test-compile-00-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") add_test(NAME test-compile-00-object-annotated-method-params COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") +add_test(NAME test-compile-00-object-annotated-method-interleaved COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_interleaved.ts") add_test(NAME test-compile-00-interface-object-array COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object_array.ts") add_test(NAME test-compile-00-interface-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_generic.ts") add_test(NAME test-compile-00-interface-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_new.ts") @@ -645,6 +646,7 @@ add_test(NAME test-jit-00-interface-function-typed-field COMMAND test-runner -ji add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") add_test(NAME test-jit-00-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") add_test(NAME test-jit-00-object-annotated-method-params COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") +add_test(NAME test-jit-00-object-annotated-method-interleaved COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_interleaved.ts") add_test(NAME test-jit-00-interface-object-array COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_object_array.ts") add_test(NAME test-jit-00-interface-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_generic.ts") add_test(NAME test-jit-00-interface-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_new.ts") @@ -857,6 +859,7 @@ add_test(NAME test-compile-shared-export-import-object-literal-untyped COMMAND t add_test(NAME test-compile-shared-export-import-object-literal-structural-typed COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed.ts") add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-params COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_params.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_params.ts") add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-multi-method COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_multi_method.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_multi_method.ts") +add_test(NAME test-compile-shared-export-import-object-literal-structural-typed-interleaved COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_interleaved.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_interleaved.ts") add_test(NAME test-compile-shared-export-import-vars COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars.ts") add_test(NAME test-compile-shared-export-import-vars-2 COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars2.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars2.ts") add_test(NAME test-compile-shared-export-import-enum COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_enum.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_enum.ts") @@ -876,6 +879,7 @@ add_test(NAME test-jit-shared-export-import-object-literal-untyped COMMAND test- add_test(NAME test-jit-shared-export-import-object-literal-structural-typed COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed.ts") add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-params COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_params.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_params.ts") add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-multi-method COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_multi_method.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_multi_method.ts") +add_test(NAME test-jit-shared-export-import-object-literal-structural-typed-interleaved COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_structural_typed_interleaved.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_structural_typed_interleaved.ts") add_test(NAME test-jit-shared-export-import-vars COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars.ts") add_test(NAME test-jit-shared-export-import-vars-2 COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_vars2.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_vars2.ts") add_test(NAME test-jit-shared-export-import-enum COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_enum.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_enum.ts") diff --git a/tslang/test/tester/tests/00object_annotated_method_interleaved.ts b/tslang/test/tester/tests/00object_annotated_method_interleaved.ts new file mode 100644 index 000000000..590c104bb --- /dev/null +++ b/tslang/test/tester/tests/00object_annotated_method_interleaved.ts @@ -0,0 +1,40 @@ +// Extends 00object_annotated_method_params.ts's coverage: that test's +// type-literal annotations always declare all fields before all methods +// (total; add(); addTwice()). Here fields and methods INTERLEAVE in +// declaration order (base; addBase(); total; add(); ...) - this used to +// silently corrupt data: the object literal's own storage laid out as +// "fields, then methods" regardless of the annotation's order, so a method +// compiled against that layout read/wrote the wrong byte offset once +// invoked against the annotation-ordered variable, up to overwriting a +// sibling method's function pointer with a field's value. See +// docs/object-literal-boxing-design.md-adjacent history; root-caused and +// fixed via a field/value reorder pass in mlirGen(ObjectLiteralExpression). + +function main() { + let acc: { base: number; addBase(n: number): void; total: number; add(n: number): void; addTwice(n: number): void; scaled(factor: number): number } = { + base: 100, + addBase(n: number) { this.base = this.base + n; }, + total: 0, + add(n: number) { this.total = this.total + n; }, + addTwice(n: number) { this.add(n); this.add(n); }, + scaled(factor: number) { return this.total * factor; }, + }; + + acc.addBase(5); + assert(acc.base == 105); + print(acc.base); + + acc.add(3); + assert(acc.total == 3); + print(acc.total); + + acc.addTwice(2); + assert(acc.total == 7); + print(acc.total); + + const result = acc.scaled(2); + assert(result == 14); + print(result); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_object_literal_structural_typed_interleaved.ts b/tslang/test/tester/tests/export_object_literal_structural_typed_interleaved.ts new file mode 100644 index 000000000..ff4c332df --- /dev/null +++ b/tslang/test/tester/tests/export_object_literal_structural_typed_interleaved.ts @@ -0,0 +1,33 @@ +namespace A { + + export interface Accumulator { + base: number; + addBase(n: number): void; + total: number; + add(n: number): void; + addTwice(n: number): void; + scaled(factor: number): number; + } + + // extends export_object_literal_structural_typed_multi_method.ts's + // coverage: that test's methods are all grouped after every field + // (total; add(); addTwice(); scaled()). Here fields and methods + // INTERLEAVE in declaration order (base; addBase(); total; add(); ...) - + // see 00object_annotated_method_interleaved.ts for the same-module + // version of this coverage and the mechanism this used to break. + export var acc: { + base: number; + addBase(n: number): void; + total: number; + add(n: number): void; + addTwice(n: number): void; + scaled(factor: number): number; + } = { + base: 100.0, + addBase(n: number) { this.base = this.base + n; }, + total: 0.0, + add(n: number) { this.total = this.total + n; }, + addTwice(n: number) { this.add(n); this.add(n); }, + scaled(factor: number) { return this.total * factor; }, + }; +} diff --git a/tslang/test/tester/tests/import_object_literal_structural_typed_interleaved.ts b/tslang/test/tester/tests/import_object_literal_structural_typed_interleaved.ts new file mode 100644 index 000000000..031063d4d --- /dev/null +++ b/tslang/test/tester/tests/import_object_literal_structural_typed_interleaved.ts @@ -0,0 +1,20 @@ +import './export_object_literal_structural_typed_interleaved' + +var acc: A.Accumulator = A.acc; + +acc.addBase(5); +print(acc.base); +assert(acc.base == 105); + +acc.add(3); +print(acc.total); +assert(acc.total == 3); + +acc.addTwice(2); +print(acc.total); +assert(acc.total == 7); + +print(acc.scaled(2)); +assert(acc.scaled(2) == 14); + +print("done.");