From 83de43d4e2fc5d117df460419689a6ad9be08cd5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 00:54:30 +0000 Subject: [PATCH] Keep the closure when a cast instantiates a capturing untyped arrow `new W((v) => { seen += 1; })` crashed in every memory model. The arrow has no parameter types, so it stays generic until the cast to the constructor parameter's type instantiates it. verifyFunctionCastRules did that through instantiateSpecializedFunction but kept only its LogicalResult: when the arrow captures, the result is a new value (the closure bound over the captured variables), and the cast went on with the bare function, whose captures were then read through a null pointer. verifyFunctionCastRules now takes value and valueType by reference and continues the cast with the returned closure, as instantiateGenericsForBinaryOp already does. Under -mm=own the program now gets the escaping-closure diagnostic the explicitly typed form already got. Regression test 00capture_in_new_untyped_arrow.ts (gc compile/jit, rc and none through the corpus). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenCast.cpp | 13 +++- tslang/lib/TypeScript/MLIRGenImpl.h | 2 +- tslang/test/tester/CMakeLists.txt | 3 + .../tests/00capture_in_new_untyped_arrow.ts | 66 +++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/00capture_in_new_untyped_arrow.ts diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 16e63f266..a830e953e 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -2044,7 +2044,7 @@ namespace mlirgen } } - mlir::LogicalResult MLIRGenImpl::verifyFunctionCastRules(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext) + mlir::LogicalResult MLIRGenImpl::verifyFunctionCastRules(mlir::Location location, mlir::Type type, mlir::Value &value, mlir::Type &valueType, const GenContext &genContext) { if (mth.isAnyFunctionType(valueType) && mth.isAnyFunctionType(type)) { @@ -2053,6 +2053,17 @@ namespace mlirgen // need to instantiate generic method auto result = instantiateSpecializedFunction(location, value, type, genContext); EXIT_IF_FAILED(result); + + // An arrow that captures comes back as a new value, the closure bound over its + // captured variables; the old reference is the bare function and calling it reads + // the captures through a null pointer (`new W((v) => { seen += 1; })`). Without + // captures the reference was retyped in place. + if (auto resultValue = V(result)) + { + value = resultValue; + } + + valueType = value.getType(); } // A C variadic function is called with C's variadic convention and a TypeScript diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 0426c791e..7a0df99a9 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -14253,7 +14253,7 @@ class MLIRGenImpl // union or optional or any or opaque source type std::optional castFromSourceSpecialCases(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); - mlir::LogicalResult verifyFunctionCastRules(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); + mlir::LogicalResult verifyFunctionCastRules(mlir::Location location, mlir::Type type, mlir::Value &value, mlir::Type &valueType, const GenContext &genContext); // cast ext method to bound method std::optional castExtensionFunctionType(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 313d4dd21..a40651ca7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -597,6 +597,7 @@ tslang_add_test(NAME test-compile-00-try-cleanup-then-catch COMMAND test-runner tslang_add_test(NAME test-compile-00-try-empty-catch COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_empty_catch.ts") tslang_add_test(NAME test-compile-00-global-union-from-call COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_union_from_call.ts") tslang_add_test(NAME test-compile-00-capture-in-new-arguments COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00capture_in_new_arguments.ts") +tslang_add_test(NAME test-compile-00-capture-in-new-untyped-arrow COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00capture_in_new_untyped_arrow.ts") tslang_add_test(NAME test-compile-01-class-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01class_new.ts") tslang_add_test(NAME test-compile-00-class-stack COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_stack.ts") tslang_add_test(NAME test-compile-00-class-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_static.ts") @@ -1202,6 +1203,7 @@ tslang_add_test(NAME test-jit-00-try-cleanup-then-catch COMMAND test-runner -jit tslang_add_test(NAME test-jit-00-try-empty-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_empty_catch.ts") tslang_add_test(NAME test-jit-00-global-union-from-call COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_union_from_call.ts") tslang_add_test(NAME test-jit-00-capture-in-new-arguments COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00capture_in_new_arguments.ts") +tslang_add_test(NAME test-jit-00-capture-in-new-untyped-arrow COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00capture_in_new_untyped_arrow.ts") tslang_add_test(NAME test-jit-01-class-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01class_new.ts") tslang_add_test(NAME test-jit-00-class-stack COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_stack.ts") tslang_add_test(NAME test-jit-00-class-static COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_static.ts") @@ -2146,6 +2148,7 @@ set(TSLANG_CORPUS 00try_scope_exit_through_finally.ts 00try_jump_from_catch.ts 00capture_in_new_arguments.ts + 00capture_in_new_untyped_arrow.ts 00cast_to_f32.ts 00catch_untyped_value.ts 00catch_value.ts diff --git a/tslang/test/tester/tests/00capture_in_new_untyped_arrow.ts b/tslang/test/tester/tests/00capture_in_new_untyped_arrow.ts new file mode 100644 index 000000000..5f7cd5519 --- /dev/null +++ b/tslang/test/tester/tests/00capture_in_new_untyped_arrow.ts @@ -0,0 +1,66 @@ +// An arrow without parameter types, `(v) => ...`, is generic until something gives it a type. As a +// `new` argument it gets one when it is cast to the constructor parameter's type, and the cast +// instantiated it there - but when the arrow captured a local, the instantiation built the closure +// over the captured variables and the cast went on with the bare function instead. The constructor +// stored a function whose captures were a null pointer, and the first call crashed - under every +// memory model, so it is a front-end bug. A non-capturing arrow, or one with typed parameters, +// never went that way. + +class ParamProperty { + constructor(private f: (v: number) => void) {} + + run(v: number) { + this.f(v); + } +} + +class StoredField { + f: (v: number) => void; + + constructor(f: (v: number) => void) { + this.f = f; + } + + run(v: number) { + this.f(v); + } +} + +class Mapper { + constructor(private f: (v: number) => number) {} + + apply(v: number) { + return this.f(v); + } +} + +let globalSeen: number = 0; + +function main() { + let seen: number = 0; + const p = new ParamProperty((v) => { + seen += v; + }); + p.run(1); + p.run(2); + assert(seen == 3, "a capturing untyped arrow in a parameter property"); + + let total: number = 0; + const s = new StoredField((v) => { + total += v; + }); + s.run(5); + assert(total == 5, "a capturing untyped arrow stored in a field"); + + const k = 10; + const m = new Mapper((v) => v * k); + assert(m.apply(4) == 40, "a capturing untyped arrow returning a value"); + + const g = new ParamProperty((v) => { + globalSeen += v; + }); + g.run(7); + assert(globalSeen == 7, "a non-capturing untyped arrow"); + + print("done."); +}