From 6fc5783dfda519ba05d53d81e7290404e84cc795 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 27 Sep 2026 23:40:41 +0100 Subject: [PATCH] new of a generic class takes its type arguments from the receiver `const m: Map = new Map()` crashed at run time: Map is declared `Map`, so `new Map()` built Map, and the cast to the declared type reinterpreted it as a different layout. A generic without defaults was a compile error instead ("Generic class 'Box' is missing type arguments"). Found in the BrowserLib sources attached to #231 (EventTarget.ts, `listeners: Map = new Map()`). A `new` without type arguments now takes the receiver's specialization (the declared variable, field or assignment target) when that is a specialization of the same generic class. Explicit type arguments are unchanged. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 33 ++++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../tests/00new_generic_from_receiver.ts | 66 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 tslang/test/tester/tests/00new_generic_from_receiver.ts diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index bdd9cd7c1..919565717 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1051,6 +1051,39 @@ namespace mlirgen EXIT_IF_FAILED_OR_NO_VALUE(result) auto value = V(result); + // `const m: Map = new Map()`: a generic class written without type + // arguments takes them from the type it is given to, when that is the same class. Its + // type parameter defaults (`Map`) made Map otherwise, a + // different layout, and the cast to the declared type crashed at run time. + if (newExpression->typeArguments.size() == 0 && genContext.receiverType) + { + if (auto classType = dyn_cast(value.getType())) + { + // the generic itself, or its specialization by the defaults + auto genericClassType = mlir_ts::ClassType(); + if (getGenericClassInfoByFullName(classType.getName().getValue())) + { + genericClassType = classType; + } + else if (auto classInfo = getClassInfoByFullName(classType.getName().getValue())) + { + genericClassType = classInfo->originClassType; + } + + auto receiverClassType = dyn_cast(mth.stripOptionalType(genContext.receiverType)); + if (genericClassType && receiverClassType && receiverClassType != classType) + { + auto receiverClassInfo = getClassInfoByFullName(receiverClassType.getName().getValue()); + if (receiverClassInfo && receiverClassInfo->originClassType == genericClassType) + { + value = builder.create( + location, receiverClassType, + mlir::FlatSymbolRefAttr::get(builder.getContext(), receiverClassType.getName().getValue())); + } + } + } + } + if (auto arrayType = dyn_cast(value.getType())) { return NewArray(location, arrayType, newExpression->arguments, genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 630d58d79..c4adfdab2 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -385,6 +385,7 @@ tslang_add_test(NAME test-compile-00-sizeof COMMAND test-runner "${PROJECT_SOURC tslang_add_test(NAME test-compile-01-sizeof COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01sizeof.ts") tslang_add_test(NAME test-compile-02-sizeof COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/02sizeof.ts") tslang_add_test(NAME test-compile-00-new-delete COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_delete.ts") +tslang_add_test(NAME test-compile-00-new-generic-from-receiver COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_generic_from_receiver.ts") tslang_add_test(NAME test-compile-00-owned-locals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_locals.ts") # The JIT variants are the ones that matter here: this shape was a JIT-only miscompile until # the catchable-type size was fixed (see the file header). Kept in all three models, since the @@ -832,6 +833,7 @@ tslang_add_test(NAME test-jit-00-sizeof COMMAND test-runner -jit "${PROJECT_SOUR tslang_add_test(NAME test-jit-01-sizeof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01sizeof.ts") tslang_add_test(NAME test-jit-02-sizeof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/02sizeof.ts") tslang_add_test(NAME test-jit-00-new-delete COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_delete.ts") +tslang_add_test(NAME test-jit-00-new-generic-from-receiver COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_generic_from_receiver.ts") tslang_add_test(NAME test-jit-00-owned-locals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_locals.ts") tslang_add_test(NAME test-jit-00-throw-in-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_in_catch.ts") tslang_add_test(NAME test-jit-00-throw-inlined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_inlined.ts") @@ -1702,6 +1704,7 @@ set(TSLANG_CORPUS 00try_mismatch_in_catch_clause.ts 00try_mismatch_in_catch_clause_using.ts 00new_delete.ts + 00new_generic_from_receiver.ts 00ns_bug.ts 00ns.ts 00ns2.ts diff --git a/tslang/test/tester/tests/00new_generic_from_receiver.ts b/tslang/test/tester/tests/00new_generic_from_receiver.ts new file mode 100644 index 000000000..ed9ab7fab --- /dev/null +++ b/tslang/test/tester/tests/00new_generic_from_receiver.ts @@ -0,0 +1,66 @@ +// `new C()` of a generic class takes its type arguments from the type it is given to, as in +// `const m: Map = new Map()`. Without them a plain generic was an error ("missing +// type arguments"), and one with defaults (`Map`) became C, whose cast +// to the declared type crashed at run time (#231). + +class Box { + items: T[] = []; + + add(v: T) { + this.items.push(v); + } + + get(i: number): T { + return this.items[i]; + } +} + +class Pair { + keys: K[] = []; + values: V[] = []; + + set(k: K, v: V) { + this.keys.push(k); + this.values.push(v); + } +} + +class Holder { + private readonly boxes: Box = new Box(); + pairs: Pair; + + constructor() { + this.pairs = new Pair(); + } + + fill() { + this.boxes.add("x"); + this.pairs.set("a", [1, 2, 3]); + return this.boxes.get(0) + this.pairs.values[0][2]; + } +} + +function main() { + const b: Box = new Box(); + b.add(5); + assert(b.get(0) == 5); + + const p: Pair = new Pair(); + p.set("k", 7); + assert(p.keys[0] == "k" && p.values[0] == 7); + + let q = new Pair(); + q.set(0, "zero"); + q = new Pair(); + q.set(1, "one"); + assert(q.values[0] == "one"); + + // explicit type arguments still win + const e: Box = new Box(); + e.add(1); + assert(e.get(0) == 1); + + assert(new Holder().fill() == "x3"); + + print("done."); +}