From 3db602bf259ce885d33f738586550b89b4a52126 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 18:47:36 +0100 Subject: [PATCH] A record literal with a run-time field can be assigned to a class type (#487) `const p: P = { x: 1, items: [i, 2] }` was "invalid cast from {x:s32, items:s32[]} to P": a record with a field known only at run time is a tuple, not a constant tuple, and castTupleLikeVariants reported the tuple-to-class cast as invalid. It now makes the instance from the record's fields with castTupleToClass, as it already did for a constant record. The same goes for a record passed to a class parameter or returned as a class, and for a record variable assigned to a class, which shares its arrays with the instance. A literal assigned to a class is now built with the class's field types, as a literal assigned to a tuple or an interface already was (getTypeByFieldNameFromReceiverType): - `[i, 2]` with `let i = 7` was an `s32[]` that the cast copied into the field's `number[]` and left with no count, never freed under rc (AOT, 1M iterations: 96.5 MB, now 4.4); constant records leaked so on main as well; - `{ items: [] }` was an `any[]` the field could not take ("invalid cast from any[] to number[]"). Under own, the class takes its fields as reads out of the record's temporary slot, which own reports as borrows ("'this value' borrows a field and cannot be stored, returned or captured"): for a run-time record, where it used to report the invalid cast, and now also for a constant record, which compiled before because its arrays were copied fresh by the cast. Moving a record literal's fields into the instance is filed separately. Closes #487 Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenCast.cpp | 6 ++- tslang/lib/TypeScript/MLIRGenImpl.h | 14 +++++ tslang/test/tester/CMakeLists.txt | 3 ++ tslang/test/tester/tests/00record_to_class.ts | 51 +++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/00record_to_class.ts diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 5d97a3483..6c1892d3e 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1294,8 +1294,10 @@ namespace mlirgen } else if (auto classType = dyn_cast(type)) { - emitError(location, "invalid cast from ") << to_print(valueType) << " to " << to_print(type); - return mlir::failure(); + // a record with a run-time field (`const p: P = { x: 1, items: [i, 2] }`) makes the + // instance from its fields, as a constant record does (#487) + fields = mlir::cast(classType.getStorageType()).getFields(); + return castTupleToClass(location, value, srcTupleType, fields, classType, genContext); } else if (auto funcType = dyn_cast(type)) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 328f1f392..32db49015 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -9875,6 +9875,20 @@ class MLIRGenImpl } } + // a record literal made into a class instance (`const p: P = { x: 1, items: [i, 2] }`) is + // built with the class's field types, as for a tuple: `[i, 2]` is the field's `number[]` + // itself, not an `s32[]` copied into one and left with no count under rc, and `[]` is no + // `any[]` the field cannot take (#487) + if (auto classType = dyn_cast(receiverType)) + { + auto classStorageType = mlir::cast(classType.getStorageType()); + auto index = classStorageType.getIndex(fieldName); + if (index >= 0 && index < (int)classStorageType.size()) + { + return classStorageType.getType(index); + } + } + if (auto interfaceType = dyn_cast(receiverType)) { auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 909c83f7e..9b306fd2d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -349,6 +349,7 @@ tslang_add_test(NAME test-compile-00-arrays6 COMMAND test-runner "${PROJECT_SOUR tslang_add_test(NAME test-compile-00-array-static-nested COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") tslang_add_test(NAME test-compile-00-array-literal-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.ts") tslang_add_test(NAME test-compile-00-tuple-runtime-arrays COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00tuple_runtime_arrays.ts") +tslang_add_test(NAME test-compile-00-record-to-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00record_to_class.ts") tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") @@ -855,6 +856,7 @@ tslang_add_test(NAME test-jit-00-arrays6 COMMAND test-runner -jit "${PROJECT_SOU tslang_add_test(NAME test-jit-00-array-static-nested COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") tslang_add_test(NAME test-jit-00-array-literal-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.ts") tslang_add_test(NAME test-jit-00-tuple-runtime-arrays COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00tuple_runtime_arrays.ts") +tslang_add_test(NAME test-jit-00-record-to-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00record_to_class.ts") tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") @@ -1690,6 +1692,7 @@ set(TSLANG_CORPUS 00array_static_nested.ts 00array_literal_copy.ts 00tuple_runtime_arrays.ts + 00record_to_class.ts 00const_record_owned_fields.ts 00array_length_valid.ts 00string_view_copy.ts diff --git a/tslang/test/tester/tests/00record_to_class.ts b/tslang/test/tester/tests/00record_to_class.ts new file mode 100644 index 000000000..f907eb7ac --- /dev/null +++ b/tslang/test/tester/tests/00record_to_class.ts @@ -0,0 +1,51 @@ +// A record literal with a run-time field can be assigned to a class type (#487): the instance is +// made from the record's fields, as it is from a constant record's, where the cast used to be an +// "invalid cast" error. A literal assigned to a class is built with the class's field types, so +// `[i, 2]` is the field's `number[]` itself (an `s32[]` copied into one was never freed under rc) +// and `[]` is no `any[]` the field cannot take. +class P { + x: number; + items: number[]; + name: string; +} + +function take(p: P) { + return p.items.length + p.name.length; +} + +function make(i: number): P { + return { x: i, items: [i, i + 1, i + 2], name: "n" + i }; +} + +function main() { + let i = 7; + const p: P = { x: 1, items: [i, 2], name: "a" }; + assert(p.items.length == 2 && p.items[0] == 7 && p.x == 1, "const of a run-time record"); + p.items.push(3); + assert(p.items.length == 3, "its array can be changed"); + + const c: P = { x: 2, items: [1, 2], name: "c" }; + assert(c.items.length == 2 && c.items[1] == 2, "a constant record"); + const e: P = { x: i, items: [], name: "e" }; + e.items.push(i); + assert(e.items.length == 1 && e.items[0] == 7, "an empty array"); + + let n: number = 5; + let r = { x: n, items: [n, 2], name: "b" + n }; + const q: P = r; + q.items.push(9); + assert(r.items.length == 3 && q.name == "b5", "a record variable, sharing its array"); + + assert(take({ x: 2, items: [i], name: "cc" + i }) == 4, "to a class parameter"); + const m = make(i); + assert(m.x == 7 && m.items.length == 3 && m.name == "n7", "returned as a class"); + + let total = 0; + for (let k = 0; k < 1000; k++) { + const z: P = { x: k, items: [k, k], name: "z" + k }; + total += z.items.length + z.name.length; + } + assert(total == 5890, "a fresh instance each pass"); + + print("done."); +}