diff --git a/src/codegen/infrastructure/type-inference.ts b/src/codegen/infrastructure/type-inference.ts index 3fcc60d8d..4cd465050 100644 --- a/src/codegen/infrastructure/type-inference.ts +++ b/src/codegen/infrastructure/type-inference.ts @@ -188,11 +188,13 @@ export class TypeInference { if (t === "boolean") return true; if (t === "null") return true; if (t === "regex") return true; - if (t === "array") return true; - if (t === "map") return true; - if (t === "set") return true; - if (t === "new") return true; - if (t === "object") return true; + // Only truly-static expression types are cacheable. Anything that + // recurses into variable / symbol-table lookups depends on state that's + // built mid-codegen — the pre-codegen annotator pass would cache stale + // wrong answers. Diagnosed: 342 array + 29 conditional + 26 binary + // cases where cached rich returned number[] for string[] / object[] + // declarations (VA_DIAG trail in memory). Force live re-resolution + // for all other shapes by returning false. return false; } diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index b6e2efcde..e3cf05f26 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -199,10 +199,8 @@ export interface VariableAllocatorContext { setLastTypeAssertionSourceVar(name: string | null): void; setCurrentVarDeclKey(key: string | null): void; isStackEligibleKey(key: string): boolean; - // New methods pinned at END of interface: adding them mid-interface shifts - // native vtable slot positions for everything below — see CLAUDE.md rule #5 - // and memory/native-method-deletion-breaks-vtable.md. - resolveExpressionTypeRich(expr: Expression): ResolvedType | null; + // New method pinned at END of interface: inserting mid-interface shifts + // native vtable slots (CLAUDE.md rule #5). typeOf(expr: Expression): ResolvedType | null; } @@ -742,14 +740,12 @@ export class VariableAllocator { const stmtDeclaredType: string = stmt.declaredType || ""; const strippedDeclType = stripNullable(stmtDeclaredType); - // NOTE: stays on flat resolveExpressionType. Switching to typeOf() - // returns a freshly-allocated enriched clone; passing that to the - // downstream consumer here produces wrong native GEP offsets (likely - // Phase C normalizer didn't canonicalize the enrichResolvedType - // literal). Calling typeOf() and DISCARDING works; USING the result - // breaks. Needs normalizer coverage on enrichResolvedType before - // this consumer can migrate. - const resolved = this.ctx.resolveExpressionType(stmtValue); + // Prefer the annotator-populated cache via typeOf when available; fall + // through to the resolver for expressions the annotator skipped. + let resolved = this.ctx.typeOf(stmtValue); + if (resolved === null) { + resolved = this.ctx.resolveExpressionType(stmtValue); + } const nodeType = (stmtValue as ExprBase).type; let isString: boolean; diff --git a/src/semantic/type-annotator.ts b/src/semantic/type-annotator.ts index 23365495b..3c8451216 100644 --- a/src/semantic/type-annotator.ts +++ b/src/semantic/type-annotator.ts @@ -242,12 +242,28 @@ class TypeAnnotator { this.visitExpr(ta.expression); } - // After recursion, annotate this expression. Skip unknown-base and - // missing sourceKind — they represent resolver gaps, not authoritative - // info. typeOf's fallback still covers them on demand. + // After recursion, annotate this expression — but ONLY for truly-static + // shapes. Array / map / set / object / new / binary / conditional all + // recurse through variable / symbol-table lookups whose answers depend + // on codegen-time state the annotator doesn't yet see; caching them + // would freeze a pre-codegen wrong answer. Typed-literal and + // template_literal results are stable (always same base). Everything + // else gets resolved live by typeOf's fallback. + if (!this.isStableExprType(e.type)) return; const resolved = this.sink.resolveExpressionTypeRich(expr); if (resolved && resolved.base && resolved.base !== "unknown") { this.sink.appendExpressionType(expr, resolved); } } + + private isStableExprType(t: string): boolean { + return ( + t === "number" || + t === "string" || + t === "template_literal" || + t === "boolean" || + t === "null" || + t === "regex" + ); + } }