From 860f24c539306d7069330f3009031db2ea2d298a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 18:20:45 +0000 Subject: [PATCH 1/4] A generic callback's T no longer takes in the closure's captures An arrow that reads `this`, given to a generic function in a class method (`this.particles.filter((p) => p.d > this.lifetime)`), is rebuilt while the method's return type is discovered, and its type then has the captures ahead of its parameters. Inference dropped that input only when the arrow was already bound (#738), not on this first pass, where it is still a bare generic reference: `T` became `Element | captures`. With the element's class declared later in the file that failed every time ("cast from array type ... to array type '!ts.array --- tslang/lib/TypeScript/MLIRGenGenerics.cpp | 26 ++++++--- tslang/lib/TypeScript/MLIRGenImpl.h | 4 +- tslang/test/tester/CMakeLists.txt | 3 ++ .../00generic_callback_closure_late_class.ts | 54 +++++++++++++++++++ 4 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 tslang/test/tester/tests/00generic_callback_closure_late_class.ts diff --git a/tslang/lib/TypeScript/MLIRGenGenerics.cpp b/tslang/lib/TypeScript/MLIRGenGenerics.cpp index b6704fd4a..3d6d2f639 100644 --- a/tslang/lib/TypeScript/MLIRGenGenerics.cpp +++ b/tslang/lib/TypeScript/MLIRGenGenerics.cpp @@ -526,7 +526,7 @@ namespace mlirgen mlir::Type MLIRGenImpl::instantiateSpecializedFunctionTypeHelper(mlir::Location location, mlir::Value functionRefValue, mlir::Type recieverType, bool discoverReturnType, - const GenContext &genContext) + const GenContext &genContext, bool *capturesFirst) { auto currValue = functionRefValue; if (auto createBoundFunctionOp = currValue.getDefiningOp()) @@ -547,7 +547,7 @@ namespace mlirgen SourceFileScope sourceFileScope(*this, functionGenericTypeInfo->sourceFile, functionGenericTypeInfo->fileName); return instantiateSpecializedFunctionTypeHelper(location, functionGenericTypeInfo->functionDeclaration, - recieverType, discoverReturnType, genContext); + recieverType, discoverReturnType, genContext, capturesFirst); } // a plausible trigger (a generic function assigned to a variable, then called through @@ -561,7 +561,7 @@ namespace mlirgen mlir::Type MLIRGenImpl::instantiateSpecializedFunctionTypeHelper(mlir::Location location, FunctionLikeDeclarationBase funcDecl, mlir::Type recieverType, bool discoverReturnType, - const GenContext &genContext) + const GenContext &genContext, bool *capturesFirst) { GenContext funcGenContext(genContext); funcGenContext.receiverFuncType = recieverType; @@ -580,6 +580,15 @@ namespace mlirgen return mlir::Type(); } + // a closure's type has its captures as an input ahead of its parameters + // (discoverFunctionReturnTypeAndCapturedVars) + if (capturesFirst) + { + auto funcType = funcOp->getFuncType(); + *capturesFirst = funcOp->getHasCapturedVars() && funcType && + funcType.getNumInputs() > funcOp->getParams().size(); + } + return funcOp->getFuncType(); } @@ -789,8 +798,9 @@ namespace mlirgen LLVM_DEBUG(llvm::dbgs() << "\n!! param type for arrow func[" << paramIndex << "]: " << recreatedParamType << "\n";); + auto capturesFirst = false; auto newArrowFuncType = instantiateSpecializedFunctionTypeHelper(location, argOp, recreatedParamType, - true, genericTypeGenContext); + true, genericTypeGenContext, &capturesFirst); LLVM_DEBUG(llvm::dbgs() << "\n!! instantiate specialized arrow type function: " << newArrowFuncType << "\n";); @@ -800,9 +810,13 @@ namespace mlirgen return {mlir::failure(), true}; } - // ...and so does the arrow's type built again here + // ...and so does the arrow's type built again here: bound already, or a closure whose + // captures were just discovered (the arrow still a bare generic reference), it has them as its + // first input, which paired with `T` made it `T | captures` - in an order of the members' type + // addresses, so the arrow's parameter was taken for its captures in some runs and not others if (auto funcType = dyn_cast(newArrowFuncType); - funcType && argOp.getDefiningOp() && funcType.getNumInputs() > 0) + funcType && (argOp.getDefiningOp() || capturesFirst) && + funcType.getNumInputs() > 0) { newArrowFuncType = mlir_ts::FunctionType::get(builder.getContext(), funcType.getInputs().drop_front(), funcType.getResults(), funcType.isVarArg()); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 2ce4409cc..a4e8e17c7 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1614,11 +1614,11 @@ class MLIRGenImpl mlir::Type instantiateSpecializedFunctionTypeHelper(mlir::Location location, mlir::Value functionRefValue, mlir::Type recieverType, bool discoverReturnType, - const GenContext &genContext); + const GenContext &genContext, bool *capturesFirst = nullptr); mlir::Type instantiateSpecializedFunctionTypeHelper(mlir::Location location, FunctionLikeDeclarationBase funcDecl, mlir::Type recieverType, bool discoverReturnType, - const GenContext &genContext); + const GenContext &genContext, bool *capturesFirst = nullptr); void rollbackPostponedErrorMessages(mlir::SmallVector> *postponedMessages, size_t size); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index cf2b15355..63898ecf6 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -616,6 +616,7 @@ tslang_add_test(NAME test-compile-own-00-object-literal-contextual-lambda COMMAN tslang_add_test(NAME test-compile-00-interface-field-getter-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") tslang_add_test(NAME test-compile-00-conditional-record-literal-temporary COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") tslang_add_test(NAME test-compile-00-conditional-nested-record-literal-temporary COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_nested_record_literal_temporary.ts") +tslang_add_test(NAME test-compile-00-generic-callback-closure-late-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_callback_closure_late_class.ts") tslang_add_test(NAME test-compile-00-interface-print COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_print.ts") tslang_add_test(NAME test-compile-00-class-method-devirtualized COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts") tslang_add_test(NAME test-compile-00-call-arg-param-field-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") @@ -1260,6 +1261,7 @@ tslang_add_test(NAME test-jit-own-00-object-literal-contextual-lambda COMMAND te tslang_add_test(NAME test-jit-00-interface-field-getter-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") tslang_add_test(NAME test-jit-00-conditional-record-literal-temporary COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") tslang_add_test(NAME test-jit-00-conditional-nested-record-literal-temporary COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_nested_record_literal_temporary.ts") +tslang_add_test(NAME test-jit-00-generic-callback-closure-late-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_callback_closure_late_class.ts") tslang_add_test(NAME test-jit-00-interface-print COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_print.ts") tslang_add_test(NAME test-jit-00-class-method-devirtualized COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts") tslang_add_test(NAME test-jit-00-call-arg-param-field-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") @@ -2285,6 +2287,7 @@ set(TSLANG_CORPUS 00interface_field_getter_owned.ts 00conditional_record_literal_temporary.ts 00conditional_nested_record_literal_temporary.ts + 00generic_callback_closure_late_class.ts 00interface_print.ts 00class_method_devirtualized.ts 00call_arg_param_field_global.ts diff --git a/tslang/test/tester/tests/00generic_callback_closure_late_class.ts b/tslang/test/tester/tests/00generic_callback_closure_late_class.ts new file mode 100644 index 000000000..2be1e967b --- /dev/null +++ b/tslang/test/tester/tests/00generic_callback_closure_late_class.ts @@ -0,0 +1,54 @@ +// A closure (it reads `this`) given to a generic function in a class method, the array's element a +// class declared further down. While the method's return type was discovered, the arrow's type was +// rebuilt with its captures as its first input, and inference paired them with `T`: `T` was +// `Late | captures`. Here that failed every time ("cast from array type ... to array type +// '!ts.array(items: T[], f: (v: T, i: number) => boolean): T[] { + const kept: T[] = []; + for (let i = 0; i < items.length; i++) { + if (f(items[i], i)) kept.push(items[i]); + } + + return kept; +} + +class Gen { + particles: Late[] = []; + lifetime = 2.0; + + update() { + const remaining = keep(this.particles, (particle) => !(particle.duration > rnd() * this.lifetime)); + this.particles = remaining; + return remaining.length; + } + + firstDuration() { + return this.particles[0].duration; + } + + longer(min: number) { + return keep(this.particles, (particle) => particle.duration >= min * this.lifetime).length; + } +} + +class Late { + constructor(public duration: number) {} +} + +function main() { + const g = new Gen(); + g.particles.push(new Late(0.0), new Late(100.0), new Late(0.0)); + assert(g.update() == 2, "the long-lived one is kept out"); + assert(g.particles.length == 2 && g.firstDuration() == 0.0); + g.particles.push(new Late(7.0)); + assert(g.longer(1.0) == 1, "a second closure over this"); + print("done."); +} From 1d17d22fd1d7031b8228fc9b431fc8f84ffaf5dc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 18:20:51 +0000 Subject: [PATCH 2/4] Anonymous function names hash the file's name, not its attribute's address hash_value of the location's StringAttr hashes the attribute's storage, so an arrow's name (`..afL89C49FH`) was different in every run. It is now xxh3 of the file name. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/MLIRLogic/MLIRHelper.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRHelper.h index e150ac066..4eabb42eb 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRHelper.h @@ -6,6 +6,7 @@ #include "llvm/Support/ConvertUTF.h" #include "llvm/ADT/TypeSwitch.h" #include "llvm/ADT/SmallPtrSet.h" +#include "llvm/Support/xxhash.h" #include "parser.h" #include "node_factory.h" @@ -159,7 +160,10 @@ class MLIRHelper assert(line != 0 || column != 0); - auto hashCode = hash_value(fileName); + // of the file's name, not of the attribute (hash_value(StringAttr) hashes its storage + // address): an anonymous function's name was different in every run, and a table keyed + // by those names visited them in a different order each time + auto hashCode = (uint32_t)llvm::xxh3_64bits(fileName.getValue()); ssName << 'L' << line << 'C' << column << "FH" << hashCode; }) .Case([&](auto loc) { From b3e5dd16ba5bc7bb91a3b69ab35728dd9d2188df Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 18:20:51 +0000 Subject: [PATCH 3/4] Register the llvm dialect's inliner interface (debug JIT failed its promise check) The llvm dialect promises a DialectInlinerInterface that only registerAllDialects provides. A debug (assertions) tslang ended every --emit=jit with "checking for an interface (`mlir::DialectInlinerInterface`) that was promised by dialect 'llvm' but never implemented". Co-Authored-By: Claude Opus 5.5 --- tslang/tslang/tslang.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tslang/tslang/tslang.cpp b/tslang/tslang/tslang.cpp index 9eea2095c..a7462ba01 100644 --- a/tslang/tslang/tslang.cpp +++ b/tslang/tslang/tslang.cpp @@ -19,6 +19,7 @@ #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/DLTI/DLTI.h" #include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/LLVMIR/Transforms/InlinerInterfaceImpl.h" #ifdef ENABLE_ASYNC #include "mlir/Dialect/Async/IR/Async.h" #endif @@ -338,6 +339,9 @@ std::unique_ptr createMLIRContext() { mlir::DialectRegistry registry; registerAllExtensions(registry); + // the llvm dialect promises an inliner interface that only registerAllDialects provides: without + // it a pass that inlines past lowering (the JIT's) failed the promise check in an assertions build + mlir::LLVM::registerInlinerInterface(registry); auto mlirContext = std::make_unique(registry); // Load our Dialect in this MLIR Context. From acec5edeb132810894b946a551d6d8fa49c6f05c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 18:22:40 +0000 Subject: [PATCH 4/4] .gitignore: every JIT cache folder (__jit*/), the debug one (__jit_dbg_gc) too Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d5116e526..974abb1b0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ libatomic_ops-* *.log # the JIT cache (tslang --emit=jit) -__jit/ +__jit*/