From e28cf684db866e46e33114322e6bdc3289bf5bdc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Thu, 23 Jul 2026 12:36:04 +0100 Subject: [PATCH] Fix chained-sibling-method-return discovery gap (root cause) Closes the third layer of chained-sibling-method-return-gap.md, left open since 2026-07-19: `return this.sibling(...)` (using a sibling method's call result) inside an object literal aborted the ENCLOSING function's own return-type/captured-var discovery with a bare `error: failed statement`, even after 2 earlier gates silenced the intermediate diagnostics. Root cause: discoverFunctionReturnTypeAndCapturedVars always tries to INFER a return type from a method's body via a speculative dummyRun walk - "we need to discover it all the time due to captured vars" - even when the method already has an EXPLICIT return-type annotation (whose value is set eagerly in mlirGenFunctionSignaturePrototype, before discovery ever runs, and doesn't need the inferred one). When a sibling method's prototype isn't registered into the object literal's storage type yet, a call to it can't produce a value during the dummy run, so the inferred return type comes back "none" - which was unconditionally treated as "discovery failed to converge", regardless of the explicit annotation making that inferred value irrelevant. Fix: skip the "return type still unresolved" failure when the function/method has an explicit return-type annotation (functionLikeDeclarationBaseAST->type), and guard the discovered-type-overwrite step so a "none" (non-converged) inferred type can never clobber an already-known (explicit or cached) return type. Also hardened MLIRGenStatements.cpp's statement-list "no progress across retries" diagnostic (mlirGen(NodeArray, ...)) to skip emitting "can't resolve dependencies in namespace" under dummyRun/allowPartialResolve, matching the same idiom as the two previously-fixed gates - a real, defensive improvement even though the root-cause fix above is what actually unblocks the original repro; per the design doc's own warning, more such call sites could still exist for other, not-yet-hit scenarios. New regression test 00object_chained_sibling_method_return.ts (compile + jit tiers) - the exact repro from the original investigation, now passing end-to-end (compiles AND runs correctly: calc.scaleAndAdd(2, 3) == 23). Full suite: 810/810 green. Co-Authored-By: Claude Sonnet 5 --- tslang/lib/TypeScript/MLIRGenFunctions.cpp | 24 +++++++++++++++-- tslang/lib/TypeScript/MLIRGenStatements.cpp | 24 +++++++++++++++-- tslang/test/tester/CMakeLists.txt | 2 ++ .../00object_chained_sibling_method_return.ts | 26 +++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 tslang/test/tester/tests/00object_chained_sibling_method_return.ts diff --git a/tslang/lib/TypeScript/MLIRGenFunctions.cpp b/tslang/lib/TypeScript/MLIRGenFunctions.cpp index 2cc81e10a..33f749b17 100644 --- a/tslang/lib/TypeScript/MLIRGenFunctions.cpp +++ b/tslang/lib/TypeScript/MLIRGenFunctions.cpp @@ -468,8 +468,25 @@ namespace mlirgen exitNamespace(); auto &passResult = genContextWithPassResult.passResult; + + // Root cause of the chained-sibling-method-return-gap's third layer (see + // that memory): discovery always runs even when the function/method + // already has an EXPLICIT return-type annotation - "// seems we need to + // discover it all the time due to captured vars" above - so + // functionReturnTypeShouldBeProvided/functionReturnType still get set from + // trying to INFER a type from the body's return expression(s), even though + // that inferred value will never be used when an explicit one already + // exists. If a sibling method's prototype isn't registered yet (e.g. + // `return this.scale(f) + e;` inside an object literal, discovered as a + // side effect of discovering an unrelated OUTER function), the call can't + // produce a value during this dummy run, so functionReturnType comes back + // "none" - previously treated as "discovery failed to converge" even + // though the explicit annotation makes that inferred value moot. + auto hasExplicitReturnType = !!functionLikeDeclarationBaseAST->type; + if (passResult->functionReturnTypeShouldBeProvided - && mth.isNoneType(passResult->functionReturnType)) + && mth.isNoneType(passResult->functionReturnType) + && !hasExplicitReturnType) { // has return value but type is not provided yet genContextWithPassResult.clean(); @@ -495,7 +512,10 @@ namespace mlirgen funcProto->setDiscovered(true); auto discoveredType = passResult->functionReturnType; - if (discoveredType && discoveredType != funcProto->getReturnType()) + // a "none" discoveredType means inference didn't converge (see above) - + // never let that silently overwrite an already-known (explicit or + // previously-cached) return type. + if (discoveredType && !mth.isNoneType(discoveredType) && discoveredType != funcProto->getReturnType()) { // TODO: do we need to convert it here? maybe send it as const object? diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index a74b01f34..d018cab6c 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -96,8 +96,28 @@ namespace mlirgen // repeat if not all resolved if (lastTimeNotResolved > 0 && lastTimeNotResolved == notResolved) { - // class can depends on other class declarations - emitError(errorLocation, "can't resolve dependencies in namespace"); + // Third layer of the chained-sibling-method-return discovery gap (see + // chained-sibling-method-return-gap memory): a statement in THIS list can + // fail to converge not because of a real dependency cycle, but because it + // contains an object literal whose method bodies are being speculatively + // walked (return-type/captured-var discovery for an OUTER function with no + // explicit return type - discoverFunctionReturnTypeAndCapturedVars) and a + // sibling method's prototype isn't registered yet. That nested discovery + // already fails silently (no diagnostic) under dummyRun/allowPartialResolve + // per the two gates in mlirGenPropertyAccessExpressionBaseLogic and + // discoverFunctionReturnTypeAndCapturedVars - but this loop's own "no + // progress across retries" diagnostic didn't check the same condition, so a + // legitimately-not-yet-resolvable statement here still hard-aborted the + // whole enclosing discovery with a real, user-facing error. Same idiom as + // those two gates: skip the diagnostic, still signal non-convergence via + // mlir::failure() so the real (non-dummy) compile pass gets a chance to + // resolve it once every sibling's prototype is registered. + if (!genContext.dummyRun && !genContext.allowPartialResolve) + { + // class can depends on other class declarations + emitError(errorLocation, "can't resolve dependencies in namespace"); + } + return mlir::failure(); } } while (notResolved > 0); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 0475c8849..21135d1b7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -291,6 +291,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-chained-sibling-method-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_chained_sibling_method_return.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-object-annotated-method-extends-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface.ts") add_test(NAME test-compile-00-object-annotated-method-extends-interface-multilevel COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface_multilevel.ts") @@ -654,6 +655,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-chained-sibling-method-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_chained_sibling_method_return.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-object-annotated-method-extends-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface.ts") add_test(NAME test-jit-00-object-annotated-method-extends-interface-multilevel COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_extends_interface_multilevel.ts") diff --git a/tslang/test/tester/tests/00object_chained_sibling_method_return.ts b/tslang/test/tester/tests/00object_chained_sibling_method_return.ts new file mode 100644 index 000000000..a663f9a14 --- /dev/null +++ b/tslang/test/tester/tests/00object_chained_sibling_method_return.ts @@ -0,0 +1,26 @@ +// Regression test for the chained-sibling-method-return-gap (see that memory +// for the full 3-layer mechanism). A method inside an object literal that +// `return`s (or otherwise uses) a sibling method's call result used to fail +// discovery of the ENCLOSING function's own return type/captured vars, +// because a sibling's prototype isn't registered into the literal's storage +// type until its own discovery completes - and a call to an +// not-yet-registered sibling can't produce a value during the speculative +// (dummyRun) discovery pass. Root cause: discovery always tries to INFER a +// return type from the body even when the method already has an EXPLICIT +// one, so a legitimately-unresolvable inferred value ("none") was wrongly +// treated as "discovery failed to converge". +function main() { + let calc = { + base: 10, + scale(factor: number): number { + return this.base * factor; + }, + scaleAndAdd(factor: number, extra: number): number { + return this.scale(factor) + extra; + } + }; + + assert(calc.scaleAndAdd(2, 3) == 23); + + print("done."); +}