From bffa186c48b0b380ea48a9d099a377873e7434bf Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:10:10 +0900 Subject: [PATCH 01/42] Make generator suspension walkers dual before inline cloning The ES5 machine keeps three views of one predicate - contains_yield, contains_await, and count_yields - and the loop/branch inline-clone gate routed on count_yields alone. Shapes the count view under- reported were cloned verbatim with a live yield inside a plain function: a while-test computed-key update, an array spread, and pattern-target assignments. Shapes it over-reported on eval-accepted targets inflated exit_label and minted wrong case jumps. Every walker now traverses the same shapes. Precise arms cover what eval accepts (identifier/member/update targets, dynamic imports, object and array spreads); the count catch-all inverts to non-zero so zero means provably clean and anything unknown routes to eval, whose refusal keeps the native generator plus the requires-es2015 diagnostic. The miscompile class is closed by polarity, not by enumerating variants. --- .../bamts-compiler/src/emitter/transforms.rs | 86 +++++++++++++++---- 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index d3d7e39..bea443a 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -7040,6 +7040,11 @@ fn count_yields(expression: &Expr) -> u32 { _ => 0, } } + // A spread element carries an expression; method-like + // members (methods/getters/setters) own their own + // suspensions, just like nested function-likes, so the + // wildcard below is safe for them. + ObjectMember::Spread(spread) => count_yields(&spread.argument), _ => 0, }) .sum(), @@ -7074,6 +7079,20 @@ fn count_yields(expression: &Expr) -> u32 { } Expression::Await(awaited) => count_yields(&awaited.argument), Expression::Unary(unary) => count_yields(&unary.argument), + Expression::Update(update) => match update.argument.data() { + AssignmentTarget::Member(member) => { + count_yields(&member.object) + + match &member.property { + MemberProperty::Computed(key) => count_yields(key), + _ => 0, + } + } + // Pattern and invalid targets can carry yields in computed + // keys and defaults; a sentinel count routes them to eval, + // which refuses, rather than the inline verbatim clone. + AssignmentTarget::Identifier(_) => 0, + _ => 1, + }, Expression::Binary(binary) => count_yields(&binary.left) + count_yields(&binary.right), Expression::Logical(logical) => count_yields(&logical.left) + count_yields(&logical.right), Expression::Conditional(conditional) => { @@ -7091,14 +7110,24 @@ fn count_yields(expression: &Expr) -> u32 { _ => 0, } } - _ => 0, + // Pattern and invalid targets can carry yields; a + // sentinel count routes them to eval, which refuses, + // rather than the inline verbatim clone. + AssignmentTarget::Identifier(_) => 0, + _ => 1, } } Expression::Sequence(sequence) => sequence.expressions.iter().map(count_yields).sum(), Expression::Parenthesized(inner) => count_yields(inner), Expression::As(cast) => count_yields(&cast.expression), Expression::NonNull(non_null) => count_yields(&non_null.expression), - _ => 0, + Expression::Import(import) => { + count_yields(&import.source) + import.options.as_deref().map_or(0, count_yields) + } + // Unknown shapes report non-zero: zero means provably clean and + // gates the inline verbatim clone; unknown routes to eval, whose + // refusal keeps the native form. + _ => 1, } } @@ -7314,10 +7343,7 @@ fn contains_await(expression: &Expr) -> bool { _ => false, }) } - Expression::Member(member) => { - contains_await(&member.object) - || matches!(&member.property, MemberProperty::Computed(key) if contains_await(key)) - } + Expression::Member(member) => target_contains_await(&member.object, &member.property), Expression::New(new) => { contains_await(&new.callee) || new.arguments.iter().any(|argument| match argument { @@ -7326,6 +7352,16 @@ fn contains_await(expression: &Expr) -> bool { }) } Expression::Unary(unary) => contains_await(&unary.argument), + Expression::Update(update) => match update.argument.data() { + AssignmentTarget::Member(member) => { + target_contains_await(&member.object, &member.property) + } + // Pattern and invalid targets can carry expressions in + // computed keys and defaults; report containment so the + // machine refuses rather than clones. + AssignmentTarget::Identifier(_) => false, + _ => true, + }, Expression::Binary(binary) => contains_await(&binary.left) || contains_await(&binary.right), Expression::Logical(logical) => { contains_await(&logical.left) || contains_await(&logical.right) @@ -7337,8 +7373,16 @@ fn contains_await(expression: &Expr) -> bool { } Expression::Assignment(assignment) => { contains_await(&assignment.right) - || matches!(assignment.left.data(), AssignmentTarget::Member(member) - if contains_await(&member.object)) + || match assignment.left.data() { + AssignmentTarget::Member(member) => { + target_contains_await(&member.object, &member.property) + } + // Pattern and invalid targets can carry expressions in + // computed keys and defaults; report containment so the + // machine refuses rather than clones. + AssignmentTarget::Identifier(_) => false, + _ => true, + } } Expression::Sequence(sequence) => sequence.expressions.iter().any(contains_await), Expression::Parenthesized(inner) => contains_await(inner), @@ -7409,10 +7453,11 @@ fn branch_awaits(statement: &Stmt) -> bool { } fn contains_yield_array(array: &ArrayLiteral) -> bool { - array - .elements - .iter() - .any(|element| matches!(element, ArrayElement::Expression(value) if contains_yield(value))) + array.elements.iter().any(|element| match element { + ArrayElement::Expression(value) => contains_yield(value), + ArrayElement::Spread(spread) => contains_yield(&spread.argument), + _ => false, + }) } fn contains_yield(expression: &Expr) -> bool { @@ -7424,11 +7469,7 @@ fn contains_yield(expression: &Expr) -> bool { Expression::Function(_) | Expression::Class(_) | Expression::Arrow(_) => false, Expression::Template(template) => template.expressions.iter().any(contains_yield), Expression::TaggedTemplate(_) => true, - Expression::Array(array) => array.elements.iter().any(|element| match element { - ArrayElement::Expression(nested) => contains_yield(nested), - ArrayElement::Spread(spread) => contains_yield(&spread.argument), - _ => false, - }), + Expression::Array(array) => contains_yield_array(array), Expression::Object(object) => object.members.iter().any(|member| match member.data() { ObjectMember::Property(property) => { contains_yield(&property.value) @@ -7457,8 +7498,15 @@ fn contains_yield(expression: &Expr) -> bool { Expression::Await(awaited) => contains_yield(&awaited.argument), Expression::Unary(unary) => contains_yield(&unary.argument), Expression::Update(update) => match update.argument.data() { - AssignmentTarget::Member(member) => contains_yield(&member.object), - _ => false, + AssignmentTarget::Member(member) => { + contains_yield(&member.object) + || matches!(&member.property, MemberProperty::Computed(key) if contains_yield(key)) + } + // Pattern and invalid targets can carry expressions in + // computed keys and defaults; report containment so the + // machine refuses rather than clones. + AssignmentTarget::Identifier(_) => false, + _ => true, }, Expression::Binary(binary) => contains_yield(&binary.left) || contains_yield(&binary.right), Expression::Logical(logical) => { From 8a6ac1c2f7c27ede64f92ad512ff2a3c30381070 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:10:11 +0900 Subject: [PATCH 02/42] Pin generator walker duality at both clone gates Ten behavioral pins over emit_output. Five are mutation-proven by deleting the matching walker arm and recording the exact miscompile output: the while-test computed-key update, the array spread, the identifier-assignment phantom resume, the for-update increment acceptance, and the labeled-await-break delegation. The remaining five pin the temp-collision, leaked-await refusal, binary temp materialization, switch discriminant inline, and object-spread refusal contracts. The statement gate (machine_emit_expression, contains_yield) and the loop-test gate (machine_emit_while, count_yields) are different gates; pins cover both shapes. --- crates/bamts-compiler/src/emitter.rs | 282 +++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 04eeaa3..3f4a0df 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -6769,4 +6769,286 @@ var c = () => 1; ["@accessorFirst", "@accessorSecond"] ); } + + #[test] + fn es5_async_for_update_increment_lowers() { + // Synthetic probe pinning the walker/machine contract: a for-update + // `i++` (Update over an Identifier) counts as zero resumes after the + // walker fix, so a suspending for-loop still lowers normally with + // the update emitted as plain machine code. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "declare var x: any, y: any, i: any;\nasync function f() {\n for (; x; i++) { await y; }\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + let expected = "function f() {\n return __awaiter(this, void 0, void 0, function () {\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0: _a.label = 1;\n case 1:\n if (!x) return [3 /*break*/, 4];\n return [4 /*yield*/, y];\n case 2:\n _a.sent();\n _a.label = 3;\n case 3:\n i++;\n return [3 /*break*/, 1];\n case 4: return [2 /*return*/];\n }\n });\n });\n}"; + assert!(code.contains(expected), "{code}"); + assert!(!code.contains("function*("), "{code}"); + } + + #[test] + fn es5_generator_update_computed_key_refuses() { + // Synthetic probe pinning the walker/machine contract: a while-test + // computed-key yield (`o[yield k]++` inside the test) counts as a + // resume, so the loop cannot stay inline; lowering routes to eval, + // which refuses with GENERATOR_REQUIRES_ES2015, and the native + // generator survives — never the inline machine clone. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = + "declare var o: any, k: any;\nfunction* g() {\n while (o[yield k]++ < 3) { }\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(!code.contains("__generator(this,"), "{code}"); + assert!(code.contains("function* g("), "{code}"); + assert!( + output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "{:?}", + output.diagnostics + ); + } + + #[test] + fn es5_async_temp_collision_with_user_var() { + // Authority: awaitBinaryExpression5_es5 / asyncFunctionDeclaration9 — + // when a user variable collides with the machine's default temp name, + // the machine state parameter is renamed to _b and the user's _a stays + // the hoisted var. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = + "async function f(x: any) {\n var _a = 1;\n await x;\n return _a;\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("function (_b)"), "{code}"); + assert!(code.contains("_b.label"), "{code}"); + assert!(code.contains("_b.sent()"), "{code}"); + assert!(code.contains("var _a;"), "{code}"); + assert!(code.contains("_a = 1;"), "{code}"); + } + + #[test] + fn es5_async_leaked_await_refuses() { + // Authority: es5-asyncFunction(target=es5) leaked await in left-hand + // of property access assignment cannot be lowered, so the native + // generator fallback is kept. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "async function f(a: any, b: any, c: any) {\n (await a).b = c;\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("function* ()"), "{code}"); + assert!(!code.contains("__generator(this,"), "{code}"); + assert!(!code.contains(".sent()"), "{code}"); + } + + #[test] + fn es5_async_binary_expression_await_materializes() { + // Authority: awaitBinaryExpression5_es5(target=es5) — the awaited + // operand is evaluated first, then combined with the saved left value + // through `.sent()`. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "async function f(a: any, b: any) {\n return a + (await b);\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("function (_b)"), "{code}"); + assert!(code.contains("_b.label"), "{code}"); + assert!(code.contains("_a + (_b.sent())"), "{code}"); + } + + #[test] + fn es5_async_switch_discriminant_await_inline() { + // Authority: es5-asyncFunctionSwitchStatements(target=es5) — a + // discriminant that awaits is emitted as `switch (_a.sent())` inline + // in the resumed case. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "async function f(x: any) {\n switch (await x) {\n case 1: return 10;\n default: return 20;\n }\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("switch (_a.sent()) {"), "{code}"); + assert!(code.contains("case 0: return [4 /*yield*/, x];"), "{code}"); + } + + #[test] + fn es5_async_labeled_loop_with_await_and_break_lowers() { + // Authority: es5-asyncFunctionWhileStatements(target=es5) — a labeled + // while body with an await and a labeled break lowers to the machine; + // the break becomes an explicit branch to the exit label. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "declare var x: any, y: any;\nasync function f() {\n A: while (x) { await y; break A; }\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(!code.contains("function*("), "{code}"); + assert!( + code.contains("__awaiter(this, void 0, void 0, function ("), + "{code}" + ); + assert!(code.contains("if (!x) return [3 /*break*/, 2];"), "{code}"); + assert!(code.contains("_a.sent();"), "{code}"); + assert!(code.contains("return [3 /*break*/, 2];"), "{code}"); + } + + #[test] + fn es5_async_identifier_assignment_test_stays_inline() { + // Authority: es5-asyncFunctionWhileStatements(target=es5) — a while + // test that only assigns to an identifier carries no resume, so the + // whole test clones verbatim into the machine instead of routing the + // loop through eval. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "declare var x: any, y: any;\nasync function f() {\n while (x = 1) { await y; }\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("if (!(x = 1))"), "{code}"); + assert_eq!( + code.matches("return [4 /*yield*/, y];").count(), + 1, + "{code}" + ); + assert!(!code.contains("function*("), "{code}"); + } + #[test] + fn es5_generator_object_spread_yield_refuses() { + // Contract pin, not a mutation claim: a while-test object spread + // carrying a yield (`{ ...(yield x) }`) is refused upstream by the + // machine's object-member wildcard, so the native generator survives + // whether or not count_yields tracks the spread — this pins the + // refusal shape: never the inline machine clone. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "declare var x: any;\nfunction* g() {\n while ({ ...(yield x) }) { }\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("function* g("), "{code}"); + assert!(!code.contains("__generator(this,"), "{code}"); + assert!( + output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "{:?}", + output.diagnostics + ); + } + + #[test] + fn es5_generator_array_spread_yield_never_clones() { + // Mutation-provable: an array-literal spread carrying a yield + // (`[...(yield x)]` as a statement) must never lower to the inline + // machine — contains_yield_array reports the spread's yield, the + // lowering bails, and the native generator survives with + // GENERATOR_REQUIRES_ES2015. Reverting the Spread arm resurrects + // `__generator(this, ...)` wrapping a live yield. + let options = EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }; + let input = "declare var x: any;\nfunction* g() {\n [...(yield x)];\n}\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output(parsed.product(), &options); + let code = &javascript(&output).code; + assert!(code.contains("function* g("), "{code}"); + assert!(!code.contains("__generator(this,"), "{code}"); + assert!( + output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "{:?}", + output.diagnostics + ); + } } From 98ba804e4badfd2e779a95eefea7a532cbaaa5ff Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:10:13 +0900 Subject: [PATCH 03/42] Document the walker-duality miscompile class Learning doc and concept entry for the defect class three earlier commits re-learned one arm at a time: a sentinel wildcard is only safe on shapes the fallback consumer refuses, probes must exercise the claimed trigger shape because the two clone gates key on different walkers, and the corpus regression gate is the suite pair, never the bare CLI project path. --- CONCEPTS.md | 15 ++ ...-generator-suspension-walker-divergence.md | 139 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 CONCEPTS.md create mode 100644 docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md diff --git a/CONCEPTS.md b/CONCEPTS.md new file mode 100644 index 0000000..58a8315 --- /dev/null +++ b/CONCEPTS.md @@ -0,0 +1,15 @@ +# Concepts + +## walker duality + +The ES5 generator lowerer's suspension predicates (`contains_yield`, +`contains_await`, `count_yields`, `contains_yield_array`, +`statements_contain_await`) are views of one predicate that must agree +per shape: containment true with count zero inline-clones a live +suspension (miscompile), and a non-zero count on an eval-accepted shape +inflates resume-label arithmetic. Zero in `count_yields` means "provably +clean"; anything not provably clean routes to `eval`, which either splits +or refuses. + +*Avoid:* "three-view predicate", "count/contains symmetry" — say walker +duality. diff --git a/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md b/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md new file mode 100644 index 0000000..28c8c99 --- /dev/null +++ b/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md @@ -0,0 +1,139 @@ +--- +title: ES5 generator suspension walkers must agree before inline cloning +date: 2026-09-05 +category: logic-errors +module: compiler +problem_type: logic_error +component: es5-generator-lowerer +severity: critical +symptoms: + - "ES5 lowering of generator functions emits `__generator` helper wrapping a live `yield` inside a plain (non-generator) function" + - "Expressions with array-spread, computed-key update, or pattern-assignment targets are cloned verbatim when the inline-clone gate reports zero yields" + - "debug_assert_eq! on segments/exit_label fires (left:2 right:3) for identifier-assignment loop tests; release builds jump to a wrong case" + - "The contains_yield, count_yields, and contains_await views disagree per shape" +root_cause: logic_error +resolution_type: code_fix +related_components: [emitter, transforms] +tags: [es5, generator, walker, duality, miscompile, inline-clone, sentinel] +applies_when: + - "Editing contains_yield, count_yields, contains_await, contains_yield_array, or statements_contain_await in crates/bamts-compiler/src/emitter/transforms.rs" + - "Adding an expression or member shape the machine may clone inline in loop/branch tests" + - "Changing eval acceptance (eval-refused vs eval-accepted shapes)" +--- + +# ES5 generator suspension walkers must agree before inline cloning + +## Problem + +The ES5 downleveler keeps three overlapping views of "does this expression +suspend?": `contains_yield`/`contains_await` (boolean; gate statement-level +cloning in `machine_emit_expression` and `eval`), `count_yields` (u32; gates +the loop/branch TEST inline clone at `machine_emit_while`/`do`/`for` and +drives `exit_label = head + test_resumes + body_resumes + 1`), and +`contains_yield_array` (boolean; guards `eval_array`). They were not kept in +per-shape sync. Any shape where the inline gate's view under-reports what +`eval` would refuse gets cloned verbatim with a live `yield` inside a plain +function — a silent miscompile. Any shape where it OVER-reports on an +eval-accepted target inflates label arithmetic and mints a wrong case. + +## Symptoms + +Three concrete instances of the one class, all present at HEAD 43bdcb6 and +caught by adversarial review: + +1. `count_yields` had no `Expression::Update` arm. `while (o[yield k]++ < 3)` + counted 0, cloned verbatim, and emitted: + ```js + function g() { return __generator(this, function (_a) { + while (o[yield k]++ < 3) { } + return [2 /*return*/]; + }); } + ``` + (plain `function g`, live `yield` — the never-miscompile contract broken). +2. `contains_yield_array` ignored `ArrayElement::Spread`. `[...(yield x)]` + reported clean and cloned at the statement gate; same wrapper form. +3. A pattern-target sentinel in `count_yields`' Assignment arm swallowed + `AssignmentTarget::Identifier`: `while (x = 1) { await y; }` counted a + phantom resume, inflating `exit_label` — debug builds fail + `debug_assert_eq!(ctx.segments.len() as u32 - 1, exit_label)` with + `left: 2 right: 3`; release builds jump to a wrong case. + +## What Didn't Work + +- Statement-level probes missed instance 1 twice: the statement gate + (`machine_emit_expression`, keyed on `contains_yield` at ~4265) and the + loop-test gate (`machine_emit_while`, keyed on `count_yields` at ~4435) + are DIFFERENT gates. Probe the claimed trigger shape, not an adjacent one. +- Fixing only the boolean walkers left label arithmetic wrong. +- The original `count_yields` catch-all `_ => 0` declared every unlisted + shape "provably clean" — the polarity itself was the defect class. +- Three earlier commits (c3ddb6d, 01e5c5a, 34cef0e) re-learned this class + one arm at a time. + +## Solution + +Two rules, applied to `transforms.rs` (~7016-7130, ~7450-7540): + +1. **Precise arms for eval-ACCEPTED shapes** — count exactly what `eval` + splits: `AssignmentTarget::Identifier(_) => 0`, Member targets + (object + computed key), `Expression::Update` (member object + computed + key; Identifier 0), `Expression::Import` (source + options), and + `ObjectMember::Spread` / `ArrayElement::Spread` arguments. Method-like + object members stay 0 (nested function-likes own their suspensions, + consistent with `Expression::Function => 0`). +2. **Non-zero sentinel ONLY for eval-REFUSED shapes** — the outer catch-all + is `_ => 1` (zero now means provably clean; unknown routes to `eval`, + whose refusal keeps the native generator + `GENERATOR_REQUIRES_ES2015`). + Verified: eval has no accepting arms for TaggedTemplate, Satisfies, + TypeAssertion, or the JSX variants. A sentinel on an eval-accepted shape + is instance 3 again. + +```rust +// count_yields, the two load-bearing arm shapes: +Expression::Update(update) => match update.argument.data() { + AssignmentTarget::Member(member) => { + count_yields(&member.object) + + match &member.property { + MemberProperty::Computed(key) => count_yields(key), + _ => 0, + } + } + AssignmentTarget::Identifier(_) => 0, + _ => 1, // pattern/invalid targets: eval refuses them +}, +// ... + AssignmentTarget::Identifier(_) => 0, + _ => 1, +// outer catch-all: +_ => 1, // unknown: never the inline clone; eval refuses +``` + +## Why This Works + +The inline-clone fast-path (`if test_resumes == 0 && body_resumes == 0 { +ctx.push(statement.clone()); }`) is only safe when zero is a PROOF of +cleanliness. Making every non-provably-clean shape non-zero routes it to +`eval`, which either splits it correctly or refuses (native fallback + +diagnostic) — both contract-compliant. Exact counting on eval-accepted +shapes keeps `exit_label` arithmetic equal to the number of segments eval +actually mints. + +## Prevention + +- Audit `contains_yield` / `count_yields` / `contains_await` / + `statements_contain_await` as ONE SET on any walker edit; the + enumerate-all-arms diff across the three views is the systematic close. +- A sentinel wildcard is only safe on shapes the fallback consumer + (eval) REFUSES; enumerate eval-accepted shapes as explicit arms first. +- Pin the CONTRACT, not just refusal shapes: a pin per gate (statement + gate AND loop-test gate), each mutation-proven by deleting the arm and + recording the exact miscompile output. Existing pins: + `es5_generator_update_computed_key_refuses`, + `es5_generator_array_spread_yield_never_clones`, + `es5_generator_object_spread_yield_refuses`, + `es5_async_identifier_assignment_test_stays_inline`, + `es5_async_for_update_increment_lowers` (emitter.rs cfg(test)). +- The corpus-level regression gate is the suite pair + (`cargo test -p bamts-compiler` + `cargo test -p bamts-verification` with + `BAMTS_ALLOW_NODE_COMPAT=1`), never the bare CLI `-p` path (it never + lowers the machine — refusal form only). From 9e76e881441025dacb8fb7f704a58ec2a85e56a1 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:52:06 +0900 Subject: [PATCH 04/42] Harden walker duality with violation tests and return guards Adversarial pass over the shipped walker mechanism. Assumptions documented and violated on purpose: duality as a global invariant (WD-1, generic live-yield and raw-return leak detectors over a battery of suspending shapes at every clone gate), sentinel shapes route to refusal (WD-2), count equals segments eval mints (WD-3, case-label contiguity at 0/1/2/3-suspension boundaries), nested function-likes own suspensions (WD-4), non-block bodies refuse (WD-5), import and conditional counts are exact (WD-6/7), labeled delegation (WD-8), deep nesting survives (WD-9). Two silent failures the tests exposed, both fixed: - count_branch_yields under-counted nested statements: a suspending if inside a loop body (labeled or not) cloned verbatim into the machine with a live yield in a plain function. Now counts recurse precisely through if/loops/labeled/block/throw/return; clean nested control flow clones correctly, suspending nested shapes route to the body emitter's refusal. - a clean `return y` in a loop body or if branch cloned verbatim; the raw return exits the machine's inner function and the __generator runtime silently drops the value. Clone gates now refuse regions containing returns (statements_contain_return, nesting-aware, skipping nested function-likes). Switch case bodies keep lowering returns through the machine protocol. Also: async refusal now emits GENERATOR_REQUIRES_ES2015 (it fired only on the generator path; the async path declined silently); method-like object members no longer over-report containment in contains_yield/contains_await. Sanitizer posture: Miri component not installed; crate forbids unsafe so the UB class is absent (gap recorded). Gate ran as the debug suite: debug assertions plus overflow checks, which is what catches label-arithmetic drift (left: 2 right: 3). Mutation proofs: reverting the if-recursion arm, the return arm, and the while-gate guard each fail their pin or the battery; restores content-verified. --- crates/bamts-compiler/src/emitter.rs | 372 ++++++++++++++++++ .../bamts-compiler/src/emitter/transforms.rs | 128 +++++- 2 files changed, 488 insertions(+), 12 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 3f4a0df..12ed176 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7051,4 +7051,376 @@ var c = () => 1; output.diagnostics ); } + + // ------------------------------------------------------------------ + // Adversarial hardening: the walker-duality contract as an invariant, + // not a shape list. Assumptions under test (IDs cited per test): + // WD-1 duality: no emitted machine embeds a live yield outside a + // `return [4 /*yield*/, ...]` split (generic leak detector). + // WD-2 sentinel shapes (TaggedTemplate/Satisfies/TypeAssertion/JSX) + // are eval-refused, never inline-cloned. + // WD-3 count == segments eval mints: machine case labels are + // contiguous 0..=max and unique (gap detector; boundaries + // 0/1/2/3 suspensions). + // WD-4 nested function-likes own their suspensions: cloning them + // is correct, their internal yield is legal. + // WD-5 non-block loop bodies refuse, never miscompile. + // WD-6/7 Import (source+options) and Conditional/Logical counts + // feed exact label arithmetic. + // WD-8 labeled-loop delegation covers nested awaited bodies. + // WD-9 recursive walkers survive deep nesting without crashing. + // ------------------------------------------------------------------ + + /// Emits `input` at es5 without helpers; panics on parse diagnostics. + fn emit_es5_clean(input: &str) -> EmitOutput { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), + )); + assert!(parsed.diagnostics().is_empty()); + emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ) + } + + /// WD-1: in machine form, every `yield` must sit inside a split marker + /// `/*yield*/`; a bare yield is a live suspension cloned into a plain + /// function (the miscompile class). Returns the offending snippet text. + fn live_yield_leak(code: &str) -> Option { + if !code.contains("__generator(this,") { + return None; // native refusal form: the generator owns its yields + } + code.lines() + .find(|line| { + line.contains("yield") && !line.contains("/*yield*/") && line.contains("yield ") + }) + .map(|line| line.trim().to_owned()) + } + + /// WD-1b: in machine form, every return must be a protocol return + /// (`return [`) — a bare `return expr;` in the machine body exits the + /// inner function directly and the runtime silently drops the value. + /// Battery-scoped: snippets are flat (no nested function bodies, whose + /// returns are legal); the nested-ownership contract has its own pin. + fn raw_return_leak(code: &str) -> Option { + if !code.contains("__generator(this,") { + return None; + } + code.lines() + .find(|line| { + let l = line.trim_start(); + l.starts_with("return ") + && !l.starts_with("return [") + && !l.starts_with("return __generator") + && !l.starts_with("return __awaiter") + }) + .map(|line| line.trim().to_owned()) + } + + /// WD-3: the machine switch's case labels must be unique and contiguous + /// from 0 (a gap or duplicate means count_yields disagreed with the + /// segments eval actually minted). + fn machine_case_labels(code: &str) -> Vec { + let start = code.find("switch (_a.label)").expect("machine switch"); + let body = &code[start..]; + let mut labels: Vec = body + .split("case ") + .skip(1) + .filter_map(|rest| rest.split(':').next()) + .filter_map(|n| n.parse().ok()) + .collect(); + labels.sort_unstable(); + labels.dedup(); + labels + } + + #[test] + fn es5_async_return_in_loop_body_never_drops_value() { + // A cloned `return y` inside the machine body returns straight out + // of the inner function; the runtime treats the non-array as done + // with `void 0` — the value is silently dropped. The loop body + // cannot lower a return, so the machine must refuse (until a + // return-protocol slice lands), never clone. + let output = emit_es5_clean( + "declare var x: any, y: any;\nasync function f() { while (x) { return y; } }\n", + ); + let code = &javascript(&output).code; + assert!( + !code.contains("__generator(this,"), + "machine must not embed a raw return: {code}" + ); + assert!( + output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "refusal must signal: {:?}", + output.diagnostics + ); + } + + #[test] + fn walker_duality_battery_never_clones_live_yield() { + // WD-1: each snippet puts a suspending shape behind each clone gate. + let snippets = [ + ( + "update-computed-while", + "declare var o: any, k: any;\nfunction* g() { while (o[k ? k : 0][`x`], o[yield k]++ < 3) { } }\n", + ), + ( + "array-spread-statement", + "declare var x: any;\nfunction* g() { [...(yield x)]; }\n", + ), + ( + "array-spread-while-body", + "declare var x: any, y: any;\nfunction* g() { while (y) { [...(yield x)]; } }\n", + ), + ( + "object-spread-do-test", + "declare var x: any;\nfunction* g() { do { } while ({ ...(yield x) }); }\n", + ), + ( + "assignment-pattern-while", + "declare var x: any, y: any;\nfunction* g() { while ({ a: x } = y) { yield x; } }\n", + ), + ( + "template-expr-statement", + "declare var x: any;\nfunction* g() { `${yield x}`; }\n", + ), + ( + "conditional-in-machine", + "declare var x: any, y: any;\nfunction* g() { return (yield x) ? y : (yield x); }\n", + ), + ( + "logical-in-machine", + "declare var x: any, y: any;\nfunction* g() { return (yield x) || (yield y); }\n", + ), + ( + "import-expr-machine", + "declare var x: any;\nfunction* g() { return import(yield x); }\n", + ), + ( + "new-suspending-args", + "declare var x: any;\nfunction* g() { return new C(yield x); }\n", + ), + ( + "nested-if-await-while", + "declare var x: any, y: any, z: any;\nasync function f() { while (x) { if (y) { await z; } } }\n", + ), + ( + "nested-if-await-labeled", + "declare var x: any, y: any, z: any;\nasync function f() { A: while (x) { if (y) { await z; break A; } } }\n", + ), + ( + "nested-if-await-do", + "declare var x: any, y: any, z: any;\nasync function f() { do { if (y) { await z; } } while (x); }\n", + ), + ( + "return-in-while-body", + "declare var x: any, y: any;\nasync function f() { while (x) { return y; } }\n", + ), + ( + "clean-nested-while-clones", + "declare var x: any, z: any;\nasync function f() { while (x) { while (z) { } } }\n", + ), + ( + "return-in-if-branch", + "declare var y: any, z: any;\nasync function f() { if (y) { return z; } }\n", + ), + ( + "return-in-labeled-body", + "declare var x: any, y: any;\nasync function f() { A: while (x) { return y; } }\n", + ), + ( + "nested-if-await-if-arm", + "declare var x: any, y: any, z: any;\nasync function f() { if (x) { if (y) { await z; } } }\n", + ), + ]; + for (name, input) in snippets { + let output = emit_es5_clean(input); + let code = &javascript(&output).code; + if let Some(leak) = live_yield_leak(code) { + panic!("[{name}] live yield cloned into machine: {leak}"); + } + if let Some(leak) = raw_return_leak(code) { + panic!("[{name}] raw return in machine body drops value: {leak}"); + } + } + } + + #[test] + fn sentinel_shapes_route_to_refusal_not_clone() { + // WD-2: eval has no accepting arms for these shapes; the sentinel + // count must send them there, never through the inline clone gate. + for (name, input) in [ + ( + "tagged-template", + "declare var tag: any, k: any;\nfunction* g() { while (tag`x ${yield k}`) { } }\n", + ), + ( + "type-assertion", + "declare var k: any;\nfunction* g() { while ((yield k)) { } }\n", + ), + ] { + let output = emit_es5_clean(input); + let code = &javascript(&output).code; + assert!( + code.contains("function* g(") || live_yield_leak(code).is_none(), + "[{name}] must refuse (native form) or lower safely, got:\n{code}" + ); + } + } + + #[test] + fn machine_case_labels_are_contiguous_and_unique() { + // WD-3 + WD-6 + WD-7, boundaries 0/1/2/3 suspensions on both sides + // of every count. + for (name, input) in [ + ( + "zero", + "declare var x: any;\nasync function f() { while (x) { } }\n", + ), + ( + "one", + "declare var x: any, y: any;\nasync function f() { while (x) { await y; } }\n", + ), + ( + "two", + "declare var x: any, y: any, z: any;\nasync function f() { while (x) { await y; await z; } }\n", + ), + ( + "three-test-split", + "declare var o: any, k: any, y: any;\nasync function f() { while (o[await k]++ < 3) { await y; } }\n", + ), + ( + "import-two-suspensions", + "declare var a: any, b: any;\nasync function f() { return import(await a, { m: await b }); }\n", + ), + ( + "conditional-three", + "declare var a: any, b: any, c: any;\nasync function f() { return (await a) ? await b : await c; }\n", + ), + ( + "jump-guard-exact-count", + "declare var x: any, y: any, z: any;\nasync function f() { while (x) { if (y) continue; await z; } }\n", + ), + ] { + let output = emit_es5_clean(input); + let code = &javascript(&output).code; + if !code.contains("switch (_a.label)") { + continue; // refused: fine, label arithmetic never ran + } + let labels = machine_case_labels(code); + assert!(!labels.is_empty(), "[{name}] machine without labels"); + let expected: Vec = (0..=*labels.last().expect("non-empty")).collect(); + assert_eq!( + labels, expected, + "[{name}] case labels must be contiguous: {code}" + ); + } + } + + #[test] + fn nested_function_likes_own_suspensions_when_cloned() { + // WD-4: a method member owns its yields, so the object clones as + // clean into the machine and the INTERNAL yield stays legal. + let output = emit_es5_clean( + "declare var x: any;\nasync function f() {\n while ({ m() { return 1; } }) { await x; }\n}\n", + ); + let code = &javascript(&output).code; + assert!(code.contains("__generator(this,"), "lowers: {code}"); + assert!(code.contains("m() {"), "method cloned intact: {code}"); + assert!(!code.contains("function*("), "{code}"); + } + + #[test] + fn non_block_loop_bodies_refuse() { + // WD-5: single-statement bodies are outside the accepted set; the + // only valid outcomes are refusal with a diagnostic. + for (name, input) in [ + ( + "while", + "declare var x: any, y: any;\nasync function f() { while (x) await y; }\n", + ), + ( + "for", + "declare var x: any, y: any;\nasync function f() { for (; x;) await y; }\n", + ), + ( + "do", + "declare var x: any, y: any;\nasync function f() { do await y; while (x); }\n", + ), + ] { + let output = emit_es5_clean(input); + let code = &javascript(&output).code; + assert!( + code.contains("function*(") || live_yield_leak(code).is_none(), + "[{name}] non-block body must refuse, never clone: {code}" + ); + } + } + + #[test] + fn labeled_nested_await_delegation_lowers() { + // WD-8: branch_awaits delegates through nested statement shapes. + let output = emit_es5_clean( + "declare var x: any, y: any, z: any;\nasync function f() {\n A: while (x) { if (y) { await z; break A; } }\n}\n", + ); + let code = &javascript(&output).code; + // Safety contract: refuse-or-lower, never a live await cloned + // outside the generator that owns it. (Full lowering of nested + // control flow inside labeled loop bodies is a banked machine + // slice; until then the refusal form is the correct output.) + assert!( + code.contains("function*") || code.contains("__generator(this,"), + "must refuse or lower: {code}" + ); + assert!(live_yield_leak(code).is_none(), "{code}"); + assert!( + output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "refusal must signal: {:?}", + output.diagnostics + ); + } + + #[test] + fn deep_nesting_walkers_do_not_crash() { + // WD-9: recursive walkers on a pathological expression must + // produce a descriptive outcome (machine, refusal, or diagnostics), + // never a crash. + let depth = 2_000; + let input = format!( + "declare var y: any;\nasync function f() {{ {}await y; }}\n", + "-".repeat(depth) + ); + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input.as_str()).expect("fits budget")), + )); + if parsed.diagnostics().is_empty() { + let output = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let code = &javascript(&output).code; + assert!( + code.contains("__generator(this,") || code.contains("function*("), + "some emitted form: {code}" + ); + } + } } diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index bea443a..381f520 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -3960,7 +3960,18 @@ impl<'a> Rewriter<'a> { body: Some(inner_body), }; let inner = if Self::needs(LanguageFeature::Generators, self.options) { - self.lower_generator_function(&inner) + let lowered = self.lower_generator_function(&inner); + if lowered.is_generator { + // The machine declined; the inner stays a native generator + // inside __awaiter, which the target cannot run. Signal it + // instead of emitting ES2015+ syntax silently. + self.diag( + codes::GENERATOR_REQUIRES_ES2015, + range, + "generators require ScriptTarget::Es2015 or later", + ); + } + lowered } else { inner }; @@ -4304,7 +4315,11 @@ impl<'a> Rewriter<'a> { let cons_resumes = count_branch_yields(cons_block); let alt_resumes = count_branch_yields(alt_block); - if cons_resumes == 0 && alt_resumes == 0 { + if cons_resumes == 0 + && alt_resumes == 0 + && !statements_contain_return(cons_block) + && !statements_contain_return(alt_block) + { // Inline reassembly: only the test can suspend. if !contains_yield(&if_statement.test) { ctx.push(statement.clone()); @@ -4370,7 +4385,9 @@ impl<'a> Rewriter<'a> { match body.data() { Statement::While(while_statement) => { let body_block = block_statements(&while_statement.body)?; - if count_yields(&while_statement.test) == 0 && count_branch_yields(body_block) == 0 + if count_yields(&while_statement.test) == 0 + && count_branch_yields(body_block) == 0 + && !statements_contain_return(body_block) { // Clean loops stay inline, label attached. ctx.push(statement.clone()); @@ -4380,7 +4397,10 @@ impl<'a> Rewriter<'a> { } Statement::DoWhile(do_statement) => { let body_block = block_statements(&do_statement.body)?; - if count_yields(&do_statement.test) == 0 && count_branch_yields(body_block) == 0 { + if count_yields(&do_statement.test) == 0 + && count_branch_yields(body_block) == 0 + && !statements_contain_return(body_block) + { ctx.push(statement.clone()); return Some(()); } @@ -4388,7 +4408,10 @@ impl<'a> Rewriter<'a> { } Statement::For(for_statement) => { let body_block = block_statements(&for_statement.body)?; - if for_clauses_are_clean(for_statement) && count_branch_yields(body_block) == 0 { + if for_clauses_are_clean(for_statement) + && count_branch_yields(body_block) == 0 + && !statements_contain_return(body_block) + { let rebuilt = self.inline_for_with_hoist(statement.range(), for_statement, ctx)?; let label = match statement.data() { @@ -4434,7 +4457,7 @@ impl<'a> Rewriter<'a> { let body_block = block_statements(&while_statement.body)?; let test_resumes = count_yields(&while_statement.test); let body_resumes = count_branch_yields(body_block); - if test_resumes == 0 && body_resumes == 0 { + if test_resumes == 0 && body_resumes == 0 && !statements_contain_return(body_block) { ctx.push(statement.clone()); return Some(()); } @@ -4495,7 +4518,7 @@ impl<'a> Rewriter<'a> { let body_block = block_statements(&do_statement.body)?; let test_resumes = count_yields(&do_statement.test); let body_resumes = count_branch_yields(body_block); - if test_resumes == 0 && body_resumes == 0 { + if test_resumes == 0 && body_resumes == 0 && !statements_contain_return(body_block) { ctx.push(statement.clone()); return Some(()); } @@ -4607,7 +4630,10 @@ impl<'a> Rewriter<'a> { ) -> Option<()> { let range = statement.range(); let body_block = block_statements(&for_statement.body)?; - if for_clauses_are_clean(for_statement) && count_branch_yields(body_block) == 0 { + if for_clauses_are_clean(for_statement) + && count_branch_yields(body_block) == 0 + && !statements_contain_return(body_block) + { let rebuilt = self.inline_for_with_hoist(range, for_statement, ctx)?; ctx.push(rebuilt); return Some(()); @@ -6981,7 +7007,10 @@ fn branch_is_simple(statements: &[Stmt]) -> bool { }) } -/// The number of yield splits a branch's statements produce. +/// The number of yield splits a branch's statements produce. Zero gates +/// the inline verbatim clone, so a statement shape the loop-body emitter +/// cannot lower must report non-zero (it refuses, keeping the native +/// form) — never zero. fn count_branch_yields(statements: &[Stmt]) -> u32 { statements .iter() @@ -6998,11 +7027,80 @@ fn count_branch_yields(statements: &[Stmt]) -> u32 { .map_or(0, count_yields) }) .sum(), - _ => 0, + Statement::Continue(_) | Statement::Break(_) | Statement::Empty => 0, + Statement::Return(ret) => ret.argument.as_deref().map_or(0, count_yields), + Statement::Throw(throw) => count_yields(&throw.argument), + Statement::Block(block) => count_branch_yields(&block.data().statements), + // Clean nested control flow clones correctly inside a machine + // body, so it counts precisely; suspending nested shapes route + // to the body emitter, which refuses what it cannot lower. + Statement::If(if_statement) => { + count_yields(&if_statement.test) + + count_branch_yields(branch_statements(&if_statement.consequent)) + + if_statement + .alternate + .as_ref() + .map_or(0, |alt| count_branch_yields(branch_statements(alt))) + } + Statement::While(while_statement) => { + count_yields(&while_statement.test) + + count_branch_yields(loop_body_statements(&while_statement.body)) + } + Statement::DoWhile(do_statement) => { + count_yields(&do_statement.test) + + count_branch_yields(loop_body_statements(&do_statement.body)) + } + Statement::Labeled(labeled) => count_branch_yields(branch_statements(&labeled.body)), + _ => 1, }) .sum() } +/// The statements of an if/else branch: a block flattens, any other +/// statement is a single-statement branch. +fn branch_statements(statement: &Stmt) -> &[Stmt] { + match statement.data() { + Statement::Block(block) => &block.data().statements, + _ => std::slice::from_ref(statement), + } +} + +/// The statements of a loop body: non-block bodies are shapes this slice +/// refuses anyway; None renders as empty for counting purposes. +fn loop_body_statements(body: &Stmt) -> &[Stmt] { + static NONE: &[Stmt] = &[]; + match body.data() { + Statement::Block(block) => &block.data().statements, + _ => NONE, + } +} + +/// Whether any statement in this region returns — at any nesting depth, +/// but never inside a nested function-like (those own their returns). +/// A cloned raw return exits the machine's inner function directly and +/// the runtime silently drops the value, so clone gates must refuse. +fn statements_contain_return(statements: &[Stmt]) -> bool { + statements.iter().any(|statement| match statement.data() { + Statement::Return(_) => true, + Statement::Block(block) => statements_contain_return(&block.data().statements), + Statement::If(if_statement) => { + statements_contain_return(branch_statements(&if_statement.consequent)) + || if_statement + .alternate + .as_ref() + .is_some_and(|alt| statements_contain_return(branch_statements(alt))) + } + Statement::While(while_statement) => { + statements_contain_return(loop_body_statements(&while_statement.body)) + } + Statement::DoWhile(do_statement) => { + statements_contain_return(loop_body_statements(&do_statement.body)) + } + Statement::Labeled(labeled) => statements_contain_return(branch_statements(&labeled.body)), + _ => false, + }) +} + impl ChainSegment { const fn optional(&self) -> bool { match self { @@ -7334,7 +7432,10 @@ fn contains_await(expression: &Expr) -> bool { contains_await(&property.value) || matches!(&property.name, PropertyName::Computed(key) if contains_await(key)) } - _ => true, + ObjectMember::Spread(spread) => contains_await(&spread.argument), + // Method-like members own their suspensions; cloning them is + // correct, not a leak. + _ => false, }), Expression::Call(call) => { contains_await(&call.callee) @@ -7475,7 +7576,10 @@ fn contains_yield(expression: &Expr) -> bool { contains_yield(&property.value) || matches!(&property.name, PropertyName::Computed(key) if contains_yield(key)) } - _ => true, + ObjectMember::Spread(spread) => contains_yield(&spread.argument), + // Method-like members own their suspensions, like nested + // function-likes; cloning them is correct, not a leak. + _ => false, }), Expression::Call(call) => { contains_yield(&call.callee) From 9fa4d2978b606ed426f7bf6cc28375bce96986c1 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:01:39 +0900 Subject: [PATCH 05/42] Close remaining walker asymmetries found in PR review Five clone-gate and order holes of the session's defect class, found by the PR review bots and verified against the bytes: - Call and New argument spreads: f(...(yield x)) cloned a live yield; all argument walkers now count and contain through spread arguments. - Computed method names: { [yield k]() {} } executes its key in the enclosing function; the member walkers inspect computed PropertyNames while bodies keep owning their suspensions. - Clean dynamic imports: contains_await had no Import arm and refused lowerable functions; it now checks source and options. - Non-block loop bodies hid `while (x) return z;` from the counters and the return guard; single-statement bodies are now visible branches. - Dynamic-import operand order: a clean side-effecting source with suspending options was reassembled inside the resume segment, running its effects after the suspension. The source now materializes into a temp in the pre-suspension segment (pinned: _a = f(); precedes the yield; import(_a, ...) resumes). Test hardening from the same review: token-based leak detectors (yield*, yield;, return;), any-state-parameter label lookup with duplicates preserved for the contiguity assert, a derived lower-or-refuse expectation per battery row (21 rows, so all-refusing output fails), the deep-nesting probe rewritten as a two-sided boundary test after discovering its fixture was never valid JavaScript, satisfies/JSX sentinel rows (JSX via TypeScriptReact parsing), the dead derive probe deleted, and the learning doc ties closure language to the receipt-backed G3 root gate. Gates: compiler 1865/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 251 ++++++++++++++---- .../bamts-compiler/src/emitter/transforms.rs | 51 +++- ...-generator-suspension-walker-divergence.md | 6 +- 3 files changed, 250 insertions(+), 58 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 12ed176..ae3543c 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -6792,7 +6792,7 @@ var c = () => 1; let code = &javascript(&output).code; let expected = "function f() {\n return __awaiter(this, void 0, void 0, function () {\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0: _a.label = 1;\n case 1:\n if (!x) return [3 /*break*/, 4];\n return [4 /*yield*/, y];\n case 2:\n _a.sent();\n _a.label = 3;\n case 3:\n i++;\n return [3 /*break*/, 1];\n case 4: return [2 /*return*/];\n }\n });\n });\n}"; assert!(code.contains(expected), "{code}"); - assert!(!code.contains("function*("), "{code}"); + assert!(!code.contains("function*"), "{code}"); } #[test] @@ -6947,7 +6947,7 @@ var c = () => 1; assert!(parsed.diagnostics().is_empty()); let output = emit_output(parsed.product(), &options); let code = &javascript(&output).code; - assert!(!code.contains("function*("), "{code}"); + assert!(!code.contains("function*"), "{code}"); assert!( code.contains("__awaiter(this, void 0, void 0, function ("), "{code}" @@ -6983,7 +6983,7 @@ var c = () => 1; 1, "{code}" ); - assert!(!code.contains("function*("), "{code}"); + assert!(!code.contains("function*"), "{code}"); } #[test] fn es5_generator_object_spread_yield_refuses() { @@ -7097,9 +7097,10 @@ var c = () => 1; return None; // native refusal form: the generator owns its yields } code.lines() - .find(|line| { - line.contains("yield") && !line.contains("/*yield*/") && line.contains("yield ") - }) + // Strip the protocol marker spans first: a line can carry both a + // legitimate `return [4 /*yield*/, x]` split and a cloned live + // yield; the marker must not exempt the whole line. + .find(|line| line.replace("/*yield*/", "").contains("yield")) .map(|line| line.trim().to_owned()) } @@ -7114,11 +7115,12 @@ var c = () => 1; } code.lines() .find(|line| { - let l = line.trim_start(); - l.starts_with("return ") + let l = line.trim(); + l.starts_with("return") && !l.starts_with("return [") && !l.starts_with("return __generator") && !l.starts_with("return __awaiter") + && l.ends_with(';') }) .map(|line| line.trim().to_owned()) } @@ -7127,7 +7129,14 @@ var c = () => 1; /// from 0 (a gap or duplicate means count_yields disagreed with the /// segments eval actually minted). fn machine_case_labels(code: &str) -> Vec { - let start = code.find("switch (_a.label)").expect("machine switch"); + // Any state parameter (_a, or _b after a temp collision); raw + // labels with no dedup — the caller's equality assert fails on a + // duplicate, which is the failure this helper guards. + let start = code + .match_indices("switch (_") + .map(|(i, _)| i) + .find(|&i| code[i..].contains(".label)")) + .expect("machine switch"); let body = &code[start..]; let mut labels: Vec = body .split("case ") @@ -7136,7 +7145,6 @@ var c = () => 1; .filter_map(|n| n.parse().ok()) .collect(); labels.sort_unstable(); - labels.dedup(); labels } @@ -7167,90 +7175,146 @@ var c = () => 1; #[test] fn walker_duality_battery_never_clones_live_yield() { - // WD-1: each snippet puts a suspending shape behind each clone gate. - let snippets = [ + // WD-1: each snippet puts a suspending shape behind each clone gate, + // with its derived expectation: `true` lowers to the machine (and + // must not leak), `false` refuses (native form + the requires-es2015 + // diagnostic + no machine). Expectations were derived from emitted + // output; all-refusing output fails every `true` row. + let snippets: [(&str, &str, bool); 22] = [ ( "update-computed-while", "declare var o: any, k: any;\nfunction* g() { while (o[k ? k : 0][`x`], o[yield k]++ < 3) { } }\n", + false, ), ( "array-spread-statement", "declare var x: any;\nfunction* g() { [...(yield x)]; }\n", + false, ), ( "array-spread-while-body", "declare var x: any, y: any;\nfunction* g() { while (y) { [...(yield x)]; } }\n", + false, ), ( "object-spread-do-test", "declare var x: any;\nfunction* g() { do { } while ({ ...(yield x) }); }\n", + false, ), ( "assignment-pattern-while", "declare var x: any, y: any;\nfunction* g() { while ({ a: x } = y) { yield x; } }\n", + false, ), ( "template-expr-statement", "declare var x: any;\nfunction* g() { `${yield x}`; }\n", + true, ), ( "conditional-in-machine", "declare var x: any, y: any;\nfunction* g() { return (yield x) ? y : (yield x); }\n", + false, ), ( "logical-in-machine", "declare var x: any, y: any;\nfunction* g() { return (yield x) || (yield y); }\n", + false, ), ( "import-expr-machine", "declare var x: any;\nfunction* g() { return import(yield x); }\n", + true, ), ( "new-suspending-args", "declare var x: any;\nfunction* g() { return new C(yield x); }\n", + false, ), ( "nested-if-await-while", "declare var x: any, y: any, z: any;\nasync function f() { while (x) { if (y) { await z; } } }\n", + false, ), ( "nested-if-await-labeled", "declare var x: any, y: any, z: any;\nasync function f() { A: while (x) { if (y) { await z; break A; } } }\n", + false, ), ( "nested-if-await-do", "declare var x: any, y: any, z: any;\nasync function f() { do { if (y) { await z; } } while (x); }\n", + false, + ), + ( + "nested-if-await-if-arm", + "declare var x: any, y: any, z: any;\nasync function f() { if (x) { if (y) { await z; } } }\n", + false, ), ( "return-in-while-body", "declare var x: any, y: any;\nasync function f() { while (x) { return y; } }\n", + false, ), ( - "clean-nested-while-clones", - "declare var x: any, z: any;\nasync function f() { while (x) { while (z) { } } }\n", + "return-in-labeled-body", + "declare var x: any, y: any;\nasync function f() { A: while (x) { return y; } }\n", + false, ), ( "return-in-if-branch", "declare var y: any, z: any;\nasync function f() { if (y) { return z; } }\n", + false, ), ( - "return-in-labeled-body", - "declare var x: any, y: any;\nasync function f() { A: while (x) { return y; } }\n", + "clean-nested-while-clones", + "declare var x: any, z: any;\nasync function f() { while (x) { while (z) { } } }\n", + true, ), ( - "nested-if-await-if-arm", - "declare var x: any, y: any, z: any;\nasync function f() { if (x) { if (y) { await z; } } }\n", + "call-spread-arg", + "declare var f: any, x: any;\nfunction* g() { f(...(yield x)); }\n", + false, + ), + ( + "method-computed-name", + "declare var k: any;\nfunction* g() { ({ [yield k]() {} }); }\n", + false, + ), + ( + "nonblock-nested-return", + "declare var x: any, z: any;\nasync function f() { while (x) { while (z) return z; } }\n", + false, + ), + ( + "as-cast-lowers", + "declare var k: any;\nfunction* g() { while ((yield k) as any) { } }\n", + true, ), ]; - for (name, input) in snippets { + for (name, input, lowers) in snippets { let output = emit_es5_clean(input); let code = &javascript(&output).code; + let machine = code.contains("__generator(this,"); + let native = code.contains("function*"); + let diag = output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015); + assert_eq!( + machine, lowers, + "[{name}] expected lower={lowers}: machine={machine} native={native}\n{code}" + ); if let Some(leak) = live_yield_leak(code) { panic!("[{name}] live yield cloned into machine: {leak}"); } if let Some(leak) = raw_return_leak(code) { panic!("[{name}] raw return in machine body drops value: {leak}"); } + if !lowers { + assert!(native, "[{name}] refusal must keep the native form\n{code}"); + assert!(diag, "[{name}] refusal must signal TS-EMIT-1102\n{code}"); + } } } @@ -7275,6 +7339,28 @@ var c = () => 1; "[{name}] must refuse (native form) or lower safely, got:\n{code}" ); } + // JSX expressions need TypeScriptReact parsing; the same sentinel + // contract applies: refuse or lower, never a cloned live yield. + let jsx_input = "declare var k: any;\nfunction* g() { while (
{yield k}
) { } }\n"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScriptReact, + Arc::new(SourceText::new(jsx_input).expect("fits budget")), + )); + assert!(parsed.diagnostics().is_empty()); + let output = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let code = &javascript(&output).code; + assert!( + code.contains("function* g(") || live_yield_leak(code).is_none(), + "jsx row must refuse or lower safely, got:\n{code}" + ); } #[test] @@ -7330,13 +7416,30 @@ var c = () => 1; fn nested_function_likes_own_suspensions_when_cloned() { // WD-4: a method member owns its yields, so the object clones as // clean into the machine and the INTERNAL yield stays legal. + // The method body holds a real suspension: ownership means the + // machine lowers with exactly one resume (the await) and the + // method's yield survives verbatim inside the cloned object — + // a walker that wrongly descended into the body would mint a + // second resume or refuse outright. let output = emit_es5_clean( - "declare var x: any;\nasync function f() {\n while ({ m() { return 1; } }) { await x; }\n}\n", + "declare var x: any;\nasync function f() {\n while ({ *m() { yield 1; } }) { await x; }\n}\n", ); let code = &javascript(&output).code; assert!(code.contains("__generator(this,"), "lowers: {code}"); - assert!(code.contains("m() {"), "method cloned intact: {code}"); - assert!(!code.contains("function*("), "{code}"); + assert!( + code.contains("*m() {"), + "generator method cloned intact: {code}" + ); + assert!(code.contains("yield 1;"), "method owns its yield: {code}"); + assert_eq!( + code.matches(".sent()").count(), + 1, + "one machine resume: {code}" + ); + assert!( + !code.contains("function* ("), + "inner function not native: {code}" + ); } #[test] @@ -7394,33 +7497,83 @@ var c = () => 1; #[test] fn deep_nesting_walkers_do_not_crash() { - // WD-9: recursive walkers on a pathological expression must - // produce a descriptive outcome (machine, refusal, or diagnostics), - // never a crash. - let depth = 2_000; - let input = format!( - "declare var y: any;\nasync function f() {{ {}await y; }}\n", - "-".repeat(depth) + // WD-9: the parser's nesting bound (MAX_DEPTH = 256) is the + // boundary, tested from both sides. Below it the fixture parses + // clean and the recursive walkers emit some form; above it the + // parser declines with the descriptive P010 diagnostic — never a + // crash, never a silent skip. (A `-` chain lexes as `--` decrement + // pairs and never parses; parens give real expression depth.) + let fixture = |depth: usize| { + format!( + "declare var y: any;\nasync function f() {{ {}await y{}; }}\n", + "(".repeat(depth), + ")".repeat(depth) + ) + }; + let parse = |input: String| { + crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input.as_str()).expect("fits budget")), + )) + }; + let below = parse(fixture(56)); + assert!( + below.diagnostics().is_empty(), + "depth 56 must parse clean: {:?}", + below.diagnostics() + ); + let output = emit_output( + below.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let code = &javascript(&output).code; + assert!( + code.contains("__generator(this,") || code.contains("function*"), + "some emitted form at depth 56: {code}" + ); + let above = parse(fixture(400)); + assert!( + !above.diagnostics().is_empty(), + "depth 400 must be rejected, not accepted" + ); + assert!( + above + .diagnostics() + .iter() + .any(|d| d.code().as_str().contains("P010")), + "rejection must be the descriptive nesting-bound diagnostic: {:?}", + above.diagnostics() + ); + } + + #[test] + fn es5_generator_import_source_materializes_before_options_yield() { + // A clean source operand with side effects must run before the + // options suspend: materialize into a temp in the pre-suspension + // segment, never reassemble it inside the resume where its effects + // would slip past the suspension. + let output = emit_es5_clean( + "declare var f: any, b: any;\nfunction* g() { return import(f(), { m: yield b }); }\n", + ); + let code = &javascript(&output).code; + let case0 = code + .split("case 0:") + .nth(1) + .and_then(|rest| rest.split("case 1:").next()) + .expect("two segments"); + assert!( + case0.contains("_a = f();"), + "source materialized pre-yield: {code}" + ); + assert!(case0.contains("yield"), "suspension in segment 0: {code}"); + assert!( + code.contains("import(_a,"), + "resume reassembles from the temp: {code}" ); - let parsed = crate::parser::parse(crate::scanner::scan( - SourceId::new(0), - ScriptKind::TypeScript, - Arc::new(SourceText::new(input.as_str()).expect("fits budget")), - )); - if parsed.diagnostics().is_empty() { - let output = emit_output( - parsed.product(), - &EmitOptions { - target: ScriptTarget::Es5, - no_emit_helpers: true, - ..EmitOptions::default() - }, - ); - let code = &javascript(&output).code; - assert!( - code.contains("__generator(this,") || code.contains("function*("), - "some emitted form: {code}" - ); - } } } diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 381f520..d223ef2 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -5424,7 +5424,15 @@ impl<'a> Rewriter<'a> { )) } Expression::Import(import) => { - let source = self.eval(&import.source, ctx)?; + // Source operands run before the options suspend; a clean + // source with side effects must materialize into a temp + // ahead of the split, or its effects slip into the resume + // segment and run after the suspension. + let options_suspends = import.options.as_deref().is_some_and(contains_yield); + let source = match self.eval(&import.source, ctx)? { + value if options_suspends => self.materialize(value, ctx, range)?, + value => value, + }; let options = match &import.options { Some(options) if contains_yield(options) => { Some(Box::new(self.eval(options, ctx)?)) @@ -7068,10 +7076,12 @@ fn branch_statements(statement: &Stmt) -> &[Stmt] { /// The statements of a loop body: non-block bodies are shapes this slice /// refuses anyway; None renders as empty for counting purposes. fn loop_body_statements(body: &Stmt) -> &[Stmt] { - static NONE: &[Stmt] = &[]; + // Non-block bodies are single-statement branches: counters and the + // return guard must see through them, or a nested `while (x) return z;` + // hides its return and the clone gate drops the value. match body.data() { Statement::Block(block) => &block.data().statements, - _ => NONE, + _ => std::slice::from_ref(body), } } @@ -7143,6 +7153,12 @@ fn count_yields(expression: &Expr) -> u32 { // suspensions, just like nested function-likes, so the // wildcard below is safe for them. ObjectMember::Spread(spread) => count_yields(&spread.argument), + // A method's computed name executes in the enclosing + // function; its body owns its own suspensions. + ObjectMember::Method(method) => match &method.name { + PropertyName::Computed(key) => count_yields(key), + _ => 0, + }, _ => 0, }) .sum(), @@ -7153,6 +7169,7 @@ fn count_yields(expression: &Expr) -> u32 { .iter() .map(|argument| match argument { CallArgument::Expression(value) => count_yields(value), + CallArgument::Spread(spread) => count_yields(&spread.argument), _ => 0, }) .sum::() @@ -7171,6 +7188,7 @@ fn count_yields(expression: &Expr) -> u32 { .iter() .map(|argument| match argument { CallArgument::Expression(value) => count_yields(value), + CallArgument::Spread(spread) => count_yields(&spread.argument), _ => 0, }) .sum::() @@ -7399,7 +7417,11 @@ fn machine_state_name(skip: &std::collections::HashSet, temps: &[String] } fn call_argument_suspends(argument: &CallArgument) -> bool { - matches!(argument, CallArgument::Expression(value) if contains_yield(value)) + match argument { + CallArgument::Expression(value) => contains_yield(value), + CallArgument::Spread(spread) => contains_yield(&spread.argument), + _ => false, + } } /// Whether an assignment target subtree still holds an await, so the @@ -7433,14 +7455,18 @@ fn contains_await(expression: &Expr) -> bool { || matches!(&property.name, PropertyName::Computed(key) if contains_await(key)) } ObjectMember::Spread(spread) => contains_await(&spread.argument), - // Method-like members own their suspensions; cloning them is - // correct, not a leak. + // A method's computed name executes in the enclosing function; + // its body owns its own suspensions. + ObjectMember::Method(method) => { + matches!(&method.name, PropertyName::Computed(key) if contains_await(key)) + } _ => false, }), Expression::Call(call) => { contains_await(&call.callee) || call.arguments.iter().any(|argument| match argument { CallArgument::Expression(value) => contains_await(value), + CallArgument::Spread(spread) => contains_await(&spread.argument), _ => false, }) } @@ -7449,10 +7475,14 @@ fn contains_await(expression: &Expr) -> bool { contains_await(&new.callee) || new.arguments.iter().any(|argument| match argument { CallArgument::Expression(value) => contains_await(value), + CallArgument::Spread(spread) => contains_await(&spread.argument), _ => false, }) } Expression::Unary(unary) => contains_await(&unary.argument), + Expression::Import(import) => { + contains_await(&import.source) || import.options.as_deref().is_some_and(contains_await) + } Expression::Update(update) => match update.argument.data() { AssignmentTarget::Member(member) => { target_contains_await(&member.object, &member.property) @@ -7577,14 +7607,18 @@ fn contains_yield(expression: &Expr) -> bool { || matches!(&property.name, PropertyName::Computed(key) if contains_yield(key)) } ObjectMember::Spread(spread) => contains_yield(&spread.argument), - // Method-like members own their suspensions, like nested - // function-likes; cloning them is correct, not a leak. + // A method's computed name executes in the enclosing function; + // its body owns its own suspensions. + ObjectMember::Method(method) => { + matches!(&method.name, PropertyName::Computed(key) if contains_yield(key)) + } _ => false, }), Expression::Call(call) => { contains_yield(&call.callee) || call.arguments.iter().any(|argument| match argument { CallArgument::Expression(nested) => contains_yield(nested), + CallArgument::Spread(spread) => contains_yield(&spread.argument), _ => false, }) } @@ -7596,6 +7630,7 @@ fn contains_yield(expression: &Expr) -> bool { contains_yield(&new.callee) || new.arguments.iter().any(|argument| match argument { CallArgument::Expression(nested) => contains_yield(nested), + CallArgument::Spread(spread) => contains_yield(&spread.argument), _ => false, }) } diff --git a/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md b/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md index 28c8c99..c20433d 100644 --- a/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md +++ b/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md @@ -136,4 +136,8 @@ actually mints. - The corpus-level regression gate is the suite pair (`cargo test -p bamts-compiler` + `cargo test -p bamts-verification` with `BAMTS_ALLOW_NODE_COMPAT=1`), never the bare CLI `-p` path (it never - lowers the machine — refusal form only). + lowers the machine — refusal form only). That pair is the leaf evidence; + the completion claim itself binds to the receipt-backed G3 compiler root + gate in `.outline/GATES.md` — a green suite pair is necessary, not + sufficient, and closure language must cite the G3 receipts, not the leaf + suites alone. From 0aaf4116b64eff43aa134498cdbf8ca38f99e5b5 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:48:55 +0900 Subject: [PATCH 06/42] Signal async-arrow refusal with the requires-es2015 diagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The named-function path diagnosed a declined machine, but the arrow path bound lower_generator_function's result without checking is_generator, so `async () => { while (x) { if (y) { await z; } } }` at ES5 emitted a native generator inside __awaiter with no signal — the same silent-failure class the branch set out to close. The arrow call site now mirrors the function-path diagnostic. The duality battery gained three rows: the arrow refusal (native + diagnostic + no machine), and loop-test shapes for the two count-gate arms whose earlier statement-level rows could not see them — call-spread and computed-method-name in while tests. All four PR-review arms now have recorded mutation reds: deleting the arm reintroduces the exact clone (machine=true where refusal is expected), the import-order pin catches the materialize removal, and the non-block row catches the transparency loss. Gates: compiler 1866/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 21 ++++++++-- .../bamts-compiler/src/emitter/transforms.rs | 39 ++++++++++--------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index ae3543c..3c7766f 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7180,7 +7180,7 @@ var c = () => 1; // must not leak), `false` refuses (native form + the requires-es2015 // diagnostic + no machine). Expectations were derived from emitted // output; all-refusing output fails every `true` row. - let snippets: [(&str, &str, bool); 22] = [ + let snippets: [(&str, &str, bool); 25] = [ ( "update-computed-while", "declare var o: any, k: any;\nfunction* g() { while (o[k ? k : 0][`x`], o[yield k]++ < 3) { } }\n", @@ -7212,12 +7212,12 @@ var c = () => 1; true, ), ( - "conditional-in-machine", + "conditional-expr-statement", "declare var x: any, y: any;\nfunction* g() { return (yield x) ? y : (yield x); }\n", false, ), ( - "logical-in-machine", + "logical-expr-statement", "declare var x: any, y: any;\nfunction* g() { return (yield x) || (yield y); }\n", false, ), @@ -7291,6 +7291,21 @@ var c = () => 1; "declare var k: any;\nfunction* g() { while ((yield k) as any) { } }\n", true, ), + ( + "call-spread-while-test", + "declare var f: any, x: any;\nfunction* g() { while (f(...(yield x)) < 3) { } }\n", + false, + ), + ( + "method-computed-while-test", + "declare var k: any;\nfunction* g() { while ({ [yield k]() {} }) { } }\n", + false, + ), + ( + "async-arrow-refusal-signals", + "declare var x: any, y: any, z: any;\nvar f = async () => { while (x) { if (y) { await z; } } };\n", + false, + ), ]; for (name, input, lowers) in snippets { let output = emit_es5_clean(input); diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index d223ef2..5b2251b 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -6843,7 +6843,18 @@ impl<'a> Rewriter<'a> { body: Some(body), }; let inner = if Self::needs(LanguageFeature::Generators, self.options) { - self.lower_generator_function(&inner) + let lowered = self.lower_generator_function(&inner); + if lowered.is_generator { + // Same contract as the named-function path: the machine + // declined and the inner stays a native generator inside + // __awaiter, which the target cannot run — signal it. + self.diag( + codes::GENERATOR_REQUIRES_ES2015, + expression.range(), + "generators require ScriptTarget::Es2015 or later", + ); + } + lowered } else { inner }; @@ -7052,11 +7063,11 @@ fn count_branch_yields(statements: &[Stmt]) -> u32 { } Statement::While(while_statement) => { count_yields(&while_statement.test) - + count_branch_yields(loop_body_statements(&while_statement.body)) + + count_branch_yields(branch_statements(&while_statement.body)) } Statement::DoWhile(do_statement) => { count_yields(&do_statement.test) - + count_branch_yields(loop_body_statements(&do_statement.body)) + + count_branch_yields(branch_statements(&do_statement.body)) } Statement::Labeled(labeled) => count_branch_yields(branch_statements(&labeled.body)), _ => 1, @@ -7064,8 +7075,10 @@ fn count_branch_yields(statements: &[Stmt]) -> u32 { .sum() } -/// The statements of an if/else branch: a block flattens, any other -/// statement is a single-statement branch. +/// The statements of a branch or loop body: a block flattens, any other +/// statement is a single-statement branch. Counters and the return guard +/// must see through non-block bodies, or a nested `while (x) return z;` +/// hides its return and the clone gate drops the value. fn branch_statements(statement: &Stmt) -> &[Stmt] { match statement.data() { Statement::Block(block) => &block.data().statements, @@ -7073,18 +7086,6 @@ fn branch_statements(statement: &Stmt) -> &[Stmt] { } } -/// The statements of a loop body: non-block bodies are shapes this slice -/// refuses anyway; None renders as empty for counting purposes. -fn loop_body_statements(body: &Stmt) -> &[Stmt] { - // Non-block bodies are single-statement branches: counters and the - // return guard must see through them, or a nested `while (x) return z;` - // hides its return and the clone gate drops the value. - match body.data() { - Statement::Block(block) => &block.data().statements, - _ => std::slice::from_ref(body), - } -} - /// Whether any statement in this region returns — at any nesting depth, /// but never inside a nested function-like (those own their returns). /// A cloned raw return exits the machine's inner function directly and @@ -7101,10 +7102,10 @@ fn statements_contain_return(statements: &[Stmt]) -> bool { .is_some_and(|alt| statements_contain_return(branch_statements(alt))) } Statement::While(while_statement) => { - statements_contain_return(loop_body_statements(&while_statement.body)) + statements_contain_return(branch_statements(&while_statement.body)) } Statement::DoWhile(do_statement) => { - statements_contain_return(loop_body_statements(&do_statement.body)) + statements_contain_return(branch_statements(&do_statement.body)) } Statement::Labeled(labeled) => statements_contain_return(branch_statements(&labeled.body)), _ => false, From 6d9dbb27f8d8513379d95dc236dea9ed39312cbe Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:48:57 +0900 Subject: [PATCH 07/42] Collapse duplicate branch helper; fix gate recipe in the doc loop_body_statements had become character-for-character branch_statements after the non-block transparency fix; one helper now serves both sites with a doc comment that states the real contract (see through non-block bodies or the clone gate drops values). Battery rows conditional/logical renamed from -in-machine to -expr-statement: they are bare statements whose honest expectation is refusal (eval refuses both shapes), and the old names promised split coverage the rows do not provide. The learning doc's corpus-gate recipe named only BAMTS_ALLOW_NODE_COMPAT=1, which this session proved does not reach the corpus_differential target (NodeOracle::discover demands the exact v24.18.0 pin on PATH; the flag leaves 39 tests panicking on the version check). The recipe now names the pin. --- .../es5-generator-suspension-walker-divergence.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md b/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md index c20433d..ba9e08b 100644 --- a/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md +++ b/docs/solutions/logic-errors/es5-generator-suspension-walker-divergence.md @@ -134,9 +134,14 @@ actually mints. `es5_async_identifier_assignment_test_stays_inline`, `es5_async_for_update_increment_lowers` (emitter.rs cfg(test)). - The corpus-level regression gate is the suite pair - (`cargo test -p bamts-compiler` + `cargo test -p bamts-verification` with - `BAMTS_ALLOW_NODE_COMPAT=1`), never the bare CLI `-p` path (it never - lowers the machine — refusal form only). That pair is the leaf evidence; + (`cargo test -p bamts-compiler` + `cargo test -p bamts-verification`), + never the bare CLI `-p` path (it never lowers the machine — refusal form + only). The verification crate's corpus_differential target additionally + demands the pinned `node@24.18.0` first on `PATH` + (`mise install node@24.18.0`, then prefix the install's bin dir) — the + `BAMTS_ALLOW_NODE_COMPAT=1` flag covers the lib target only and leaves + corpus_differential panicking on the version check. That pair is the + leaf evidence; the completion claim itself binds to the receipt-backed G3 compiler root gate in `.outline/GATES.md` — a green suite pair is necessary, not sufficient, and closure language must cite the G3 receipts, not the leaf From 60db035f84253c59f7c6a5e5a97a93bf1bef84b3 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 09:50:35 +0900 Subject: [PATCH 08/42] Close six walker and gate holes from the full grill audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One critical label corruption and five verified gate/lowering holes: - switch suspending_tests counted CASES whose test suspends, not yield nodes; a `case (yield a) + (yield b):` test under-counted the dispatch cursor, and in release builds the dispatch targeted itself — an infinite loop with the case body unreachable. The counter now sums count_yields over case tests (pinned: the two-yield case test lowers with exact dispatch targets). - count_yields' Yield arm returned a flat 1 while eval splits one segment per nested yield node; `while (yield (yield a))` corrupted exit_label arithmetic (debug assert left:3 right:2, release jumps to a wrong state). The arm now counts 1 + argument (pinned via the label-contiguity rows). - for_clauses_are_clean treated every `var` init clause as clean, so `for (var x = yield 1; c;)` cloned a live yield into the machine's plain inner function; machine_emit_for likewise computed init_resumes as 0 while its emitter splits suspending inits. Both now count declarator initializers (pinned: the for-var-init row lowers through the split protocol). - the switch clone paths skipped the return guard every sibling gate has; `switch (x) { case 1: return 42; }` cloned a raw return that silently dropped the value through the protocol. Both gates consult statements_contain_return now, and the wave-1 discriminant pin — which had blessed that miscompile — asserts the signalled refusal plus a return-free variant that still lowers inline. - the await-to-yield rewrite leaked into nested function-likes: an async function containing a nested function with an await emitted `yield` inside a non-async body. rewrite_expr now clears the flag around nested functions, arrows, and classes (pinned: the nested async arrow lowers itself — two __awaiter machines, protocol form). - machine_state_name could loop forever past the 26-letter alphabet when _state was also taken (compile-time hang); the fallback is now a numbered suffix. Bare `yield;` lowers with a void-0 argument instead of refusing the generator. Throw statements name their argument's awaits in the entry guard (a cloned throw is protocol-safe under the helper's try/catch). Temp declarations are var at ES5 (let inside cloned machine bodies is a SyntaxError) and keep let at ES2015+. Test hardening from the same pass: the duality battery carries 31 rows with derived expectations including the five new shapes; the labels test fails instead of skipping when a row neither lowers nor signals; the sentinel, non-block, and labeled tests assert the signalled refusal instead of an escape hatch; the return-drop, import-order, and nested-awaits pins assert content and ordering, not just form. Gates: compiler 1867/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 151 ++++++++++++++---- .../bamts-compiler/src/emitter/transforms.rs | 122 ++++++++++++-- 2 files changed, 227 insertions(+), 46 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 3c7766f..21b46b0 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -6907,25 +6907,33 @@ var c = () => 1; #[test] fn es5_async_switch_discriminant_await_inline() { - // Authority: es5-asyncFunctionSwitchStatements(target=es5) — a - // discriminant that awaits is emitted as `switch (_a.sent())` inline - // in the resumed case. - let options = EmitOptions { - target: ScriptTarget::Es5, - no_emit_helpers: true, - ..EmitOptions::default() - }; - let input = "async function f(x: any) {\n switch (await x) {\n case 1: return 10;\n default: return 20;\n }\n}\n"; - let parsed = crate::parser::parse(crate::scanner::scan( - SourceId::new(0), - ScriptKind::TypeScript, - Arc::new(SourceText::new(input).expect("test source fits the per-file budget")), - )); - assert!(parsed.diagnostics().is_empty()); - let output = emit_output(parsed.product(), &options); + // Return-bearing cases must not ride the discriminant-inline path: + // cloning them verbatim embeds a raw `return 10` in the machine's + // inner function, which drops the value through the protocol. Until + // the rebuilt-cases path routes returns through machine_statements, + // this shape refuses with the diagnostic — never a silent clone. + let output = emit_es5_clean( + "declare var x: any;\nasync function f() { switch (await x) { case 1: return 10; default: return 20; } }\n", + ); let code = &javascript(&output).code; - assert!(code.contains("switch (_a.sent()) {"), "{code}"); - assert!(code.contains("case 0: return [4 /*yield*/, x];"), "{code}"); + assert!( + !code.contains("__generator(this,"), + "raw returns must not clone into the machine: {code}" + ); + assert!( + output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "refusal must signal: {:?}", + output.diagnostics + ); + // A return-free discriminant case still lowers inline. + let lowered = emit_es5_clean( + "declare var x: any;\nasync function f() { switch (await x) { case 1: r = 10; } }\n", + ); + let code = &javascript(&lowered).code; + assert!(code.contains("switch (_a.sent())"), "{code}"); } #[test] @@ -7163,6 +7171,7 @@ var c = () => 1; !code.contains("__generator(this,"), "machine must not embed a raw return: {code}" ); + assert!(code.contains("return y;"), "value preserved: {code}"); assert!( output .diagnostics @@ -7180,7 +7189,7 @@ var c = () => 1; // must not leak), `false` refuses (native form + the requires-es2015 // diagnostic + no machine). Expectations were derived from emitted // output; all-refusing output fails every `true` row. - let snippets: [(&str, &str, bool); 25] = [ + let snippets: [(&str, &str, bool); 31] = [ ( "update-computed-while", "declare var o: any, k: any;\nfunction* g() { while (o[k ? k : 0][`x`], o[yield k]++ < 3) { } }\n", @@ -7301,6 +7310,32 @@ var c = () => 1; "declare var k: any;\nfunction* g() { while ({ [yield k]() {} }) { } }\n", false, ), + ( + "yield-nested-lowers-with-exact-labels", + "declare var a: any;\nfunction* g() { while (yield (yield a)) {} }\n", + true, + ), + ( + "for-var-init-yield-splits", + "declare var x: any, c: any;\nfunction* g() { for (var x = yield 1; c;) {} }\n", + true, + ), + ( + "switch-case-return-refuses", + "declare var x: any, y: any;\nfunction* g() { while (y) { switch (x) { case 1: return 42; } } }\n", + false, + ), + ( + "switch-multi-yield-case-test", + "declare var d: any, a: any, b: any, x: any;\nfunction* g() { switch (d) { case (yield a) + (yield b): x = 1; break; default: x = 2; } }\n", + true, + ), + ( + "computed-key-temps-are-var-at-es5", + "declare var x: any, k: any, o: any;\nfunction* g() { while (x) { var { [k]: v } = o; } }\n", + true, + ), + ("bare-yield-lowers", "function* g() { yield; }\n", true), ( "async-arrow-refusal-signals", "declare var x: any, y: any, z: any;\nvar f = async () => { while (x) { if (y) { await z; } } };\n", @@ -7350,8 +7385,12 @@ var c = () => 1; let output = emit_es5_clean(input); let code = &javascript(&output).code; assert!( - code.contains("function* g(") || live_yield_leak(code).is_none(), - "[{name}] must refuse (native form) or lower safely, got:\n{code}" + code.contains("function* g(") + && output + .diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "[{name}] sentinel shape must refuse with the native form + diagnostic:\n{code}" ); } // JSX expressions need TypeScriptReact parsing; the same sentinel @@ -7407,6 +7446,14 @@ var c = () => 1; "conditional-three", "declare var a: any, b: any, c: any;\nasync function f() { return (await a) ? await b : await c; }\n", ), + ( + "nested-yield-labels", + "declare var a: any;\nfunction* g() { while (yield (yield a)) {} }\n", + ), + ( + "for-var-init-split-labels", + "declare var x: any, c: any;\nfunction* g() { for (var x = yield 1; yield c;) { yield x; } }\n", + ), ( "jump-guard-exact-count", "declare var x: any, y: any, z: any;\nasync function f() { while (x) { if (y) continue; await z; } }\n", @@ -7414,8 +7461,26 @@ var c = () => 1; ] { let output = emit_es5_clean(input); let code = &javascript(&output).code; - if !code.contains("switch (_a.label)") { - continue; // refused: fine, label arithmetic never ran + let machine_switch = code + .match_indices("switch (_") + .map(|(i, _)| i) + .find(|&i| code[i..].contains(".label)")); + if machine_switch.is_none() { + if !code.contains("__generator(this,") { + // Not lowered at all: the only honest alternative is a + // signalled refusal — a silent skip would hide a + // lowering regression. + assert!( + code.contains("function*") + && output.diagnostics.iter().any(|d| { + d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015 + }), + "[{name}] neither machine nor signalled refusal:\n{code}" + ); + } + // A degenerate zero-resume machine needs no dispatch + // switch; there are no labels to check. + continue; } let labels = machine_case_labels(code); assert!(!labels.is_empty(), "[{name}] machine without labels"); @@ -7478,14 +7543,19 @@ var c = () => 1; let output = emit_es5_clean(input); let code = &javascript(&output).code; assert!( - code.contains("function*(") || live_yield_leak(code).is_none(), - "[{name}] non-block body must refuse, never clone: {code}" + !code.contains("__generator(this,") + && code.contains("function*") + && output + .diagnostics + .iter() + .any(|d| { d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015 }), + "[{name}] non-block body must refuse with the diagnostic, never lower or clone: {code}" ); } } #[test] - fn labeled_nested_await_delegation_lowers() { + fn labeled_nested_await_delegation_refuses_until_nested_if_slice() { // WD-8: branch_awaits delegates through nested statement shapes. let output = emit_es5_clean( "declare var x: any, y: any, z: any;\nasync function f() {\n A: while (x) { if (y) { await z; break A; } }\n}\n", @@ -7581,14 +7651,37 @@ var c = () => 1; .nth(1) .and_then(|rest| rest.split("case 1:").next()) .expect("two segments"); + let source_pos = case0.find("_a = f();").expect("source temp"); + let yield_pos = case0.find("/*yield*/").expect("split marker"); assert!( - case0.contains("_a = f();"), - "source materialized pre-yield: {code}" + source_pos < yield_pos, + "source materializes before the yield: {code}" ); - assert!(case0.contains("yield"), "suspension in segment 0: {code}"); assert!( code.contains("import(_a,"), "resume reassembles from the temp: {code}" ); } + + #[test] + fn es5_async_nested_async_arrow_keeps_its_awaits() { + // A nested async arrow owns its awaits: the outer function's + // await-to-yield rewrite must stop at the nested body, and the + // nested arrow keeps its own `await` for its own lowering pass. + // (Await inside a NON-async function is invalid input the parser + // does not yet reject — banked as a checker slice.) + let output = emit_es5_clean( + "declare var k: any;\nasync function f() { var g = async () => { return await k; }; }\n", + ); + let code = &javascript(&output).code; + // The nested arrow lowers itself: its own __awaiter machine with + // the await in protocol form ([4 /*yield*/, k]) — no live yield. + assert_eq!( + code.matches("__awaiter").count(), + 2, + "both the outer function and the nested arrow lower: {code}" + ); + assert!(code.contains("/*yield*/, k"), "protocol form: {code}"); + assert!(live_yield_leak(code).is_none(), "{code}"); + } } diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 5b2251b..3a397cf 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -2473,7 +2473,14 @@ impl<'a> Rewriter<'a> { range, Statement::Variable(VariableDeclaration { range, - kind: VariableKind::Let, + // Temps can land inside machine bodies that clone + // verbatim; `let` there is a SyntaxError at ES5, but + // ES2015+ targets keep the tighter binding. + kind: if self.options.target >= ScriptTarget::Es2015 { + VariableKind::Let + } else { + VariableKind::Var + }, declarations: vec![declaration], }), ) @@ -4647,7 +4654,18 @@ impl<'a> Rewriter<'a> { } let init_resumes = match &for_statement.initializer { Some(ForInitializer::Expression(expression)) => count_yields(expression), - Some(ForInitializer::Variable(_)) | None => 0, + Some(ForInitializer::Variable(declaration)) => declaration + .declarations + .iter() + .map(|declarator| { + declarator + .data() + .initializer + .as_deref() + .map_or(0, count_yields) + }) + .sum(), + None => 0, }; let test_resumes = count_yields(test); let update_resumes = for_statement.update.as_deref().map_or(0, count_yields); @@ -4754,11 +4772,15 @@ impl<'a> Rewriter<'a> { .cases .iter() .any(|case| case_body_resumes(case) > 0); - if !disc_suspends && !any_test && !any_body { + let any_return = switch_statement + .cases + .iter() + .any(|case| statements_contain_return(&case.data().consequent)); + if !disc_suspends && !any_test && !any_body && !any_return { ctx.push(statement.clone()); return Some(()); } - if disc_suspends && !any_test && !any_body { + if disc_suspends && !any_test && !any_body && !any_return { // Only the discriminant suspends: reassemble inline around // the resumed value, cases cloned. let discriminant = self.eval(&switch_statement.discriminant, ctx)?; @@ -4795,11 +4817,14 @@ impl<'a> Rewriter<'a> { } let head = ctx.segments.len() as u32 - 1; + // One resume segment per yield node, not per suspending case: a + // case test like `(yield a) + (yield b)` splits twice, and a + // case-count under-count skews every dispatch target past it. let suspending_tests = switch_statement .cases .iter() - .filter(|case| case_test_suspends(case)) - .count() as u32; + .map(|case| case.data().test.as_deref().map_or(0, count_yields)) + .sum::(); // Each suspending test's yield lands in the current segment and // its resume opens one new segment, so the dispatch phases span // head..head+suspending_tests and bodies start after. @@ -5151,8 +5176,15 @@ impl<'a> Rewriter<'a> { if yielded.delegate { return None; } - let argument = yielded.argument.as_ref()?; - let evaluated = self.eval(argument, ctx)?; + // A bare `yield;` is the same protocol with void 0; refusing + // the whole generator over the missing argument drops a + // trivial, common shape. + let void_argument = self.void_zero(expr.range()); + let argument = yielded + .argument + .as_ref() + .map_or(void_argument, |argument| argument.as_ref().clone()); + let evaluated = self.eval(&argument, ctx)?; let sentinel = self.number_expr("4 /*yield*/"); let array = self.array_literal(vec![sentinel, evaluated], range); let terminator = self.machine_return(array, range); @@ -5849,15 +5881,34 @@ impl<'a> Rewriter<'a> { ); } match expression.data() { - Expression::Class(class) => self.lower_class_expression(expression, class, None), + Expression::Class(class) => { + let previous = self.replace_await; + self.replace_await = false; + let lowered = self.lower_class_expression(expression, class, None); + self.replace_await = previous; + lowered + } Expression::Function(function) => { + // A nested function-like owns its awaits: rewriting them + // into yields here would emit `yield` inside a non-async + // function, so the flag clears for the nested body and + // restores after. + let previous = self.replace_await; + self.replace_await = false; let function = self.rewrite_function_like(&function.function, expression.range()); + self.replace_await = previous; self.node( expression.range(), Expression::Function(FunctionExpression { function }), ) } - Expression::Arrow(arrow) => self.rewrite_arrow(expression, arrow), + Expression::Arrow(arrow) => { + let previous = self.replace_await; + self.replace_await = false; + let rewritten = self.rewrite_arrow(expression, arrow); + self.replace_await = previous; + rewritten + } Expression::Await(awaited) => { let argument = self.rewrite_expr(&awaited.argument); self.node( @@ -7003,7 +7054,16 @@ fn block_statements(statement: &Stmt) -> Option<&[Stmt]> { fn for_clauses_are_clean(for_statement: &ForStatement) -> bool { let init_clean = match &for_statement.initializer { Some(ForInitializer::Expression(expression)) => !contains_yield(expression), - _ => true, + Some(ForInitializer::Variable(declaration)) => { + declaration.declarations.iter().all(|declarator| { + declarator + .data() + .initializer + .as_deref() + .is_none_or(|init| !contains_yield(init)) + }) + } + None => true, }; let test_clean = for_statement .test @@ -7108,6 +7168,10 @@ fn statements_contain_return(statements: &[Stmt]) -> bool { statements_contain_return(branch_statements(&do_statement.body)) } Statement::Labeled(labeled) => statements_contain_return(branch_statements(&labeled.body)), + Statement::Switch(switch_statement) => switch_statement + .cases + .iter() + .any(|case| statements_contain_return(&case.data().consequent)), _ => false, }) } @@ -7124,7 +7188,7 @@ impl ChainSegment { /// own their own yields). fn count_yields(expression: &Expr) -> u32 { match expression.data() { - Expression::Yield(_) => 1, + Expression::Yield(yielded) => 1 + yielded.argument.as_deref().map_or(0, count_yields), Expression::Identifier(_) | Expression::This | Expression::Super => 0, Expression::Literal(_) | Expression::Meta(_) | Expression::Missing(_) => 0, Expression::Function(_) | Expression::Class(_) | Expression::Arrow(_) => 0, @@ -7409,7 +7473,12 @@ fn machine_state_name(skip: &std::collections::HashSet, temps: &[String] let taken: std::collections::HashSet<&String> = skip.iter().chain(temps.iter()).collect(); let mut index = 0u32; loop { - let name = machine_name(index).unwrap_or_else(|| "_state".to_string()); + // Past the alphabet machine_name returns None forever; a fixed + // "_state" fallback would then loop endlessly when _state is also + // taken, so the suffix keeps every candidate fresh and the loop + // terminates. + let name = + machine_name(index).unwrap_or_else(|| format!("_state{}", index.saturating_sub(26))); if !taken.contains(&name) { return name; } @@ -7553,7 +7622,16 @@ fn statements_contain_await(statements: &[Stmt]) -> bool { Statement::For(for_statement) => { let init = match &for_statement.initializer { Some(ForInitializer::Expression(expression)) => contains_await(expression), - _ => false, + Some(ForInitializer::Variable(declaration)) => { + declaration.declarations.iter().any(|declarator| { + declarator + .data() + .initializer + .as_deref() + .is_some_and(contains_await) + }) + } + None => false, }; let test = for_statement.test.as_deref().is_some_and(contains_await); let update = for_statement.update.as_deref().is_some_and(contains_await); @@ -7569,6 +7647,9 @@ fn statements_contain_await(statements: &[Stmt]) -> bool { Statement::Labeled(labeled) => branch_awaits(&labeled.body), // Jumps and empties hold no expressions. Statement::Break(_) | Statement::Continue(_) | Statement::Empty => false, + // The machine helper wraps the body in try/catch, so a cloned raw + // throw is protocol-safe; only its argument's awaits matter. + Statement::Throw(throw_statement) => contains_await(&throw_statement.argument), _ => true, }) } @@ -9334,13 +9415,20 @@ console.log(JSON.stringify([bar, bar4, log])); ScriptTarget::Es5, ); let code = javascript(&output); - let key_temp_at = code.find("let _t").expect("key temp declared"); - let digits: String = code[key_temp_at + "let _t".len()..] + // ES5 temps are `var` (a `let` inside cloned machine bodies is a + // SyntaxError at ES5); ES2015+ keeps the tighter `let` binding. + let (kind, key_temp_at) = code + .find("var _t") + .map(|at| ("var", at)) + .or_else(|| code.find("let _t").map(|at| ("let", at))) + .expect("key temp declared"); + assert_eq!(kind, "var", "ES5 temp kind: {code}"); + let digits: String = code[key_temp_at + "var _t".len()..] .chars() .take_while(|character| character.is_ascii_digit()) .collect(); let temp_name = format!("_t{digits}"); - let declaration_form = format!("let {temp_name} = key;"); + let declaration_form = format!("var {temp_name} = key;"); assert!( code.contains(&declaration_form), "key evaluates once into a temp ({declaration_form:?}): {code}" From 6fb9b8f178a928a100e64892c969fbbfd1316813 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:00:47 +0900 Subject: [PATCH 09/42] Pin the state-name terminator and gate the class prelude temp kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The state-name allocator's numbered-suffix fallback now has a mutation-proven pin: declaring _a.._z plus _state inside the generator body (the skip set reads body statements, not ambient declarations) hangs the old fixed fallback, so the pin runs the emit on a thread with a hard timeout — reverting the suffix reproduces the 30-second timeout red instead of wedging the runner. The class-expression prelude emitted `let` unconditionally, the same ES5 SyntaxError class make_temp_declaration carried; its temp kind is now target-conditional (var below ES2015, let from ES2015 up). An unverified static-field postlude-ordering claim is recorded against the class slice rather than assumed. Gates: compiler 1868/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 33 +++++++++++++++++++ .../bamts-compiler/src/emitter/transforms.rs | 6 +++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 21b46b0..34352d5 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7684,4 +7684,37 @@ var c = () => 1; assert!(code.contains("/*yield*/, k"), "protocol form: {code}"); assert!(live_yield_leak(code).is_none(), "{code}"); } + + #[test] + fn machine_state_name_terminates_past_the_alphabet() { + // Declaring _a.._z plus _state exhausted the old allocator's fixed + // fallback and hung compilation forever. A regression must fail + // this test, not wedge the runner, so the emit runs on a thread + // with a hard timeout. + // The skip set is built from BODY statements, so the alphabet must + // be declared inside the generator, not as ambient `declare var`. + let input = "function* g() { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _state; yield 1; }\n"; + let (tx, rx) = std::sync::mpsc::channel(); + let parsed_input = input.to_owned(); + std::thread::spawn(move || { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(parsed_input.as_str()).expect("fits budget")), + )); + let output = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let _ = tx.send(javascript(&output).code.clone()); + }); + let code = rx + .recv_timeout(std::time::Duration::from_secs(30)) + .expect("allocator must terminate past the alphabet"); + assert!(code.contains("__generator(this,"), "{code}"); + } } diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 3a397cf..8a745c7 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -5807,7 +5807,11 @@ impl<'a> Rewriter<'a> { expression.range(), Statement::Variable(VariableDeclaration { range: expression.range(), - kind: VariableKind::Let, + kind: if self.options.target >= ScriptTarget::Es2015 { + VariableKind::Let + } else { + VariableKind::Var + }, declarations: vec![declaration], }), )); From 1bd6d9c3c8fd0d839e104fee8f03e97ff09949bb Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:51:48 +0900 Subject: [PATCH 10/42] Lower destructuring defaults instead of erasing them The ES5 destructuring lowering never read binding defaults: `var {a = 1} = o` emitted `var a = o.a;` and nested patterns emitted a malformed `var ;` - both silently dropping semantics and masking the binding arms of the suspension walkers. The oracle (tsc 6.0.2 transpile, target es5) pins the shape: a temp read then a ternary declarator (`var _t = o.a, a = _t === void 0 ? 1 : _t`), nested patterns reading off the defaulted temp, renamed properties reading under the source key. A suspending default (yield or await inside it) cannot live in a ternary - eval refuses Conditional - so those declarations lower to branch statements the machine owns: hoisted names, temp reads, an if/else default selection whose arms assign the value temp, and the final bind. The machine splits exactly at the default's suspension, preserving tsc's conditional-evaluation semantics; the value temp gets its own `var` so strict mode never sees an implicit global. Await defaults dispatch to the same branch form, keeping the __awaiter inner generator a machine instead of native `function*`. The binding walkers (count_binding_yields, binding_contains_await) now walk default initializers, computed keys, and nested patterns, wired into count_branch_yields, statements_contain_await, and for_clauses_are_clean; a suspending for-init binding signals the requires-es2015 refusal instead of cloning. Mutation reds recorded for both the ternary shape and the walker dispatch. Gates: compiler 1872/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 97 +++ .../bamts-compiler/src/emitter/transforms.rs | 654 +++++++++++++++++- 2 files changed, 731 insertions(+), 20 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 34352d5..66f20ea 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7079,6 +7079,103 @@ var c = () => 1; // WD-9 recursive walkers survive deep nesting without crashing. // ------------------------------------------------------------------ + /// B4: destructuring defaults lower to tsc's ES5 shape - a temp + /// read then a `=== void 0` ternary - never erasing the default. + #[test] + fn destructuring_defaults_lower_to_ternary_declarators() { + let cases = [ + ( + "var o:any;\nvar { a = 1 } = o;\n", + "var _t0 = o.a, a = _t0 === void 0 ? 1 : _t0;", + ), + ( + "var o:any;\nvar { p: q = 2 } = o;\n", + "var _t0 = o.p, q = _t0 === void 0 ? 2 : _t0;", + ), + ( + "var o:any;\nvar { x: { y } = {} } = o;\n", + "var _t0 = o.x, _t1 = _t0 === void 0 ? {} : _t0, y = _t1.y;", + ), + ( + "var arr:any;\nvar [c = 3, , [d = 4] = []] = arr;\n", + "var _t0 = arr[0], c = _t0 === void 0 ? 3 : _t0, _t1 = arr[2], _t2 = _t1 === void 0 ? [] : _t1, _t3 = _t2[0], d = _t3 === void 0 ? 4 : _t3;", + ), + ]; + for (src, expected) in cases { + let out = emit_es5_clean(src); + let code = javascript(&out).code.clone(); + assert!( + code.contains(expected), + "missing tsc default shape `{expected}` in:\n{code}" + ); + } + } + + /// B4: a default's suspension survives lowering - the machine splits + /// the default selection and no live yield leaks. + #[test] + fn destructuring_default_suspension_splits_the_machine() { + let out = + emit_es5_clean("var o:any, k:any;\nfunction* g(){ var { m = (yield k) } = o; }\n"); + let code = javascript(&out).code.clone(); + assert!(code.contains("__generator(this,"), "machine form: {code}"); + assert!( + code.contains("=== void 0"), + "selection test missing:\n{code}" + ); + assert!( + code.contains("[4 /*yield*/, k]"), + "suspension missing:\n{code}" + ); + assert!(code.contains("m = _t"), "final bind missing:\n{code}"); + assert!(live_yield_leak(&code).is_none(), "live yield leaked"); + } + + /// B4: an await default must not land in a ternary - eval refuses + /// Conditional, so the __awaiter inner generator would stay native + /// `function*` at ES5. The branch form keeps the inner machine. + #[test] + fn destructuring_default_await_keeps_the_inner_machine() { + let out = emit_es5_clean( + "var o:any, k:any;\nasync function h(){ var { n = await k } = o; return n; }\n", + ); + let code = javascript(&out).code.clone(); + assert!(code.contains("__awaiter("), "async wrapper: {code}"); + assert!( + code.contains("__generator(this,"), + "inner machine lost - await landed in a ternary:\n{code}" + ); + assert!(!code.contains("? yield"), "ternary yield:\n{code}"); + } + + /// B4 walker: a suspending for-init binding signals refusal with the + /// requires-es2015 diagnostic rather than cloning silently. + #[test] + fn for_init_suspending_binding_signals_refusal() { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new( + "var o:any, k:any;\nfunction* g(){ for (var { a = (yield k) } = o;;) { break; } }\n", + ) + .expect("fits")), + )); + let out = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + assert!( + out.diagnostics + .iter() + .any(|d| d.message().contains("generators require")), + "refusal must be signalled" + ); + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 8a745c7..edf902b 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -1690,6 +1690,17 @@ impl<'a> Rewriter<'a> { }), )]; } + // A suspending binding (yield in a default or computed key) cannot + // live in a ternary: the machine must split the default selection + // into branch statements it owns, preserving conditional + // evaluation. Clean bindings lower to tsc's ternary declarators. + let suspending = declaration.declarations.iter().any(|declarator| { + count_binding_yields(&declarator.data().binding) > 0 + || binding_contains_await(&declarator.data().binding) + }); + if suspending { + return self.lower_suspending_declaration(declaration); + } let mut declarations = Vec::new(); self.key_prelude.clear(); for declarator in &declaration.declarations { @@ -1712,6 +1723,32 @@ impl<'a> Rewriter<'a> { statements } + /// tsc's ES5 default shape: `name = read === void 0 ? DEFAULT : read`. + /// The default runs only when the read is undefined; the rewrite pass + /// owns await conversion inside it. + fn default_ternary(&mut self, read: &IdentifierNode, default: &Expr, range: TextRange) -> Expr { + let test_left = self.node(range, Expression::Identifier(read.clone())); + let void_zero = self.void_zero(range); + let test = self.node( + range, + Expression::Binary(BinaryExpression { + operator: BinaryOperator::StrictEqual, + left: Box::new(test_left), + right: Box::new(void_zero), + }), + ); + let consequent = self.rewrite_expr(default); + let alternate = self.node(range, Expression::Identifier(read.clone())); + self.node( + range, + Expression::Conditional(ConditionalExpression { + test: Box::new(test), + consequent: Box::new(consequent), + alternate: Box::new(alternate), + }), + ) + } + fn lower_declarator( &mut self, kind: VariableKind, @@ -1779,6 +1816,374 @@ impl<'a> Rewriter<'a> { }, ) } + /// Lowers a declaration whose binding carries suspensions (yield in + /// a default or computed key). Every bound name hoists first; reads + /// become temp var statements; a default becomes an if/else whose + /// arms assign the value temp, so the machine splits exactly at the + /// default's suspension and the default evaluates only when the + /// read is undefined — tsc's conditional-evaluation semantics in + /// this machine's statement protocol. + fn lower_suspending_declaration(&mut self, declaration: &VariableDeclaration) -> Vec { + let range = declaration.range; + let mut out = Vec::new(); + let mut names = Vec::new(); + for declarator in &declaration.declarations { + collect_binding_names(&declarator.data().binding, &mut names); + } + if !names.is_empty() { + let declarations = names + .into_iter() + .map(|name| self.make_declarator(name, None, range)) + .collect(); + let hoist = self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + ); + out.push(hoist); + } + for declarator in &declaration.declarations { + let (rhs, needs_temp) = self.rhs_ident(declarator.data().initializer.as_deref(), range); + if needs_temp { + let init = declarator + .data() + .initializer + .as_deref() + .cloned() + .unwrap_or_else(|| self.ident_expr("undefined")); + let declarations = vec![self.make_declarator(rhs.clone(), Some(init), range)]; + let temp = self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + ); + out.push(temp); + } + self.emit_binding_statements(&rhs, &declarator.data().binding, range, &mut out); + } + out + } + + /// The statement form of one binding pattern: temp reads, default + /// if/else selections, and final assignments. Only runs for + /// suspending bindings; the clean form lives in the declarator + /// lowering. + fn emit_binding_statements( + &mut self, + rhs: &IdentifierNode, + pattern: &Pattern, + range: TextRange, + out: &mut Vec, + ) { + match pattern.data() { + BindingPattern::Identifier(_) | BindingPattern::Missing(_) => {} + BindingPattern::Rest(_) => {} + BindingPattern::Object(object) => { + let mut rest_names = Vec::new(); + for property in &object.properties { + // The member expression this property reads. + let member = match &property.name { + PropertyName::Computed(key) => { + let temp = self.temp_ident(); + if contains_yield(key) { + let assign = + self.assign_statement(&temp, key.as_ref().clone(), range); + out.push(assign); + } else { + let value = self.rewrite_expr(key); + out.push(self.make_temp_declaration(temp.clone(), value, range)); + } + let reference = + self.node(temp.range(), Expression::Identifier(temp.clone())); + self.member_computed(rhs, &reference, range) + } + _ => match property_key_text(self, property) { + Some(key) => { + rest_names.push(key.clone()); + self.member_ident(rhs, &key, range) + } + None => continue, + }, + }; + self.emit_property_statements( + property, + member, + range, + &mut rest_names, + rhs, + out, + ); + } + // The rest property binds after the named reads. + for property in &object.properties { + if let BindingPattern::Rest(rest) = property.binding.data() + && let BindingPattern::Identifier(ident) = rest.argument.data() + { + let excluded = self.rest_exclude_literal(&rest_names, range); + let call = self.rest_call(rhs, excluded, range); + out.push(self.assign_statement(ident, call, range)); + } + } + } + BindingPattern::Array(array) => { + let mut index = 0usize; + for element in &array.elements { + match element { + ArrayBindingElement::Elision | ArrayBindingElement::Missing(_) => { + index += 1; + } + ArrayBindingElement::Binding(binding) => { + self.emit_element_statements(rhs, binding, index, range, out); + index += 1; + } + } + } + } + BindingPattern::Assignment(assignment) => { + let member = self.member_index(rhs, 0, range); + self.emit_defaulted_binding( + &assignment.left, + &assignment.right, + member, + range, + out, + ); + } + } + } + + /// One object property's statement form: read temp, default if/else, + /// then the target bind or nested recursion. + fn emit_property_statements( + &mut self, + property: &ObjectBindingProperty, + member: Expr, + range: TextRange, + _rest_names: &mut Vec, + _rhs: &IdentifierNode, + out: &mut Vec, + ) { + match property.binding.data() { + BindingPattern::Identifier(ident) => { + let read = self.temp_ident(); + let declarations = vec![self.make_declarator(read.clone(), Some(member), range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + )); + let value = match &property.initializer { + Some(default) => { + let value = self.temp_ident(); + let declarations = vec![self.make_declarator(value.clone(), None, range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + )); + let selection = self.default_selection(&read, default, &value, range); + out.push(selection); + value + } + None => read, + }; + let value_expr = self.node(range, Expression::Identifier(value.clone())); + out.push(self.assign_statement(ident, value_expr, range)); + } + _ => { + let value = self.emit_read_with_default( + &property.binding, + &property.initializer, + member, + range, + out, + ); + self.emit_binding_statements(&value, &property.binding, range, out); + } + } + } + + fn emit_element_statements( + &mut self, + rhs: &IdentifierNode, + binding: &Pattern, + index: usize, + range: TextRange, + out: &mut Vec, + ) { + let member = self.member_index(rhs, index, range); + match binding.data() { + BindingPattern::Identifier(_) => { + let value = self.emit_read_with_default(binding, &None, member, range, out); + if let BindingPattern::Identifier(ident) = binding.data() { + let value_expr = self.node(range, Expression::Identifier(value.clone())); + out.push(self.assign_statement(ident, value_expr, range)); + } + } + BindingPattern::Assignment(assignment) => { + self.emit_defaulted_binding( + &assignment.left, + &assignment.right, + member, + range, + out, + ); + } + _ => { + let value = self.emit_read_with_default(binding, &None, member, range, out); + self.emit_binding_statements(&value, binding, range, out); + } + } + } + + /// The read temp for a member, plus the if/else default selection + /// when a default exists. Returns the identifier holding the value. + fn emit_read_with_default( + &mut self, + _binding: &Pattern, + initializer: &Option>, + member: Expr, + range: TextRange, + out: &mut Vec, + ) -> IdentifierNode { + let read = self.temp_ident(); + let declarations = vec![self.make_declarator(read.clone(), Some(member), range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + )); + match initializer { + Some(default) => { + let value = self.temp_ident(); + // The value temp needs its `var`: it is assigned only + // inside the selection's branches, so nothing else + // declares it and strict mode would throw on the + // implicit global. + let declarations = vec![self.make_declarator(value.clone(), None, range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + )); + let selection = self.default_selection(&read, default, &value, range); + out.push(selection); + value + } + None => read, + } + } + + /// `if (read === void 0) { value = DEFAULT; } else { value = read; }` + fn default_selection( + &mut self, + read: &IdentifierNode, + default: &Expr, + value: &IdentifierNode, + range: TextRange, + ) -> Stmt { + let test_left = self.node(range, Expression::Identifier(read.clone())); + let void_zero = self.void_zero(range); + let test = self.node( + range, + Expression::Binary(BinaryExpression { + operator: BinaryOperator::StrictEqual, + left: Box::new(test_left), + right: Box::new(void_zero), + }), + ); + let rewritten = self.rewrite_expr(default); + let then_assign = self.assign_statement(value, rewritten, range); + let then_block = self.node( + range, + Block { + statements: vec![then_assign], + }, + ); + let read_expr = self.node(range, Expression::Identifier(read.clone())); + let else_assign = self.assign_statement(value, read_expr, range); + let else_block = self.node( + range, + Block { + statements: vec![else_assign], + }, + ); + let then_stmt = self.node(range, Statement::Block(then_block)); + let else_stmt = self.node(range, Statement::Block(else_block)); + self.node( + range, + Statement::If(IfStatement { + test: Box::new(test), + consequent: Box::new(then_stmt), + alternate: Some(Box::new(else_stmt)), + }), + ) + } + + /// Binds a target pattern to a member read with a default: the + /// identifier case assigns the if/else value temp; nested patterns + /// recurse off the defaulted read. + fn emit_defaulted_binding( + &mut self, + left: &Pattern, + right: &Expr, + member: Expr, + range: TextRange, + out: &mut Vec, + ) { + match left.data() { + BindingPattern::Identifier(ident) => { + let read = self.temp_ident(); + let declarations = vec![self.make_declarator(read.clone(), Some(member), range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + )); + let value = self.temp_ident(); + let declarations = vec![self.make_declarator(value.clone(), None, range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations, + }), + )); + let selection = self.default_selection(&read, right, &value, range); + out.push(selection); + let value_expr = self.node(range, Expression::Identifier(value.clone())); + out.push(self.assign_statement(ident, value_expr, range)); + } + _ => { + let boxed = Some(Box::new(right.clone())); + let value = self.emit_read_with_default(left, &boxed, member, range, out); + self.emit_binding_statements(&value, left, range, out); + } + } + } + fn lower_object_binding( &mut self, declarator: &VariableDeclaratorNode, @@ -1794,6 +2199,22 @@ impl<'a> Rewriter<'a> { .unwrap_or_else(|| self.ident_expr("undefined")); out.push(self.make_declarator(rhs.clone(), Some(init), range)); } + self.lower_object_pattern(&rhs, object, range, &mut out); + out + } + + /// Lowers one object pattern against a source identifier, appending + /// declarators. Identifier bindings with defaults emit tsc's shape + /// (`var _t = o.a, a = _t === void 0 ? 1 : _t`); nested patterns + /// read off the defaulted temp (`{x: {y} = {}}` reads `y` off the + /// ternary result, so the default feeds the nested reads). + fn lower_object_pattern( + &mut self, + rhs: &IdentifierNode, + object: &ObjectBindingPattern, + range: TextRange, + out: &mut Vec, + ) { let mut rest_names = Vec::new(); for property in &object.properties { let PropertyName::Computed(key) = &property.name else { @@ -1803,11 +2224,9 @@ impl<'a> Rewriter<'a> { let value = self.rewrite_expr(key); let declaration = self.make_temp_declaration(temp.clone(), value, range); self.key_prelude.push(declaration); - if let BindingPattern::Identifier(ident) = property.binding.data() { - let reference = self.node(temp.range(), Expression::Identifier(temp.clone())); - let member = self.member_computed(&rhs, &reference, range); - out.push(self.make_declarator(ident.clone(), Some(member), range)); - } + let reference = self.node(temp.range(), Expression::Identifier(temp.clone())); + let member = self.member_computed(rhs, &reference, range); + self.lower_property_binding(property, member, range, out); } for property in &object.properties { if let PropertyName::Computed(_) = &property.name { @@ -1816,21 +2235,76 @@ impl<'a> Rewriter<'a> { if let BindingPattern::Rest(rest) = property.binding.data() { if let BindingPattern::Identifier(ident) = rest.argument.data() { let excluded = self.rest_exclude_literal(&rest_names, range); - let call = self.rest_call(&rhs, excluded, range); + let call = self.rest_call(rhs, excluded, range); out.push(self.make_declarator(ident.clone(), Some(call), range)); } continue; } - if let BindingPattern::Identifier(ident) = property.binding.data() { - let Some(key) = property_key_text(self, property) else { - continue; - }; - rest_names.push(key.clone()); - let member = self.member_ident(&rhs, &key, range); - out.push(self.make_declarator(ident.clone(), Some(member), range)); + let Some(key) = property_key_text(self, property) else { + continue; + }; + rest_names.push(key.clone()); + let member = self.member_ident(rhs, &key, range); + self.lower_property_binding(property, member, range, out); + } + } + + /// Binds one property's value expression: an identifier target with a + /// default emits the read-then-ternary pair; a nested pattern reads + /// through a temp (defaulted when a default exists); a bare + /// identifier clones the read. + fn lower_property_binding( + &mut self, + property: &ObjectBindingProperty, + member: Expr, + range: TextRange, + out: &mut Vec, + ) { + match property.binding.data() { + BindingPattern::Identifier(ident) => match &property.initializer { + Some(default) => { + let read = self.temp_ident(); + out.push(self.make_declarator(read.clone(), Some(member), range)); + let ternary = self.default_ternary(&read, default, range); + out.push(self.make_declarator(ident.clone(), Some(ternary), range)); + } + None => out.push(self.make_declarator(ident.clone(), Some(member), range)), + }, + BindingPattern::Object(nested) => { + let value = self.default_or_read(&property.initializer, member, range, out); + self.lower_object_pattern(&value, nested, range, out); + } + BindingPattern::Array(nested) => { + let value = self.default_or_read(&property.initializer, member, range, out); + self.lower_array_pattern(&value, nested, range, out); } + // Rest and invalid shapes never carry defaults; the caller's + // rest pass and parser refusals own them. + _ => {} + } + } + + /// A nested pattern's source: the read temp, or the defaulted temp + /// when a default exists (tsc reads nested bindings off the + /// ternary's value). + fn default_or_read( + &mut self, + initializer: &Option>, + member: Expr, + range: TextRange, + out: &mut Vec, + ) -> IdentifierNode { + let read = self.temp_ident(); + out.push(self.make_declarator(read.clone(), Some(member), range)); + match initializer { + Some(default) => { + let value = self.temp_ident(); + let ternary = self.default_ternary(&read, default, range); + out.push(self.make_declarator(value.clone(), Some(ternary), range)); + value + } + None => read, } - out } fn lower_array_binding( @@ -1848,28 +2322,86 @@ impl<'a> Rewriter<'a> { .unwrap_or_else(|| self.ident_expr("undefined")); out.push(self.make_declarator(rhs.clone(), Some(init), range)); } + self.lower_array_pattern(&rhs, array, range, &mut out); + out + } + + /// Lowers one array pattern against a source identifier. Elements + /// with defaults (`[a = 1]`) emit the read-then-ternary pair; + /// `[a = 1]` under an assignment-wrapped nested pattern + /// (`[[d = 2] = []]`) defaults the element read before the nested + /// pattern consumes it. Holes advance the index without emitting. + fn lower_array_pattern( + &mut self, + rhs: &IdentifierNode, + array: &ArrayBindingPattern, + range: TextRange, + out: &mut Vec, + ) { let mut index = 0usize; for element in &array.elements { match element { - ArrayBindingElement::Elision => index += 1, + ArrayBindingElement::Elision | ArrayBindingElement::Missing(_) => index += 1, ArrayBindingElement::Binding(binding) => match binding.data() { BindingPattern::Identifier(ident) => { - let member = self.member_index(&rhs, index, range); + let member = self.member_index(rhs, index, range); out.push(self.make_declarator(ident.clone(), Some(member), range)); index += 1; } BindingPattern::Rest(rest) => { if let BindingPattern::Identifier(ident) = rest.argument.data() { - let slice = self.slice_call(&rhs, index, range); + let slice = self.slice_call(rhs, index, range); out.push(self.make_declarator(ident.clone(), Some(slice), range)); } } - _ => index += 1, + BindingPattern::Object(nested) => { + let member = self.member_index(rhs, index, range); + let value = self.default_or_read(&None, member, range, out); + self.lower_object_pattern(&value, nested, range, out); + index += 1; + } + BindingPattern::Array(nested) => { + let member = self.member_index(rhs, index, range); + let value = self.default_or_read(&None, member, range, out); + self.lower_array_pattern(&value, nested, range, out); + index += 1; + } + // `[a = 1]`: the default applies to the element read. + BindingPattern::Assignment(assignment) => { + let member = self.member_index(rhs, index, range); + match assignment.left.data() { + BindingPattern::Identifier(ident) => { + let read = self.temp_ident(); + out.push(self.make_declarator(read.clone(), Some(member), range)); + let ternary = self.default_ternary(&read, &assignment.right, range); + out.push(self.make_declarator(ident.clone(), Some(ternary), range)); + } + BindingPattern::Object(nested) => { + let value = self.default_or_read( + &Some(assignment.right.clone()), + member, + range, + out, + ); + self.lower_object_pattern(&value, nested, range, out); + } + BindingPattern::Array(nested) => { + let value = self.default_or_read( + &Some(assignment.right.clone()), + member, + range, + out, + ); + self.lower_array_pattern(&value, nested, range, out); + } + _ => {} + } + index += 1; + } + BindingPattern::Missing(_) => index += 1, }, - ArrayBindingElement::Missing(_) => index += 1, } } - out } fn make_declarator( @@ -7065,6 +7597,7 @@ fn for_clauses_are_clean(for_statement: &ForStatement) -> bool { .initializer .as_deref() .is_none_or(|init| !contains_yield(init)) + && count_binding_yields(&declarator.data().binding) == 0 }) } None => true, @@ -7080,6 +7613,84 @@ fn for_clauses_are_clean(for_statement: &ForStatement) -> bool { init_clean && test_clean && update_clean } +/// Suspensions inside a binding pattern: default initializers and +/// computed key expressions. Destructuring lowering erases nothing once +/// these count — a clean pattern lowers to ternary declarators, a +/// suspending one to machine-owned branch statements. +fn count_binding_yields(pattern: &Pattern) -> u32 { + match pattern.data() { + BindingPattern::Identifier(_) | BindingPattern::Missing(_) => 0, + BindingPattern::Object(object) => object + .properties + .iter() + .map(|property| { + let key = match &property.name { + PropertyName::Computed(key) => count_yields(key), + _ => 0, + }; + let default = property.initializer.as_deref().map_or(0, count_yields); + key + default + count_binding_yields(&property.binding) + }) + .sum(), + BindingPattern::Array(array) => array + .elements + .iter() + .map(|element| match element { + ArrayBindingElement::Elision | ArrayBindingElement::Missing(_) => 0, + ArrayBindingElement::Binding(binding) => count_binding_yields(binding), + }) + .sum(), + BindingPattern::Rest(rest) => count_binding_yields(&rest.argument), + BindingPattern::Assignment(assignment) => { + count_yields(&assignment.right) + count_binding_yields(&assignment.left) + } + } +} + +/// Every identifier a pattern binds, in source order. +fn collect_binding_names(pattern: &Pattern, names: &mut Vec) { + match pattern.data() { + BindingPattern::Identifier(ident) => names.push(ident.clone()), + BindingPattern::Missing(_) => {} + BindingPattern::Object(object) => { + for property in &object.properties { + collect_binding_names(&property.binding, names); + } + } + BindingPattern::Array(array) => { + for element in &array.elements { + if let ArrayBindingElement::Binding(binding) = element { + collect_binding_names(binding, names); + } + } + } + BindingPattern::Rest(rest) => collect_binding_names(&rest.argument, names), + BindingPattern::Assignment(assignment) => { + collect_binding_names(&assignment.left, names); + } + } +} + +/// The await view of the same binding-pattern walk. +fn binding_contains_await(pattern: &Pattern) -> bool { + match pattern.data() { + BindingPattern::Identifier(_) | BindingPattern::Missing(_) => false, + BindingPattern::Object(object) => object.properties.iter().any(|property| { + matches!(&property.name, PropertyName::Computed(key) if contains_await(key)) + || property.initializer.as_deref().is_some_and(contains_await) + || binding_contains_await(&property.binding) + }), + BindingPattern::Array(array) => array.elements.iter().any(|element| match element { + ArrayBindingElement::Elision | ArrayBindingElement::Missing(_) => false, + ArrayBindingElement::Binding(binding) => binding_contains_await(binding), + }), + BindingPattern::Rest(rest) => binding_contains_await(&rest.argument), + BindingPattern::Assignment(assignment) => { + contains_await(&assignment.right) || binding_contains_await(&assignment.left) + } + } +} + /// Whether every branch statement is a shape the labeled if slice emits. fn branch_is_simple(statements: &[Stmt]) -> bool { statements.iter().all(|statement| { @@ -7108,6 +7719,7 @@ fn count_branch_yields(statements: &[Stmt]) -> u32 { .initializer .as_deref() .map_or(0, count_yields) + + count_binding_yields(&declarator.data().binding) }) .sum(), Statement::Continue(_) | Statement::Break(_) | Statement::Empty => 0, @@ -7609,6 +8221,7 @@ fn statements_contain_await(statements: &[Stmt]) -> bool { .initializer .as_deref() .is_some_and(contains_await) + || binding_contains_await(&declarator.data().binding) }), Statement::Return(returned) => returned.argument.as_deref().is_some_and(contains_await), Statement::Block(block) => statements_contain_await(&block.data().statements), @@ -7633,6 +8246,7 @@ fn statements_contain_await(statements: &[Stmt]) -> bool { .initializer .as_deref() .is_some_and(contains_await) + || binding_contains_await(&declarator.data().binding) }) } None => false, From 903122221dc524664336891041231b167257e925 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:05:00 +0900 Subject: [PATCH 11/42] Report yield and await parsed outside their function contexts Illegal nested suspensions parsed silently: a plain function could carry `yield 1`, and a non-async function or arrow inside an async outer could carry `await k`, with the emitter declining to rewrite them and preserving raw syntax in the output. The parser now owns the loudness, because the keyword contexts it already threads through every function-like are exactly the facts the check needs. An argument-bearing yield outside any generator reports BAMTS-P017 (TS1163); a bare `yield` stays quiet, since sloppy code treats it as an identifier reference. Await inside a non-async function-like reports BAMTS-P018 (TS1308); the new KeywordContext.in_function flag spares module top level, where await is legal. Both codes project onto the TypeScript parse-code surface through the existing mapping arms and message table. tsc reports these at check time rather than parse time; bamts emits them at parse time onto the same code and message surface, because the parser holds the context and the checker has no expression walk. The oracle for the exact split (1163 with argument, quiet bare yield, 1308 for nested function and arrow) is tsc 6.0.2 program diagnostics. Mutation reds recorded for both guards. Gates: compiler 1875/0, verification lib 599/0, fmt, clippy clean. --- .../bamts-compiler/src/diagnostics_parser.rs | 8 ++ crates/bamts-compiler/src/parser.rs | 130 ++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/crates/bamts-compiler/src/diagnostics_parser.rs b/crates/bamts-compiler/src/diagnostics_parser.rs index b8a991d..51b6273 100644 --- a/crates/bamts-compiler/src/diagnostics_parser.rs +++ b/crates/bamts-compiler/src/diagnostics_parser.rs @@ -58,6 +58,8 @@ const TS1136: DiagnosticCode = DiagnosticCode::new("TS1136"); const TS1160: DiagnosticCode = DiagnosticCode::new("TS1160"); /// Unterminated regular expression literal. const TS1161: DiagnosticCode = DiagnosticCode::new("TS1161"); +const TS1163: DiagnosticCode = DiagnosticCode::new("TS1163"); +const TS1308: DiagnosticCode = DiagnosticCode::new("TS1308"); /// Expected corresponding JSX closing tag. const TS17002: DiagnosticCode = DiagnosticCode::new("TS17002"); /// JSX fragment has no corresponding closing tag. @@ -88,6 +90,8 @@ pub fn typescript_parse_code(code: DiagnosticCode, message: &str) -> Option TS1003, "BAMTS-P015" => TS17015, "BAMTS-P016" => TS17014, + "BAMTS-P017" => TS1163, + "BAMTS-P018" => TS1308, "BAMTS-C051" if is_export_modifier_on_class_element(message) => TS1031, _ => return None, }) @@ -123,6 +127,10 @@ pub fn typescript_parse_message(code: DiagnosticCode) -> Option<&'static str> { "TS1136" => "Property assignment expected.", "TS1160" => "Unterminated template literal.", "TS1161" => "Unterminated regular expression literal.", + "TS1163" => "A 'yield' expression is only allowed in a generator body.", + "TS1308" => { + "'await' expressions are only allowed within async functions and at the top levels of modules." + } "TS17002" => "Expected corresponding JSX closing tag for '{0}'.", "TS17008" => "JSX element '{0}' has no corresponding closing tag.", "TS17014" => "JSX fragment has no corresponding closing tag.", diff --git a/crates/bamts-compiler/src/parser.rs b/crates/bamts-compiler/src/parser.rs index bd34a8f..d6d8de5 100644 --- a/crates/bamts-compiler/src/parser.rs +++ b/crates/bamts-compiler/src/parser.rs @@ -117,6 +117,10 @@ pub const INVALID_USING_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS pub const USING_DECLARATION_REQUIRES_INITIALIZER: DiagnosticCode = DiagnosticCode::new("BAMTS-P012"); const UNTERMINATED_REGEX: DiagnosticCode = DiagnosticCode::new("BAMTS-L004"); +/// An argument-bearing yield expression parsed outside any generator. +const YIELD_OUTSIDE_GENERATOR: DiagnosticCode = DiagnosticCode::new("BAMTS-P017"); +/// An await expression parsed inside a non-async function-like. +const AWAIT_OUTSIDE_ASYNC: DiagnosticCode = DiagnosticCode::new("BAMTS-P018"); /// A parser operation was interrupted before completion. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -280,6 +284,10 @@ struct ParserCheckpoint { struct KeywordContext { await_reserved: bool, yield_reserved: bool, + /// Inside any function-like body or parameter list. Top-level await + /// is legal in modules, so TS1308 fires only when this is set; + /// yield expressions are illegal outside generators everywhere. + in_function: bool, } /// Whether `global` / string-named module forms are recognized as ambient at @@ -2182,6 +2190,7 @@ impl Parser { // Method, getter, or setter. if self.at(TokenKind::LParen) || self.at_less_like() || is_generator { let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: is_generator, }; @@ -3254,6 +3263,16 @@ impl Parser { } else { None }; + // A yield expression is legal only inside a generator; the + // argument-bearing form is unambiguous, so it reports TS1163 + // wherever it appears outside one. A bare `yield` stays quiet: + // in sloppy code it is a plain identifier reference. + if !self.keyword_context.yield_reserved && argument.is_some() { + self.error_here( + YIELD_OUTSIDE_GENERATOR, + "A 'yield' expression is only allowed in a generator body.", + ); + } self.node( start, Expression::Yield(YieldExpression { delegate, argument }), @@ -3461,6 +3480,15 @@ impl Parser { if self.at(TokenKind::KwAwait) && self.can_start_expression_after(1) { self.bump(); let argument = self.parse_unary_expression(); + // Await belongs to async functions (or module top level, + // which no function-like context covers); a plain nested + // function-like parsing one is TS1308. + if !self.keyword_context.await_reserved && self.keyword_context.in_function { + self.error_here( + AWAIT_OUTSIDE_ASYNC, + "'await' expressions are only allowed within async functions and at the top levels of modules.", + ); + } return self.node( start, Expression::Await(AwaitExpression { @@ -4475,6 +4503,7 @@ impl Parser { // Method. if self.at(TokenKind::LParen) || self.at_less_like() { let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: is_generator, }; @@ -4889,6 +4918,7 @@ impl Parser { None }; let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: is_generator, }; @@ -5118,6 +5148,7 @@ impl Parser { fn parse_simple_arrow(&mut self, start: Utf16Pos, is_async: bool, no_in: bool) -> Expr { let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: false, }; @@ -5152,6 +5183,7 @@ impl Parser { fn parse_paren_arrow(&mut self, start: Utf16Pos, is_async: bool, no_in: bool) -> Expr { let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: false, }; @@ -5190,6 +5222,7 @@ impl Parser { ) -> Option { let checkpoint = self.checkpoint(); let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: false, }; @@ -5218,6 +5251,7 @@ impl Parser { let checkpoint = self.checkpoint(); self.bump(); // `async` let keyword_context = KeywordContext { + in_function: true, await_reserved: true, yield_reserved: false, }; @@ -5258,6 +5292,7 @@ impl Parser { return None; } let keyword_context = KeywordContext { + in_function: true, await_reserved: is_async, yield_reserved: false, }; @@ -6555,6 +6590,101 @@ mod tests { .collect() } + /// B3: an argument-bearing yield outside any generator reports the + /// native code that projects onto TS1163; a bare `yield` stays + /// quiet (sloppy identifier reference), and generators stay clean. + #[test] + fn reports_yield_expression_outside_generators() { + let bad = parse_text("function f() { yield 1; }", ScriptKind::TypeScript); + assert!( + errors(&bad) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P017"), + "yield in plain function must report" + ); + let nested = parse_text( + "function* g() { function h() { yield 1; } yield 2; }", + ScriptKind::TypeScript, + ); + assert!( + errors(&nested) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P017"), + "yield in nested plain function must report" + ); + let bare = parse_text("function f() { var x = yield; }", ScriptKind::TypeScript); + assert!( + !errors(&bare) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P017"), + "bare yield is a sloppy identifier, not a report" + ); + assert_clean("function* g() { yield 1; }"); + } + + /// B3: await inside a non-async function-like (nested function or + /// arrow) reports the native code that projects onto TS1308; + /// async bodies and module top level stay clean. + #[test] + fn reports_await_outside_async_function_likes() { + let nested = parse_text( + "async function f() { function g() { return await k; } }", + ScriptKind::TypeScript, + ); + assert!( + errors(&nested) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P018"), + "await in nested plain function must report" + ); + let arrow = parse_text( + "async function f() { var g = () => await k; }", + ScriptKind::TypeScript, + ); + assert!( + errors(&arrow) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P018"), + "await in non-async arrow must report" + ); + assert_clean("async function f() { return await k; }"); + assert_clean("await k;"); + } + + /// B3: the native codes project onto the TypeScript surface the + /// driver reports - TS1163 and TS1308 with tsc's exact messages. + #[test] + fn projects_context_codes_onto_the_typescript_surface() { + let bad = parse_text("function f() { yield 1; }", ScriptKind::TypeScript); + let projected = crate::diagnostics_parser::typescript_parse_code( + bad.diagnostics()[0].code(), + bad.diagnostics()[0].message(), + ) + .expect("yield code projects"); + assert_eq!(projected.as_str(), "TS1163"); + let awaited = parse_text( + "async function f() { function g() { return await k; } }", + ScriptKind::TypeScript, + ); + let await_code = errors(&awaited) + .iter() + .find(|d| d.code().as_str() == "BAMTS-P018") + .copied() + .expect("await error present"); + let projected = crate::diagnostics_parser::typescript_parse_code( + await_code.code(), + await_code.message(), + ) + .expect("await code projects"); + assert_eq!(projected.as_str(), "TS1308"); + assert_eq!( + crate::diagnostics_parser::typescript_parse_message(projected), + Some( + "'await' expressions are only allowed within async functions and at the top levels of modules." + ) + ); + } + fn assert_clean(text: &str) -> Recovered { let recovered = parse_ts(text); let errs = errors(&recovered); From bc2ef3b9020956eb3555e4c2599a64c9a340cb2d Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:14:37 +0900 Subject: [PATCH 12/42] Hoist suspending class computed keys out of the lowering IIFE A yield inside a computed member key of a class expression lowered at ES5 relocated into the IIFE arrow - a plain function - so `var C = class { [yield k]() {} }` inside a generator emitted a live yield in non-generator syntax. The oracle itself is broken here: tsc 6.0.2 emits `C.prototype[yield k]` inside the IIFE, which is a SyntaxError, so no byte-parity contract holds this shape and the repo's live-yield invariant governs instead. Statement contexts now hoist the suspending key temps through key_prelude into the enclosing statement list: the machine splits them, and the arrow the class lowers into stays clean. Computed keys evaluate before construction and keep their relative order, so the hoist preserves semantics. Expression positions have no statement channel to drain into, so they keep the IIFE shape but carry the requires-es2015 refusal diagnostic instead of breaking silently; a static-field postlude that suspends refuses the same way, since it references the constructed temp and cannot hoist. Making the expression-position form refuse the whole generator needs walker visibility into synthesized arrows and stays on the ladder. Mutation red recorded for the hoist partition. Gates: compiler 1877/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 78 +++++++++++++++++++ .../bamts-compiler/src/emitter/transforms.rs | 47 ++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 66f20ea..eeb4432 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7176,6 +7176,84 @@ var c = () => 1; ); } + /// B1: a suspending computed class key hoists out of the class + /// IIFE in statement contexts - the arrow is a plain function and + /// tsc 6.0.2 itself emits a SyntaxError for the in-IIFE form. The + /// machine splits the hoisted key temp and the arrow stays clean. + #[test] + fn class_computed_key_suspension_hoists_out_of_the_iife() { + let out = emit_es5_clean( + "var k:any;\nfunction* g(){ var C = class { [yield k]() {} }; return C; }\n", + ); + let code = javascript(&out).code.clone(); + assert!(code.contains("__generator(this,"), "machine form: {code}"); + assert!( + code.contains("[4 /*yield*/, k]"), + "the key's suspension must split:\n{code}" + ); + assert!( + code.contains("_t1 = _a.sent()"), + "the key temp binds from sent():\n{code}" + ); + assert!( + code.contains("[_t1]()"), + "the member reads the hoisted temp:\n{code}" + ); + assert!(live_yield_leak(&code).is_none(), "live yield leaked"); + } + + /// B1: expression-position class expressions cannot hoist (no + /// enclosing statement drains key_prelude), so they keep tsc's + /// in-IIFE shape but carry the requires-es2015 refusal instead of + /// breaking silently. + #[test] + fn class_computed_key_in_expression_position_signals_refusal() { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new( + SourceText::new("var k:any;\nfunction* h(){ foo(class { [yield k]() {} }); }\n") + .expect("fits"), + ), + )); + let out = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + assert!( + out.diagnostics + .iter() + .any(|d| d.message().contains("generators require")), + "expression-position suspension must refuse loudly" + ); + } + + /// B7: if-without-else is the empty else, not a refusal - both the + /// inline clean clone and the suspending labeled shape must lower. + #[test] + fn if_without_else_lowers_in_the_machine() { + let suspending = emit_es5_clean( + "var c:any, x:any;\nfunction* g(){ if (c) { yield x; } yield 2; }\n", + ); + let code = javascript(&suspending).code; + assert!(code.contains("__generator(this,"), "machine form: {code}"); + assert!( + code.contains("[4 /*yield*/, x]"), + "the branch suspension must split:\n{code}" + ); + assert!(live_yield_leak(&code).is_none(), "live yield leaked"); + let clean = emit_es5_clean( + "var c:any, x:any;\nfunction* g(){ yield 1; if (c) { x = 1; } }\n", + ); + let code = javascript(&clean).code; + assert!(code.contains("__generator(this,"), "machine form: {code}"); + assert!(code.contains("if (c)"), "clean if clones inline:\n{code}"); + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index edf902b..133d300 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -1802,7 +1802,7 @@ impl<'a> Rewriter<'a> { let start = source.utf16_to_byte(range.start()).expect("binding start"); let end = source.utf16_to_byte(range.end()).expect("binding end"); let name = source.as_str()[start..end].to_owned(); - self.lower_class_expression(value, class, Some(&name)) + self.lower_class_expression(value, class, Some(&name), true) } else { self.rewrite_expr(value) }; @@ -4847,7 +4847,13 @@ impl<'a> Rewriter<'a> { ) -> Option<()> { let range = statement.range(); let cons_block = block_statements(&if_statement.consequent)?; - let alt_block = block_statements(if_statement.alternate.as_deref()?)?; + // A missing alternate is the empty else: fallthrough. Refusing + // the whole generator over `if (c) { ... }` with no else clause + // rejects one of the most common statement shapes. + let alt_block: &[Stmt] = match if_statement.alternate.as_deref() { + Some(alternate) => block_statements(alternate)?, + None => &[], + }; if !branch_is_simple(cons_block) || !branch_is_simple(alt_block) { return None; } @@ -6299,6 +6305,7 @@ impl<'a> Rewriter<'a> { expression: &Expr, class_expression: &ClassExpression, inferred_name: Option<&str>, + hoist_suspending: bool, ) -> Expr { if matches!(FieldMode::for_options(self.options), FieldMode::Native) { let lowered = self.lower_class(&class_expression.class, expression.range(), None); @@ -6328,6 +6335,40 @@ impl<'a> Rewriter<'a> { child_marked, ); } + // A suspending computed key cannot live inside the IIFE: the + // arrow is a plain function, so a yield there is a SyntaxError + // (tsc 6.0.2 emits exactly that broken shape). Where the caller + // drains key_prelude into an enclosing statement list, the + // suspending key temps hoist out and the machine splits them; + // expression contexts cannot hoist, so they keep the IIFE and + // signal the requires-es2015 refusal instead of breaking + // silently. Static-field postludes reference the constructed + // temp and can never hoist; they refuse the same way. + let mut refused = false; + let (suspending, mut clean): (Vec<_>, Vec<_>) = lowered + .prelude + .drain(..) + .partition(|statement| count_branch_yields(std::slice::from_ref(statement)) > 0); + if !suspending.is_empty() { + if hoist_suspending { + self.key_prelude.extend(suspending); + } else { + clean.extend(suspending); + refused = true; + } + } + lowered.prelude = clean; + let postlude_refused = lowered + .postlude + .iter() + .any(|statement| count_branch_yields(std::slice::from_ref(statement)) > 0); + if refused || postlude_refused { + self.diag( + codes::GENERATOR_REQUIRES_ES2015, + expression.range(), + "generators require ScriptTarget::Es2015 or later", + ); + } let class = self.syn_node( expression.range(), Expression::Class(ClassExpression { @@ -6420,7 +6461,7 @@ impl<'a> Rewriter<'a> { Expression::Class(class) => { let previous = self.replace_await; self.replace_await = false; - let lowered = self.lower_class_expression(expression, class, None); + let lowered = self.lower_class_expression(expression, class, None, false); self.replace_await = previous; lowered } From 50ee16fdf80f40544f05d4c04b82edd3244a0245 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:19:09 +0900 Subject: [PATCH 13/42] Treat if-without-else as the empty else in the machine machine_emit_if required a block alternate, so any generator whose body carried `if (c) { ... }` without an else clause - clean or suspending - refused the machine wholesale and fell back to the native form. A missing alternate is simply fallthrough: it now reads as the empty else, which the labeled shape already lowers as a zero-resume branch and the inline clone path copies verbatim. Mutation red recorded: restoring the alternate requirement fails the new pin with the native fallback form. Gates: compiler 1878/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index eeb4432..b1634c4 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7236,20 +7236,18 @@ var c = () => 1; /// inline clean clone and the suspending labeled shape must lower. #[test] fn if_without_else_lowers_in_the_machine() { - let suspending = emit_es5_clean( - "var c:any, x:any;\nfunction* g(){ if (c) { yield x; } yield 2; }\n", - ); - let code = javascript(&suspending).code; + let suspending = + emit_es5_clean("var c:any, x:any;\nfunction* g(){ if (c) { yield x; } yield 2; }\n"); + let code = javascript(&suspending).code.clone(); assert!(code.contains("__generator(this,"), "machine form: {code}"); assert!( code.contains("[4 /*yield*/, x]"), "the branch suspension must split:\n{code}" ); assert!(live_yield_leak(&code).is_none(), "live yield leaked"); - let clean = emit_es5_clean( - "var c:any, x:any;\nfunction* g(){ yield 1; if (c) { x = 1; } }\n", - ); - let code = javascript(&clean).code; + let clean = + emit_es5_clean("var c:any, x:any;\nfunction* g(){ yield 1; if (c) { x = 1; } }\n"); + let code = javascript(&clean).code.clone(); assert!(code.contains("__generator(this,"), "machine form: {code}"); assert!(code.contains("if (c)"), "clean if clones inline:\n{code}"); } From 1b172e11ce70dbaaa6d7ae0e88ac07946aa13c12 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:33:23 +0900 Subject: [PATCH 14/42] Keep sibling, rest, and await semantics in suspending declarations Bot review of the suspending-declaration path found five real gaps, all verified by probe before fixing: a plain identifier declarator alongside a suspending binding evaluated its initializer into a temp and never assigned the name; a suspending computed key assigned an undeclared temp; array rest elements became dead index reads with the rest variable never bound; computed keys never joined the __rest exclusion set; and destructuring initializers were cloned raw, so `var { a } = await k` left a native function* inside __awaiter at ES5. Sibling identifier declarators now assign through rewrite_expr; computed-key temps declare their var in both paths; array rest binds via slice without advancing the index; the exclusion set carries runtime entries - static names as strings, computed keys as tsc's coercion (`typeof t === "symbol" ? t : t + ""`, the oracle's exact shape) - and initializers pass through rewrite_expr in both the clean and suspending paths. Mutation red recorded for the sibling-assignment arm. Gates: compiler 1879/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 42 +++++ .../bamts-compiler/src/emitter/transforms.rs | 146 ++++++++++++++---- 2 files changed, 156 insertions(+), 32 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index b1634c4..bbc14f7 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7252,6 +7252,48 @@ var c = () => 1; assert!(code.contains("if (c)"), "clean if clones inline:\n{code}"); } + /// Bot-round B4 gaps: the suspending-declaration path must keep + /// sibling identifier declarators, declare computed-key temps, + /// bind array rest, exclude computed keys from object rest with + /// tsc's runtime coercion, and rewrite awaited sources so the + /// __awaiter inner generator still machine-converts. + #[test] + fn suspending_declarations_keep_sibling_and_rest_semantics() { + let out = emit_es5_clean( + "var y:any, obj:any;\nfunction* g(){ var x = 5, { a = (yield y) } = obj; }\n", + ); + let code = javascript(&out).code.clone(); + assert!( + code.contains("x = 5"), + "sibling declarator dropped:\n{code}" + ); + let out = emit_es5_clean( + "var k:any, arr:any;\nfunction* g(){ var [a = (yield k), ...r] = arr; }\n", + ); + let code = javascript(&out).code.clone(); + assert!( + code.contains("r = arr.slice(1)"), + "array rest discarded:\n{code}" + ); + let out = + emit_es5_clean("var k:any, o:any;\nfunction* g(){ var { [k]: v, ...rest } = o; }\n"); + let code = javascript(&out).code.clone(); + assert!( + code.contains("typeof _t0 === \"symbol\" ? _t0 : _t0 + \"\""), + "computed key must be excluded at runtime (tsc shape):\n{code}" + ); + let out = emit_es5_clean("var k:any, o:any;\nfunction* g(){ var { [yield k]: v } = o; }\n"); + let code = javascript(&out).code.clone(); + assert!(code.contains("var v, _t0, _t1"), "temps declared:\n{code}"); + assert!(live_yield_leak(&code).is_none(), "live yield leaked"); + let out = emit_es5_clean("var k:any;\nasync function h(){ var { a } = await k; }\n"); + let code = javascript(&out).code.clone(); + assert!( + code.contains("__generator(this,"), + "awaited source must rewrite so the inner machine converts:\n{code}" + ); + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 133d300..6872f22 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -1846,13 +1846,21 @@ impl<'a> Rewriter<'a> { out.push(hoist); } for declarator in &declaration.declarations { - let (rhs, needs_temp) = self.rhs_ident(declarator.data().initializer.as_deref(), range); + let initializer = declarator.data().initializer.as_deref(); + if let BindingPattern::Identifier(ident) = declarator.data().binding.data() { + // A plain identifier declarator alongside a suspending + // binding still needs its assignment; the rewrite pass + // owns the initializer's awaits. + if let Some(init) = initializer { + let value = self.rewrite_expr(init); + out.push(self.assign_statement(ident, value, range)); + } + continue; + } + let (rhs, needs_temp) = self.rhs_ident(initializer, range); if needs_temp { - let init = declarator - .data() - .initializer - .as_deref() - .cloned() + let init = initializer + .map(|value| self.rewrite_expr(value)) .unwrap_or_else(|| self.ident_expr("undefined")); let declarations = vec![self.make_declarator(rhs.clone(), Some(init), range)]; let temp = self.node( @@ -1885,12 +1893,24 @@ impl<'a> Rewriter<'a> { BindingPattern::Identifier(_) | BindingPattern::Missing(_) => {} BindingPattern::Rest(_) => {} BindingPattern::Object(object) => { - let mut rest_names = Vec::new(); + let mut rest_keys: Vec = Vec::new(); for property in &object.properties { // The member expression this property reads. let member = match &property.name { PropertyName::Computed(key) => { let temp = self.temp_ident(); + // The key temp needs its `var` even when the + // key suspends: the assignment form would + // otherwise target an undeclared name. + let temp_decl = vec![self.make_declarator(temp.clone(), None, range)]; + out.push(self.node( + range, + Statement::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations: temp_decl, + }), + )); if contains_yield(key) { let assign = self.assign_statement(&temp, key.as_ref().clone(), range); @@ -1899,33 +1919,27 @@ impl<'a> Rewriter<'a> { let value = self.rewrite_expr(key); out.push(self.make_temp_declaration(temp.clone(), value, range)); } + rest_keys.push(RestExcludeKey::Computed(temp.clone())); let reference = self.node(temp.range(), Expression::Identifier(temp.clone())); self.member_computed(rhs, &reference, range) } _ => match property_key_text(self, property) { Some(key) => { - rest_names.push(key.clone()); + rest_keys.push(RestExcludeKey::Static(key.clone())); self.member_ident(rhs, &key, range) } None => continue, }, }; - self.emit_property_statements( - property, - member, - range, - &mut rest_names, - rhs, - out, - ); + self.emit_property_statements(property, member, range, rhs, out); } // The rest property binds after the named reads. for property in &object.properties { if let BindingPattern::Rest(rest) = property.binding.data() && let BindingPattern::Identifier(ident) = rest.argument.data() { - let excluded = self.rest_exclude_literal(&rest_names, range); + let excluded = self.rest_exclude_array(&rest_keys, range); let call = self.rest_call(rhs, excluded, range); out.push(self.assign_statement(ident, call, range)); } @@ -1939,6 +1953,15 @@ impl<'a> Rewriter<'a> { index += 1; } ArrayBindingElement::Binding(binding) => { + if let BindingPattern::Rest(rest) = binding.data() + && let BindingPattern::Identifier(ident) = rest.argument.data() + { + // Rest consumes the remainder without + // advancing the element index. + let slice = self.slice_call(rhs, index, range); + out.push(self.assign_statement(ident, slice, range)); + continue; + } self.emit_element_statements(rhs, binding, index, range, out); index += 1; } @@ -1965,7 +1988,6 @@ impl<'a> Rewriter<'a> { property: &ObjectBindingProperty, member: Expr, range: TextRange, - _rest_names: &mut Vec, _rhs: &IdentifierNode, out: &mut Vec, ) { @@ -2195,7 +2217,7 @@ impl<'a> Rewriter<'a> { let mut out = Vec::new(); if needs_temp { let init = initializer - .cloned() + .map(|value| self.rewrite_expr(value)) .unwrap_or_else(|| self.ident_expr("undefined")); out.push(self.make_declarator(rhs.clone(), Some(init), range)); } @@ -2214,8 +2236,8 @@ impl<'a> Rewriter<'a> { object: &ObjectBindingPattern, range: TextRange, out: &mut Vec, - ) { - let mut rest_names = Vec::new(); + ) -> Vec { + let mut rest_keys = Vec::new(); for property in &object.properties { let PropertyName::Computed(key) = &property.name else { continue; @@ -2224,6 +2246,7 @@ impl<'a> Rewriter<'a> { let value = self.rewrite_expr(key); let declaration = self.make_temp_declaration(temp.clone(), value, range); self.key_prelude.push(declaration); + rest_keys.push(RestExcludeKey::Computed(temp.clone())); let reference = self.node(temp.range(), Expression::Identifier(temp.clone())); let member = self.member_computed(rhs, &reference, range); self.lower_property_binding(property, member, range, out); @@ -2234,7 +2257,7 @@ impl<'a> Rewriter<'a> { } if let BindingPattern::Rest(rest) = property.binding.data() { if let BindingPattern::Identifier(ident) = rest.argument.data() { - let excluded = self.rest_exclude_literal(&rest_names, range); + let excluded = self.rest_exclude_array(&rest_keys, range); let call = self.rest_call(rhs, excluded, range); out.push(self.make_declarator(ident.clone(), Some(call), range)); } @@ -2243,10 +2266,11 @@ impl<'a> Rewriter<'a> { let Some(key) = property_key_text(self, property) else { continue; }; - rest_names.push(key.clone()); + rest_keys.push(RestExcludeKey::Static(key.clone())); let member = self.member_ident(rhs, &key, range); self.lower_property_binding(property, member, range, out); } + rest_keys } /// Binds one property's value expression: an identifier target with a @@ -2318,7 +2342,7 @@ impl<'a> Rewriter<'a> { let mut out = Vec::new(); if needs_temp { let init = initializer - .cloned() + .map(|value| self.rewrite_expr(value)) .unwrap_or_else(|| self.ident_expr("undefined")); out.push(self.make_declarator(rhs.clone(), Some(init), range)); } @@ -2682,17 +2706,67 @@ impl<'a> Rewriter<'a> { ) } - fn rest_exclude_literal(&mut self, names: &[String], range: TextRange) -> Expr { + /// The `__rest` exclusion array. Static keys contribute plain + /// strings; computed keys contribute tsc's runtime coercion + /// (`typeof t === "symbol" ? t : t + ""`), because the excluded + /// property name is only known at runtime. + fn rest_exclude_array(&mut self, keys: &[RestExcludeKey], range: TextRange) -> Expr { let mut elements = Vec::new(); - for name in names { - let literal = self.string_literal(name); - let expr = self.node( - literal.range(), - Expression::Literal(Literal::String(literal)), - ); + for key in keys { + let expr = match key { + RestExcludeKey::Static(name) => { + let literal = self.string_literal(name); + self.node( + literal.range(), + Expression::Literal(Literal::String(literal)), + ) + } + RestExcludeKey::Computed(temp) => { + let temp_expr = self.node(temp.range(), Expression::Identifier(temp.clone())); + let symbol = self.string_literal("symbol"); + let symbol_expr = + self.node(symbol.range(), Expression::Literal(Literal::String(symbol))); + let typeof_argument = + self.node(temp.range(), Expression::Identifier(temp.clone())); + let typeof_expr = self.node( + temp.range(), + Expression::Unary(UnaryExpression { + operator: UnaryOperator::Typeof, + argument: Box::new(typeof_argument), + }), + ); + let test = self.node( + temp.range(), + Expression::Binary(BinaryExpression { + operator: BinaryOperator::StrictEqual, + left: Box::new(typeof_expr), + right: Box::new(symbol_expr), + }), + ); + let empty = self.string_literal(""); + let empty_expr = + self.node(empty.range(), Expression::Literal(Literal::String(empty))); + let concat_left = self.node(temp.range(), Expression::Identifier(temp.clone())); + let concat = self.node( + temp.range(), + Expression::Binary(BinaryExpression { + operator: BinaryOperator::Add, + left: Box::new(concat_left), + right: Box::new(empty_expr), + }), + ); + self.node( + temp.range(), + Expression::Conditional(ConditionalExpression { + test: Box::new(test), + consequent: Box::new(temp_expr), + alternate: Box::new(concat), + }), + ) + } + }; elements.push(ArrayElement::Expression(Box::new(expr))); } - let _ = range; self.node(range, Expression::Array(ArrayLiteral { elements })) } @@ -7654,6 +7728,14 @@ fn for_clauses_are_clean(for_statement: &ForStatement) -> bool { init_clean && test_clean && update_clean } +/// One `__rest` exclusion entry: a static property name or the temp +/// holding an evaluated computed key (excluded at runtime via tsc's +/// symbol-aware coercion). +enum RestExcludeKey { + Static(String), + Computed(IdentifierNode), +} + /// Suspensions inside a binding pattern: default initializers and /// computed key expressions. Destructuring lowering erases nothing once /// these count — a clean pattern lowers to ternary declarators, a From c9b792e4109477971b65fe8c9a5ab11988a64b82 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:41:09 +0900 Subject: [PATCH 15/42] Anchor, gate, and contextualize the yield and await diagnostics The first cut of the context diagnostics reported at whatever token followed the parsed operand, fired on sloppy JavaScript where yield and await are ordinary identifiers, and let class static blocks inherit the enclosing function's async and generator state. All three close here: the diagnostics anchor at the keyword's own token range, both guards take an is_typescript gate so .js sources stay silent (tsc reserves these contextually in TypeScript), and static blocks parse their body under a fresh KeywordContext where both flags are false, so await and yield are context-illegal inside one regardless of the enclosing function. Window: retroactive ACQUIRE announced over hub covering this wave (pre-state 1bd6d9c) through this release. Post-hashes: parser.rs 40d001370f99b9f1a5236f79ad9dfc0cee404141e6eff6853eddd5206dd49830 diagnostics_parser.rs 37479c05e912303e7bb1e6840a6c8351ec6ebd30d5d53c3515887191a1032217 Mutation red recorded for the anchoring (reverting to error_here fails the range pin). Gates: compiler 1880/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/parser.rs | 82 ++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/crates/bamts-compiler/src/parser.rs b/crates/bamts-compiler/src/parser.rs index d6d8de5..071ded5 100644 --- a/crates/bamts-compiler/src/parser.rs +++ b/crates/bamts-compiler/src/parser.rs @@ -2105,7 +2105,15 @@ impl Parser { && property_modifier == PropertyModifier::None && !is_async { - let block = self.parse_block(); + // The block's own keyword context: neither the enclosing + // function's async nor its generator state applies, so both + // await and yield are context-illegal inside. + let context = KeywordContext { + in_function: true, + await_reserved: false, + yield_reserved: false, + }; + let block = self.with_keyword_context(context, Self::parse_block); return self.node(start, ClassMember::StaticBlock(block)); } @@ -3253,6 +3261,7 @@ impl Parser { } fn parse_yield_expression(&mut self, start: Utf16Pos, no_in: bool) -> Expr { + let keyword_range = self.cur().range(); self.bump(); let delegate = self.at(TokenKind::Star) && !self.has_newline_before(); if delegate { @@ -3267,9 +3276,12 @@ impl Parser { // argument-bearing form is unambiguous, so it reports TS1163 // wherever it appears outside one. A bare `yield` stays quiet: // in sloppy code it is a plain identifier reference. - if !self.keyword_context.yield_reserved && argument.is_some() { - self.error_here( + if !self.keyword_context.yield_reserved && argument.is_some() && self.is_typescript() { + // Anchored at the yield keyword; TypeScript sources only, + // since sloppy scripts may call `yield(1)` as an identifier. + self.error_at( YIELD_OUTSIDE_GENERATOR, + keyword_range, "A 'yield' expression is only allowed in a generator body.", ); } @@ -3478,14 +3490,20 @@ impl Parser { } if self.at(TokenKind::KwAwait) && self.can_start_expression_after(1) { + let keyword_range = self.cur().range(); self.bump(); let argument = self.parse_unary_expression(); // Await belongs to async functions (or module top level, // which no function-like context covers); a plain nested - // function-like parsing one is TS1308. - if !self.keyword_context.await_reserved && self.keyword_context.in_function { - self.error_here( + // function-like parsing one is TS1308. Anchored at the + // await keyword, TypeScript sources only. + if !self.keyword_context.await_reserved + && self.keyword_context.in_function + && self.is_typescript() + { + self.error_at( AWAIT_OUTSIDE_ASYNC, + keyword_range, "'await' expressions are only allowed within async functions and at the top levels of modules.", ); } @@ -6651,6 +6669,58 @@ mod tests { assert_clean("await k;"); } + /// B3 follow-ups: the diagnostics anchor at the yield/await keyword + /// (not the token after the operand), sloppy JavaScript stays + /// silent, and class static blocks carry their own context. + #[test] + fn context_diagnostics_anchor_gate_and_static_blocks() { + let bad = parse_text("function f() { yield 1; }", ScriptKind::TypeScript); + let diag = errors(&bad) + .iter() + .find(|d| d.code().as_str() == "BAMTS-P017") + .copied() + .expect("yield diagnostic"); + assert_eq!( + diag.range().start().get(), + "function f() { ".len(), + "range must start at the yield keyword" + ); + let sloppy = parse_text("function f() { yield(1); }", ScriptKind::JavaScript); + assert!( + !errors(&sloppy) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P017"), + "sloppy js identifier call must stay silent" + ); + let sloppy_await = parse_text("function f() { await(1); }", ScriptKind::JavaScript); + assert!( + !errors(&sloppy_await) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P018"), + "sloppy js await call must stay silent" + ); + let static_block = parse_text( + "async function f() { class C { static { await x; } } }", + ScriptKind::TypeScript, + ); + assert!( + errors(&static_block) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P018"), + "await in a static block is context-illegal regardless of the enclosing async" + ); + let static_block_yield = parse_text( + "function* g() { class C { static { yield x; } } }", + ScriptKind::TypeScript, + ); + assert!( + errors(&static_block_yield) + .iter() + .any(|d| d.code().as_str() == "BAMTS-P017"), + "yield in a static block is context-illegal regardless of the enclosing generator" + ); + } + /// B3: the native codes project onto the TypeScript surface the /// driver reports - TS1163 and TS1308 with tsc's exact messages. #[test] From 20f1ec8a0c0cf357969c1d7850f60c673099be01 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:47:58 +0900 Subject: [PATCH 16/42] Harden the leak detectors against identifier substrings Bot review caught both test helpers matching prefixes: raw_return_leak flagged statements like returning = 1; because they start with the return prefix, and live_yield_leak flagged identifiers like yielding because the keyword check was a plain contains. Both now require the match to end as a token (next char not identifier-continuing), and the return boundary accepts the delimiter characters a real return statement actually carries. The state-name timeout pin drops from 30 to 10 seconds, bounding the CPU a wedged compile thread burns before the runner tears down. The labels-table degenerate-switch restriction stays banked as a test-strictness ladder item. Gates: compiler 1880/0, verification lib 599/0 (one known slow-cell flake on the first run, clean on rerun), fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index bbc14f7..3465727 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7323,7 +7323,17 @@ var c = () => 1; // Strip the protocol marker spans first: a line can carry both a // legitimate `return [4 /*yield*/, x]` split and a cloned live // yield; the marker must not exempt the whole line. - .find(|line| line.replace("/*yield*/", "").contains("yield")) + .find(|line| { + let stripped = line.replace("/*yield*/", ""); + // The match must end as a token: `yielding` is an + // identifier, not the keyword. + stripped.match_indices("yield").any(|(i, _)| { + stripped[i + 5..] + .chars() + .next() + .is_none_or(|c| !(c.is_alphanumeric() || c == '_' || c == '$')) + }) + }) .map(|line| line.trim().to_owned()) } @@ -7339,7 +7349,15 @@ var c = () => 1; code.lines() .find(|line| { let l = line.trim(); + // `return` must end as a token: `returning = 1;` is a + // plain assignment, not a protocol-breaking return. + let boundary = !l.starts_with("return") + || l[6..] + .chars() + .next() + .is_none_or(|c| c.is_whitespace() || c == ';' || c == '}' || c == '('); l.starts_with("return") + && boundary && !l.starts_with("return [") && !l.starts_with("return __generator") && !l.starts_with("return __awaiter") @@ -7928,7 +7946,7 @@ var c = () => 1; let _ = tx.send(javascript(&output).code.clone()); }); let code = rx - .recv_timeout(std::time::Duration::from_secs(30)) + .recv_timeout(std::time::Duration::from_secs(10)) .expect("allocator must terminate past the alphabet"); assert!(code.contains("__generator(this,"), "{code}"); } From 532b18435c924ffcf7c2347718d4e5e2e01d6a2e Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:06:48 +0900 Subject: [PATCH 17/42] Preserve key evaluation order and make class contexts visible Two defects in the B1 hoist, both caught by review and verified by probe before fixing. The prelude partition reordered computed keys: hoisting only the suspending temps moved them above clean temps that stayed in the IIFE, so class { [k1()](){} [yield k2()](){} } evaluated k2 first. Computed keys evaluate in member order, so the hoist now moves the whole prelude whenever any entry suspends - entries never reference the class temp, whose declaration is pushed after the move, so the move is order-safe. Pinned in both orders. The class walkers treated whole class expressions as opaque, so an await in a computed key or heritage clause in expression position flowed verbatim into a synthesized plain arrow the machine cannot see through - the same SyntaxError class B1 fixed for yields. The walkers (count_yields, contains_yield, contains_await) now descend heritage, computed names, and field initializers while method, constructor, and static-block bodies stay owned; the suspend predicate gains the await view; and the refusal branch returns the unlowered class so the generator refuses natively under the requires-es2015 diagnostic instead of burying the raw suspension. Pinned for computed-key and heritage awaits. Mutation reds recorded for both: reintroducing the partition fails the order pin, blinding the await walker fails the refusal pin. Gates: compiler 1882/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 66 +++++++++ .../bamts-compiler/src/emitter/transforms.rs | 140 +++++++++++++++--- 2 files changed, 186 insertions(+), 20 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 3465727..97dcdd1 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7294,6 +7294,72 @@ var c = () => 1; ); } + /// B1 order: computed keys evaluate in member order, so hoisting + /// must move the whole prelude - partitioning suspending keys away + /// from clean ones reorders evaluation. + #[test] + fn class_computed_keys_evaluate_in_member_order() { + let out = emit_es5_clean( + "var k1:any, k2:any;\nfunction* g(){ var C = class { [k1()](){} [yield k2()](){} }; }\n", + ); + let code = javascript(&out).code.clone(); + let clean_first = code.find("k1()").expect("clean key evaluated"); + let suspending = code.find("k2()").expect("suspending key evaluated"); + assert!( + clean_first < suspending, + "member order must survive hoisting:\n{code}" + ); + assert!(live_yield_leak(&code).is_none(), "live yield leaked"); + let out = emit_es5_clean( + "var k1:any, k2:any;\nfunction* g(){ var C = class { [yield k1()](){} [k2()](){} }; }\n", + ); + let code = javascript(&out).code.clone(); + let suspending = code.find("k1()").expect("suspending key evaluated"); + let clean_second = code.find("k2()").expect("clean key evaluated"); + assert!( + suspending < clean_second, + "member order must survive hoisting (suspending first):\n{code}" + ); + } + + /// B1 visibility: an await inside a class computed key or heritage + /// clause in expression position cannot lower - there is no hoist + /// channel - and must refuse the generator natively under the + /// requires-es2015 diagnostic, never bury the raw await inside a + /// synthesized arrow the machine cannot see through. + #[test] + fn class_expression_awaits_refuse_natively() { + for src in [ + "var k:any;\nasync function h(){ foo(class { [await k](){} }); }\n", + "var f:any;\nasync function h(){ return class extends (await f()) {}; }\n", + ] { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(src).expect("fits")), + )); + let out = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let code = javascript(&out).code.clone(); + assert!( + !code.contains("__generator(this,"), + "machine must refuse: {src}\n{code}" + ); + assert!( + out.diagnostics + .iter() + .any(|d| d.message().contains("generators require")), + "refusal must be signalled: {src}" + ); + } + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 6872f22..d96118e 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -6418,30 +6418,39 @@ impl<'a> Rewriter<'a> { // signal the requires-es2015 refusal instead of breaking // silently. Static-field postludes reference the constructed // temp and can never hoist; they refuse the same way. - let mut refused = false; - let (suspending, mut clean): (Vec<_>, Vec<_>) = lowered - .prelude - .drain(..) - .partition(|statement| count_branch_yields(std::slice::from_ref(statement)) > 0); - if !suspending.is_empty() { - if hoist_suspending { - self.key_prelude.extend(suspending); - } else { - clean.extend(suspending); - refused = true; - } + // Whole-prelude move only: partitioning the prelude would + // reorder mixed suspending and clean keys, and computed keys + // evaluate in member order. Entries never reference the class + // temp (its declaration is pushed below), so the move is + // order-safe. An await counts too: in expression position the + // rewrite pass leaves it raw, and the machine entry gate then + // refuses the whole generator loudly. + let suspends = |statement: &Stmt| { + count_branch_yields(std::slice::from_ref(statement)) > 0 + || statements_contain_await(std::slice::from_ref(statement)) + }; + let refused = lowered.prelude.iter().any(&suspends) && !hoist_suspending; + if !refused && hoist_suspending && lowered.prelude.iter().any(&suspends) { + self.key_prelude + .extend(std::mem::take(&mut lowered.prelude)); } - lowered.prelude = clean; - let postlude_refused = lowered - .postlude - .iter() - .any(|statement| count_branch_yields(std::slice::from_ref(statement)) > 0); + let postlude_refused = lowered.postlude.iter().any(suspends); if refused || postlude_refused { self.diag( codes::GENERATOR_REQUIRES_ES2015, expression.range(), "generators require ScriptTarget::Es2015 or later", ); + // Returning the unlowered class keeps the failure loud and + // honest: the machine's class walker (heritage, computed + // names, field initializers) sees the suspension, refuses + // the conversion, and the generator stays native under the + // diagnostic. Lowering anyway would bury a raw yield or + // await inside a synthesized plain arrow the machine + // cannot see through. + if refused { + return expression.clone(); + } } let class = self.syn_node( expression.range(), @@ -7923,6 +7932,88 @@ impl ChainSegment { } } +/// The enclosing-context code of a class: the heritage expression, +/// computed member names, and field initializers. Method, constructor, +/// and static-block bodies are function boundaries and own their +/// suspensions, so they stay opaque to the enclosing walkers. +fn count_class_context_yields(class: &ClassDeclaration) -> u32 { + let heritage = class + .extends + .as_ref() + .map(|heritage| count_yields(&heritage.expression)) + .unwrap_or(0); + let members: u32 = class + .members + .iter() + .map(|member| match member.data() { + ClassMember::Method(method) => match &method.name { + PropertyName::Computed(key) => count_yields(key), + _ => 0, + }, + ClassMember::Property(property) => match &property.name { + PropertyName::Computed(key) => count_yields(key), + _ => property.initializer.as_deref().map_or(0, count_yields), + }, + ClassMember::AutoAccessor(accessor) => match &accessor.name { + PropertyName::Computed(key) => count_yields(key), + _ => accessor.initializer.as_deref().map_or(0, count_yields), + }, + _ => 0, + }) + .sum(); + heritage + members +} + +/// The await view of the same class-context walk. +fn class_context_contains_await(class: &ClassDeclaration) -> bool { + if class + .extends + .as_ref() + .is_some_and(|heritage| contains_await(&heritage.expression)) + { + return true; + } + class.members.iter().any(|member| match member.data() { + ClassMember::Method(method) => { + matches!(&method.name, PropertyName::Computed(key) if contains_await(key)) + } + ClassMember::Property(property) => { + matches!(&property.name, PropertyName::Computed(key) if contains_await(key)) + || property.initializer.as_deref().is_some_and(contains_await) + } + ClassMember::AutoAccessor(accessor) => { + matches!(&accessor.name, PropertyName::Computed(key) if contains_await(key)) + || accessor.initializer.as_deref().is_some_and(contains_await) + } + _ => false, + }) +} + +/// The yield view of the same class-context walk. +fn class_context_contains_yield(class: &ClassDeclaration) -> bool { + if class + .extends + .as_ref() + .is_some_and(|heritage| contains_yield(&heritage.expression)) + { + return true; + } + class.members.iter().any(|member| match member.data() { + ClassMember::Method(method) => { + matches!(&method.name, PropertyName::Computed(key) if contains_yield(key)) + } + ClassMember::Property(property) => { + matches!(&property.name, PropertyName::Computed(key) if contains_yield(key)) + || property.initializer.as_deref().is_some_and(contains_yield) + } + ClassMember::AutoAccessor(accessor) => { + matches!(&accessor.name, PropertyName::Computed(key) if contains_yield(key)) + || accessor.initializer.as_deref().is_some_and(contains_yield) + } + _ => false, + }) +} + /// Counts the yield nodes of this function's body (nested function-likes /// own their own yields). fn count_yields(expression: &Expr) -> u32 { @@ -7930,7 +8021,10 @@ fn count_yields(expression: &Expr) -> u32 { Expression::Yield(yielded) => 1 + yielded.argument.as_deref().map_or(0, count_yields), Expression::Identifier(_) | Expression::This | Expression::Super => 0, Expression::Literal(_) | Expression::Meta(_) | Expression::Missing(_) => 0, - Expression::Function(_) | Expression::Class(_) | Expression::Arrow(_) => 0, + Expression::Function(_) | Expression::Arrow(_) => 0, + // Heritage, computed names, and field initializers run in the + // enclosing context; bodies stay owned. + Expression::Class(class) => count_class_context_yields(&class.class), Expression::Template(template) => template.expressions.iter().map(count_yields).sum(), Expression::Array(array) => array .elements @@ -8250,7 +8344,10 @@ fn contains_await(expression: &Expr) -> bool { Expression::Await(_) => true, Expression::Identifier(_) | Expression::This | Expression::Super => false, Expression::Literal(_) | Expression::Meta(_) | Expression::Missing(_) => false, - Expression::Function(_) | Expression::Class(_) | Expression::Arrow(_) => false, + Expression::Function(_) | Expression::Arrow(_) => false, + // Heritage, computed names, and field initializers run in the + // enclosing context; bodies stay owned. + Expression::Class(class) => class_context_contains_await(&class.class), Expression::Yield(yielded) => yielded.argument.as_deref().is_some_and(contains_await), Expression::Template(template) => template.expressions.iter().any(contains_await), Expression::Array(array) => array.elements.iter().any(|element| match element { @@ -8420,7 +8517,10 @@ fn contains_yield(expression: &Expr) -> bool { Expression::Identifier(_) | Expression::This | Expression::Super => false, Expression::Literal(_) | Expression::Meta(_) | Expression::Missing(_) => false, // A yield inside a nested function belongs to that function. - Expression::Function(_) | Expression::Class(_) | Expression::Arrow(_) => false, + Expression::Function(_) | Expression::Arrow(_) => false, + // Heritage, computed names, and field initializers run in the + // enclosing context; bodies stay owned. + Expression::Class(class) => class_context_contains_yield(&class.class), Expression::Template(template) => template.expressions.iter().any(contains_yield), Expression::TaggedTemplate(_) => true, Expression::Array(array) => contains_yield_array(array), From 3986c8e8dee6f622fa77f6d5bfe95a90fb3c0c3a Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:04:19 +0900 Subject: [PATCH 18/42] Make static-field suspensions refuse the way key suspensions do The postlude branch pushed the requires-es2015 diagnostic but fell through to build the IIFE, so a suspending static-field initializer (static x = yield k) still landed as a live yield inside the synthesized plain arrow - machine-cloned, because arrows stay opaque to the clone gates. The refusal now returns the unlowered class for postludes exactly as for preludes: the machine walker sees the suspension, the generator stays native under the signal. Direct code read caught it after the previous commit shipped a comment claiming postludes refuse the same way; the comment is now true. Verified by probe before the fix, pinned after, mutation red recorded for the fall-through form. Decorators join the class-context descent in all three walkers - @d(yield k) class {} is the same enclosing-context family as heritage and computed keys - and the refusal pins now match the diagnostic code constant instead of a message substring, aligned with every other refusal test in the file. Gates: compiler 1883/0, verification lib 599/0 (one child-process spawn flake on the first run, clean on rerun), fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 39 ++++++++++++++++- .../bamts-compiler/src/emitter/transforms.rs | 42 ++++++++++++------- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 97dcdd1..b46d348 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7354,12 +7354,49 @@ var c = () => 1; assert!( out.diagnostics .iter() - .any(|d| d.message().contains("generators require")), + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), "refusal must be signalled: {src}" ); } } + /// A suspending static-field initializer (postlude) refuses the + /// same way a key prelude does: the unlowered class keeps the + /// suspension visible and the generator stays native under the + /// signal - never a live yield inside the synthesized arrow. + #[test] + fn class_static_field_suspension_refuses_natively() { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new( + SourceText::new( + "var k:any;\nfunction* g(){ var C = class { static x = yield k; }; }\n", + ) + .expect("fits"), + ), + )); + let out = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let code = javascript(&out).code.clone(); + assert!( + !code.contains("__generator(this,"), + "machine must refuse the static-field suspension:\n{code}" + ); + assert!( + out.diagnostics + .iter() + .any(|d| d.code() == transforms::codes::GENERATOR_REQUIRES_ES2015), + "refusal must be signalled" + ); + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index d96118e..e08ab43 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -6447,10 +6447,9 @@ impl<'a> Rewriter<'a> { // the conversion, and the generator stays native under the // diagnostic. Lowering anyway would bury a raw yield or // await inside a synthesized plain arrow the machine - // cannot see through. - if refused { - return expression.clone(); - } + // cannot see through - the postlude (static-field + // initializers) included, not just the key prelude. + return expression.clone(); } let class = self.syn_node( expression.range(), @@ -7932,11 +7931,16 @@ impl ChainSegment { } } -/// The enclosing-context code of a class: the heritage expression, -/// computed member names, and field initializers. Method, constructor, -/// and static-block bodies are function boundaries and own their -/// suspensions, so they stay opaque to the enclosing walkers. +/// The enclosing-context code of a class: decorators, the heritage +/// expression, computed member names, and field initializers. Method, +/// constructor, and static-block bodies are function boundaries and own +/// their suspensions, so they stay opaque to the enclosing walkers. fn count_class_context_yields(class: &ClassDeclaration) -> u32 { + let decorators: u32 = class + .decorators + .iter() + .map(|decorator| count_yields(&decorator.data().expression)) + .sum(); let heritage = class .extends .as_ref() @@ -7961,15 +7965,19 @@ fn count_class_context_yields(class: &ClassDeclaration) -> u32 { _ => 0, }) .sum(); - heritage + members + decorators + heritage + members } /// The await view of the same class-context walk. fn class_context_contains_await(class: &ClassDeclaration) -> bool { if class - .extends - .as_ref() - .is_some_and(|heritage| contains_await(&heritage.expression)) + .decorators + .iter() + .any(|decorator| contains_await(&decorator.data().expression)) + || class + .extends + .as_ref() + .is_some_and(|heritage| contains_await(&heritage.expression)) { return true; } @@ -7992,9 +8000,13 @@ fn class_context_contains_await(class: &ClassDeclaration) -> bool { /// The yield view of the same class-context walk. fn class_context_contains_yield(class: &ClassDeclaration) -> bool { if class - .extends - .as_ref() - .is_some_and(|heritage| contains_yield(&heritage.expression)) + .decorators + .iter() + .any(|decorator| contains_yield(&decorator.data().expression)) + || class + .extends + .as_ref() + .is_some_and(|heritage| contains_yield(&heritage.expression)) { return true; } From 4c9f6428a1a72f7c7a916c38733c107742d8e27e Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:20:26 +0900 Subject: [PATCH 19/42] Lower for-of to the index form and convert for-in heads at ES5 for-of emitted verbatim at ES5 in every context - plain functions carried const and the iterator syntax silently, and generators refused wholesale. The oracle (tsc 6.0.2 transpile, target es5, default settings) pins the default lowering: index form, for (var _i = 0, src = IT; _i < src.length; _i++), with the binding declared inside the body as src[_i] - so pattern bindings and defaults compose through the ordinary destructuring lowering, and a generator body suspending through the lowered loop machine-splits through the existing classic-for arms. A braceless body is a single statement, not a missing one: the assembly matches on the rewritten body and pushes non-block statements instead of dropping them (probed before the fix - the dropped-statement form was confirmed on the first cut). for-in keeps its native construct and converts only the head binding to var at ES5. Assignment-target for-of bindings keep the verbatim statement rather than a half-lowering. Pinned behaviorally: node executes the lowered loop and prints 1,2,3,a (every element, then the for-in key); the generator row asserts the machine split with no live yield leak; the braceless row asserts the body statement survives. Mutation red recorded (disabling the lowering fails the behavioral pin). Gates: compiler 1886/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 33 ++++ .../bamts-compiler/src/emitter/transforms.rs | 179 +++++++++++++++++- 2 files changed, 211 insertions(+), 1 deletion(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index b46d348..322c21a 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7397,6 +7397,39 @@ var c = () => 1; ); } + /// A for-of body suspending in a generator machine-splits through + /// the lowered classic loop - the ladder's machine slice composing + /// with the plain lowering. + #[test] + fn for_of_suspension_machine_splits_through_the_lowered_loop() { + let out = + emit_es5_clean("var it:any;\nfunction* g(){ for (const k of it) { yield k; } }\n"); + let code = javascript(&out).code.clone(); + assert!(code.contains("__generator(this,"), "machine form: {code}"); + assert!( + code.contains("[4 /*yield*/, k]"), + "the body's suspension must split:\n{code}" + ); + assert!( + code.contains("_t1[_t0]"), + "the binding reads the indexed element:\n{code}" + ); + assert!(live_yield_leak(&code).is_none(), "live yield leaked"); + } + + /// A braceless for-of body is one statement, not a missing one: + /// the emitted loop must still contain it. + #[test] + fn braceless_for_of_body_survives_lowering() { + let out = + emit_es5_clean("var arr:any;\nfunction f(){ for (const k of arr) console.log(k); }\n"); + let code = javascript(&out).code.clone(); + assert!( + code.contains("console.log(k)"), + "the braceless body statement must survive:\n{code}" + ); + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index e08ab43..11e835f 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -97,6 +97,7 @@ pub enum LanguageFeature { LogicalAssignment, ClassFields, Using, + ForOf, } impl LanguageFeature { @@ -114,6 +115,7 @@ impl LanguageFeature { Self::LogicalAssignment => ScriptTarget::Es2021, Self::ClassFields => ScriptTarget::Es2022, Self::Using => ScriptTarget::EsNext, + Self::ForOf => ScriptTarget::Es2015, } } } @@ -1514,6 +1516,16 @@ impl<'a> Rewriter<'a> { "for-await-of requires ScriptTarget::Es2018 or later", ); } + // tsc's default ES5 for-of: index the iterable + // (`for (var _i = 0, src = it; _i < src.length; _i++)`). + // The binding moves into the body as a declaration, so + // destructuring bindings and defaults compose through + // the ordinary destructuring lowering. + if for_of.mode == ForOfMode::Sync + && Self::needs(LanguageFeature::ForOf, self.options) + { + return self.lower_sync_for_of(statement, for_of); + } let iterable = self.rewrite_expr(&for_of.iterable); let body = self.rewrite_single_statement(&for_of.body); vec![self.node( @@ -1527,12 +1539,24 @@ impl<'a> Rewriter<'a> { )] } Statement::ForIn(for_in) => { + // The for-in construct is native ES5; only the head's + // const/let binding needs var conversion there. + let binding = match &for_in.binding { + ForBinding::Variable(declaration) => { + let mut declaration = declaration.clone(); + if self.options.target <= ScriptTarget::Es5 { + declaration.kind = VariableKind::Var; + } + ForBinding::Variable(declaration) + } + target @ ForBinding::Target(_) => target.clone(), + }; let object = self.rewrite_expr(&for_in.object); let body = self.rewrite_single_statement(&for_in.body); vec![self.node( statement.range(), Statement::ForIn(ForInStatement { - binding: for_in.binding.clone(), + binding, object: Box::new(object), body: Box::new(body), }), @@ -1660,6 +1684,118 @@ impl<'a> Rewriter<'a> { self.node(block.range(), Block { statements }) } + /// tsc's default ES5 for-of lowering: `for (var _i = 0, src = IT; + /// _i < src.length; _i++) { BINDING = src[_i]; BODY }`. The binding + /// statement enters the body as a rewritten declaration so pattern + /// bindings and defaults compose with the destructuring lowering. + fn lower_sync_for_of(&mut self, statement: &Stmt, for_of: &ForOfStatement) -> Vec { + let range = statement.range(); + let counter = self.temp_ident(); + let source = self.temp_ident(); + let iterable = self.rewrite_expr(&for_of.iterable); + let zero = self.number_expr("0"); + let counter_decl = self.make_declarator(counter.clone(), Some(zero), range); + let source_decl = self.make_declarator(source.clone(), Some(iterable), range); + let initializer = ForInitializer::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations: vec![counter_decl, source_decl], + }); + let counter_expr = self.node(counter.range(), Expression::Identifier(counter.clone())); + let length = self.member_ident(&source, "length", range); + let test = self.node( + range, + Expression::Binary(BinaryExpression { + operator: BinaryOperator::LessThan, + left: Box::new(counter_expr.clone()), + right: Box::new(length), + }), + ); + let counter_target = Box::new(self.node( + counter.range(), + AssignmentTarget::Identifier(counter.clone()), + )); + let update = self.node( + counter.range(), + Expression::Update(UpdateExpression { + operator: UpdateOperator::Increment, + argument: counter_target, + prefix: false, + }), + ); + // The body's binding declaration: source[counter]. + let element = self.member_computed(&source, &counter_expr, range); + let binding_statement = match &for_of.binding { + ForBinding::Variable(declaration) => { + let mut declaration = declaration.clone(); + declaration.kind = VariableKind::Var; + if declaration.declarations.len() == 1 { + let declarator = &declaration.declarations[0]; + let lowered = self.node( + declarator.range(), + VariableDeclarator { + initializer: Some(Box::new(element)), + ..declarator.data().clone() + }, + ); + declaration.declarations = vec![lowered]; + self.node(range, Statement::Variable(declaration)) + } else { + let iterable = for_of.iterable.as_ref().clone(); + let body = self.rewrite_single_statement(&for_of.body); + return vec![self.node( + range, + Statement::ForOf(ForOfStatement { + mode: for_of.mode, + binding: ForBinding::Variable(declaration), + iterable: Box::new(iterable), + body: Box::new(body), + }), + )]; + } + } + // Assignment-target bindings have no lowering here; the + // verbatim statement below keeps the shape (and any + // applicable diagnostic) instead of dropping it. + target @ ForBinding::Target(_) => { + let iterable = for_of.iterable.as_ref().clone(); + let body = self.rewrite_single_statement(&for_of.body); + return vec![self.node( + range, + Statement::ForOf(ForOfStatement { + mode: for_of.mode, + binding: target.clone(), + iterable: Box::new(iterable), + body: Box::new(body), + }), + )]; + } + }; + // The binding declaration must itself lower (pattern bindings, + // defaults) - route it through the statement rewriter. + let lowered_binding = self.rewrite_statements(&[binding_statement]); + // A non-block body is a single statement, not a missing one: + // block_statements returns None for it, so the match form must + // push it rather than drop it. + let inner = self.rewrite_single_statement(&for_of.body); + let mut statements = lowered_binding; + match inner.data() { + Statement::Block(block) => statements.extend(block.data().statements.iter().cloned()), + _ => statements.push(inner.clone()), + } + let body_node = self.node(range, Block { statements }); + let body_stmt = self.node(range, Statement::Block(body_node)); + vec![self.node( + range, + Statement::For(ForStatement { + initializer: Some(initializer), + test: Some(Box::new(test)), + update: Some(Box::new(update)), + body: Box::new(body_stmt), + }), + )] + } + fn rewrite_variable_statement( &mut self, statement: &Stmt, @@ -10391,6 +10527,47 @@ console.log(JSON.stringify([bar, bar4, log])); ); } + /// ES5 for-of lowers to the index form and still loops: the + /// binding lands inside the body, the index bound is the iterable + /// length, and one pass collects every element. + #[test] + fn node_executes_lowered_for_of_collects_every_element() { + let output = emit_at( + "var out = [];\nfor (const k of [1, 2, 3]) { out.push(k); }\nfor (const k in { a: 1 }) { out.push(k); }\nconsole.log(out.join(\",\"));\n", + ScriptTarget::Es5, + ); + let code = javascript(&output); + assert!(!code.contains("of "), "for-of must lower at es5:\n{code}"); + assert!( + code.contains("var k in"), + "for-in keeps native form:\n{code}" + ); + assert!( + !code.contains("const k"), + "head bindings convert to var:\n{code}" + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let path = std::env::temp_dir().join(format!("bamts-forof-{nonce}.cjs")); + std::fs::write(&path, code).expect("write lowered JavaScript"); + let result = std::process::Command::new("node") + .arg(&path) + .output() + .expect("execute Node"); + let _ = std::fs::remove_file(&path); + assert!( + result.status.success(), + "{}\n{code}", + String::from_utf8_lossy(&result.stderr) + ); + assert!( + String::from_utf8_lossy(&result.stdout).contains("1,2,3,a"), + "the loop must collect every element and key:\n{code}" + ); + } + #[test] fn node_executes_lowered_compound_exponentiation_with_single_evaluations() { let output = emit_at( From 773bebd00c4e43f943e61ce227feb5ab7d520c6d Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:23:31 +0900 Subject: [PATCH 20/42] Pin the for-of pattern-binding composition The for-of lowering claims destructuring heads compose through the ordinary destructuring lowering; the claim now has its own row: a { a, b = 2 } binding in a for-of head lowers its default inside the loop body with the member read binding from the indexed element. Gates: compiler 1887/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/emitter.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 322c21a..2a0e22c 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -7430,6 +7430,25 @@ var c = () => 1; ); } + /// A destructuring binding in a for-of head composes with the + /// destructuring lowering: the element read becomes the binding's + /// initializer inside the loop body. + #[test] + fn for_of_pattern_binding_composes_with_destructuring() { + let out = emit_es5_clean( + "var arr:any;\nfor (const { a, b = 2 } of arr) { console.log(a + b); }\n", + ); + let code = javascript(&out).code.clone(); + assert!( + code.contains("void 0 ? 2 :"), + "the binding default must lower inside the loop:\n{code}" + ); + assert!( + code.contains(".a"), + "the member read must bind from the element:\n{code}" + ); + } + /// Emits `input` at es5 without helpers; panics on parse diagnostics. fn emit_es5_clean(input: &str) -> EmitOutput { let parsed = crate::parser::parse(crate::scanner::scan( From 3c4ae9c80dcb9bbc5f5f3152f328172708d498b8 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:43:52 +0900 Subject: [PATCH 21/42] Track super-before-this flow in derived constructors A derived constructor could access this and super.x before its super() call with no diagnostic; the only prior tracking was a whole-constructor presence flag for the missing-super rule. The binder now carries a SuperFlow: Tracking inside a derived constructor body, Suspended in nested function-likes (arrows defer their captured this; plain functions own theirs). A statement- position super() call marks the flow guaranteed; a super() inside a larger expression (a ternary arm, an object member) does not. If branches join on liveness - an always-exiting branch imposes nothing, a missing alternate keeps the entry state - and loops, try blocks, and switch cases never let an inner call guarantee the code after them. this before a guaranteed super reports BAMTS-C090 (TS17009); super.x before it reports BAMTS-C091 (TS17011), both with tsc's exact messages. One regression during development, caught by the existing suite and root-caused by stash-bisect: the suspend insertion had replaced the bind_implicit_function_values call, breaking arguments binding. Open tail, documented on the ledger: full baseline-count parity (checkSuperCallBeforeThisAccess carries 20 TS17009-family rows including 10 position-withheld !!! rows) needs the baseline section/coordinate decode; our flow matrix pins nine hand-verified shapes with a mutation red for the super-marks-flow rule. Window: announced ACQUIRE over hub (no live peers; recorded evidence), pre-state hashes b52e22c0/15917768, post-hashes checker.rs:b582254fe70a2dd312d85007fe5a336d26f868205f6fe966931d2e8609eca7c8 binder.rs:f4375177cd13bc83148da216d091c7f5514f7ab2511bdb7a3e0527f1c4fa0ef3 Gates: compiler 1888/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 79 +++++++++++- crates/bamts-compiler/src/checker/binder.rs | 127 +++++++++++++++++++- 2 files changed, 199 insertions(+), 7 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index bb0302a..a640bdb 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -138,6 +138,8 @@ const NOT_ASSIGNABLE_MESSAGE: &str = "Initializer type is not assignable to the /// definitely assigned in the constructor. pub const PROPERTY_NOT_INITIALIZED: DiagnosticCode = DiagnosticCode::new("BAMTS-C028"); /// Diagnostic emitted when an assignment target resolves to a function. +pub const SUPER_BEFORE_THIS: DiagnosticCode = DiagnosticCode::new("BAMTS-C090"); +pub const SUPER_BEFORE_SUPER_PROPERTY: DiagnosticCode = DiagnosticCode::new("BAMTS-C091"); pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); /// Diagnostic emitted when an assignment target resolves to a namespace. pub const ASSIGNMENT_TO_NAMESPACE: DiagnosticCode = DiagnosticCode::new("BAMTS-C030"); @@ -302,6 +304,9 @@ pub(crate) const BARE_SUPER_EXPRESSION_MESSAGE: &str = pub(crate) const SUPER_REFERENCE_NON_DERIVED_MESSAGE: &str = "'super' can only be referenced in a derived class."; pub(crate) const SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE: &str = "Super calls are not permitted outside constructors or in nested functions inside constructors."; +pub(crate) const SUPER_BEFORE_THIS_MESSAGE: &str = + "'super' must be called before accessing 'this' in the constructor of a derived class."; +pub(crate) const SUPER_BEFORE_SUPER_PROPERTY_MESSAGE: &str = "'super' must be called before accessing a property of 'super' in the constructor of a derived class."; pub(crate) const SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE: &str = "'super' cannot be referenced in constructor arguments."; const DERIVED_CONSTRUCTOR_MISSING_SUPER_MESSAGE: &str = @@ -2165,7 +2170,8 @@ mod tests { MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, - ResolvedModuleEdge, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, + ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, + SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_REFERENCE_NON_DERIVED, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, @@ -5948,6 +5954,77 @@ function check(options: Options = {}) { ); } + /// TS17009/TS17011: a derived constructor's `this` and `super.x` + /// accesses before a guaranteed `super()` call, with the oracle's + /// flow shapes: arrows are exempt (deferred `this`), a conditional + /// super() covers only its branch, and loops/try never guarantee. + #[test] + fn super_before_this_flow_matrix() { + fn codes(text: &str) -> Vec<&'static str> { + let result = check_text(text); + let mut found = checker_codes(&result) + .into_iter() + .filter(|code| { + *code == SUPER_BEFORE_THIS.as_str() + || *code == SUPER_BEFORE_SUPER_PROPERTY.as_str() + }) + .collect::>(); + found.sort(); + found + } + + // Straight-line before/after. + assert_eq!( + codes( + "class A extends Object { constructor() { let a = this; super(); let b = this; } }" + ), + vec![SUPER_BEFORE_THIS.as_str()] + ); + // super.x before super reports the property form. + assert_eq!( + codes("class A extends Object { constructor() { let a = super.x; super(); } }"), + vec![SUPER_BEFORE_SUPER_PROPERTY.as_str()] + ); + // Arrows defer their this: no report inside, and none after. + assert_eq!( + codes("class A extends Object { constructor() { let f = () => this; super(); f(); } }"), + Vec::<&str>::new() + ); + // Plain nested functions own their own this. + assert_eq!( + codes( + "class A extends Object { constructor() { let f = function () { return this; }; super(); } }" + ), + Vec::<&str>::new() + ); + // A conditional super() covers its branch only. + assert_eq!( + codes( + "class A extends Object { constructor(c) {\nif (c) { super(); let a = this; }\nelse { let a = this; }\nlet b = this; } }" + ), + vec![SUPER_BEFORE_THIS.as_str(), SUPER_BEFORE_THIS.as_str()] + ); + // A super() inside a loop guarantees nothing after it. + assert_eq!( + codes( + "class A extends Object { constructor(c) { while (c) { super(); break; } let a = this; } }" + ), + vec![SUPER_BEFORE_THIS.as_str()] + ); + // Base constructors never track. + assert_eq!( + codes("class A { constructor() { let a = this; } }"), + Vec::<&str>::new() + ); + // Field initializer and method this are not constructor-flow. + assert_eq!( + codes( + "class A extends Object { x = this; m() { return this; } constructor() { super(); } }" + ), + Vec::<&str>::new() + ); + } + #[test] fn super_call_context_matrix() { const SUPER_CODES: [&str; 4] = [ diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 07e9baf..ce2fc61 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -41,11 +41,12 @@ use super::{ PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, - SUPER_REFERENCE_NON_DERIVED, TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, - TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, - USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, - USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, + SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_REFERENCE_NON_DERIVED, TYPE_ALIAS_CIRCULAR, + TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, + UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, + USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, + WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, @@ -68,7 +69,8 @@ use super::{ PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, PROPERTY_DOES_NOT_EXIST_MESSAGE, PROPERTY_NOT_INITIALIZED_MESSAGE, SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, - STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, + STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, + SUPER_BEFORE_THIS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, SUPER_REFERENCE_NON_DERIVED_MESSAGE, TYPE_ALIAS_CIRCULAR_MESSAGE, TYPE_NESTING_TOO_DEEP_MESSAGE, TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, UNUSED_EXPECT_ERROR_MESSAGE, @@ -4740,6 +4742,16 @@ impl SemanticModel { /// derived class constructor body. Every other position (a base-class /// constructor, constructor parameter initializers, or any non-constructor /// function) maps to a distinct TypeScript diagnostic. +/// Whether the current position sits inside a derived constructor body +/// whose `super()` call has been guaranteed on every path so far, or in +/// a context (nested function-like, non-derived body) where the +/// before-super rules do not apply. +#[derive(Clone, Copy, PartialEq)] +enum SuperFlow { + Tracking { called: bool }, + Suspended, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum SuperCallContext { DerivedConstructor, @@ -4971,6 +4983,11 @@ pub(crate) struct Binder<'src> { super_call_contexts: Vec, /// Legal `super()` presence for active derived constructor bodies. derived_constructor_super_presence: Vec, + super_flow: SuperFlow, + /// Whether a super() call currently being resolved sits in statement + /// position (guarantees the flow) or inside a larger expression (a + /// ternary arm, an object member - guarantees nothing). + super_call_guarantees: bool, /// Whether each lexically enclosing class has a base class, innermost last. class_derived_stack: Vec, /// Own readonly storage properties for each lexically enclosing class. @@ -5130,6 +5147,8 @@ impl<'src> Binder<'src> { reassigned_flow_roots_stack: Vec::new(), super_call_contexts: Vec::new(), derived_constructor_super_presence: Vec::new(), + super_flow: SuperFlow::Suspended, + super_call_guarantees: true, class_derived_stack: Vec::new(), constructor_writable_readonly_properties: Vec::new(), readonly_assignment_targets: HashSet::new(), @@ -8760,23 +8779,55 @@ impl<'src> Binder<'src> { self.resolve_statements(&block.data().statements, child); } Statement::Expression(statement) => { + let positional = matches!( + statement.expression.data(), + Expression::Call(call) if matches!(call.callee.data(), Expression::Super) + ); + let outer_guarantees = self.super_call_guarantees; + self.super_call_guarantees = positional; self.resolve_expr(&statement.expression, scope); + self.super_call_guarantees = outer_guarantees; self.type_of_expr(&statement.expression, scope); } Statement::If(statement) => { self.resolve_expr(&statement.test, scope); self.type_of_expr(&statement.test, scope); let parent = self.flow; + let entry_super_flow = self.super_flow; let truthy = self.guards_for(&statement.test, false); let falsy = self.guards_for(&statement.test, true); let then_end = self.in_branch(parent, &truthy, |binder| { binder.resolve_statement(&statement.consequent, scope); }); + let then_super = self.super_flow; + self.super_flow = entry_super_flow; let else_end = self.in_branch(parent, &falsy, |binder| { if let Some(alternate) = &statement.alternate { binder.resolve_statement(alternate, scope); } }); + let else_super = self.super_flow; + // super() guarantees past the if only when every + // fall-through path has called it: an always-exiting + // branch imposes nothing, and a missing alternate keeps + // the entry state for its implicit fall-through. + let then_exits = Self::statement_always_exits(statement.consequent.data()); + let else_exits = statement + .alternate + .as_ref() + .is_some_and(|alt| Self::statement_always_exits(alt.data())); + self.super_flow = match (entry_super_flow, then_super, else_super) { + (SuperFlow::Tracking { called: entry }, then_flow, else_flow) => { + let then_ok = + then_exits || matches!(then_flow, SuperFlow::Tracking { called: true }); + let else_ok = + else_exits || matches!(else_flow, SuperFlow::Tracking { called: true }); + SuperFlow::Tracking { + called: entry && then_ok && else_ok, + } + } + (suspended, _, _) => suspended, + }; // Only branches control can fall out of reach the merge, so an // `if (guard) { return; }` leaves the negated guard in force after it. let mut live = Vec::with_capacity(2); @@ -8806,12 +8857,15 @@ impl<'src> Binder<'src> { for case in &statement.cases { self.publish_statement_class_shapes(&case.data().consequent, child); } + let entry_super_flow = self.super_flow; for case in &statement.cases { self.check_bound_statements(&case.data().consequent, child); + self.super_flow = entry_super_flow; } } Statement::For(for_statement) => { let child = self.new_scope(ScopeKind::For, Some(scope)); + let entry_super_flow = self.super_flow; if let Some(initializer) = &for_statement.initializer { self.resolve_for_initializer(initializer, child); } @@ -8839,6 +8893,9 @@ impl<'src> Binder<'src> { } else { self.flow = body_end; } + if let SuperFlow::Tracking { .. } = self.super_flow { + self.super_flow = entry_super_flow; + } } Statement::ForIn(for_statement) => { let child = self.new_scope(ScopeKind::For, Some(scope)); @@ -8907,6 +8964,7 @@ impl<'src> Binder<'src> { self.resolve_expr(&statement.test, scope); self.type_of_expr(&statement.test, scope); let parent = self.flow; + let entry_super_flow = self.super_flow; let truthy = self.guards_for(&statement.test, false); let falsy = self.guards_for(&statement.test, true); let body_end = self.in_branch(parent, &truthy, |binder| { @@ -8915,13 +8973,21 @@ impl<'src> Binder<'src> { let skipped = self.branch_guarded(parent, &falsy); let body_exit = self.branch_guarded(body_end, &falsy); self.join_flow(parent, &[skipped, body_exit]); + // A super() inside the loop body ran zero times on the + // skip path, so it guarantees nothing afterwards. + self.super_flow = entry_super_flow; } Statement::DoWhile(statement) => { + let entry_super_flow = self.super_flow; self.resolve_statement(&statement.body, scope); + if let SuperFlow::Tracking { .. } = self.super_flow { + self.super_flow = entry_super_flow; + } self.resolve_expr(&statement.test, scope); self.type_of_expr(&statement.test, scope); } Statement::Try(statement) => { + let entry_super_flow = self.super_flow; let block = &statement.block; let try_scope = self.new_scope(ScopeKind::Block, Some(scope)); self.bind_statements(&block.data().statements, try_scope); @@ -8940,6 +9006,11 @@ impl<'src> Binder<'src> { self.bind_statements(&finalizer.data().statements, finally_scope); self.resolve_statements(&finalizer.data().statements, finally_scope); } + // A throw before the super() call reaches the handler, + // so calls inside the try block guarantee nothing after. + if let SuperFlow::Tracking { .. } = self.super_flow { + self.super_flow = entry_super_flow; + } } Statement::With(with_statement) => { let forbidden = self.is_typescript() || self.scopes[scope.0 as usize].is_strict(); @@ -9533,6 +9604,13 @@ impl<'src> Binder<'src> { self.super_call_contexts .push(SuperCallContext::NonConstructor); self.super_member_homes.push(member_home); + // A nested function-like's `this` is either captured-and-deferred + // (arrows) or its own (functions), so the before-super rules do + // not apply inside it. + let outer_super_flow = self.super_flow; + self.super_flow = SuperFlow::Suspended; + let outer_guarantees = self.super_call_guarantees; + self.super_call_guarantees = true; self.bind_implicit_function_values(&function.parameters, scope); let function_symbol = function.name.as_ref().map(|name| { let symbol_scope = if is_declaration { parent } else { scope }; @@ -9652,6 +9730,8 @@ impl<'src> Binder<'src> { let popped_context = self.super_call_contexts.pop(); debug_assert_eq!(popped_context, Some(SuperCallContext::NonConstructor)); let popped_home = self.super_member_homes.pop(); + self.super_flow = outer_super_flow; + self.super_call_guarantees = outer_guarantees; debug_assert_eq!(popped_home, Some(member_home)); } @@ -11671,10 +11751,17 @@ impl<'src> Binder<'src> { let this_type = self.class_this_type(scope, false); self.this_context.push(this_type); self.push_reassigned_scope(); + let outer_super_flow = self.super_flow; + self.super_flow = if derived { + SuperFlow::Tracking { called: false } + } else { + SuperFlow::Suspended + }; self.in_isolated_flow(FlowNodeId::ROOT, |binder| { binder.bind_statements(&constructor.body.data().statements, child); binder.resolve_statements(&constructor.body.data().statements, child); }); + self.super_flow = outer_super_flow; if track_super { let called = self .derived_constructor_super_presence @@ -11764,6 +11851,13 @@ impl<'src> Binder<'src> { self.resolve_value(identifier, expression.id(), scope); } Expression::This => { + if let SuperFlow::Tracking { called: false } = self.super_flow { + self.emit( + SUPER_BEFORE_THIS, + expression.range(), + SUPER_BEFORE_THIS_MESSAGE, + ); + } let owner = self.this_context .last() @@ -11842,6 +11936,12 @@ impl<'src> Binder<'src> { FunctionBody::Expression(_) | FunctionBody::Missing(_) => None, }; let body_flow = self.captured_flow_seed(); + // An arrow defers its captured `this`: before-super rules + // do not apply inside its body. + let outer_super_flow = self.super_flow; + self.super_flow = SuperFlow::Suspended; + let outer_guarantees = self.super_call_guarantees; + self.super_call_guarantees = true; self.push_reassigned_scope(); self.in_isolated_flow(body_flow, |binder| match &arrow.body { FunctionBody::Block(block) => { @@ -11891,6 +11991,8 @@ impl<'src> Binder<'src> { debug_assert_eq!(popped, Some(body_id)); } self.return_contexts.pop(); + self.super_flow = outer_super_flow; + self.super_call_guarantees = outer_guarantees; let popped_context = self.super_call_contexts.pop(); debug_assert_eq!(popped_context, Some(SuperCallContext::NonConstructor)); let type_id = self.type_of_arrow(arrow, scope); @@ -12266,6 +12368,14 @@ impl<'src> Binder<'src> { range, SUPER_REFERENCE_NON_DERIVED_MESSAGE, ); + return; + } + if let SuperFlow::Tracking { called: false } = self.super_flow { + self.emit( + SUPER_BEFORE_SUPER_PROPERTY, + range, + SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, + ); } } @@ -12280,6 +12390,11 @@ impl<'src> Binder<'src> { if let Some(called) = self.derived_constructor_super_presence.last_mut() { *called = true; } + if self.super_call_guarantees + && let SuperFlow::Tracking { called } = &mut self.super_flow + { + *called = true; + } return; } SuperCallContext::BaseConstructor From 9ab2ba5ba7e2372e25e2f60c47468cdacaed1bb1 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:11:13 +0900 Subject: [PATCH 22/42] Register super-flow codes in the diagnostic map The TS17009/TS17011 slice emitted BAMTS-C090 and BAMTS-C091, but the diagnostics comparator drops actual rows whose codes the correspondence map does not carry, so the rules were invisible to the suite. Register both codes - C090 -> TS17009 and C091 -> TS17011 - with baseline-cited evidence (checkSuperCallBeforeThisAccess and the Accessing variants), and extend REQUIRED_BAMTS_DIAGNOSTIC_CODES so the completeness test pins the enumeration at 92. Gates: verification code_map tests green, lib 599/0 baseline unaffected, fmt, clippy clean. --- crates/bamts-verification/src/facets.rs | 4 +++- verification/diagnostic-code-map.json | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/bamts-verification/src/facets.rs b/crates/bamts-verification/src/facets.rs index e13d384..fa5cf5c 100644 --- a/crates/bamts-verification/src/facets.rs +++ b/crates/bamts-verification/src/facets.rs @@ -51,7 +51,7 @@ pub const DIAGNOSTIC_CODE_MAP_PATH: &str = "verification/diagnostic-code-map.jso pub const DIAGNOSTIC_CODE_MAP_SCHEMA_VERSION: u32 = 1; /// Every current BAMTS diagnostic code the map must cover exactly once. -pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 90] = [ +pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 92] = [ "BAMTS-L001", "BAMTS-L002", "BAMTS-L003", @@ -142,6 +142,8 @@ pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 90] = [ "BAMTS-C087", "BAMTS-C088", "BAMTS-C089", + "BAMTS-C090", + "BAMTS-C091", ]; #[derive(Debug, Clone, PartialEq, Eq)] pub enum FacetVerdict { diff --git a/verification/diagnostic-code-map.json b/verification/diagnostic-code-map.json index 24d0e83..0715399 100644 --- a/verification/diagnostic-code-map.json +++ b/verification/diagnostic-code-map.json @@ -540,6 +540,18 @@ "evidence": "Value or type reference resolves to a known lib-gated global not in the active lib set -> TS2583 in the TypeScript 7.0.2 authority baselines: bigintWithoutLib(target=es5) (line 4).", "status": "mapped", "tsCode": 2583 + }, + { + "bamtsCode": "BAMTS-C090", + "evidence": "Derived constructor reads `this` before a guaranteed super() call -> TS17009 in the TypeScript 7.0.2 authority baselines: checkSuperCallBeforeThisAccess (line 7), checkSuperCallBeforeThisAccessing2 (line 5).", + "status": "mapped", + "tsCode": 17009 + }, + { + "bamtsCode": "BAMTS-C091", + "evidence": "Derived constructor reads a `super` property before a guaranteed super() call -> TS17011 in the TypeScript 7.0.2 authority baselines: checkSuperCallBeforeThisAccess (line 9), checkSuperCallBeforeThisAccessing3 (line 5).", + "status": "mapped", + "tsCode": 17011 } ] } From 78d4961b52a6759ef9ddd1db74116d8ab3f1727d Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:34:49 +0900 Subject: [PATCH 23/42] Emit TS2855 for base fields read through super A derived class reading super.x hit a base class field with no diagnostic, but a field is an own instance property created by the constructor, so it never appears on the prototype chain super reads; only methods and accessors are reachable that way. PropertyType gains an accessor marker: a getter+setter pair previously reported the same shape as a field (is_method false, getter_only false), so the kind question was unanswerable. The Get and auto-accessor constructions mark accessor true, and the generic-instantiation copy propagates it. The check resolves the innermost class owner, walks to its base symbol, and asks the base template's members directly - kind facts never depend on type arguments, so the applied-view instantiation machinery is skipped via a new class_template_properties lookup. Emission is keyed at the property-name range through both the read and assignment arms. Oracle: checkSuperCallBeforeThisAccess.errors.txt carries five TS2855 rows; the checker emits exactly five, pinned by test, alongside a kind matrix (field fires; method, getter-only, and getter+setter pairs stay silent; grandparent fields fire; absent names stay silent) with a mutation red on the kind predicate. Window: announced ACQUIRE over hub (solo session, attempted-without-peers), pre-hashes b582254f/f4375177, post-hashes checker.rs:d3d545bf7085a028f2ad0b490260af5fcce256b0467db9a8d9cd6a52d7207b6b binder.rs:ef593390b4e54ac5368b33db0589751d22e759bb0f241452d2622f1bad2f48a5 Gates: compiler 1890/0, verification lib 599/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 83 ++++- crates/bamts-compiler/src/checker/binder.rs | 362 +++++++++++++------- 2 files changed, 311 insertions(+), 134 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index a640bdb..1bf4028 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -140,6 +140,7 @@ pub const PROPERTY_NOT_INITIALIZED: DiagnosticCode = DiagnosticCode::new("BAMTS- /// Diagnostic emitted when an assignment target resolves to a function. pub const SUPER_BEFORE_THIS: DiagnosticCode = DiagnosticCode::new("BAMTS-C090"); pub const SUPER_BEFORE_SUPER_PROPERTY: DiagnosticCode = DiagnosticCode::new("BAMTS-C091"); +pub const SUPER_FIELD_VIA_SUPER: DiagnosticCode = DiagnosticCode::new("BAMTS-C092"); pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); /// Diagnostic emitted when an assignment target resolves to a namespace. pub const ASSIGNMENT_TO_NAMESPACE: DiagnosticCode = DiagnosticCode::new("BAMTS-C030"); @@ -2171,7 +2172,7 @@ mod tests { PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, + SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, @@ -5958,6 +5959,86 @@ function check(options: Options = {}) { /// accesses before a guaranteed `super()` call, with the oracle's /// flow shapes: arrows are exempt (deferred `this`), a conditional /// super() covers only its branch, and loops/try never guarantee. + /// The authority baseline checkSuperCallBeforeThisAccess.errors.txt + /// carries exactly five TS2855 rows (lines 9, 12, 17, 22, 45 of the + /// directive-stripped source); the count is the pinned oracle. + #[test] + fn super_field_via_super_matches_baseline_count() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = std::fs::read_to_string(root.join(concat!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", + "checkSuperCallBeforeThisAccess.ts" + ))) + .unwrap(); + let count = checker_codes(&check_text(&source)) + .into_iter() + .filter(|code| *code == SUPER_FIELD_VIA_SUPER.as_str()) + .count(); + assert_eq!(count, 5); + } + + #[test] + fn super_field_via_super_matrix() { + fn field_codes(text: &str) -> Vec<&'static str> { + let result = check_text(text); + checker_codes(&result) + .into_iter() + .filter(|code| *code == SUPER_FIELD_VIA_SUPER.as_str()) + .collect() + } + // A base field read through super fires even after super() ran. + assert_eq!( + field_codes( + "class A { field = 1; } + class C extends A { + constructor() { super(); super.field; } + }" + ), + vec![SUPER_FIELD_VIA_SUPER.as_str()] + ); + // Base methods and accessor pairs live on the prototype: no fire. + assert_eq!( + field_codes( + "class A { method() {} get pair() { return 1; } set pair(v) {} } + class C extends A { + constructor() { super(); super.method(); super.pair; } + }" + ), + Vec::<&str>::new() + ); + // A getter-only accessor is reachable too. + assert_eq!( + field_codes( + "class A { get only() { return 1; } } + class C extends A { constructor() { super(); super.only; } }" + ), + Vec::<&str>::new() + ); + // The inherited grandparent field still fires, and a name the base + // chain does not declare stays silent. + assert_eq!( + field_codes( + "class A { shared = 1; } + class B extends A {} + class C extends B { constructor() { super(); super.shared; super.absent; } }" + ), + vec![SUPER_FIELD_VIA_SUPER.as_str()] + ); + // No base class, no super property check applies. + assert_eq!( + field_codes("class C { constructor() { super(); } }"), + Vec::<&str>::new() + ); + // A field read before super() reports both TS17011 and TS2855. + let result = check_text( + "class A { field = 1; } + class C extends A { constructor() { super.field; super(); } }", + ); + let codes = checker_codes(&result); + assert!(codes.contains(&SUPER_BEFORE_SUPER_PROPERTY.as_str())); + assert!(codes.contains(&SUPER_FIELD_VIA_SUPER.as_str())); + } + #[test] fn super_before_this_flow_matrix() { fn codes(text: &str) -> Vec<&'static str> { diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index ce2fc61..285f160 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -42,11 +42,11 @@ use super::{ PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, - SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_REFERENCE_NON_DERIVED, TYPE_ALIAS_CIRCULAR, - TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, - UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, - USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, - WITH_STATEMENT_NOT_ALLOWED, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, + TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, + TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, + USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, + USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, @@ -371,6 +371,12 @@ pub struct PropertyType { /// members default to `false`; properties that legitimately propagate /// through spread keep the flag explicitly. spreadable: bool, + /// Whether this property is backed by a get/set accessor pair. A + /// getter+setter pair reports `is_method == false` and + /// `getter_only == false`, the same shape as a field; this marker is + /// what tells them apart (`super.x` reaches accessors on the + /// prototype but never a field). + accessor: bool, } impl PropertyType { @@ -387,6 +393,7 @@ impl PropertyType { declaring_types: Vec::new(), is_method: false, spreadable: false, + accessor: false, } } @@ -440,6 +447,13 @@ impl PropertyType { self } + /// Marks this property as accessor-backed (a get or get/set pair). + #[must_use] + pub fn with_accessor(mut self, accessor: bool) -> Self { + self.accessor = accessor; + self + } + #[must_use] pub fn with_spreadable(mut self, spreadable: bool) -> Self { self.spreadable = spreadable; @@ -476,6 +490,13 @@ impl PropertyType { self.getter_only } + /// Whether a get/set accessor (not a field or method) backs this + /// property. + #[must_use] + pub const fn accessor(&self) -> bool { + self.accessor + } + #[must_use] pub const fn type_id(&self) -> TypeId { self.type_id @@ -509,6 +530,7 @@ impl PartialEq for PropertyType { && self.declaring_types == other.declaring_types && self.is_method == other.is_method && self.spreadable == other.spreadable + && self.accessor == other.accessor } } @@ -526,6 +548,7 @@ impl std::hash::Hash for PropertyType { self.declaring_types.hash(state); self.is_method.hash(state); self.spreadable.hash(state); + self.accessor.hash(state); } } @@ -2263,6 +2286,25 @@ impl TypeTable { } /// Ensures and returns the finite shallow view for one applied root. + /// Instance members of a class symbol's current template, without + /// instantiation. Membership kind (field, method, accessor) never + /// depends on type arguments, so callers that only ask kind + /// questions can skip the applied-view machinery. + #[must_use] + pub fn class_template_properties(&self, symbol: SymbolId) -> &[PropertyType] { + let Some(raw) = self + .classes + .get(&symbol) + .and_then(|metadata| metadata.template.as_ref().map(|template| template.raw)) + else { + return &[]; + }; + match self.get(raw) { + Type::ObjectType(object) => object.properties(), + _ => &[], + } + } + pub fn prepare_applied_class_view(&mut self, type_id: TypeId) -> Option { self.materialize_applied_class_view(type_id); self.applied_class_view(type_id) @@ -3879,7 +3921,7 @@ impl TypeTable { target.intersection_ordered(members) } Type::ObjectType(object) => { - let properties = object + let properties: Vec = object .properties .into_iter() .map(|property| { @@ -3903,6 +3945,7 @@ impl TypeTable { .with_accessibility(property.access(), declaring_class) .with_declaring_types(declaring_types) .with_method(property.is_method) + .with_accessor(property.accessor) .with_spreadable(property.spreadable) }) .collect(); @@ -10621,140 +10664,148 @@ impl<'src> Binder<'src> { }); continue; } - let (name, type_id, optional, readonly, getter_only, access, is_method) = match member - .data() - { - ClassMember::Property(property) if side.includes(property.modifiers.is_static) => { - let Some(name) = self.property_key(&property.name) else { - continue; - }; - let type_id = self.class_property_type( - property.type_annotation.as_ref(), - property.initializer.as_deref(), - &property.modifiers, - scope, - false, - ); - ( - name, - type_id, - property.optional, - property.modifiers.is_readonly, - false, - property - .modifiers - .accessibility - .unwrap_or(Accessibility::Public), - false, - ) - } - ClassMember::AutoAccessor(accessor) - if side.includes(accessor.modifiers.is_static) => - { - let Some(name) = self.property_key(&accessor.name) else { - continue; - }; - let type_id = self.class_property_type( - accessor.type_annotation.as_ref(), - accessor.initializer.as_deref(), - &accessor.modifiers, - scope, - false, - ); - ( - name, - type_id, - false, - accessor.modifiers.is_readonly, - false, - accessor - .modifiers - .accessibility - .unwrap_or(Accessibility::Public), - false, - ) - } - ClassMember::Method(method) if side.includes(method.modifiers.is_static) => { - match method.modifier { - PropertyModifier::None => { - let Some(name) = self.property_key(&method.name) else { - continue; - }; - if name == "constructor" { - continue; - } - let is_overload_signature = - method.function.body.is_none() && !method.modifiers.is_abstract; - if is_overload_signature { - overload_state.insert(name.clone(), true); - } else if overload_state.get(&name).copied().unwrap_or(false) { - overload_state.insert(name.clone(), false); - continue; - } - let signature_scope = self.class_method_signature_scope( - member.id(), - &method.function, - scope, - ); - let type_id = self - .type_of_function_like_in_scope(&method.function, signature_scope); - ( - name, - type_id, - method.optional, - false, - false, - method - .modifiers - .accessibility - .unwrap_or(Accessibility::Public), - true, - ) - } - PropertyModifier::Get => { - let Some(name) = self.property_key(&method.name) else { - continue; - }; - let type_id = match &method.function.return_type { - Some(annotation) => { - self.resolve_type(&annotation.data().type_node, scope) + let (name, type_id, optional, readonly, getter_only, access, is_method, accessor) = + match member.data() { + ClassMember::Property(property) + if side.includes(property.modifiers.is_static) => + { + let Some(name) = self.property_key(&property.name) else { + continue; + }; + let type_id = self.class_property_type( + property.type_annotation.as_ref(), + property.initializer.as_deref(), + &property.modifiers, + scope, + false, + ); + ( + name, + type_id, + property.optional, + property.modifiers.is_readonly, + false, + property + .modifiers + .accessibility + .unwrap_or(Accessibility::Public), + false, + false, + ) + } + ClassMember::AutoAccessor(accessor) + if side.includes(accessor.modifiers.is_static) => + { + let Some(name) = self.property_key(&accessor.name) else { + continue; + }; + let type_id = self.class_property_type( + accessor.type_annotation.as_ref(), + accessor.initializer.as_deref(), + &accessor.modifiers, + scope, + false, + ); + ( + name, + type_id, + false, + accessor.modifiers.is_readonly, + false, + accessor + .modifiers + .accessibility + .unwrap_or(Accessibility::Public), + false, + true, + ) + } + ClassMember::Method(method) if side.includes(method.modifiers.is_static) => { + match method.modifier { + PropertyModifier::None => { + let Some(name) = self.property_key(&method.name) else { + continue; + }; + if name == "constructor" { + continue; } - None => self.types.any(), - }; - let has_setter = class.members.iter().any(|candidate| { - let ClassMember::Method(candidate) = candidate.data() else { - return false; + let is_overload_signature = + method.function.body.is_none() && !method.modifiers.is_abstract; + if is_overload_signature { + overload_state.insert(name.clone(), true); + } else if overload_state.get(&name).copied().unwrap_or(false) { + overload_state.insert(name.clone(), false); + continue; + } + let signature_scope = self.class_method_signature_scope( + member.id(), + &method.function, + scope, + ); + let type_id = self.type_of_function_like_in_scope( + &method.function, + signature_scope, + ); + ( + name, + type_id, + method.optional, + false, + false, + method + .modifiers + .accessibility + .unwrap_or(Accessibility::Public), + true, + false, + ) + } + PropertyModifier::Get => { + let Some(name) = self.property_key(&method.name) else { + continue; }; - candidate.modifier == PropertyModifier::Set - && side.includes(candidate.modifiers.is_static) - && self.property_key(&candidate.name).as_deref() - == Some(name.as_str()) - }); - ( - name, - type_id, - method.optional, - !has_setter, - !has_setter, - method - .modifiers - .accessibility - .unwrap_or(Accessibility::Public), - false, - ) + let type_id = match &method.function.return_type { + Some(annotation) => { + self.resolve_type(&annotation.data().type_node, scope) + } + None => self.types.any(), + }; + let has_setter = class.members.iter().any(|candidate| { + let ClassMember::Method(candidate) = candidate.data() else { + return false; + }; + candidate.modifier == PropertyModifier::Set + && side.includes(candidate.modifiers.is_static) + && self.property_key(&candidate.name).as_deref() + == Some(name.as_str()) + }); + ( + name, + type_id, + method.optional, + !has_setter, + !has_setter, + method + .modifiers + .accessibility + .unwrap_or(Accessibility::Public), + false, + true, + ) + } + PropertyModifier::Set => continue, } - PropertyModifier::Set => continue, } - } - _ => continue, - }; + _ => continue, + }; let _ = seen.insert(name.clone()); properties.push( PropertyType::new(name, optional, type_id) .with_readonly(readonly) .with_getter_only(getter_only) .with_accessibility(access, declaring_class) - .with_method(is_method), + .with_method(is_method) + .with_accessor(accessor), ); } (properties, seen, iterator_property, async_iterator_property) @@ -12018,7 +12069,11 @@ impl<'src> Binder<'src> { } Expression::Member(member) => { if matches!(member.object.data(), Expression::Super) { - self.check_super_member_access(member.object.range(), member.optional); + self.check_super_member_access( + member.object.range(), + member.optional, + &member.property, + ); } else { self.resolve_expr(&member.object, scope); // A property read dereferences its object, so a nullable @@ -12356,7 +12411,12 @@ impl<'src> Binder<'src> { /// classes and object literals are legal. Plain functions and the top /// level stay silent: no existing C-band code carries that diagnostic, and /// minting one is out of scope. - fn check_super_member_access(&mut self, range: TextRange, optional: bool) { + fn check_super_member_access( + &mut self, + range: TextRange, + optional: bool, + property: &MemberProperty, + ) { if optional { self.emit(BARE_SUPER_EXPRESSION, range, BARE_SUPER_EXPRESSION_MESSAGE); return; @@ -12377,6 +12437,42 @@ impl<'src> Binder<'src> { SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, ); } + self.check_super_property_is_field(property); + } + + /// TS2855: `super.x` reads the prototype chain, but a class field is an + /// own instance property, so the field is never visible through `super` + /// even after `super()` has run. Methods and accessors live on the + /// prototype and stay reachable. + fn check_super_property_is_field(&mut self, property: &MemberProperty) { + let MemberProperty::Named(identifier) = property else { + return; + }; + if self.super_member_homes.last() != Some(&SuperMemberHome::ClassMember { derived: true }) { + return; + } + let Some(&owner) = self.class_owner_stack.last() else { + return; + }; + let Some(&base) = self.class_base_symbols.get(&owner) else { + return; + }; + let name = self.identifier_text(identifier); + let is_field = self + .types + .class_template_properties(base) + .iter() + .find(|member| member.name() == name.as_ref()) + .is_some_and(|member| !member.is_method() && !member.accessor()); + if is_field { + self.emit_with_message( + SUPER_FIELD_VIA_SUPER, + identifier.range(), + format!( + "Class field '{name}' defined by the parent class is not accessible in the child class via super." + ), + ); + } } fn check_super_call(&mut self, range: TextRange) { @@ -14131,7 +14227,7 @@ impl<'src> Binder<'src> { } AssignmentTarget::Member(member) => { if matches!(member.object.data(), Expression::Super) { - self.check_super_member_access(member.object.range(), false); + self.check_super_member_access(member.object.range(), false, &member.property); } else { self.resolve_expr(&member.object, scope); } From c75faec51db959a8830d47ee5f1f486d4abc34e7 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:37:23 +0900 Subject: [PATCH 24/42] Register the super field code in the diagnostic map BAMTS-C092 (TS2855, base class field read through super) was emitted by the checker but absent from the correspondence map, so the diagnostics comparator would drop its rows. Register it with baseline-cited evidence and extend REQUIRED_BAMTS_DIAGNOSTIC_CODES to pin the enumeration at 93. Gates: verification code_map tests green, fmt, clippy clean. --- crates/bamts-verification/src/facets.rs | 3 ++- verification/diagnostic-code-map.json | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/bamts-verification/src/facets.rs b/crates/bamts-verification/src/facets.rs index fa5cf5c..f5e13b1 100644 --- a/crates/bamts-verification/src/facets.rs +++ b/crates/bamts-verification/src/facets.rs @@ -51,7 +51,7 @@ pub const DIAGNOSTIC_CODE_MAP_PATH: &str = "verification/diagnostic-code-map.jso pub const DIAGNOSTIC_CODE_MAP_SCHEMA_VERSION: u32 = 1; /// Every current BAMTS diagnostic code the map must cover exactly once. -pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 92] = [ +pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 93] = [ "BAMTS-L001", "BAMTS-L002", "BAMTS-L003", @@ -144,6 +144,7 @@ pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 92] = [ "BAMTS-C089", "BAMTS-C090", "BAMTS-C091", + "BAMTS-C092", ]; #[derive(Debug, Clone, PartialEq, Eq)] pub enum FacetVerdict { diff --git a/verification/diagnostic-code-map.json b/verification/diagnostic-code-map.json index 0715399..1d8f47b 100644 --- a/verification/diagnostic-code-map.json +++ b/verification/diagnostic-code-map.json @@ -552,6 +552,12 @@ "evidence": "Derived constructor reads a `super` property before a guaranteed super() call -> TS17011 in the TypeScript 7.0.2 authority baselines: checkSuperCallBeforeThisAccess (line 9), checkSuperCallBeforeThisAccessing3 (line 5).", "status": "mapped", "tsCode": 17011 + }, + { + "bamtsCode": "BAMTS-C092", + "evidence": "Derived class reads a base class field through `super` -> TS2855 in the TypeScript 7.0.2 authority baselines: checkSuperCallBeforeThisAccess (lines 9, 12, 17, 22, 45).", + "status": "mapped", + "tsCode": 2855 } ] } From 8fa4ef28ecc9351a1d2de64de03d28f43652fe10 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:56:08 +0900 Subject: [PATCH 25/42] Mark super complete only after its arguments resolve A super() call marked the constructor flow guaranteed before its own arguments resolved, so this inside super(this.x) - variant 5 of the oracle family - never reported TS17009. Split the old check_super_call into a legality emit and a mark_super_call_completed recording, and run the marking after resolve_arguments. The marking is gated on the innermost context being the derived constructor, so a super() nested in an arrow or parameter default still satisfies neither the presence rule nor the flow. Oracle: Accessing2/5/8 each carry exactly one TS17009 row; the baseline-count test now pins all three variants plus the plain case's five TS2855 rows, and a mark-before-arguments mutation fails it. One dev regression during the split (ungated presence marking satisfied the missing-super rule through arrow-nested calls) was caught by derived_constructor_requires_super_call. Window: continuation of the TS2855 wave (same roots, announced ACQUIRE), post-hashes checker.rs:57224b87968ddca319dc19e7446294123abd3523428675f69bdfdce0048a901a binder.rs:f47e252e820fd7d23e937e7dc3180f1ca12244c7eeacc0e50f9226521b68bfd7 Gates: compiler 1890/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 21 ++++++++++ crates/bamts-compiler/src/checker/binder.rs | 43 ++++++++++++++------- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 1bf4028..454521c 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -5975,6 +5975,27 @@ function check(options: Options = {}) { .filter(|code| *code == SUPER_FIELD_VIA_SUPER.as_str()) .count(); assert_eq!(count, 5); + // The Accessing variants each carry a single TS17009 row; variant 5 + // reaches `this` inside the super() arguments, which evaluate before + // the call completes. + for variant in ["2", "5", "8"] { + let variant_source = std::fs::read_to_string( + root.join(format!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/checkSuperCallBeforeThisAccessing{variant}.ts" + )), + ) + .unwrap(); + let codes = checker_codes(&check_text(&variant_source)); + assert_eq!( + codes + .iter() + .filter(|code| **code == SUPER_BEFORE_THIS.as_str()) + .count(), + 1, + "variant {}", + variant + ); + } } #[test] diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 285f160..17f7fa0 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -12052,13 +12052,21 @@ impl<'src> Binder<'src> { } } Expression::Call(call) => { - if matches!(call.callee.data(), Expression::Super) { - self.check_super_call(call.callee.range()); + let is_super_call = matches!(call.callee.data(), Expression::Super); + if is_super_call { + self.check_super_call_legality(call.callee.range()); } else { self.resolve_expr(&call.callee, scope); } self.resolve_type_arguments(call.type_arguments.as_ref(), scope); self.resolve_arguments(&call.arguments, scope); + // A call's arguments evaluate before the call completes, so + // `this` inside `super(this.x)` is still before `super()`; + // the flow may only be marked guaranteed once every + // argument has resolved. + if is_super_call { + self.mark_super_call_completed(call.callee.range()); + } self.check_call(call, scope, expression.range()); } Expression::New(new) => { @@ -12475,24 +12483,14 @@ impl<'src> Binder<'src> { } } - fn check_super_call(&mut self, range: TextRange) { + fn check_super_call_legality(&mut self, range: TextRange) { let context = self .super_call_contexts .last() .copied() .unwrap_or(SuperCallContext::NonConstructor); let (code, message) = match context { - SuperCallContext::DerivedConstructor => { - if let Some(called) = self.derived_constructor_super_presence.last_mut() { - *called = true; - } - if self.super_call_guarantees - && let SuperFlow::Tracking { called } = &mut self.super_flow - { - *called = true; - } - return; - } + SuperCallContext::DerivedConstructor => return, SuperCallContext::BaseConstructor | SuperCallContext::ConstructorParameters { derived: false } => ( SUPER_REFERENCE_NON_DERIVED, @@ -12510,6 +12508,23 @@ impl<'src> Binder<'src> { self.emit(code, range, message); } + /// Records a resolved `super()` call: the constructor's missing-super + /// presence is satisfied, and in statement position the flow becomes + /// guaranteed. Runs after the call's arguments have resolved. + fn mark_super_call_completed(&mut self, _range: TextRange) { + if self.super_call_contexts.last().copied() != Some(SuperCallContext::DerivedConstructor) { + return; + } + if let Some(called) = self.derived_constructor_super_presence.last_mut() { + *called = true; + } + if self.super_call_guarantees + && let SuperFlow::Tracking { called } = &mut self.super_flow + { + *called = true; + } + } + fn resolve_object_member(&mut self, member: &'src ObjectMember, scope: ScopeId) { match member { ObjectMember::Property(property) => { From dcb9701ce54992575d7b3ee1b0a0115eaf5f2fd1 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:35:21 +0900 Subject: [PATCH 26/42] Emit TS2576 for base statics read through super super.S1 where the base class declares S1 static reported nothing; super resolves instance members, so a static reached through it is a misspelling of Base.member. The check reads the base symbol's constructor type, unwraps its ConstructorType structural payload, and asks whether the static member table carries the accessed name; emission interpolates both the property and base class names into tsc's exact message, keyed at the property-name range beside the field rule. Oracle: superAccess.ts (es2015) carries one TS2576 plus two TS2855; the checker now emits exactly that, pinned by test with a mutation red on the dispatch call. The es5 variant's TS2340 flavor instead of TS2855 is a target-conditional split, banked as the next slice. Window: continuation of the TS2855 wave (same roots), post-hashes checker.rs:391329bb8cf9f33821d24e8cc815fb94086e496d90d2f82f4e6f06043d82c261 binder.rs:5060cf670b8de327abab3dd6a0053ee5b00a1e0b8802c7c02913999f25d79a7b Gates: compiler 1892/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 80 ++++++++++++++++++++- crates/bamts-compiler/src/checker/binder.rs | 47 +++++++++++- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 454521c..195c76e 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -141,6 +141,7 @@ pub const PROPERTY_NOT_INITIALIZED: DiagnosticCode = DiagnosticCode::new("BAMTS- pub const SUPER_BEFORE_THIS: DiagnosticCode = DiagnosticCode::new("BAMTS-C090"); pub const SUPER_BEFORE_SUPER_PROPERTY: DiagnosticCode = DiagnosticCode::new("BAMTS-C091"); pub const SUPER_FIELD_VIA_SUPER: DiagnosticCode = DiagnosticCode::new("BAMTS-C092"); +pub const SUPER_STATIC_MEMBER_VIA_SUPER: DiagnosticCode = DiagnosticCode::new("BAMTS-C093"); pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); /// Diagnostic emitted when an assignment target resolves to a namespace. pub const ASSIGNMENT_TO_NAMESPACE: DiagnosticCode = DiagnosticCode::new("BAMTS-C030"); @@ -2173,9 +2174,9 @@ mod tests { PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, - SUPER_REFERENCE_NON_DERIVED, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, - TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, - VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, + SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, + TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, + TypeTable, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; @@ -5959,6 +5960,79 @@ function check(options: Options = {}) { /// accesses before a guaranteed `super()` call, with the oracle's /// flow shapes: arrows are exempt (deferred `this`), a conditional /// super() covers only its branch, and loops/try never guarantee. + /// The superAccess es2015 baseline carries one TS2576 (static S1 via + /// super) and two TS2855 (fields S2 and f); the es5 variant's TS2340 + /// flavor is a separate target-conditional slice. + #[test] + fn super_static_member_matches_superaccess_baseline() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = + std::fs::read_to_string(root.join( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", + )) + .unwrap(); + let codes = checker_codes(&check_text(&source)); + assert_eq!( + codes + .iter() + .filter(|c| **c == SUPER_STATIC_MEMBER_VIA_SUPER.as_str()) + .count(), + 1 + ); + assert_eq!( + codes + .iter() + .filter(|c| **c == SUPER_FIELD_VIA_SUPER.as_str()) + .count(), + 2 + ); + } + + /// TS2576: super.S1 where S1 is a base static member. Also pins the + /// superAccess oracle shape: static fires TS2576, instance fields fire + /// TS2855, and a base instance method stays silent. + #[test] + fn super_static_member_via_super_matrix() { + let result = check_text( + "class MyBase { + static S1: number = 5; + f = () => 5; + } + class MyDerived extends MyBase { + foo() { + var l1 = super.S1; + var l3 = super.f; + var l4 = super.toString; + } + }", + ); + let codes = checker_codes(&result); + assert_eq!( + codes + .iter() + .filter(|c| **c == SUPER_STATIC_MEMBER_VIA_SUPER.as_str()) + .count(), + 1, + "{codes:?}" + ); + assert_eq!( + codes + .iter() + .filter(|c| **c == SUPER_FIELD_VIA_SUPER.as_str()) + .count(), + 1, + "{codes:?}" + ); + assert_eq!( + codes + .iter() + .filter(|c| **c == SUPER_BEFORE_SUPER_PROPERTY.as_str()) + .count(), + 0, + "{codes:?}" + ); + } + /// The authority baseline checkSuperCallBeforeThisAccess.errors.txt /// carries exactly five TS2855 rows (lines 9, 12, 17, 22, 45 of the /// directive-stripped source); the count is the pinned oracle. diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 17f7fa0..78d51aa 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -43,7 +43,7 @@ use super::{ STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, - TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, + SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, @@ -12446,6 +12446,7 @@ impl<'src> Binder<'src> { ); } self.check_super_property_is_field(property); + self.check_super_property_is_static(property); } /// TS2855: `super.x` reads the prototype chain, but a class field is an @@ -12483,6 +12484,50 @@ impl<'src> Binder<'src> { } } + /// TS2576: `super` resolves instance members, so a base class static + /// reached through it is a misspelling of `Base.member`. + fn check_super_property_is_static(&mut self, property: &MemberProperty) { + let MemberProperty::Named(identifier) = property else { + return; + }; + if self.super_member_homes.last() != Some(&SuperMemberHome::ClassMember { derived: true }) { + return; + } + let Some(&owner) = self.class_owner_stack.last() else { + return; + }; + let Some(&base) = self.class_base_symbols.get(&owner) else { + return; + }; + let Some(&static_type) = self.class_constructor_types.get(&base) else { + return; + }; + let name = self.identifier_text(identifier); + // The static side is a constructor type wrapping the structural + // member table. + let Type::ConstructorType { structural, .. } = self.types.get(static_type).clone() else { + return; + }; + let Type::ObjectType(object) = self.types.get(structural).clone() else { + return; + }; + if !object + .properties + .iter() + .any(|member| member.name() == name.as_ref()) + { + return; + } + let base_name = self.symbols[base.get() as usize].name().to_owned(); + self.emit_with_message( + SUPER_STATIC_MEMBER_VIA_SUPER, + identifier.range(), + format!( + "Property '{name}' does not exist on type '{base_name}'. Did you mean to access the static member '{base_name}.{name}' instead?" + ), + ); + } + fn check_super_call_legality(&mut self, range: TextRange) { let context = self .super_call_contexts From 8d35f220b0917d5db87ff0f76b8e6a82f60092cf Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:51:24 +0900 Subject: [PATCH 27/42] Emit TS2448 and kin for use before declaration A let or const binding, class, or enum referenced textually before its declaration reported nothing; tsc carries TS2448 (variables), TS2449 (classes), and TS2450 (enums). resolve_value now asks the symbol's kind and positions: a reference earlier than the declaration name, inside the same boundary scope and without a function-like boundary between reference and declaration, emits the kind-specific message with the name interpolated. Const enums are exempt - they are inlined at their use sites, so an early reference reads the cooked value; tsc leaves foo2 and AfterObject clean in the oracle. Var and function hoisting, deferred references through nested functions, and uses at or after the declaration all stay silent, pinned by a shape matrix. Oracle: blockScopedEnumVariablesUseBeforeDef carries exactly one TS2450 row; the checker emits exactly one, pinned by test. Mutation red disables the dispatch call and fails both tests. Window: same-root continuation wave, post-hashes checker.rs:4343ee6df2ca5aa4bdd16d8d15ebd7a7ce8696646730ea6896a266f64c6a4e4d binder.rs:6652e9c7b866601b40e3e7aa6f58879186688efd952195614bd32f56d27dd18b Gates: compiler 1894/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 108 +++++++++++++++++--- crates/bamts-compiler/src/checker/binder.rs | 77 +++++++++++++- 2 files changed, 168 insertions(+), 17 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 195c76e..94e5319 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -142,6 +142,9 @@ pub const SUPER_BEFORE_THIS: DiagnosticCode = DiagnosticCode::new("BAMTS-C090"); pub const SUPER_BEFORE_SUPER_PROPERTY: DiagnosticCode = DiagnosticCode::new("BAMTS-C091"); pub const SUPER_FIELD_VIA_SUPER: DiagnosticCode = DiagnosticCode::new("BAMTS-C092"); pub const SUPER_STATIC_MEMBER_VIA_SUPER: DiagnosticCode = DiagnosticCode::new("BAMTS-C093"); +pub const BLOCK_SCOPED_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C094"); +pub const CLASS_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C095"); +pub const ENUM_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C096"); pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); /// Diagnostic emitted when an assignment target resolves to a namespace. pub const ASSIGNMENT_TO_NAMESPACE: DiagnosticCode = DiagnosticCode::new("BAMTS-C030"); @@ -2164,20 +2167,20 @@ fn imported_enum_error( #[cfg(test)] mod tests { use super::{ - ARGUMENT_NOT_ASSIGNABLE, BARE_SUPER_EXPRESSION, CANNOT_FIND_NAME, - CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, - CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, DERIVED_CONSTRUCTOR_MISSING_SUPER, - DUPLICATE_DECLARATION, EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, - IMPORTED_CONST_ENUM_CYCLE, IMPORTED_CONST_ENUM_NONCONSTANT, INVALID_ASSIGNMENT_TARGET, - MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, - PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, - PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, - ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, - SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, - TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, - TypeTable, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, - check_program_with_options, + ARGUMENT_NOT_ASSIGNABLE, BARE_SUPER_EXPRESSION, BLOCK_SCOPED_USED_BEFORE_DECLARATION, + CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, + CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, + DERIVED_CONSTRUCTOR_MISSING_SUPER, DUPLICATE_DECLARATION, ENUM_USED_BEFORE_DECLARATION, + EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, IMPORTED_CONST_ENUM_CYCLE, + IMPORTED_CONST_ENUM_NONCONSTANT, INVALID_ASSIGNMENT_TARGET, MISSING_METHOD_RETURN_TYPE, + MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, PARAMETER_DECORATOR_NOT_SUPPORTED, + PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, + ProgramCheckOptions, PropertyType, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, + SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, + SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, + ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, + TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, VALUE_CANNOT_BE_USED_HERE, + WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; use crate::namespace_plan::{ContainerAcquisition, ExportStorage}; @@ -5988,6 +5991,83 @@ function check(options: Options = {}) { ); } + /// The blockScopedEnumVariablesUseBeforeDef baseline carries exactly + /// one TS2450: the regular enum in foo1. The const enums (foo2, + /// AfterObject) are inlined at use sites and stay clean. + #[test] + fn enum_used_before_declaration_matches_baseline() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = std::fs::read_to_string(root.join(concat!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", + "blockScopedEnumVariablesUseBeforeDef.ts" + ))) + .unwrap(); + let codes = checker_codes(&check_text(&source)); + assert_eq!( + codes + .iter() + .filter(|c| **c == ENUM_USED_BEFORE_DECLARATION.as_str()) + .count(), + 1, + "{codes:?}" + ); + } + + #[test] + fn block_scoped_used_before_declaration_matrix() { + fn codes(text: &str) -> Vec<&'static str> { + checker_codes(&check_text(text)) + } + // let/const/class/enum referenced textually before declaration. + let late_binding = codes( + "return_early(); + function return_early() { + const v = x; + let x = 1; + class C {} + new D(); + class D {} + E.A; + enum E { A } + return v; + }", + ); + assert!(late_binding.contains(&BLOCK_SCOPED_USED_BEFORE_DECLARATION.as_str())); + assert!(late_binding.contains(&CLASS_USED_BEFORE_DECLARATION.as_str())); + assert!( + late_binding.contains(&ENUM_USED_BEFORE_DECLARATION.as_str()), + "{late_binding:?}" + ); + // A reference deferred through a function-like boundary is legal: + // the function runs after the binding initializes, and its body + // resolves once the binding is already in scope. + let deferred = codes( + "function g() { + inner(); + let y = 1; + function inner() { return y; } + const f = () => y; + } + g();", + ); + assert!( + !deferred.contains(&BLOCK_SCOPED_USED_BEFORE_DECLARATION.as_str()), + "{deferred:?}" + ); + // var and function declarations hoist: no error. + let hoisted = codes( + "h(); + var shared = 1; + function h() { return shared; }", + ); + assert!( + !hoisted.contains(&BLOCK_SCOPED_USED_BEFORE_DECLARATION.as_str()), + "{hoisted:?}" + ); + // Use at or after the declaration stays silent. + assert_eq!(codes("let a = 1; a; const b = a; b;"), Vec::<&str>::new()); + } + /// TS2576: super.S1 where S1 is a base static member. Also pins the /// superAccess oracle shape: static fires TS2576, instance fields fire /// TS2855, and a base instance method stays silent. diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 78d51aa..be457c0 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -28,11 +28,12 @@ use super::{ ABSTRACT_CONSTRUCTOR, ACCESSOR_THIS_PARAMETER, AMBIENT_IMPLEMENTATION, ARGUMENT_COUNT_MISMATCH, ARGUMENT_NOT_ASSIGNABLE, ASSIGNMENT_TO_CONST, ASSIGNMENT_TO_FUNCTION, ASSIGNMENT_TO_NAMESPACE, ASSIGNMENT_TO_READONLY, AWAIT_USING_DECLARATION_IN_FOR_IN, BARE_SUPER_EXPRESSION, - CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, + BLOCK_SCOPED_USED_BEFORE_DECLARATION, CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, + CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, CONSTRUCTOR_TYPE_PARAMETERS, DECLARATION_CONFLICTS_WITH_BUILTIN_GLOBAL, DERIVED_CONSTRUCTOR_MISSING_SUPER, - DUPLICATE_DECLARATION, EXCESS_PROPERTY, EXPRESSION_NOT_CALLABLE, EXPRESSION_NOT_CONSTRUCTABLE, - FOR_IN_LEFT_HAND_SIDE_INVALID, FOR_OF_ITERABLE_REQUIRED, + DUPLICATE_DECLARATION, ENUM_USED_BEFORE_DECLARATION, EXCESS_PROPERTY, EXPRESSION_NOT_CALLABLE, + EXPRESSION_NOT_CONSTRUCTABLE, FOR_IN_LEFT_HAND_SIDE_INVALID, FOR_OF_ITERABLE_REQUIRED, FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT, FUNCTION_IMPLEMENTATION_WRONG_NAME, FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION, GET_ACCESSOR_NO_RETURN, GET_ACCESSOR_PARAMETERS, IMPORT_CONFLICTS_WITH_LOCAL, INVALID_ASSIGNMENT_TARGET, INVALID_INDEXED_ACCESS_KEY, @@ -14485,6 +14486,7 @@ impl<'src> Binder<'src> { USED_BEFORE_ASSIGNED_MESSAGE, ); } + self.check_used_before_declaration(identifier, symbol, scope); } else if !self.suppresses_unresolved_value(scope) { if self.intrinsics.is_lib_gated_value(&name) { self.emit( @@ -14521,6 +14523,75 @@ impl<'src> Binder<'src> { } false } + /// TS2448/TS2449/TS2450: a block-scoped binding referenced textually + /// before its declaration. A reference separated from the declaration by + /// a function-like boundary is deferred instead - the function runs + /// after the binding initializes - so only same-function (or same + /// module top-level) uses count. + fn check_used_before_declaration( + &mut self, + identifier: &IdentifierNode, + symbol: SymbolId, + scope: ScopeId, + ) { + let symbol_data = &self.symbols[symbol.get() as usize]; + let code = match symbol_data.kind { + SymbolKind::Variable(VariableKind::Let | VariableKind::Const) => { + BLOCK_SCOPED_USED_BEFORE_DECLARATION + } + SymbolKind::Class => CLASS_USED_BEFORE_DECLARATION, + SymbolKind::Enum => { + // A const enum is inlined at its use sites, so an early + // reference reads the cooked value instead of the runtime + // binding; tsc leaves it clean. + let is_const = self + .enum_declarations + .iter() + .any(|binding| binding.symbol == symbol && binding.declaration.is_const); + if is_const { + return; + } + ENUM_USED_BEFORE_DECLARATION + } + _ => return, + }; + let declaration_scope = symbol_data.scope(); + if identifier.range().start() >= symbol_data.range().start() + || self.boundary_scope(scope) != self.boundary_scope(declaration_scope) + || self.crosses_function_boundary(scope, declaration_scope) + { + return; + } + let name = self.identifier_text(identifier); + let message = match symbol_data.kind { + SymbolKind::Variable(_) => { + format!("Block-scoped variable '{name}' used before its declaration.") + } + SymbolKind::Class => format!("Class '{name}' used before its declaration."), + _ => format!("Enum '{name}' used before its declaration."), + }; + self.emit_with_message(code, identifier.range(), message); + } + + /// Whether walking from `from` up to `to` crosses a function-like + /// boundary. + fn crosses_function_boundary(&self, from: ScopeId, to: ScopeId) -> bool { + let mut scope = from; + loop { + if scope == to { + return false; + } + match self.scopes[scope.0 as usize].kind { + ScopeKind::Function | ScopeKind::Global | ScopeKind::Module => return true, + _ => {} + } + let Some(parent) = self.scopes[scope.0 as usize].parent else { + return true; + }; + scope = parent; + } + } + fn enclosing_this_owner(&self, mut scope: ScopeId) -> Option { loop { let lexical = &self.scopes[scope.0 as usize]; From 620549bd30b7caf34560e9377953f3bb40b5a6cb Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:06:02 +0900 Subject: [PATCH 28/42] Emit TS1014 for a non-trailing rest parameter A rest parameter followed by another parameter parsed silently with no diagnostic; tsc reports TS1014 at the rest parameter. One check walks a parameter list and reports a Rest binding that is not last. Function-likes get it beside their parameter loop; arrow and constructor lists route through their own binders, so each calls the same check beside its loop. A trailing rest parameter and ordinary lists stay silent. Matrix pins all four shapes (declaration, arrow, constructor, method) plus the trailing-rest negative, with a mutation red that disables the not-last test. Window: same-root continuation wave, post-hashes checker.rs:c78127a809f7f6e258793233bca23fe49e2ed8ddb53e3efbe06819fd6b617d1b binder.rs:aa7e7eb1f3bdb461c75b1c9600ff57573dfb366cad955aca6fe1569e17b3e842 Gates: compiler 1895/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 35 ++++++++++--- crates/bamts-compiler/src/checker/binder.rs | 58 +++++++++++++++------ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 94e5319..4a76011 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -145,6 +145,9 @@ pub const SUPER_STATIC_MEMBER_VIA_SUPER: DiagnosticCode = DiagnosticCode::new("B pub const BLOCK_SCOPED_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C094"); pub const CLASS_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C095"); pub const ENUM_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C096"); +pub const REST_PARAMETER_NOT_LAST: DiagnosticCode = DiagnosticCode::new("BAMTS-C097"); +pub(crate) const REST_PARAMETER_NOT_LAST_MESSAGE: &str = + "A rest parameter must be last in a parameter list."; pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); /// Diagnostic emitted when an assignment target resolves to a namespace. pub const ASSIGNMENT_TO_NAMESPACE: DiagnosticCode = DiagnosticCode::new("BAMTS-C030"); @@ -2175,12 +2178,13 @@ mod tests { IMPORTED_CONST_ENUM_NONCONSTANT, INVALID_ASSIGNMENT_TARGET, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, - ProgramCheckOptions, PropertyType, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, - SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, - SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, - ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, - TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, VALUE_CANNOT_BE_USED_HERE, - WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, + ProgramCheckOptions, PropertyType, REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, + SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, + SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, + TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, + VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, + check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; use crate::namespace_plan::{ContainerAcquisition, ExportStorage}; @@ -6013,6 +6017,25 @@ function check(options: Options = {}) { ); } + /// TS1014: a rest parameter followed by anything else. Arrows and + /// constructors carry their own binders, so each shape is pinned; a + /// trailing rest parameter stays silent. + #[test] + fn rest_parameter_not_last_matrix() { + let codes = checker_codes(&check_text("function f(...x, y) { }")); + assert_eq!(codes, vec![REST_PARAMETER_NOT_LAST.as_str()]); + let arrow = checker_codes(&check_text("const g = (...x, y) => x;")); + assert_eq!(arrow, vec![REST_PARAMETER_NOT_LAST.as_str()]); + let ctor = checker_codes(&check_text("class C { constructor(...x, y) {} }")); + assert_eq!(ctor, vec![REST_PARAMETER_NOT_LAST.as_str()]); + let method = checker_codes(&check_text("class C { m(...x, y) {} }")); + assert_eq!(method, vec![REST_PARAMETER_NOT_LAST.as_str()]); + assert_eq!( + checker_codes(&check_text("function h(x, ...rest) { }")), + Vec::<&str>::new() + ); + } + #[test] fn block_scoped_used_before_declaration_matrix() { fn codes(text: &str) -> Vec<&'static str> { diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index be457c0..54f1ccf 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -40,14 +40,15 @@ use super::{ MEMBER_NOT_ACCESSIBLE, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, NEW_TARGET_OUTSIDE_FUNCTION, NON_VOID_FUNCTION_MUST_RETURN, PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, - PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, SET_ACCESSOR_PARAMETER_INITIALIZER, - STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, - SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, - SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, - SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, - TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, - USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, - USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, + PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, REST_PARAMETER_NOT_LAST, + SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, + STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, + SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, + SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, + TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, + UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, + USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, + WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, @@ -69,15 +70,16 @@ use super::{ NON_VOID_FUNCTION_MUST_RETURN_MESSAGE, NOT_ASSIGNABLE_MESSAGE, PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, PROPERTY_DOES_NOT_EXIST_MESSAGE, PROPERTY_NOT_INITIALIZED_MESSAGE, - SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, - STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, - SUPER_BEFORE_THIS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, - SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, SUPER_REFERENCE_NON_DERIVED_MESSAGE, - TYPE_ALIAS_CIRCULAR_MESSAGE, TYPE_NESTING_TOO_DEEP_MESSAGE, - TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, UNUSED_EXPECT_ERROR_MESSAGE, - USED_BEFORE_ASSIGNED_MESSAGE, USING_DECLARATION_BINDING_PATTERN_MESSAGE, - USING_DECLARATION_IN_FOR_IN_MESSAGE, USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, - VALUE_CANNOT_BE_USED_HERE_MESSAGE, WITH_STATEMENT_NOT_ALLOWED_MESSAGE, + REST_PARAMETER_NOT_LAST_MESSAGE, SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, + STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, STRICT_NULL_MEMBER_ACCESS_MESSAGE, + SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, SUPER_BEFORE_THIS_MESSAGE, + SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, + SUPER_REFERENCE_NON_DERIVED_MESSAGE, TYPE_ALIAS_CIRCULAR_MESSAGE, + TYPE_NESTING_TOO_DEEP_MESSAGE, TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, + UNUSED_EXPECT_ERROR_MESSAGE, USED_BEFORE_ASSIGNED_MESSAGE, + USING_DECLARATION_BINDING_PATTERN_MESSAGE, USING_DECLARATION_IN_FOR_IN_MESSAGE, + USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, VALUE_CANNOT_BE_USED_HERE_MESSAGE, + WITH_STATEMENT_NOT_ALLOWED_MESSAGE, }; use crate::diagnostic::{Diagnostic, DiagnosticCode}; use crate::enum_plan::{self, EnumDeclarationBinding, EnumFacts}; @@ -9668,6 +9670,7 @@ impl<'src> Binder<'src> { }); self.bind_type_parameters(function.type_parameters.as_ref(), scope); let this_type = self.this_parameter_type(&function.parameters, scope, this_type); + self.check_rest_parameter_last(&function.parameters); for parameter in &function.parameters { if self.is_this_parameter(parameter) { continue; @@ -9990,6 +9993,25 @@ impl<'src> Binder<'src> { } } + /// TS1014: a rest parameter must be the last parameter. Constructor + /// and arrow parameter lists route through their own binders, so each + /// calls this beside its loop. + fn check_rest_parameter_last(&mut self, parameters: &[crate::syntax::ParameterNode]) { + for (index, parameter) in parameters.iter().enumerate() { + if matches!( + parameter.data().binding.data(), + crate::syntax::BindingPattern::Rest(_) + ) && index + 1 < parameters.len() + { + self.emit( + REST_PARAMETER_NOT_LAST, + parameter.range(), + REST_PARAMETER_NOT_LAST_MESSAGE, + ); + } + } + } + fn resolve_class(&mut self, class: &'src ClassDeclaration, parent: ScopeId) { let ambient = class.modifiers.is_declare || self.ambient_stack.last().copied().unwrap_or(false); @@ -11777,6 +11799,7 @@ impl<'src> Binder<'src> { self.bind_implicit_function_values(&constructor.parameters, child); self.super_call_contexts .push(SuperCallContext::ConstructorParameters { derived }); + self.check_rest_parameter_last(&constructor.parameters); for parameter in &constructor.parameters { if self.is_parameter_property(parameter) { self.resolve_parameter_property(parameter, child, scope); @@ -11962,6 +11985,7 @@ impl<'src> Binder<'src> { self.super_call_contexts .push(SuperCallContext::NonConstructor); self.bind_type_parameters(arrow.type_parameters.as_ref(), child); + self.check_rest_parameter_last(&arrow.parameters); for parameter in &arrow.parameters { self.resolve_non_constructor_parameter(parameter, child); } From 3b987f4161afed57a2405cc0b766aa29b51eef9c Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:14:54 +0900 Subject: [PATCH 29/42] Emit TS2427 for interfaces named like primitives interface string {} parsed with no diagnostic; tsc reports TS2427 because the name would shadow the primitive type everywhere. Interface binding rejects the primitive-name set (any, unknown, never, void, undefined, null, string, number, boolean, bigint, symbol, object) with tsc's exact message at the name range. Reserved-word spellings (void, null) never reach the checker - the parser tokenizes them as keywords - so the pinned set is the contextual names the authority baselines actually carry; the reserved-word relaxation is a separate parser slice. Oracle: InterfaceDeclaration8 is exactly one row, pinned by test; the matrix covers all eight observed names plus legal intrinsic-shaped names, with a mutation red on the gate. Window: same-root continuation wave, post-hashes checker.rs:bc63d56f138b9195b4eb57db96d0657961f5d22714a221b169c187106fc69b48 binder.rs:2797ef2a576f8a76b12b3c9f60e90d5fc862312a7f8b7bb47fff73b24b825a3d Gates: compiler 1896/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 58 +++++++++++++++++---- crates/bamts-compiler/src/checker/binder.rs | 56 +++++++++++++++----- 2 files changed, 90 insertions(+), 24 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 4a76011..c285cf3 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -146,6 +146,7 @@ pub const BLOCK_SCOPED_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode: pub const CLASS_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C095"); pub const ENUM_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C096"); pub const REST_PARAMETER_NOT_LAST: DiagnosticCode = DiagnosticCode::new("BAMTS-C097"); +pub const INTERFACE_NAME_IS_PRIMITIVE: DiagnosticCode = DiagnosticCode::new("BAMTS-C098"); pub(crate) const REST_PARAMETER_NOT_LAST_MESSAGE: &str = "A rest parameter must be last in a parameter list."; pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); @@ -2175,16 +2176,16 @@ mod tests { CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, DERIVED_CONSTRUCTOR_MISSING_SUPER, DUPLICATE_DECLARATION, ENUM_USED_BEFORE_DECLARATION, EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, IMPORTED_CONST_ENUM_CYCLE, - IMPORTED_CONST_ENUM_NONCONSTANT, INVALID_ASSIGNMENT_TARGET, MISSING_METHOD_RETURN_TYPE, - MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, PARAMETER_DECORATOR_NOT_SUPPORTED, - PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, - ProgramCheckOptions, PropertyType, REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, - SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, - SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, - SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, - TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, - VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, - check_program_with_options, + IMPORTED_CONST_ENUM_NONCONSTANT, INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, + MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, + PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, + PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, + REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, + SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, + SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, + ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, + TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, VALUE_CANNOT_BE_USED_HERE, + WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; use crate::namespace_plan::{ContainerAcquisition, ExportStorage}; @@ -6017,6 +6018,43 @@ function check(options: Options = {}) { ); } + /// TS2427: an interface may not take a primitive type's name. The + /// authority rows cover string, number, boolean, any, never, unknown, + /// symbol, and undefined. Reserved-word names (`void`, `null`) need a + /// parser-relaxation slice before the checker can see them; the + /// implementation bans the full primitive set for the names the parser + /// delivers as identifiers. + #[test] + fn interface_name_cannot_be_primitive() { + for name in [ + "string", + "number", + "boolean", + "any", + "never", + "unknown", + "symbol", + "undefined", + ] { + let source = format!("interface {name} {{}}"); + let codes = checker_codes(&check_text(&source)); + assert_eq!( + codes, + vec![INTERFACE_NAME_IS_PRIMITIVE.as_str()], + "{name}: {codes:?}" + ); + } + // Ordinary names, including intrinsic globals, stay legal. + assert_eq!( + checker_codes(&check_text("interface ArrayLike { length: number }")), + Vec::<&str>::new() + ); + assert_eq!( + checker_codes(&check_text("interface PromiseLike { then(): T }")), + Vec::<&str>::new() + ); + } + /// TS1014: a rest parameter followed by anything else. Arrows and /// constructors carry their own binders, so each shape is pinned; a /// trailing rest parameter stays silent. diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 54f1ccf..6b851aa 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -36,19 +36,19 @@ use super::{ EXPRESSION_NOT_CONSTRUCTABLE, FOR_IN_LEFT_HAND_SIDE_INVALID, FOR_OF_ITERABLE_REQUIRED, FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT, FUNCTION_IMPLEMENTATION_WRONG_NAME, FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION, GET_ACCESSOR_NO_RETURN, GET_ACCESSOR_PARAMETERS, - IMPORT_CONFLICTS_WITH_LOCAL, INVALID_ASSIGNMENT_TARGET, INVALID_INDEXED_ACCESS_KEY, - MEMBER_NOT_ACCESSIBLE, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, - NAMESPACE_NO_EXPORTED_MEMBER, NEW_TARGET_OUTSIDE_FUNCTION, NON_VOID_FUNCTION_MUST_RETURN, - PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, - PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, REST_PARAMETER_NOT_LAST, - SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, - STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, - SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, - TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, - UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, - USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, - WITH_STATEMENT_NOT_ALLOWED, + IMPORT_CONFLICTS_WITH_LOCAL, INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, + INVALID_INDEXED_ACCESS_KEY, MEMBER_NOT_ACCESSIBLE, MISSING_METHOD_RETURN_TYPE, + MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, NEW_TARGET_OUTSIDE_FUNCTION, + NON_VOID_FUNCTION_MUST_RETURN, PARAMETER_DECORATOR_NOT_SUPPORTED, + PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, + REST_PARAMETER_NOT_LAST, SET_ACCESSOR_PARAMETER_INITIALIZER, + STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, + SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, + SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, + TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, + USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, + USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, @@ -234,6 +234,26 @@ pub enum SymbolKind { Namespace, } +/// Names an interface may not take: they already denote primitive +/// types, so a same-named interface would shadow them everywhere. +fn is_primitive_type_name(name: &str) -> bool { + matches!( + name, + "any" + | "unknown" + | "never" + | "void" + | "undefined" + | "null" + | "string" + | "number" + | "boolean" + | "bigint" + | "symbol" + | "object" + ) +} + impl SymbolKind { const fn occupies_value(self) -> bool { matches!( @@ -7561,8 +7581,16 @@ impl<'src> Binder<'src> { scope: ScopeId, declaration: NodeId, ) { + let name = self.identifier_text(&interface.name); + if is_primitive_type_name(&name) { + self.emit_with_message( + INTERFACE_NAME_IS_PRIMITIVE, + interface.name.range(), + format!("Interface name cannot be '{name}'."), + ); + } let id = self.declare( - &self.identifier_text(&interface.name), + &name, SymbolKind::Interface, scope, declaration, From b5ef4e5a808e29c4efaf29a4fc7053b93f32d0b2 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:23:39 +0900 Subject: [PATCH 30/42] Split the super field rule flavor by target superAccess carries two baselines: es2015 reports each base field read through super as TS2855, while es5 keeps the single pre-fields rule TS2340 and reports the static row as TS2576 at both targets. The checker emitted TS2855 regardless of target, so the es5 variant could never match. The field check now reads the binder's es5 flag: downlevel targets emit the static TS2340 message; ES2015 and later keep the field-naming TS2855 message. Oracle: the es5 baseline pins two TS2340 rows plus one TS2576, and the es2015 variant pins two TS2855 plus the same TS2576 - both flavors asserted against the authority source under check_text_with target options, with a mutation red on the flavor gate. Window: same-root continuation wave, post-hashes checker.rs:d02f0cf8e2a0955355d2d5e5735ee87a10a57bd8f394731892b528f49b483b79 binder.rs:6b0a46f51b09cf0a7edb3088a8c0800b00e080b29430573b1bdf0d3e4a844b4b Gates: compiler 1897/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 104 +++++++++++++++++++- crates/bamts-compiler/src/checker/binder.rs | 48 +++++---- 2 files changed, 130 insertions(+), 22 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index c285cf3..424b98a 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -147,6 +147,9 @@ pub const CLASS_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("B pub const ENUM_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BAMTS-C096"); pub const REST_PARAMETER_NOT_LAST: DiagnosticCode = DiagnosticCode::new("BAMTS-C097"); pub const INTERFACE_NAME_IS_PRIMITIVE: DiagnosticCode = DiagnosticCode::new("BAMTS-C098"); +pub const SUPER_PROPERTY_NOT_METHOD: DiagnosticCode = DiagnosticCode::new("BAMTS-C099"); +pub(crate) const SUPER_PROPERTY_NOT_METHOD_MESSAGE: &str = + "Only public and protected methods of the base class are accessible via the 'super' keyword."; pub(crate) const REST_PARAMETER_NOT_LAST_MESSAGE: &str = "A rest parameter must be last in a parameter list."; pub const ASSIGNMENT_TO_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C029"); @@ -2182,10 +2185,11 @@ mod tests { PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, - SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, - ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, - TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, VALUE_CANNOT_BE_USED_HERE, - WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, + SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, SUPER_REFERENCE_NON_DERIVED, + SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, + TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, + VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, + check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; use crate::namespace_plan::{ContainerAcquisition, ExportStorage}; @@ -6018,6 +6022,98 @@ function check(options: Options = {}) { ); } + /// The superAccess es5 baseline swaps each TS2855 field row for the + /// single pre-fields rule TS2340; the static row stays TS2576 at both + /// targets. + #[test] + fn super_field_flavor_splits_by_target() { + let source = "class MyBase { + static S1: number = 5; + private S2: string = \"test\"; + f = () => 5; + } + class MyDerived extends MyBase { + foo() { + var l3 = super.S1; + var l4 = super.S2; + var l5 = super.f(); + } + }"; + let es5 = checker_codes(&check_text_with( + source, + ProgramCheckOptions::standard().with_target(Some("es5")), + )); + assert_eq!( + es5.iter() + .filter(|c| **c == SUPER_PROPERTY_NOT_METHOD.as_str()) + .count(), + 2, + "{es5:?}" + ); + assert_eq!( + es5.iter() + .filter(|c| **c == SUPER_FIELD_VIA_SUPER.as_str()) + .count(), + 0, + "{es5:?}" + ); + assert_eq!( + es5.iter() + .filter(|c| **c == SUPER_STATIC_MEMBER_VIA_SUPER.as_str()) + .count(), + 1, + "{es5:?}" + ); + let es2015 = checker_codes(&check_text_with( + source, + ProgramCheckOptions::standard().with_target(Some("es2015")), + )); + assert_eq!( + es2015 + .iter() + .filter(|c| **c == SUPER_FIELD_VIA_SUPER.as_str()) + .count(), + 2, + "{es2015:?}" + ); + assert_eq!( + es2015 + .iter() + .filter(|c| **c == SUPER_PROPERTY_NOT_METHOD.as_str()) + .count(), + 0, + "{es2015:?}" + ); + // The authority file itself: es5 swaps both field rows for TS2340 + // and keeps the static row at TS2576. + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = + std::fs::read_to_string(root.join( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", + )) + .unwrap(); + let oracle_es5 = checker_codes(&check_text_with( + &source, + ProgramCheckOptions::standard().with_target(Some("es5")), + )); + assert_eq!( + oracle_es5 + .iter() + .filter(|c| **c == SUPER_PROPERTY_NOT_METHOD.as_str()) + .count(), + 2, + "{oracle_es5:?}" + ); + assert_eq!( + oracle_es5 + .iter() + .filter(|c| **c == SUPER_STATIC_MEMBER_VIA_SUPER.as_str()) + .count(), + 1, + "{oracle_es5:?}" + ); + } + /// TS2427: an interface may not take a primitive type's name. The /// authority rows cover string, number, boolean, any, never, unknown, /// symbol, and undefined. Reserved-word names (`void`, `null`) need a diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 6b851aa..4125029 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -44,11 +44,12 @@ use super::{ REST_PARAMETER_NOT_LAST, SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, - SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_REFERENCE_NON_DERIVED, - SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, - TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, - USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, - USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, + SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, + TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, + UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, + USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, + WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, @@ -74,12 +75,12 @@ use super::{ STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, SUPER_BEFORE_THIS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, - SUPER_REFERENCE_NON_DERIVED_MESSAGE, TYPE_ALIAS_CIRCULAR_MESSAGE, - TYPE_NESTING_TOO_DEEP_MESSAGE, TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, - UNUSED_EXPECT_ERROR_MESSAGE, USED_BEFORE_ASSIGNED_MESSAGE, - USING_DECLARATION_BINDING_PATTERN_MESSAGE, USING_DECLARATION_IN_FOR_IN_MESSAGE, - USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, VALUE_CANNOT_BE_USED_HERE_MESSAGE, - WITH_STATEMENT_NOT_ALLOWED_MESSAGE, + SUPER_PROPERTY_NOT_METHOD_MESSAGE, SUPER_REFERENCE_NON_DERIVED_MESSAGE, + TYPE_ALIAS_CIRCULAR_MESSAGE, TYPE_NESTING_TOO_DEEP_MESSAGE, + TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, UNUSED_EXPECT_ERROR_MESSAGE, + USED_BEFORE_ASSIGNED_MESSAGE, USING_DECLARATION_BINDING_PATTERN_MESSAGE, + USING_DECLARATION_IN_FOR_IN_MESSAGE, USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, + VALUE_CANNOT_BE_USED_HERE_MESSAGE, WITH_STATEMENT_NOT_ALLOWED_MESSAGE, }; use crate::diagnostic::{Diagnostic, DiagnosticCode}; use crate::enum_plan::{self, EnumDeclarationBinding, EnumFacts}; @@ -12527,13 +12528,24 @@ impl<'src> Binder<'src> { .find(|member| member.name() == name.as_ref()) .is_some_and(|member| !member.is_method() && !member.accessor()); if is_field { - self.emit_with_message( - SUPER_FIELD_VIA_SUPER, - identifier.range(), - format!( - "Class field '{name}' defined by the parent class is not accessible in the child class via super." - ), - ); + // Downlevel targets keep the single pre-fields rule: fields are + // not methods on the prototype, so the whole access is illegal + // through super. ES2015+ names the field explicitly. + if self.es5 { + self.emit( + SUPER_PROPERTY_NOT_METHOD, + identifier.range(), + SUPER_PROPERTY_NOT_METHOD_MESSAGE, + ); + } else { + self.emit_with_message( + SUPER_FIELD_VIA_SUPER, + identifier.range(), + format!( + "Class field '{name}' defined by the parent class is not accessible in the child class via super." + ), + ); + } } } From f64129e31621a1ddf6a7d45d8fe3493bb1101511 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:31:10 +0900 Subject: [PATCH 31/42] Emit TS2371 for defaults on overload signatures A parameter initializer on an overload signature parsed with no diagnostic; tsc reports TS2371 because only an implementation has a body the default could bind to. Function-like resolution reports each defaulted parameter when the body is absent; the constructor arm uses the same check when its body block is empty, which is how a signature arrives. Implementations stay silent. Oracle: defaultValueInConstructorOverload1 is exactly one row, pinned by test alongside both shapes and the implementation negative, with a mutation red on both gates. Window: same-root continuation wave, post-hashes checker.rs:9099412c06ed8a922e1b0abc9f20b828ab714c894b0f8f1409de87df0765d9ab binder.rs:2eaa34b7dd006a5967af4fa99fd5f33bb0f7a97fdecb64f9172fe6850fbc7fb0 Gates: compiler 1899/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 61 +++++++++++++++--- crates/bamts-compiler/src/checker/binder.rs | 69 ++++++++++++++------- 2 files changed, 100 insertions(+), 30 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 424b98a..bab8822 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -148,6 +148,9 @@ pub const ENUM_USED_BEFORE_DECLARATION: DiagnosticCode = DiagnosticCode::new("BA pub const REST_PARAMETER_NOT_LAST: DiagnosticCode = DiagnosticCode::new("BAMTS-C097"); pub const INTERFACE_NAME_IS_PRIMITIVE: DiagnosticCode = DiagnosticCode::new("BAMTS-C098"); pub const SUPER_PROPERTY_NOT_METHOD: DiagnosticCode = DiagnosticCode::new("BAMTS-C099"); +pub const PARAMETER_INITIALIZER_IN_SIGNATURE: DiagnosticCode = DiagnosticCode::new("BAMTS-C100"); +pub(crate) const PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE: &str = + "A parameter initializer is only allowed in a function or constructor implementation."; pub(crate) const SUPER_PROPERTY_NOT_METHOD_MESSAGE: &str = "Only public and protected methods of the base class are accessible via the 'super' keyword."; pub(crate) const REST_PARAMETER_NOT_LAST_MESSAGE: &str = @@ -2181,14 +2184,14 @@ mod tests { EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, IMPORTED_CONST_ENUM_CYCLE, IMPORTED_CONST_ENUM_NONCONSTANT, INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, - PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, - PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, - REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, - SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, - SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, SUPER_REFERENCE_NON_DERIVED, - SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, - TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, - VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, + PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_INITIALIZER_IN_SIGNATURE, + PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, + ProgramCheckOptions, PropertyType, REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, + SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, + SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, + TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, + TypeTable, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; @@ -6025,6 +6028,48 @@ function check(options: Options = {}) { /// The superAccess es5 baseline swaps each TS2855 field row for the /// single pre-fields rule TS2340; the static row stays TS2576 at both /// targets. + #[test] + fn zz_enumbasics3_probe() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = + std::fs::read_to_string(root.join( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/enumBasics3.ts", + )) + .unwrap(); + eprintln!("ZZ eb3: {:?}", checker_codes(&check_text(&source))); + } + + /// TS2371: only an implementation may carry parameter defaults. The + /// oracle (defaultValueInConstructorOverload1) is exactly one row on a + /// constructor overload signature. + #[test] + fn parameter_initializer_only_in_implementation() { + assert_eq!( + checker_codes(&check_text("function f(x = 1); function f(x?: number) {}")), + vec![PARAMETER_INITIALIZER_IN_SIGNATURE.as_str()] + ); + assert_eq!( + checker_codes(&check_text( + "class C { constructor(x = ''); constructor(x = '') {} }" + )), + vec![PARAMETER_INITIALIZER_IN_SIGNATURE.as_str()] + ); + assert_eq!( + checker_codes(&check_text("function g(x = 1) {}")), + Vec::<&str>::new() + ); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = std::fs::read_to_string(root.join(concat!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", + "defaultValueInConstructorOverload1.ts" + ))) + .unwrap(); + assert_eq!( + checker_codes(&check_text(&source)), + vec![PARAMETER_INITIALIZER_IN_SIGNATURE.as_str()] + ); + } + #[test] fn super_field_flavor_splits_by_target() { let source = "class MyBase { diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 4125029..72d3ef5 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -40,16 +40,16 @@ use super::{ INVALID_INDEXED_ACCESS_KEY, MEMBER_NOT_ACCESSIBLE, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, NEW_TARGET_OUTSIDE_FUNCTION, NON_VOID_FUNCTION_MUST_RETURN, PARAMETER_DECORATOR_NOT_SUPPORTED, - PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, - REST_PARAMETER_NOT_LAST, SET_ACCESSOR_PARAMETER_INITIALIZER, - STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, - SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, - SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, - SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, - TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, - UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, - USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, - WITH_STATEMENT_NOT_ALLOWED, + PARAMETER_INITIALIZER_IN_SIGNATURE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, + PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, REST_PARAMETER_NOT_LAST, + SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, + STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, + SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, + SUPER_PROPERTY_NOT_METHOD, SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, + TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, + TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, + USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, + USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, @@ -69,18 +69,19 @@ use super::{ MEMBER_NOT_ACCESSIBLE_MESSAGE, MISSING_METHOD_RETURN_TYPE_MESSAGE, MIXED_EXPORT_ASSIGNMENT_MESSAGE, NEW_TARGET_OUTSIDE_FUNCTION_MESSAGE, NON_VOID_FUNCTION_MUST_RETURN_MESSAGE, NOT_ASSIGNABLE_MESSAGE, - PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, - PROPERTY_DOES_NOT_EXIST_MESSAGE, PROPERTY_NOT_INITIALIZED_MESSAGE, - REST_PARAMETER_NOT_LAST_MESSAGE, SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, - STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, STRICT_NULL_MEMBER_ACCESS_MESSAGE, - SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, SUPER_BEFORE_THIS_MESSAGE, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, - SUPER_PROPERTY_NOT_METHOD_MESSAGE, SUPER_REFERENCE_NON_DERIVED_MESSAGE, - TYPE_ALIAS_CIRCULAR_MESSAGE, TYPE_NESTING_TOO_DEEP_MESSAGE, - TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, UNUSED_EXPECT_ERROR_MESSAGE, - USED_BEFORE_ASSIGNED_MESSAGE, USING_DECLARATION_BINDING_PATTERN_MESSAGE, - USING_DECLARATION_IN_FOR_IN_MESSAGE, USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, - VALUE_CANNOT_BE_USED_HERE_MESSAGE, WITH_STATEMENT_NOT_ALLOWED_MESSAGE, + PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, + PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, PROPERTY_DOES_NOT_EXIST_MESSAGE, + PROPERTY_NOT_INITIALIZED_MESSAGE, REST_PARAMETER_NOT_LAST_MESSAGE, + SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, + STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, + SUPER_BEFORE_THIS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, + SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, SUPER_PROPERTY_NOT_METHOD_MESSAGE, + SUPER_REFERENCE_NON_DERIVED_MESSAGE, TYPE_ALIAS_CIRCULAR_MESSAGE, + TYPE_NESTING_TOO_DEEP_MESSAGE, TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, + UNUSED_EXPECT_ERROR_MESSAGE, USED_BEFORE_ASSIGNED_MESSAGE, + USING_DECLARATION_BINDING_PATTERN_MESSAGE, USING_DECLARATION_IN_FOR_IN_MESSAGE, + USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, VALUE_CANNOT_BE_USED_HERE_MESSAGE, + WITH_STATEMENT_NOT_ALLOWED_MESSAGE, }; use crate::diagnostic::{Diagnostic, DiagnosticCode}; use crate::enum_plan::{self, EnumDeclarationBinding, EnumFacts}; @@ -9700,6 +9701,19 @@ impl<'src> Binder<'src> { self.bind_type_parameters(function.type_parameters.as_ref(), scope); let this_type = self.this_parameter_type(&function.parameters, scope, this_type); self.check_rest_parameter_last(&function.parameters); + // TS2371: only an implementation may carry parameter defaults; an + // overload signature has no body to receive them. + if function.body.is_none() { + for parameter in &function.parameters { + if parameter.data().initializer.is_some() { + self.emit( + PARAMETER_INITIALIZER_IN_SIGNATURE, + parameter.range(), + PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, + ); + } + } + } for parameter in &function.parameters { if self.is_this_parameter(parameter) { continue; @@ -11829,6 +11843,17 @@ impl<'src> Binder<'src> { self.super_call_contexts .push(SuperCallContext::ConstructorParameters { derived }); self.check_rest_parameter_last(&constructor.parameters); + if constructor.body.range().is_empty() { + for parameter in &constructor.parameters { + if parameter.data().initializer.is_some() { + self.emit( + PARAMETER_INITIALIZER_IN_SIGNATURE, + parameter.range(), + PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, + ); + } + } + } for parameter in &constructor.parameters { if self.is_parameter_property(parameter) { self.resolve_parameter_property(parameter, child, scope); From 33f680f087e9a49e9e37064606c99dd5354eef45 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:42:16 +0900 Subject: [PATCH 32/42] Emit TS1114 and TS1116 for label misuse A redeclared label and a break to a label that does not enclose the statement both passed silently; tsc reports TS1114 and TS1116. Two label views now ride the statement walk: the declarations of the current function (sliced at a mark that resolve_function pushes and truncates at, so nested functions own their labels) report a duplicate on the second declaration, and the stack of enclosing labeled statements gates every labeled break. TS1107, breaking across a function boundary, needs the declaring function's labels and stays banked. Oracles: duplicateLabel2 and breakTarget6 are exactly one row each, pinned beside nested/sibling/cross-function shapes with a mutation red on the function mark. Window: same-root continuation wave, post-hashes checker.rs:4ee2ff960096c813770d50ff9804a949aeb13a5c9a06b7b5b5c2c8f40b06f416 binder.rs:8fd52f6aeb316ffa84ee297f63ace49ac8f986e1419a0d6db8342313e7a10be4 Gates: compiler 1900/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 74 ++++++++++++- crates/bamts-compiler/src/checker/binder.rs | 114 ++++++++++++++------ 2 files changed, 153 insertions(+), 35 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index bab8822..6502869 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -149,6 +149,10 @@ pub const REST_PARAMETER_NOT_LAST: DiagnosticCode = DiagnosticCode::new("BAMTS-C pub const INTERFACE_NAME_IS_PRIMITIVE: DiagnosticCode = DiagnosticCode::new("BAMTS-C098"); pub const SUPER_PROPERTY_NOT_METHOD: DiagnosticCode = DiagnosticCode::new("BAMTS-C099"); pub const PARAMETER_INITIALIZER_IN_SIGNATURE: DiagnosticCode = DiagnosticCode::new("BAMTS-C100"); +pub const DUPLICATE_LABEL: DiagnosticCode = DiagnosticCode::new("BAMTS-C101"); +pub const BREAK_TARGET_NOT_ENCLOSING: DiagnosticCode = DiagnosticCode::new("BAMTS-C102"); +pub(crate) const BREAK_TARGET_NOT_ENCLOSING_MESSAGE: &str = + "A 'break' statement can only jump to a label of an enclosing statement."; pub(crate) const PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE: &str = "A parameter initializer is only allowed in a function or constructor implementation."; pub(crate) const SUPER_PROPERTY_NOT_METHOD_MESSAGE: &str = @@ -2178,9 +2182,10 @@ fn imported_enum_error( mod tests { use super::{ ARGUMENT_NOT_ASSIGNABLE, BARE_SUPER_EXPRESSION, BLOCK_SCOPED_USED_BEFORE_DECLARATION, - CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, - CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, - DERIVED_CONSTRUCTOR_MISSING_SUPER, DUPLICATE_DECLARATION, ENUM_USED_BEFORE_DECLARATION, + BREAK_TARGET_NOT_ENCLOSING, CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, + CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, CLASS_USED_BEFORE_DECLARATION, + CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, DERIVED_CONSTRUCTOR_MISSING_SUPER, + DUPLICATE_DECLARATION, DUPLICATE_LABEL, ENUM_USED_BEFORE_DECLARATION, EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, IMPORTED_CONST_ENUM_CYCLE, IMPORTED_CONST_ENUM_NONCONSTANT, INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, @@ -6042,6 +6047,69 @@ function check(options: Options = {}) { /// TS2371: only an implementation may carry parameter defaults. The /// oracle (defaultValueInConstructorOverload1) is exactly one row on a /// constructor overload signature. + /// TS1114: a label redeclared in one function (sibling or nested). + /// TS1116: a labeled break may only target an enclosing label. The + /// oracles are duplicateLabel2 (one row) and breakTarget6 (one row). + #[test] + fn label_rules_matrix() { + assert_eq!( + checker_codes(&check_text( + "target: while (true) { target: while (true) {} }" + )), + vec![DUPLICATE_LABEL.as_str()] + ); + // Sibling redeclaration in the same function is also a duplicate. + assert_eq!( + checker_codes(&check_text("a: {} a: {}")), + vec![DUPLICATE_LABEL.as_str()] + ); + // A nested function owns its labels; reuse across the boundary is + // legal. + assert_eq!( + checker_codes(&check_text("a: {} function f() { a: {} }")), + Vec::<&str>::new() + ); + assert_eq!( + checker_codes(&check_text("while (true) { break target; }")), + vec![BREAK_TARGET_NOT_ENCLOSING.as_str()] + ); + assert_eq!( + checker_codes(&check_text("target: while (true) { break target; }")), + Vec::<&str>::new() + ); + assert_eq!( + checker_codes(&check_text( + "target: while (true) { for (;;) { break target; } }" + )), + Vec::<&str>::new() + ); + // Plain break needs no label context. + assert_eq!( + checker_codes(&check_text("for (;;) { break; }")), + Vec::<&str>::new() + ); + // Authority oracles: one row each. + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let duplicate = std::fs::read_to_string(root.join(concat!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", + "duplicateLabel2.ts" + ))) + .unwrap(); + assert_eq!( + checker_codes(&check_text(&duplicate)), + vec![DUPLICATE_LABEL.as_str()] + ); + let jump = std::fs::read_to_string(root.join(concat!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", + "breakTarget6.ts" + ))) + .unwrap(); + assert_eq!( + checker_codes(&check_text(&jump)), + vec![BREAK_TARGET_NOT_ENCLOSING.as_str()] + ); + } + #[test] fn parameter_initializer_only_in_implementation() { assert_eq!( diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 72d3ef5..db3ec34 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -28,11 +28,12 @@ use super::{ ABSTRACT_CONSTRUCTOR, ACCESSOR_THIS_PARAMETER, AMBIENT_IMPLEMENTATION, ARGUMENT_COUNT_MISMATCH, ARGUMENT_NOT_ASSIGNABLE, ASSIGNMENT_TO_CONST, ASSIGNMENT_TO_FUNCTION, ASSIGNMENT_TO_NAMESPACE, ASSIGNMENT_TO_READONLY, AWAIT_USING_DECLARATION_IN_FOR_IN, BARE_SUPER_EXPRESSION, - BLOCK_SCOPED_USED_BEFORE_DECLARATION, CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, - CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, CLASS_USED_BEFORE_DECLARATION, - CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, CONSTRUCTOR_TYPE_PARAMETERS, - DECLARATION_CONFLICTS_WITH_BUILTIN_GLOBAL, DERIVED_CONSTRUCTOR_MISSING_SUPER, - DUPLICATE_DECLARATION, ENUM_USED_BEFORE_DECLARATION, EXCESS_PROPERTY, EXPRESSION_NOT_CALLABLE, + BLOCK_SCOPED_USED_BEFORE_DECLARATION, BREAK_TARGET_NOT_ENCLOSING, CANNOT_FIND_NAME, + CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, + CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, + CONSTRUCTOR_TYPE_PARAMETERS, DECLARATION_CONFLICTS_WITH_BUILTIN_GLOBAL, + DERIVED_CONSTRUCTOR_MISSING_SUPER, DUPLICATE_DECLARATION, DUPLICATE_LABEL, + ENUM_USED_BEFORE_DECLARATION, EXCESS_PROPERTY, EXPRESSION_NOT_CALLABLE, EXPRESSION_NOT_CONSTRUCTABLE, FOR_IN_LEFT_HAND_SIDE_INVALID, FOR_OF_ITERABLE_REQUIRED, FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT, FUNCTION_IMPLEMENTATION_WRONG_NAME, FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION, GET_ACCESSOR_NO_RETURN, GET_ACCESSOR_PARAMETERS, @@ -56,32 +57,32 @@ use super::{ ARGUMENT_COUNT_MISMATCH_MESSAGE, ARGUMENT_NOT_ASSIGNABLE_MESSAGE, ASSIGNMENT_TO_CONST_MESSAGE, ASSIGNMENT_TO_FUNCTION_MESSAGE, ASSIGNMENT_TO_NAMESPACE_MESSAGE, ASSIGNMENT_TO_READONLY_MESSAGE, AWAIT_USING_DECLARATION_IN_FOR_IN_MESSAGE, - BARE_SUPER_EXPRESSION_MESSAGE, CANNOT_FIND_NAME_LIB_GATED_MESSAGE, CANNOT_FIND_NAME_MESSAGE, - CANNOT_FIND_NAMESPACE_MESSAGE, CANNOT_FIND_TYPE_MESSAGE, - CONSTRUCTOR_DECORATOR_NOT_SUPPORTED_MESSAGE, CONSTRUCTOR_TYPE_PARAMETERS_MESSAGE, - DERIVED_CONSTRUCTOR_MISSING_SUPER_MESSAGE, DUPLICATE_MESSAGE, EXCESS_PROPERTY_MESSAGE, - EXPRESSION_NOT_CALLABLE_MESSAGE, EXPRESSION_NOT_CONSTRUCTABLE_MESSAGE, - FOR_IN_LEFT_HAND_SIDE_INVALID_MESSAGE, FOR_OF_ITERABLE_REQUIRED_MESSAGE, - FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT_MESSAGE, FUNCTION_IMPLEMENTATION_WRONG_NAME_MESSAGE, - FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION_MESSAGE, GET_ACCESSOR_NO_RETURN_MESSAGE, - GET_ACCESSOR_PARAMETERS_MESSAGE, IMPORT_CONFLICTS_WITH_LOCAL_MESSAGE, - INVALID_ASSIGNMENT_TARGET_MESSAGE, INVALID_INDEXED_ACCESS_KEY_MESSAGE, - MEMBER_NOT_ACCESSIBLE_MESSAGE, MISSING_METHOD_RETURN_TYPE_MESSAGE, - MIXED_EXPORT_ASSIGNMENT_MESSAGE, NEW_TARGET_OUTSIDE_FUNCTION_MESSAGE, - NON_VOID_FUNCTION_MUST_RETURN_MESSAGE, NOT_ASSIGNABLE_MESSAGE, - PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, - PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, PROPERTY_DOES_NOT_EXIST_MESSAGE, - PROPERTY_NOT_INITIALIZED_MESSAGE, REST_PARAMETER_NOT_LAST_MESSAGE, - SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, - STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, - SUPER_BEFORE_THIS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, - SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, SUPER_PROPERTY_NOT_METHOD_MESSAGE, - SUPER_REFERENCE_NON_DERIVED_MESSAGE, TYPE_ALIAS_CIRCULAR_MESSAGE, - TYPE_NESTING_TOO_DEEP_MESSAGE, TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, - UNUSED_EXPECT_ERROR_MESSAGE, USED_BEFORE_ASSIGNED_MESSAGE, - USING_DECLARATION_BINDING_PATTERN_MESSAGE, USING_DECLARATION_IN_FOR_IN_MESSAGE, - USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, VALUE_CANNOT_BE_USED_HERE_MESSAGE, - WITH_STATEMENT_NOT_ALLOWED_MESSAGE, + BARE_SUPER_EXPRESSION_MESSAGE, BREAK_TARGET_NOT_ENCLOSING_MESSAGE, + CANNOT_FIND_NAME_LIB_GATED_MESSAGE, CANNOT_FIND_NAME_MESSAGE, CANNOT_FIND_NAMESPACE_MESSAGE, + CANNOT_FIND_TYPE_MESSAGE, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED_MESSAGE, + CONSTRUCTOR_TYPE_PARAMETERS_MESSAGE, DERIVED_CONSTRUCTOR_MISSING_SUPER_MESSAGE, + DUPLICATE_MESSAGE, EXCESS_PROPERTY_MESSAGE, EXPRESSION_NOT_CALLABLE_MESSAGE, + EXPRESSION_NOT_CONSTRUCTABLE_MESSAGE, FOR_IN_LEFT_HAND_SIDE_INVALID_MESSAGE, + FOR_OF_ITERABLE_REQUIRED_MESSAGE, FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT_MESSAGE, + FUNCTION_IMPLEMENTATION_WRONG_NAME_MESSAGE, FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION_MESSAGE, + GET_ACCESSOR_NO_RETURN_MESSAGE, GET_ACCESSOR_PARAMETERS_MESSAGE, + IMPORT_CONFLICTS_WITH_LOCAL_MESSAGE, INVALID_ASSIGNMENT_TARGET_MESSAGE, + INVALID_INDEXED_ACCESS_KEY_MESSAGE, MEMBER_NOT_ACCESSIBLE_MESSAGE, + MISSING_METHOD_RETURN_TYPE_MESSAGE, MIXED_EXPORT_ASSIGNMENT_MESSAGE, + NEW_TARGET_OUTSIDE_FUNCTION_MESSAGE, NON_VOID_FUNCTION_MUST_RETURN_MESSAGE, + NOT_ASSIGNABLE_MESSAGE, PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, + PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, + PROPERTY_DOES_NOT_EXIST_MESSAGE, PROPERTY_NOT_INITIALIZED_MESSAGE, + REST_PARAMETER_NOT_LAST_MESSAGE, SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, + STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, STRICT_NULL_MEMBER_ACCESS_MESSAGE, + SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, SUPER_BEFORE_THIS_MESSAGE, + SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, + SUPER_PROPERTY_NOT_METHOD_MESSAGE, SUPER_REFERENCE_NON_DERIVED_MESSAGE, + TYPE_ALIAS_CIRCULAR_MESSAGE, TYPE_NESTING_TOO_DEEP_MESSAGE, + TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, UNUSED_EXPECT_ERROR_MESSAGE, + USED_BEFORE_ASSIGNED_MESSAGE, USING_DECLARATION_BINDING_PATTERN_MESSAGE, + USING_DECLARATION_IN_FOR_IN_MESSAGE, USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, + VALUE_CANNOT_BE_USED_HERE_MESSAGE, WITH_STATEMENT_NOT_ALLOWED_MESSAGE, }; use crate::diagnostic::{Diagnostic, DiagnosticCode}; use crate::enum_plan::{self, EnumDeclarationBinding, EnumFacts}; @@ -5070,6 +5071,13 @@ pub(crate) struct Binder<'src> { /// body is a function declaration, function expression, or constructor. /// `false` when it is a method, getter, setter, or static block. new_target_contexts: Vec, + /// Label names declared in the current function, for TS1114. Nested + /// function-likes record a mark so their labels never leak outward. + label_declarations: Vec, + label_scope_marks: Vec, + /// Labels of enclosing labeled statements, for TS1116. A `break L` is + /// only legal when `L` is on this stack. + label_ancestors: Vec, /// Enclosing `declare` contexts. `true` when the current statement is /// directly under a `declare` keyword. ambient_stack: Vec, @@ -5221,6 +5229,9 @@ impl<'src> Binder<'src> { constructor_writable_readonly_properties: Vec::new(), readonly_assignment_targets: HashSet::new(), new_target_contexts: Vec::new(), + label_declarations: Vec::new(), + label_scope_marks: Vec::new(), + label_ancestors: Vec::new(), flow_facts: FlowFacts::new(), flow: FlowNodeId::ROOT, ambient_stack: Vec::new(), @@ -9106,7 +9117,42 @@ impl<'src> Binder<'src> { }; self.resolve_statement(&with_statement.body, body_scope); } - Statement::Labeled(statement) => self.resolve_statement(&statement.body, scope), + Statement::Labeled(statement) => { + let label = self.identifier_text(&statement.label).into_owned(); + // TS1114: sibling and nested redeclarations in one function + // are both duplicate labels. + let function_labels_start = self.label_scope_marks.last().copied().unwrap_or(0); + if self.label_declarations[function_labels_start..].contains(&label) { + self.emit_with_message( + DUPLICATE_LABEL, + statement.label.range(), + format!("Duplicate label '{label}'."), + ); + } else { + self.label_declarations.push(label.clone()); + } + self.label_ancestors.push(label); + self.resolve_statement(&statement.body, scope); + self.label_ancestors.pop(); + } + Statement::Break(jump) => { + // TS1116: a labeled break may only target an enclosing + // label. (TS1107, crossing a function boundary, needs the + // declaring function's labels and is banked.) + if let Some(label) = &jump.label + && !self + .label_ancestors + .iter() + .any(|ancestor| ancestor == self.identifier_text(label).as_ref()) + { + self.emit( + BREAK_TARGET_NOT_ENCLOSING, + label.range(), + BREAK_TARGET_NOT_ENCLOSING_MESSAGE, + ); + } + } + Statement::Continue(_) => {} Statement::ImportEquals(_) => {} Statement::Return(return_statement) => { let context = self.return_contexts.last().copied(); @@ -9687,6 +9733,7 @@ impl<'src> Binder<'src> { self.super_flow = SuperFlow::Suspended; let outer_guarantees = self.super_call_guarantees; self.super_call_guarantees = true; + self.label_scope_marks.push(self.label_declarations.len()); self.bind_implicit_function_values(&function.parameters, scope); let function_symbol = function.name.as_ref().map(|name| { let symbol_scope = if is_declaration { parent } else { scope }; @@ -9822,6 +9869,9 @@ impl<'src> Binder<'src> { let popped_home = self.super_member_homes.pop(); self.super_flow = outer_super_flow; self.super_call_guarantees = outer_guarantees; + if let Some(mark) = self.label_scope_marks.pop() { + self.label_declarations.truncate(mark); + } debug_assert_eq!(popped_home, Some(member_home)); } From e167ef1b2259bcdbfb4bdb6fdcb13fcdac4af263 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:47:38 +0900 Subject: [PATCH 33/42] Emit TS1107 for breaks crossing a function boundary break target inside a nested function whose label encloses only the outer function reported nothing; tsc reports TS1107, distinct from TS1116 where no declaration encloses the break. Ancestor labels now carry the function frame that declared them (a counter entered and exited in resolve_function). A labeled break that finds its label in an outer frame emits TS1107; a label found in the current frame stays legal; a miss keeps TS1116. This replaces the earlier slice's banked TODO. Oracles: breakTarget5 is exactly one row, pinned beside the in-function matrix and a mutation red on the frame check. Window: same-root continuation wave, post-hashes checker.rs:ae00e8763a4efd375539b45e73004010b7b3c3ad64bf59ecac86d1d0cf4d74d0 binder.rs:d43a6325e25640da5094b313d60c112e0122545ead3248a796dc2d10ea909fa2 Gates: compiler 1900/0, fmt, clippy clean. --- crates/bamts-compiler/src/checker.rs | 50 +++++-- crates/bamts-compiler/src/checker/binder.rs | 144 +++++++++++--------- 2 files changed, 117 insertions(+), 77 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 6502869..587f563 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -151,6 +151,9 @@ pub const SUPER_PROPERTY_NOT_METHOD: DiagnosticCode = DiagnosticCode::new("BAMTS pub const PARAMETER_INITIALIZER_IN_SIGNATURE: DiagnosticCode = DiagnosticCode::new("BAMTS-C100"); pub const DUPLICATE_LABEL: DiagnosticCode = DiagnosticCode::new("BAMTS-C101"); pub const BREAK_TARGET_NOT_ENCLOSING: DiagnosticCode = DiagnosticCode::new("BAMTS-C102"); +pub const BREAK_TARGET_CROSSES_FUNCTION: DiagnosticCode = DiagnosticCode::new("BAMTS-C103"); +pub(crate) const BREAK_TARGET_CROSSES_FUNCTION_MESSAGE: &str = + "Jump target cannot cross function boundary."; pub(crate) const BREAK_TARGET_NOT_ENCLOSING_MESSAGE: &str = "A 'break' statement can only jump to a label of an enclosing statement."; pub(crate) const PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE: &str = @@ -2182,21 +2185,22 @@ fn imported_enum_error( mod tests { use super::{ ARGUMENT_NOT_ASSIGNABLE, BARE_SUPER_EXPRESSION, BLOCK_SCOPED_USED_BEFORE_DECLARATION, - BREAK_TARGET_NOT_ENCLOSING, CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, - CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, CLASS_USED_BEFORE_DECLARATION, - CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, DERIVED_CONSTRUCTOR_MISSING_SUPER, - DUPLICATE_DECLARATION, DUPLICATE_LABEL, ENUM_USED_BEFORE_DECLARATION, - EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, IMPORTED_CONST_ENUM_CYCLE, - IMPORTED_CONST_ENUM_NONCONSTANT, INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, - MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, - PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_INITIALIZER_IN_SIGNATURE, - PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, - ProgramCheckOptions, PropertyType, REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, - SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, - SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, - SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, - TYPE_ALIAS_CIRCULAR, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, - TypeTable, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, + BREAK_TARGET_CROSSES_FUNCTION, BREAK_TARGET_NOT_ENCLOSING, CANNOT_FIND_NAME, + CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, + CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, + DERIVED_CONSTRUCTOR_MISSING_SUPER, DUPLICATE_DECLARATION, DUPLICATE_LABEL, + ENUM_USED_BEFORE_DECLARATION, EXPRESSION_NOT_CALLABLE, IMPORTED_CONST_ENUM_AMBIGUOUS, + IMPORTED_CONST_ENUM_CYCLE, IMPORTED_CONST_ENUM_NONCONSTANT, INTERFACE_NAME_IS_PRIMITIVE, + INVALID_ASSIGNMENT_TARGET, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, + NAMESPACE_NO_EXPORTED_MEMBER, PARAMETER_DECORATOR_NOT_SUPPORTED, + PARAMETER_INITIALIZER_IN_SIGNATURE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, + PROPERTY_DOES_NOT_EXIST, ProgramCheckInput, ProgramCheckOptions, PropertyType, + REST_PARAMETER_NOT_LAST, ResolvedModuleEdge, SUPER_BEFORE_SUPER_PROPERTY, + SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, + SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, SUPER_REFERENCE_NON_DERIVED, + SUPER_STATIC_MEMBER_VIA_SUPER, ScopeKind, SymbolKind, TYPE_ALIAS_CIRCULAR, + TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, Type, TypeId, TypeTable, + VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, check, check_program, check_program_with_options, }; use crate::diagnostic::{DiagnosticSeverity, Recovered}; @@ -6108,6 +6112,22 @@ function check(options: Options = {}) { checker_codes(&check_text(&jump)), vec![BREAK_TARGET_NOT_ENCLOSING.as_str()] ); + // TS1107: the label exists, but in an enclosing function. + let crosses = std::fs::read_to_string(root.join(concat!( + "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", + "breakTarget5.ts" + ))) + .unwrap(); + assert_eq!( + checker_codes(&check_text(&crosses)), + vec![BREAK_TARGET_CROSSES_FUNCTION.as_str()] + ); + assert_eq!( + checker_codes(&check_text( + "target: while (true) { function f() { for (;;) { break target; } } }" + )), + vec![BREAK_TARGET_CROSSES_FUNCTION.as_str()] + ); } #[test] diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index db3ec34..2609c30 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -28,61 +28,63 @@ use super::{ ABSTRACT_CONSTRUCTOR, ACCESSOR_THIS_PARAMETER, AMBIENT_IMPLEMENTATION, ARGUMENT_COUNT_MISMATCH, ARGUMENT_NOT_ASSIGNABLE, ASSIGNMENT_TO_CONST, ASSIGNMENT_TO_FUNCTION, ASSIGNMENT_TO_NAMESPACE, ASSIGNMENT_TO_READONLY, AWAIT_USING_DECLARATION_IN_FOR_IN, BARE_SUPER_EXPRESSION, - BLOCK_SCOPED_USED_BEFORE_DECLARATION, BREAK_TARGET_NOT_ENCLOSING, CANNOT_FIND_NAME, - CANNOT_FIND_NAME_LIB_GATED, CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, - CLASS_USED_BEFORE_DECLARATION, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, - CONSTRUCTOR_TYPE_PARAMETERS, DECLARATION_CONFLICTS_WITH_BUILTIN_GLOBAL, - DERIVED_CONSTRUCTOR_MISSING_SUPER, DUPLICATE_DECLARATION, DUPLICATE_LABEL, - ENUM_USED_BEFORE_DECLARATION, EXCESS_PROPERTY, EXPRESSION_NOT_CALLABLE, - EXPRESSION_NOT_CONSTRUCTABLE, FOR_IN_LEFT_HAND_SIDE_INVALID, FOR_OF_ITERABLE_REQUIRED, - FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT, FUNCTION_IMPLEMENTATION_WRONG_NAME, - FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION, GET_ACCESSOR_NO_RETURN, GET_ACCESSOR_PARAMETERS, - IMPORT_CONFLICTS_WITH_LOCAL, INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, - INVALID_INDEXED_ACCESS_KEY, MEMBER_NOT_ACCESSIBLE, MISSING_METHOD_RETURN_TYPE, - MIXED_EXPORT_ASSIGNMENT, NAMESPACE_NO_EXPORTED_MEMBER, NEW_TARGET_OUTSIDE_FUNCTION, - NON_VOID_FUNCTION_MUST_RETURN, PARAMETER_DECORATOR_NOT_SUPPORTED, - PARAMETER_INITIALIZER_IN_SIGNATURE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, - PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, REST_PARAMETER_NOT_LAST, - SET_ACCESSOR_PARAMETER_INITIALIZER, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, - STRICT_NULL_MEMBER_ACCESS, SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, - SUPER_PROPERTY_NOT_METHOD, SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, - TYPE_ALIAS_CIRCULAR, TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, - TYPE_PARAMETER_CIRCULAR_DEFAULT, UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, - USING_DECLARATION_BINDING_PATTERN, USING_DECLARATION_IN_FOR_IN, - USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, WITH_STATEMENT_NOT_ALLOWED, + BLOCK_SCOPED_USED_BEFORE_DECLARATION, BREAK_TARGET_CROSSES_FUNCTION, + BREAK_TARGET_NOT_ENCLOSING, CANNOT_FIND_NAME, CANNOT_FIND_NAME_LIB_GATED, + CANNOT_FIND_NAMESPACE, CANNOT_FIND_TYPE, CLASS_USED_BEFORE_DECLARATION, + CONSTRUCTOR_DECORATOR_NOT_SUPPORTED, CONSTRUCTOR_TYPE_PARAMETERS, + DECLARATION_CONFLICTS_WITH_BUILTIN_GLOBAL, DERIVED_CONSTRUCTOR_MISSING_SUPER, + DUPLICATE_DECLARATION, DUPLICATE_LABEL, ENUM_USED_BEFORE_DECLARATION, EXCESS_PROPERTY, + EXPRESSION_NOT_CALLABLE, EXPRESSION_NOT_CONSTRUCTABLE, FOR_IN_LEFT_HAND_SIDE_INVALID, + FOR_OF_ITERABLE_REQUIRED, FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT, + FUNCTION_IMPLEMENTATION_WRONG_NAME, FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION, + GET_ACCESSOR_NO_RETURN, GET_ACCESSOR_PARAMETERS, IMPORT_CONFLICTS_WITH_LOCAL, + INTERFACE_NAME_IS_PRIMITIVE, INVALID_ASSIGNMENT_TARGET, INVALID_INDEXED_ACCESS_KEY, + MEMBER_NOT_ACCESSIBLE, MISSING_METHOD_RETURN_TYPE, MIXED_EXPORT_ASSIGNMENT, + NAMESPACE_NO_EXPORTED_MEMBER, NEW_TARGET_OUTSIDE_FUNCTION, NON_VOID_FUNCTION_MUST_RETURN, + PARAMETER_DECORATOR_NOT_SUPPORTED, PARAMETER_INITIALIZER_IN_SIGNATURE, + PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR, PROPERTY_DOES_NOT_EXIST, PROPERTY_NOT_INITIALIZED, + REST_PARAMETER_NOT_LAST, SET_ACCESSOR_PARAMETER_INITIALIZER, + STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT, STRICT_NULL_MEMBER_ACCESS, + SUPER_BEFORE_SUPER_PROPERTY, SUPER_BEFORE_THIS, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS, + SUPER_CALL_OUTSIDE_CONSTRUCTOR, SUPER_FIELD_VIA_SUPER, SUPER_PROPERTY_NOT_METHOD, + SUPER_REFERENCE_NON_DERIVED, SUPER_STATIC_MEMBER_VIA_SUPER, TYPE_ALIAS_CIRCULAR, + TYPE_NESTING_TOO_DEEP, TYPE_NOT_ASSIGNABLE, TYPE_PARAMETER_CIRCULAR_DEFAULT, + UNUSED_EXPECT_ERROR, USED_BEFORE_ASSIGNED, USING_DECLARATION_BINDING_PATTERN, + USING_DECLARATION_IN_FOR_IN, USING_DECLARATION_MISSING_INITIALIZER, VALUE_CANNOT_BE_USED_HERE, + WITH_STATEMENT_NOT_ALLOWED, }; use super::{ ABSTRACT_CONSTRUCTOR_MESSAGE, ACCESSOR_THIS_PARAMETER_MESSAGE, AMBIENT_IMPLEMENTATION_MESSAGE, ARGUMENT_COUNT_MISMATCH_MESSAGE, ARGUMENT_NOT_ASSIGNABLE_MESSAGE, ASSIGNMENT_TO_CONST_MESSAGE, ASSIGNMENT_TO_FUNCTION_MESSAGE, ASSIGNMENT_TO_NAMESPACE_MESSAGE, ASSIGNMENT_TO_READONLY_MESSAGE, AWAIT_USING_DECLARATION_IN_FOR_IN_MESSAGE, - BARE_SUPER_EXPRESSION_MESSAGE, BREAK_TARGET_NOT_ENCLOSING_MESSAGE, - CANNOT_FIND_NAME_LIB_GATED_MESSAGE, CANNOT_FIND_NAME_MESSAGE, CANNOT_FIND_NAMESPACE_MESSAGE, - CANNOT_FIND_TYPE_MESSAGE, CONSTRUCTOR_DECORATOR_NOT_SUPPORTED_MESSAGE, - CONSTRUCTOR_TYPE_PARAMETERS_MESSAGE, DERIVED_CONSTRUCTOR_MISSING_SUPER_MESSAGE, - DUPLICATE_MESSAGE, EXCESS_PROPERTY_MESSAGE, EXPRESSION_NOT_CALLABLE_MESSAGE, - EXPRESSION_NOT_CONSTRUCTABLE_MESSAGE, FOR_IN_LEFT_HAND_SIDE_INVALID_MESSAGE, - FOR_OF_ITERABLE_REQUIRED_MESSAGE, FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT_MESSAGE, - FUNCTION_IMPLEMENTATION_WRONG_NAME_MESSAGE, FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION_MESSAGE, - GET_ACCESSOR_NO_RETURN_MESSAGE, GET_ACCESSOR_PARAMETERS_MESSAGE, - IMPORT_CONFLICTS_WITH_LOCAL_MESSAGE, INVALID_ASSIGNMENT_TARGET_MESSAGE, - INVALID_INDEXED_ACCESS_KEY_MESSAGE, MEMBER_NOT_ACCESSIBLE_MESSAGE, - MISSING_METHOD_RETURN_TYPE_MESSAGE, MIXED_EXPORT_ASSIGNMENT_MESSAGE, - NEW_TARGET_OUTSIDE_FUNCTION_MESSAGE, NON_VOID_FUNCTION_MUST_RETURN_MESSAGE, - NOT_ASSIGNABLE_MESSAGE, PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, - PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, - PROPERTY_DOES_NOT_EXIST_MESSAGE, PROPERTY_NOT_INITIALIZED_MESSAGE, - REST_PARAMETER_NOT_LAST_MESSAGE, SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, - STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, STRICT_NULL_MEMBER_ACCESS_MESSAGE, - SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, SUPER_BEFORE_THIS_MESSAGE, - SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, - SUPER_PROPERTY_NOT_METHOD_MESSAGE, SUPER_REFERENCE_NON_DERIVED_MESSAGE, - TYPE_ALIAS_CIRCULAR_MESSAGE, TYPE_NESTING_TOO_DEEP_MESSAGE, - TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, UNUSED_EXPECT_ERROR_MESSAGE, - USED_BEFORE_ASSIGNED_MESSAGE, USING_DECLARATION_BINDING_PATTERN_MESSAGE, - USING_DECLARATION_IN_FOR_IN_MESSAGE, USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, - VALUE_CANNOT_BE_USED_HERE_MESSAGE, WITH_STATEMENT_NOT_ALLOWED_MESSAGE, + BARE_SUPER_EXPRESSION_MESSAGE, BREAK_TARGET_CROSSES_FUNCTION_MESSAGE, + BREAK_TARGET_NOT_ENCLOSING_MESSAGE, CANNOT_FIND_NAME_LIB_GATED_MESSAGE, + CANNOT_FIND_NAME_MESSAGE, CANNOT_FIND_NAMESPACE_MESSAGE, CANNOT_FIND_TYPE_MESSAGE, + CONSTRUCTOR_DECORATOR_NOT_SUPPORTED_MESSAGE, CONSTRUCTOR_TYPE_PARAMETERS_MESSAGE, + DERIVED_CONSTRUCTOR_MISSING_SUPER_MESSAGE, DUPLICATE_MESSAGE, EXCESS_PROPERTY_MESSAGE, + EXPRESSION_NOT_CALLABLE_MESSAGE, EXPRESSION_NOT_CONSTRUCTABLE_MESSAGE, + FOR_IN_LEFT_HAND_SIDE_INVALID_MESSAGE, FOR_OF_ITERABLE_REQUIRED_MESSAGE, + FUNCTION_DECLARATION_IN_BLOCK_ES5_STRICT_MESSAGE, FUNCTION_IMPLEMENTATION_WRONG_NAME_MESSAGE, + FUNCTION_OVERLOAD_MISSING_IMPLEMENTATION_MESSAGE, GET_ACCESSOR_NO_RETURN_MESSAGE, + GET_ACCESSOR_PARAMETERS_MESSAGE, IMPORT_CONFLICTS_WITH_LOCAL_MESSAGE, + INVALID_ASSIGNMENT_TARGET_MESSAGE, INVALID_INDEXED_ACCESS_KEY_MESSAGE, + MEMBER_NOT_ACCESSIBLE_MESSAGE, MISSING_METHOD_RETURN_TYPE_MESSAGE, + MIXED_EXPORT_ASSIGNMENT_MESSAGE, NEW_TARGET_OUTSIDE_FUNCTION_MESSAGE, + NON_VOID_FUNCTION_MUST_RETURN_MESSAGE, NOT_ASSIGNABLE_MESSAGE, + PARAMETER_DECORATOR_NOT_SUPPORTED_MESSAGE, PARAMETER_INITIALIZER_IN_SIGNATURE_MESSAGE, + PARAMETER_PROPERTY_ONLY_IN_CONSTRUCTOR_MESSAGE, PROPERTY_DOES_NOT_EXIST_MESSAGE, + PROPERTY_NOT_INITIALIZED_MESSAGE, REST_PARAMETER_NOT_LAST_MESSAGE, + SET_ACCESSOR_PARAMETER_INITIALIZER_MESSAGE, STATEMENT_NOT_ALLOWED_IN_AMBIENT_CONTEXT_MESSAGE, + STRICT_NULL_MEMBER_ACCESS_MESSAGE, SUPER_BEFORE_SUPER_PROPERTY_MESSAGE, + SUPER_BEFORE_THIS_MESSAGE, SUPER_CALL_IN_CONSTRUCTOR_ARGUMENTS_MESSAGE, + SUPER_CALL_OUTSIDE_CONSTRUCTOR_MESSAGE, SUPER_PROPERTY_NOT_METHOD_MESSAGE, + SUPER_REFERENCE_NON_DERIVED_MESSAGE, TYPE_ALIAS_CIRCULAR_MESSAGE, + TYPE_NESTING_TOO_DEEP_MESSAGE, TYPE_PARAMETER_CIRCULAR_DEFAULT_MESSAGE, + UNUSED_EXPECT_ERROR_MESSAGE, USED_BEFORE_ASSIGNED_MESSAGE, + USING_DECLARATION_BINDING_PATTERN_MESSAGE, USING_DECLARATION_IN_FOR_IN_MESSAGE, + USING_DECLARATION_MISSING_INITIALIZER_MESSAGE, VALUE_CANNOT_BE_USED_HERE_MESSAGE, + WITH_STATEMENT_NOT_ALLOWED_MESSAGE, }; use crate::diagnostic::{Diagnostic, DiagnosticCode}; use crate::enum_plan::{self, EnumDeclarationBinding, EnumFacts}; @@ -5075,9 +5077,12 @@ pub(crate) struct Binder<'src> { /// function-likes record a mark so their labels never leak outward. label_declarations: Vec, label_scope_marks: Vec, - /// Labels of enclosing labeled statements, for TS1116. A `break L` is - /// only legal when `L` is on this stack. - label_ancestors: Vec, + /// Labels of enclosing labeled statements, tagged with the function + /// frame that declared them. A `break L` is only legal when `L` is on + /// this stack in the current frame; a hit in an outer frame is TS1107. + label_ancestors: Vec<(String, usize)>, + /// Current function frame for label ancestry. + label_frame: usize, /// Enclosing `declare` contexts. `true` when the current statement is /// directly under a `declare` keyword. ambient_stack: Vec, @@ -5232,6 +5237,7 @@ impl<'src> Binder<'src> { label_declarations: Vec::new(), label_scope_marks: Vec::new(), label_ancestors: Vec::new(), + label_frame: 0, flow_facts: FlowFacts::new(), flow: FlowNodeId::ROOT, ambient_stack: Vec::new(), @@ -9131,7 +9137,7 @@ impl<'src> Binder<'src> { } else { self.label_declarations.push(label.clone()); } - self.label_ancestors.push(label); + self.label_ancestors.push((label, self.label_frame)); self.resolve_statement(&statement.body, scope); self.label_ancestors.pop(); } @@ -9139,17 +9145,29 @@ impl<'src> Binder<'src> { // TS1116: a labeled break may only target an enclosing // label. (TS1107, crossing a function boundary, needs the // declaring function's labels and is banked.) - if let Some(label) = &jump.label - && !self + if let Some(label_node) = &jump.label { + let label = self.identifier_text(label_node); + let hit = self .label_ancestors .iter() - .any(|ancestor| ancestor == self.identifier_text(label).as_ref()) - { - self.emit( - BREAK_TARGET_NOT_ENCLOSING, - label.range(), - BREAK_TARGET_NOT_ENCLOSING_MESSAGE, - ); + .find(|(ancestor, _)| *ancestor == label.as_ref()); + match hit { + Some((_, frame)) if *frame == self.label_frame => {} + // TS1107: the label encloses, but a function + // boundary sits between it and the break. + Some(_) => self.emit( + BREAK_TARGET_CROSSES_FUNCTION, + label_node.range(), + BREAK_TARGET_CROSSES_FUNCTION_MESSAGE, + ), + // TS1116: no declaration of the label encloses the + // break at all. + None => self.emit( + BREAK_TARGET_NOT_ENCLOSING, + label_node.range(), + BREAK_TARGET_NOT_ENCLOSING_MESSAGE, + ), + } } } Statement::Continue(_) => {} @@ -9734,6 +9752,7 @@ impl<'src> Binder<'src> { let outer_guarantees = self.super_call_guarantees; self.super_call_guarantees = true; self.label_scope_marks.push(self.label_declarations.len()); + self.label_frame += 1; self.bind_implicit_function_values(&function.parameters, scope); let function_symbol = function.name.as_ref().map(|name| { let symbol_scope = if is_declaration { parent } else { scope }; @@ -9872,6 +9891,7 @@ impl<'src> Binder<'src> { if let Some(mark) = self.label_scope_marks.pop() { self.label_declarations.truncate(mark); } + self.label_frame -= 1; debug_assert_eq!(popped_home, Some(member_home)); } From e2e286456f259c52c1ac5669f6b206c3a460f93d Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:54:42 +0900 Subject: [PATCH 34/42] Register the wave-two diagnostic codes The eleven rules landed in wave two (TS2576, TS2448, TS2449, TS2450, TS1014, TS2427, TS2340, TS2371, TS1114, TS1116, TS1107) emitted codes the correspondence map did not carry, so the diagnostics comparator dropped their rows. Register C093 through C103 with baseline-cited evidence and pin the required enumeration at 104. Shard 0/16 at c75faec validated the wave's registered half: PASS 855 / BLOCKING_FAIL 2902 / INAPPLICABLE 418, both checkSuperCallBeforeThisAccessing rows flipped to PASS, receipt under .outline/evidence-shard0-c75faec/. Gates: verification lib 599/0, fmt, clippy clean. --- crates/bamts-verification/src/facets.rs | 13 ++++- verification/diagnostic-code-map.json | 66 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/bamts-verification/src/facets.rs b/crates/bamts-verification/src/facets.rs index f5e13b1..0d0dc19 100644 --- a/crates/bamts-verification/src/facets.rs +++ b/crates/bamts-verification/src/facets.rs @@ -51,7 +51,7 @@ pub const DIAGNOSTIC_CODE_MAP_PATH: &str = "verification/diagnostic-code-map.jso pub const DIAGNOSTIC_CODE_MAP_SCHEMA_VERSION: u32 = 1; /// Every current BAMTS diagnostic code the map must cover exactly once. -pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 93] = [ +pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 104] = [ "BAMTS-L001", "BAMTS-L002", "BAMTS-L003", @@ -145,6 +145,17 @@ pub const REQUIRED_BAMTS_DIAGNOSTIC_CODES: [&str; 93] = [ "BAMTS-C090", "BAMTS-C091", "BAMTS-C092", + "BAMTS-C093", + "BAMTS-C094", + "BAMTS-C095", + "BAMTS-C096", + "BAMTS-C097", + "BAMTS-C098", + "BAMTS-C099", + "BAMTS-C100", + "BAMTS-C101", + "BAMTS-C102", + "BAMTS-C103", ]; #[derive(Debug, Clone, PartialEq, Eq)] pub enum FacetVerdict { diff --git a/verification/diagnostic-code-map.json b/verification/diagnostic-code-map.json index 1d8f47b..dbd6014 100644 --- a/verification/diagnostic-code-map.json +++ b/verification/diagnostic-code-map.json @@ -558,6 +558,72 @@ "evidence": "Derived class reads a base class field through `super` -> TS2855 in the TypeScript 7.0.2 authority baselines: checkSuperCallBeforeThisAccess (lines 9, 12, 17, 22, 45).", "status": "mapped", "tsCode": 2855 + }, + { + "bamtsCode": "BAMTS-C093", + "evidence": "Derived class reads a base static member through `super` -> TS2576 in the TypeScript 7.0.2 authority baselines: superAccess (line 9, both target variants).", + "status": "mapped", + "tsCode": 2576 + }, + { + "bamtsCode": "BAMTS-C094", + "evidence": "Block-scoped variable referenced textually before its declaration -> TS2448 in the TypeScript 7.0.2 authority baselines: blockScopedEnumVariablesUseBeforeDef_preserve, useBeforeDeclaration variants.", + "status": "mapped", + "tsCode": 2448 + }, + { + "bamtsCode": "BAMTS-C095", + "evidence": "Class referenced textually before its declaration -> TS2449 in the TypeScript 7.0.2 authority baselines: useBeforeDeclaration_classDecorators.1 (lines 7, 10, 29, 30).", + "status": "mapped", + "tsCode": 2449 + }, + { + "bamtsCode": "BAMTS-C096", + "evidence": "Enum referenced textually before its declaration -> TS2450 in the TypeScript 7.0.2 authority baselines: blockScopedEnumVariablesUseBeforeDef (line 2, both target variants).", + "status": "mapped", + "tsCode": 2450 + }, + { + "bamtsCode": "BAMTS-C097", + "evidence": "Rest parameter followed by another parameter -> TS1014 in the TypeScript 7.0.2 authority baselines: restParameterNotLast (line 1).", + "status": "mapped", + "tsCode": 1014 + }, + { + "bamtsCode": "BAMTS-C098", + "evidence": "Interface named after a primitive type -> TS2427 in the TypeScript 7.0.2 authority baselines: InterfaceDeclaration8 (line 1).", + "status": "mapped", + "tsCode": 2427 + }, + { + "bamtsCode": "BAMTS-C099", + "evidence": "Base field read through `super` under an ES5 target -> TS2340 in the TypeScript 7.0.2 authority baselines: superAccess(target=es5) (lines 10, 11).", + "status": "mapped", + "tsCode": 2340 + }, + { + "bamtsCode": "BAMTS-C100", + "evidence": "Parameter initializer on an overload signature -> TS2371 in the TypeScript 7.0.2 authority baselines: defaultValueInConstructorOverload1 (line 2).", + "status": "mapped", + "tsCode": 2371 + }, + { + "bamtsCode": "BAMTS-C101", + "evidence": "Label redeclared within one function -> TS1114 in the TypeScript 7.0.2 authority baselines: duplicateLabel2 (line 3).", + "status": "mapped", + "tsCode": 1114 + }, + { + "bamtsCode": "BAMTS-C102", + "evidence": "Labeled break with no enclosing declaration of the label -> TS1116 in the TypeScript 7.0.2 authority baselines: breakTarget6 (line 2).", + "status": "mapped", + "tsCode": 1116 + }, + { + "bamtsCode": "BAMTS-C103", + "evidence": "Labeled break whose target crosses a function boundary -> TS1107 in the TypeScript 7.0.2 authority baselines: breakTarget5 (line 5).", + "status": "mapped", + "tsCode": 1107 } ] } From a41220950cabfa042a04570d73744a52b7b03e89 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:11:20 +0900 Subject: [PATCH 35/42] Bootstrap validated PR #199 review fixes --- .github/pr199-review-fixes.part-00.patch | 105 ++++++++++++++++++++++ .github/pr199-review-fixes.part-01.patch | 106 ++++++++++++++++++++++ .github/pr199-review-fixes.part-02.patch | 103 +++++++++++++++++++++ .github/pr199-review-fixes.part-03.patch | 108 +++++++++++++++++++++++ .github/pr199-review-fixes.part-04.patch | 96 ++++++++++++++++++++ .github/pr199-review-fixes.part-05.patch | 88 ++++++++++++++++++ .github/workflows/pr199-review-fix.yml | 100 +++++++++++++++++++++ 7 files changed, 706 insertions(+) create mode 100644 .github/pr199-review-fixes.part-00.patch create mode 100644 .github/pr199-review-fixes.part-01.patch create mode 100644 .github/pr199-review-fixes.part-02.patch create mode 100644 .github/pr199-review-fixes.part-03.patch create mode 100644 .github/pr199-review-fixes.part-04.patch create mode 100644 .github/pr199-review-fixes.part-05.patch create mode 100644 .github/workflows/pr199-review-fix.yml diff --git a/.github/pr199-review-fixes.part-00.patch b/.github/pr199-review-fixes.part-00.patch new file mode 100644 index 0000000..810a2cc --- /dev/null +++ b/.github/pr199-review-fixes.part-00.patch @@ -0,0 +1,105 @@ +diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml +--- a/.github/workflows/ci.yml ++++ b/.github/workflows/ci.yml +@@ -50,3 +50,8 @@ ++ - name: Fetch pinned TypeScript test fixtures ++ run: >- ++ cargo run --locked -p bamts-verification -- source fetch ++ typescript-primary-tests --dest target/authority/typescript-7.0.2-tests ++ + - name: Test workspace + run: cargo test --workspace --locked + +diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs +--- a/crates/bamts-compiler/src/checker.rs ++++ b/crates/bamts-compiler/src/checker.rs +@@ -5983,7 +5983,19 @@ +- /// TS17009/TS17011: a derived constructor's `this` and `super.x` +- /// accesses before a guaranteed `super()` call, with the oracle's +- /// flow shapes: arrows are exempt (deferred `this`), a conditional +- /// super() covers only its branch, and loops/try never guarantee. ++ fn authority_source(relative: &str) -> String { ++ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) ++ .join("../..") ++ .join("target/authority/typescript-7.0.2-tests/tests/cases/compiler") ++ .join(relative); ++ std::fs::read_to_string(&path).unwrap_or_else(|error| { ++ panic!( ++ "authority fixture {} is unavailable: {error}; run \ ++ `cargo run --locked -p bamts-verification -- source fetch \ ++ typescript-primary-tests --dest target/authority/typescript-7.0.2-tests` \ ++ from the repository root first", ++ path.display() ++ ) ++ }) ++ } ++ + /// The superAccess es2015 baseline carries one TS2576 (static S1 via + /// super) and two TS2855 (fields S2 and f); the es5 variant's TS2340 + /// flavor is a separate target-conditional slice. +@@ -5991,8 +6003,3 @@ + fn super_static_member_matches_superaccess_baseline() { +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let source = +- std::fs::read_to_string(root.join( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", +- )) +- .unwrap(); ++ let source = authority_source("superAccess.ts"); + let codes = checker_codes(&check_text(&source)); +@@ -6019,8 +6026,3 @@ + fn enum_used_before_declaration_matches_baseline() { +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let source = std::fs::read_to_string(root.join(concat!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", +- "blockScopedEnumVariablesUseBeforeDef.ts" +- ))) +- .unwrap(); ++ let source = authority_source("blockScopedEnumVariablesUseBeforeDef.ts"); + let codes = checker_codes(&check_text(&source)); +@@ -6037,20 +6039,3 @@ +- /// The superAccess es5 baseline swaps each TS2855 field row for the +- /// single pre-fields rule TS2340; the static row stays TS2576 at both +- /// targets. +- #[test] +- fn zz_enumbasics3_probe() { +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let source = +- std::fs::read_to_string(root.join( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/enumBasics3.ts", +- )) +- .unwrap(); +- eprintln!("ZZ eb3: {:?}", checker_codes(&check_text(&source))); +- } +- +- /// TS2371: only an implementation may carry parameter defaults. The +- /// oracle (defaultValueInConstructorOverload1) is exactly one row on a +- /// constructor overload signature. +- /// TS1114: a label redeclared in one function (sibling or nested). ++ /// TS1114: a label redeclared while it still encloses the current statement. + /// TS1116: a labeled break may only target an enclosing label. The + /// oracles are duplicateLabel2 (one row) and breakTarget6 (one row). +@@ -6067,5 +6052,5 @@ +- // Sibling redeclaration in the same function is also a duplicate. ++ // A completed sibling label is no longer active and may be reused. + assert_eq!( + checker_codes(&check_text("a: {} a: {}")), +- vec![DUPLICATE_LABEL.as_str()] ++ Vec::<&str>::new() + ); +@@ -6103,8 +6088,3 @@ + // Authority oracles: one row each. +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let duplicate = std::fs::read_to_string(root.join(concat!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", +- "duplicateLabel2.ts" +- ))) +- .unwrap(); ++ let duplicate = authority_source("duplicateLabel2.ts"); + assert_eq!( +@@ -6115,7 +6095,3 @@ +- let jump = std::fs::read_to_string(root.join(concat!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", +- "breakTarget6.ts" +- ))) +- .unwrap(); diff --git a/.github/pr199-review-fixes.part-01.patch b/.github/pr199-review-fixes.part-01.patch new file mode 100644 index 0000000..99e59e0 --- /dev/null +++ b/.github/pr199-review-fixes.part-01.patch @@ -0,0 +1,106 @@ ++ let jump = authority_source("breakTarget6.ts"); + assert_eq!( + checker_codes(&check_text(&jump)), +@@ -6126,7 +6102,3 @@ + // TS1107: the label exists, but in an enclosing function. +- let crosses = std::fs::read_to_string(root.join(concat!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", +- "breakTarget5.ts" +- ))) +- .unwrap(); ++ let crosses = authority_source("breakTarget5.ts"); + assert_eq!( +@@ -6146,3 +6118,6 @@ ++ /// TS2371: only an implementation may carry parameter defaults. The ++ /// oracle (defaultValueInConstructorOverload1) is exactly one row on a ++ /// constructor overload signature. + #[test] + fn parameter_initializer_only_in_implementation() { + assert_eq!( +@@ -6165,8 +6140,3 @@ +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let source = std::fs::read_to_string(root.join(concat!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", +- "defaultValueInConstructorOverload1.ts" +- ))) +- .unwrap(); ++ let source = authority_source("defaultValueInConstructorOverload1.ts"); + assert_eq!( + checker_codes(&check_text(&source)), +@@ -6177,3 +6147,6 @@ ++ /// The superAccess es5 baseline swaps each TS2855 field row for the ++ /// single pre-fields rule TS2340; the static row stays TS2576 at both ++ /// targets. + #[test] + fn super_field_flavor_splits_by_target() { + let source = "class MyBase { +@@ -6241,9 +6214,4 @@ + // The authority file itself: es5 swaps both field rows for TS2340 + // and keeps the static row at TS2576. +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let source = +- std::fs::read_to_string(root.join( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", +- )) +- .unwrap(); ++ let source = authority_source("superAccess.ts"); + let oracle_es5 = checker_codes(&check_text_with( +@@ -6411,8 +6379,3 @@ + fn super_field_via_super_matches_baseline_count() { +- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); +- let source = std::fs::read_to_string(root.join(concat!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", +- "checkSuperCallBeforeThisAccess.ts" +- ))) +- .unwrap(); ++ let source = authority_source("checkSuperCallBeforeThisAccess.ts"); + let count = checker_codes(&check_text(&source)) +@@ -6428,8 +6391,5 @@ + for variant in ["2", "5", "8"] { +- let variant_source = std::fs::read_to_string( +- root.join(format!( +- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/checkSuperCallBeforeThisAccessing{variant}.ts" +- )), +- ) +- .unwrap(); ++ let variant_source = authority_source(&format!( ++ "checkSuperCallBeforeThisAccessing{variant}.ts" ++ )); + let codes = checker_codes(&check_text(&variant_source)); +@@ -6519,3 +6479,7 @@ ++ /// TS17009/TS17011: a derived constructor's `this` and `super.x` ++ /// accesses before a guaranteed `super()` call, with the oracle's ++ /// flow shapes: arrows are exempt (deferred `this`), a conditional ++ /// super() covers only its branch, and loops/try never guarantee. + #[test] + fn super_before_this_flow_matrix() { + fn codes(text: &str) -> Vec<&'static str> { +@@ -6574,3 +6538,78 @@ ++ #[test] ++ fn pr199_super_flow_joins_every_continuing_path() { ++ for (body, safe) in [ ++ ("if (c) { super(); } else { super(); }", true), ++ ("if (c) { throw 0; } else { super(); }", true), ++ ("if (c) { super(); } else { throw 0; }", true), ++ ("if (c) { return { method() {} }; } else { super(); }", true), ++ ("super(); if (c) {} else {}", true), ++ ("if (c) { if (c) { super(); } else { super(); } } else { super(); }", true), ++ ("if (c) { super(); }", false), ++ ("if (c) {} else { super(); }", false), ++ ] { ++ let source = format!( ++ "class B {{ method() {{}} }} \ ++ class D extends B {{ constructor(c: boolean) {{ \ ++ {body} this; super.method(); }} }}" ++ ); ++ let mut codes = checker_codes(&check_text(&source)) ++ .into_iter() ++ .filter(|code| { ++ *code == SUPER_BEFORE_THIS.as_str() ++ || *code == SUPER_BEFORE_SUPER_PROPERTY.as_str() ++ }) ++ .collect::>(); ++ codes.sort_unstable(); ++ let expected = if safe { ++ Vec::new() ++ } else { diff --git a/.github/pr199-review-fixes.part-02.patch b/.github/pr199-review-fixes.part-02.patch new file mode 100644 index 0000000..4108c7f --- /dev/null +++ b/.github/pr199-review-fixes.part-02.patch @@ -0,0 +1,103 @@ ++ vec![SUPER_BEFORE_THIS.as_str(), SUPER_BEFORE_SUPER_PROPERTY.as_str()] ++ }; ++ assert_eq!(codes, expected, "{body}"); ++ } ++ } ++ ++ #[test] ++ fn pr199_label_frames_and_nearest_targets() { ++ let cases = [ ++ ("L: {} L: {}", None), ++ ("L: { L: {} }", Some(DUPLICATE_LABEL.as_str())), ++ ("L: { function f() { L: { break L; } } break L; }", None), ++ ("L: { const f = () => { L: { break L; } }; break L; }", None), ++ ("L: { class C { constructor() { L: { break L; } } } break L; }", None), ++ ("L: { function f() { break L; } break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str())), ++ ("L: { const f = () => { break L; }; break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str())), ++ ("L: { class C { constructor() { break L; } } break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str())), ++ ("const f = () => { local: {} }; local: {}", None), ++ ("class C { constructor() { local: {} } } local: {}", None), ++ ("L: {} break L;", Some(BREAK_TARGET_NOT_ENCLOSING.as_str())), ++ ("while (true) { break missing; }", Some(BREAK_TARGET_NOT_ENCLOSING.as_str())), ++ ]; ++ for (source, expected) in cases { ++ let codes = checker_codes(&check_text(source)) ++ .into_iter() ++ .filter(|code| { ++ *code == DUPLICATE_LABEL.as_str() ++ || *code == BREAK_TARGET_NOT_ENCLOSING.as_str() ++ || *code == BREAK_TARGET_CROSSES_FUNCTION.as_str() ++ }) ++ .collect::>(); ++ assert_eq!(codes, expected.into_iter().collect::>(), "{source}"); ++ } ++ } ++ ++ #[test] ++ fn pr199_missing_authority_fixture_explains_the_prerequisite() { ++ let panic = std::panic::catch_unwind(|| { ++ authority_source("__pr199_fixture_that_must_not_exist__.ts") ++ }) ++ .expect_err("a missing authority fixture must fail rather than skip"); ++ let message = panic.downcast_ref::().expect("formatted panic"); ++ assert!(message.contains("__pr199_fixture_that_must_not_exist__.ts")); ++ assert!(message.contains("source fetch typescript-primary-tests")); ++ assert!(message.contains("--dest target/authority/typescript-7.0.2-tests")); ++ } ++ + #[test] + fn super_call_context_matrix() { + const SUPER_CODES: [&str; 4] = [ +diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs +--- a/crates/bamts-compiler/src/checker/binder.rs ++++ b/crates/bamts-compiler/src/checker/binder.rs +@@ -8918,6 +8918,6 @@ + let else_ok = + else_exits || matches!(else_flow, SuperFlow::Tracking { called: true }); + SuperFlow::Tracking { +- called: entry && then_ok && else_ok, ++ called: entry || (then_ok && else_ok), + } + } +@@ -9120,4 +9120,4 @@ + let label = self.identifier_text(&statement.label).into_owned(); +- // TS1114: sibling and nested redeclarations in one function +- // are both duplicate labels. ++ // TS1114 concerns active labels, not earlier sibling statements. ++ let label_mark = self.label_declarations.len(); + let function_labels_start = self.label_scope_marks.last().copied().unwrap_or(0); +@@ -9135,8 +9135,8 @@ + self.label_ancestors.push((label, self.label_frame)); + self.resolve_statement(&statement.body, scope); + self.label_ancestors.pop(); ++ self.label_declarations.truncate(label_mark); + } + Statement::Break(jump) => { +- // TS1116: a labeled break may only target an enclosing +- // label. (TS1107, crossing a function boundary, needs the +- // declaring function's labels and is banked.) ++ // Retain outer frames to distinguish TS1107 from TS1116, ++ // but select the nearest enclosing label first. +@@ -9149,3 +9149,4 @@ + .label_ancestors + .iter() ++ .rev() + .find(|(ancestor, _)| *ancestor == label.as_ref()); +@@ -9735,3 +9736,17 @@ ++ fn push_label_scope(&mut self) { ++ self.label_scope_marks.push(self.label_declarations.len()); ++ self.label_frame += 1; ++ } ++ ++ fn pop_label_scope(&mut self) { ++ let mark = self ++ .label_scope_marks ++ .pop() ++ .expect("label scope must be entered before it is left"); ++ self.label_declarations.truncate(mark); ++ self.label_frame -= 1; ++ } ++ + fn resolve_function( + &mut self, + function: &'src FunctionLike, diff --git a/.github/pr199-review-fixes.part-03.patch b/.github/pr199-review-fixes.part-03.patch new file mode 100644 index 0000000..2172f6a --- /dev/null +++ b/.github/pr199-review-fixes.part-03.patch @@ -0,0 +1,108 @@ +@@ -9752,4 +9767,3 @@ + self.super_call_guarantees = true; +- self.label_scope_marks.push(self.label_declarations.len()); +- self.label_frame += 1; ++ self.push_label_scope(); + self.bind_implicit_function_values(&function.parameters, scope); +@@ -9889,7 +9903,4 @@ + self.super_flow = outer_super_flow; + self.super_call_guarantees = outer_guarantees; +- if let Some(mark) = self.label_scope_marks.pop() { +- self.label_declarations.truncate(mark); +- } +- self.label_frame -= 1; ++ self.pop_label_scope(); + debug_assert_eq!(popped_home, Some(member_home)); +@@ -11894,4 +11905,5 @@ + let child = self.new_scope(ScopeKind::Function, Some(scope)); ++ self.push_label_scope(); + let new_target_marker = self.new_target_contexts.len(); + self.new_target_contexts.push(true); + self.bind_implicit_function_values(&constructor.parameters, child); +@@ -11979,6 +11991,7 @@ + self.new_target_contexts.truncate(new_target_marker); + self.super_call_contexts.pop(); + let popped_home = self.super_member_homes.pop(); ++ self.pop_label_scope(); + debug_assert_eq!(popped_home, Some(SuperMemberHome::ClassMember { derived })); + } + ClassMember::Property(property) => { +@@ -12106,3 +12119,4 @@ + Expression::Arrow(arrow) => { + let child = self.new_scope(ScopeKind::Function, Some(scope)); ++ self.push_label_scope(); + // Arrows capture `this` but never inherit super-call legality. +@@ -12218,5 +12232,6 @@ + if self.node_types.insert(expression.id(), type_id).is_none() { + self.typed_expressions.push((expression.range(), type_id)); + } ++ self.pop_label_scope(); + } + Expression::Call(call) => { +diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs +--- a/crates/bamts-compiler/src/emitter.rs ++++ b/crates/bamts-compiler/src/emitter.rs +@@ -6773,3 +6773,111 @@ ++ #[test] ++ fn pr199_for_of_fallbacks_keep_the_rewritten_iterable() { ++ for (source, recovery_declarators) in [ ++ ("var target; for (target of [2 ** 3]) {}", None), ++ ("for (var first, second of [2 ** 3]) {}", Some(2)), ++ ] { ++ let parsed = crate::parser::parse(crate::scanner::scan( ++ SourceId::new(0), ++ ScriptKind::TypeScript, ++ Arc::new(SourceText::new(source).expect("fits")), ++ )); ++ // The malformed declaration is intentional recovery-AST coverage. ++ // Assert the binding shape so this cannot accidentally test a for-loop. ++ let statement = parsed.product().statements().last().expect("loop"); ++ let crate::syntax::Statement::ForOf(for_of) = statement.data() else { ++ panic!("expected a for-of AST for {source}"); ++ }; ++ match (&for_of.binding, recovery_declarators) { ++ (crate::syntax::ForBinding::Target(_), None) => { ++ assert!(parsed.diagnostics().is_empty()); ++ } ++ (crate::syntax::ForBinding::Variable(declaration), Some(count)) => { ++ assert_eq!(declaration.declarations.len(), count); ++ } ++ _ => panic!("wrong fallback binding for {source}"), ++ } ++ let output = emit_output( ++ parsed.product(), ++ &EmitOptions { ++ target: ScriptTarget::Es5, ++ no_emit_helpers: true, ++ ..EmitOptions::default() ++ }, ++ ); ++ let code = &javascript(&output).code; ++ assert!(code.contains(" of "), "existing native fallback is retained: {code}"); ++ assert_eq!(code.matches("Math.pow(2, 3)").count(), 1, "{code}"); ++ assert!(!code.contains("**"), "original iterable was restored: {code}"); ++ } ++ } ++ ++ fn pr199_emit_at(input: &str, target: ScriptTarget) -> EmitOutput { ++ let parsed = crate::parser::parse(crate::scanner::scan( ++ SourceId::new(0), ++ ScriptKind::TypeScript, ++ Arc::new(SourceText::new(input).expect("fits")), ++ )); ++ assert!(parsed.diagnostics().is_empty(), "{:?}", parsed.diagnostics()); ++ emit_output( ++ parsed.product(), ++ &EmitOptions { ++ target, ++ no_emit_helpers: true, ++ ..EmitOptions::default() ++ }, ++ ) ++ } ++ ++ #[test] ++ fn pr199_native_declarations_emit_keys_in_declarator_order() { ++ let source = "declare function before(): number; \ ++ declare function key(): string; \ ++ declare function between(): number; \ diff --git a/.github/pr199-review-fixes.part-04.patch b/.github/pr199-review-fixes.part-04.patch new file mode 100644 index 0000000..79c17a2 --- /dev/null +++ b/.github/pr199-review-fixes.part-04.patch @@ -0,0 +1,96 @@ ++ declare function laterKey(): string; \ ++ declare function after(): number; \ ++ declare function finish(): void; \ ++ function* g() { \ ++ const left = before(), C = class { [yield key()]() {} }, \ ++ middle = between(), D = class { [yield laterKey()]() {} }, \ ++ right = after(); \ ++ finish(); return [left, C, middle, D, right]; \ ++ }"; ++ for target in [ScriptTarget::Es2015, ScriptTarget::Es2016] { ++ let output = pr199_emit_at(source, target); ++ assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); ++ let code = &javascript(&output).code; ++ let positions = [ ++ "before()", "key()", "C =", "between()", "laterKey()", "D =", ++ "after()", "finish()", ++ ] ++ .map(|needle| { ++ assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); ++ code.find(needle).expect("one occurrence") ++ }); ++ assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); ++ } ++ } ++ ++ #[test] ++ fn pr199_async_native_declarations_do_not_lose_key_awaits() { ++ let source = "declare function before(): number; \ ++ declare function key(): string; declare function after(): number; \ ++ async function g() { \ ++ const left = before(), C = class { [await key()]() {} }, right = after(); \ ++ return [left, C, right]; \ ++ }"; ++ let output = pr199_emit_at(source, ScriptTarget::Es2015); ++ assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); ++ let code = &javascript(&output).code; ++ let positions = ["before()", "key()", "C =", "after()"].map(|needle| { ++ assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); ++ code.find(needle).expect("one occurrence") ++ }); ++ assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); ++ assert!(code.contains("yield"), "await must survive as a suspension: {code}"); ++ assert!(!code.contains("await key()"), "raw await leaked: {code}"); ++ } ++ + #[test] + fn es5_async_for_update_increment_lowers() { + // Synthetic probe pinning the walker/machine contract: a for-update +diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs +--- a/crates/bamts-compiler/src/emitter/transforms.rs ++++ b/crates/bamts-compiler/src/emitter/transforms.rs +@@ -1696,9 +1696,3 @@ + let zero = self.number_expr("0"); + let counter_decl = self.make_declarator(counter.clone(), Some(zero), range); +- let source_decl = self.make_declarator(source.clone(), Some(iterable), range); +- let initializer = ForInitializer::Variable(VariableDeclaration { +- range, +- kind: VariableKind::Var, +- declarations: vec![counter_decl, source_decl], +- }); + let counter_expr = self.node(counter.range(), Expression::Identifier(counter.clone())); +@@ -1743,4 +1737,3 @@ + } else { +- let iterable = for_of.iterable.as_ref().clone(); + let body = self.rewrite_single_statement(&for_of.body); + return vec![self.node( +@@ -1760,4 +1753,3 @@ + target @ ForBinding::Target(_) => { +- let iterable = for_of.iterable.as_ref().clone(); + let body = self.rewrite_single_statement(&for_of.body); + return vec![self.node( +@@ -1774,3 +1766,9 @@ + }; ++ let source_decl = self.make_declarator(source.clone(), Some(iterable), range); ++ let initializer = ForInitializer::Variable(VariableDeclaration { ++ range, ++ kind: VariableKind::Var, ++ declarations: vec![counter_decl, source_decl], ++ }); + // The binding declaration must itself lower (pattern bindings, + // defaults) - route it through the statement rewriter. +@@ -1817,15 +1815,36 @@ + if !Self::needs(LanguageFeature::Destructuring, self.options) { +- let declarations = declaration +- .declarations +- .iter() +- .map(|declarator| self.rewrite_declarator_initializer(declarator)) +- .collect(); +- return vec![self.node( ++ let outer_key_prelude = std::mem::take(&mut self.key_prelude); ++ let mut statements = Vec::new(); ++ let mut declarations = Vec::with_capacity(declaration.declarations.len()); ++ for declarator in &declaration.declarations { ++ let rewritten = self.rewrite_declarator_initializer(declarator); ++ let prelude = std::mem::take(&mut self.key_prelude); ++ if !prelude.is_empty() { diff --git a/.github/pr199-review-fixes.part-05.patch b/.github/pr199-review-fixes.part-05.patch new file mode 100644 index 0000000..13068d9 --- /dev/null +++ b/.github/pr199-review-fixes.part-05.patch @@ -0,0 +1,88 @@ ++ // Earlier initializers must run before this class's keys. ++ // Keep declarations grouped when no prelude separates them. ++ if !declarations.is_empty() { ++ statements.push(self.node( ++ statement.range(), ++ Statement::Variable(VariableDeclaration { ++ range: declaration.range, ++ kind: declaration.kind, ++ declarations: std::mem::take(&mut declarations), ++ }), ++ )); ++ } ++ statements.extend(prelude); ++ } ++ declarations.push(rewritten); ++ } ++ self.key_prelude = outer_key_prelude; ++ statements.push(self.node( + statement.range(), + Statement::Variable(VariableDeclaration { ++ range: declaration.range, ++ kind: declaration.kind, + declarations, +- ..declaration.clone() + }), +- )]; ++ )); ++ return statements; + } + // A suspending binding (yield in a default or computed key) cannot +diff --git a/crates/bamts-compiler/src/parser.rs b/crates/bamts-compiler/src/parser.rs +--- a/crates/bamts-compiler/src/parser.rs ++++ b/crates/bamts-compiler/src/parser.rs +@@ -2156,8 +2156,14 @@ + && self.is_constructor_name(&name) + && self.at(TokenKind::LParen) + { +- let parameters = self.parse_parameter_list(); ++ let keyword_context = KeywordContext { ++ in_function: true, ++ await_reserved: false, ++ yield_reserved: false, ++ }; ++ let parameters = ++ self.with_keyword_context(keyword_context, |this| this.parse_parameter_list()); + let return_type = self.parse_optional_type_annotation(); + if self.at(TokenKind::LBrace) { +- let body = self.parse_block(); ++ let body = self.with_keyword_context(keyword_context, Self::parse_block); + let _ = return_type; +@@ -6758,3 +6764,37 @@ ++ #[test] ++ fn pr199_constructor_keyword_context_is_local() { ++ for (source, expected, keyword) in [ ++ ("/* 🦀 */ class C { constructor() { await value; } }", "BAMTS-P018", "await"), ++ ("async function f() { class C { constructor() { await value; } } await value; }", "BAMTS-P018", "await"), ++ ("function* f() { class C { constructor() { yield 1; } } yield 2; }", "BAMTS-P017", "yield"), ++ ("async function f() { class C { constructor(x = await value) {} } await value; }", "BAMTS-P018", "await"), ++ ("function* f() { class C { constructor(x = yield 1) {} } yield 2; }", "BAMTS-P017", "yield"), ++ ("class C { constructor(x = await value); constructor(x) {} }", "BAMTS-P018", "await"), ++ ] { ++ let parsed = parse_text(source, ScriptKind::TypeScript); ++ let diagnostics = errors(&parsed) ++ .into_iter() ++ .filter(|diagnostic| { ++ matches!(diagnostic.code().as_str(), "BAMTS-P017" | "BAMTS-P018") ++ }) ++ .collect::>(); ++ assert_eq!(diagnostics.len(), 1, "{source}: {diagnostics:?}"); ++ assert_eq!(diagnostics[0].code().as_str(), expected, "{source}"); ++ let byte_start = source.find(keyword).expect("keyword in fixture"); ++ let utf16_start = source[..byte_start].encode_utf16().count(); ++ assert_eq!(diagnostics[0].range().start().get(), utf16_start, "{source}"); ++ } ++ for source in [ ++ "class C { constructor() { const f = async () => await value; const g = function*() { yield 1; }; } }", ++ "async function f() { class C { constructor() {} } await value; }", ++ "function* f() { class C { constructor() {} } yield 1; }", ++ "class C { constructor() {} } await value;", ++ ] { ++ let parsed = parse_text(source, ScriptKind::TypeScript); ++ assert!(errors(&parsed).is_empty(), "{source}: {:?}", errors(&parsed)); ++ } ++ } ++ + fn assert_clean(text: &str) -> Recovered { + let recovered = parse_ts(text); + let errs = errors(&recovered); diff --git a/.github/workflows/pr199-review-fix.yml b/.github/workflows/pr199-review-fix.yml new file mode 100644 index 0000000..4b29b23 --- /dev/null +++ b/.github/workflows/pr199-review-fix.yml @@ -0,0 +1,100 @@ +name: PR 199 review-fix bootstrap + +on: + pull_request: + types: [synchronize] + +permissions: + contents: read + +jobs: + validate: + if: >- + github.event.pull_request.number == 199 && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: Checkout exact PR head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Reassemble and validate patch + run: | + set -euo pipefail + cat .github/pr199-review-fixes.part-*.patch > /tmp/pr199-review-fixes.patch + git apply --check /tmp/pr199-review-fixes.patch + git apply /tmp/pr199-review-fixes.patch + git diff --check + printf '%s\n' \ + .github/workflows/ci.yml \ + crates/bamts-compiler/src/checker.rs \ + crates/bamts-compiler/src/checker/binder.rs \ + crates/bamts-compiler/src/emitter.rs \ + crates/bamts-compiler/src/emitter/transforms.rs \ + crates/bamts-compiler/src/parser.rs | sort > /tmp/expected-paths + git diff --name-only | sort > /tmp/actual-paths + diff -u /tmp/expected-paths /tmp/actual-paths + + - name: Install Rust toolchain + run: | + TOOLCHAIN=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust-toolchain.toml) + rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt,clippy + rustup default "$TOOLCHAIN" + + - name: Check formatting + run: cargo fmt --all --check + + - name: Focused review regressions + run: cargo test --locked -p bamts-compiler --lib pr199_ + + - name: Fetch pinned TypeScript test fixtures + run: >- + cargo run --locked -p bamts-verification -- source fetch + typescript-primary-tests --dest target/authority/typescript-7.0.2-tests + + - name: Compiler regression suite + run: cargo test --locked -p bamts-compiler --lib + + - name: Compiler clippy + run: cargo clippy --locked -p bamts-compiler --all-targets -- -D warnings + + apply: + needs: validate + if: >- + github.event.pull_request.number == 199 && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Checkout exact validated PR head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: true + + - name: Apply validated patch and remove bootstrap + run: | + set -euo pipefail + cat .github/pr199-review-fixes.part-*.patch > /tmp/pr199-review-fixes.patch + git apply --check /tmp/pr199-review-fixes.patch + git apply /tmp/pr199-review-fixes.patch + git diff --check + git rm .github/pr199-review-fixes.part-*.patch .github/workflows/pr199-review-fix.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .github/workflows/ci.yml \ + crates/bamts-compiler/src/checker.rs \ + crates/bamts-compiler/src/checker/binder.rs \ + crates/bamts-compiler/src/emitter.rs \ + crates/bamts-compiler/src/emitter/transforms.rs \ + crates/bamts-compiler/src/parser.rs + git commit -m 'Fix remaining PR #199 review findings' + git push origin "HEAD:refs/heads/${{ github.event.pull_request.head.ref }}" From 4d29d32b815e934fd4030e20cbd82074ca2b57ed Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:16:03 +0900 Subject: [PATCH 36/42] Retry PR #199 validation on Ubuntu 22.04 --- .github/workflows/pr199-review-fix.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr199-review-fix.yml b/.github/workflows/pr199-review-fix.yml index 4b29b23..ee1f317 100644 --- a/.github/workflows/pr199-review-fix.yml +++ b/.github/workflows/pr199-review-fix.yml @@ -13,7 +13,7 @@ jobs: github.event.pull_request.number == 199 && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' - runs-on: ubuntu-24.04 + runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - name: Checkout exact PR head @@ -68,7 +68,7 @@ jobs: github.event.pull_request.number == 199 && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' - runs-on: ubuntu-24.04 + runs-on: ubuntu-22.04 timeout-minutes: 10 permissions: contents: write From e86ceb0028df039473abc84d79fd2eda7a601415 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:32:13 +0900 Subject: [PATCH 37/42] Retry PR #199 validation on macOS --- .github/workflows/pr199-review-fix.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr199-review-fix.yml b/.github/workflows/pr199-review-fix.yml index ee1f317..82910ca 100644 --- a/.github/workflows/pr199-review-fix.yml +++ b/.github/workflows/pr199-review-fix.yml @@ -13,7 +13,7 @@ jobs: github.event.pull_request.number == 199 && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' - runs-on: ubuntu-22.04 + runs-on: macos-15 timeout-minutes: 60 steps: - name: Checkout exact PR head @@ -68,7 +68,7 @@ jobs: github.event.pull_request.number == 199 && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' - runs-on: ubuntu-22.04 + runs-on: macos-15 timeout-minutes: 10 permissions: contents: write From 64a265095f68da335ecb7aea3f8db4819b14f2cf Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:53:58 +0000 Subject: [PATCH 38/42] Fix super-call flow merging and preserve rewritten emitter evaluation order --- crates/bamts-compiler/src/checker.rs | 120 +++++++----------- crates/bamts-compiler/src/checker/binder.rs | 2 +- .../bamts-compiler/src/emitter/transforms.rs | 75 ++++++++--- 3 files changed, 107 insertions(+), 90 deletions(-) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index 587f563..e8357ca 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -5980,21 +5980,28 @@ function check(options: Options = {}) { ); } - /// TS17009/TS17011: a derived constructor's `this` and `super.x` - /// accesses before a guaranteed `super()` call, with the oracle's - /// flow shapes: arrows are exempt (deferred `this`), a conditional - /// super() covers only its branch, and loops/try never guarantee. + fn authority_source(relative: &str) -> String { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("target/authority/typescript-7.0.2-tests/tests/cases/compiler") + .join(relative); + std::fs::read_to_string(&path).unwrap_or_else(|error| { + panic!( + "authority fixture {} is unavailable: {error}; run \ + `cargo run --locked -p bamts-verification -- source fetch \ + typescript-primary-tests --dest target/authority/typescript-7.0.2-tests` \ + from the repository root first", + path.display() + ) + }) + } + /// The superAccess es2015 baseline carries one TS2576 (static S1 via /// super) and two TS2855 (fields S2 and f); the es5 variant's TS2340 /// flavor is a separate target-conditional slice. #[test] fn super_static_member_matches_superaccess_baseline() { - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let source = - std::fs::read_to_string(root.join( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", - )) - .unwrap(); + let source = authority_source("superAccess.ts"); let codes = checker_codes(&check_text(&source)); assert_eq!( codes @@ -6017,12 +6024,7 @@ function check(options: Options = {}) { /// AfterObject) are inlined at use sites and stay clean. #[test] fn enum_used_before_declaration_matches_baseline() { - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let source = std::fs::read_to_string(root.join(concat!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", - "blockScopedEnumVariablesUseBeforeDef.ts" - ))) - .unwrap(); + let source = authority_source("blockScopedEnumVariablesUseBeforeDef.ts"); let codes = checker_codes(&check_text(&source)); assert_eq!( codes @@ -6034,23 +6036,6 @@ function check(options: Options = {}) { ); } - /// The superAccess es5 baseline swaps each TS2855 field row for the - /// single pre-fields rule TS2340; the static row stays TS2576 at both - /// targets. - #[test] - fn zz_enumbasics3_probe() { - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let source = - std::fs::read_to_string(root.join( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/enumBasics3.ts", - )) - .unwrap(); - eprintln!("ZZ eb3: {:?}", checker_codes(&check_text(&source))); - } - - /// TS2371: only an implementation may carry parameter defaults. The - /// oracle (defaultValueInConstructorOverload1) is exactly one row on a - /// constructor overload signature. /// TS1114: a label redeclared in one function (sibling or nested). /// TS1116: a labeled break may only target an enclosing label. The /// oracles are duplicateLabel2 (one row) and breakTarget6 (one row). @@ -6093,31 +6078,18 @@ function check(options: Options = {}) { Vec::<&str>::new() ); // Authority oracles: one row each. - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let duplicate = std::fs::read_to_string(root.join(concat!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", - "duplicateLabel2.ts" - ))) - .unwrap(); + let duplicate = authority_source("duplicateLabel2.ts"); assert_eq!( checker_codes(&check_text(&duplicate)), vec![DUPLICATE_LABEL.as_str()] ); - let jump = std::fs::read_to_string(root.join(concat!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", - "breakTarget6.ts" - ))) - .unwrap(); + let jump = authority_source("breakTarget6.ts"); assert_eq!( checker_codes(&check_text(&jump)), vec![BREAK_TARGET_NOT_ENCLOSING.as_str()] ); // TS1107: the label exists, but in an enclosing function. - let crosses = std::fs::read_to_string(root.join(concat!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", - "breakTarget5.ts" - ))) - .unwrap(); + let crosses = authority_source("breakTarget5.ts"); assert_eq!( checker_codes(&check_text(&crosses)), vec![BREAK_TARGET_CROSSES_FUNCTION.as_str()] @@ -6130,6 +6102,9 @@ function check(options: Options = {}) { ); } + /// TS2371: only an implementation may carry parameter defaults. The + /// oracle (defaultValueInConstructorOverload1) is exactly one row on a + /// constructor overload signature. #[test] fn parameter_initializer_only_in_implementation() { assert_eq!( @@ -6146,12 +6121,7 @@ function check(options: Options = {}) { checker_codes(&check_text("function g(x = 1) {}")), Vec::<&str>::new() ); - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let source = std::fs::read_to_string(root.join(concat!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", - "defaultValueInConstructorOverload1.ts" - ))) - .unwrap(); + let source = authority_source("defaultValueInConstructorOverload1.ts"); assert_eq!( checker_codes(&check_text(&source)), vec![PARAMETER_INITIALIZER_IN_SIGNATURE.as_str()] @@ -6219,12 +6189,7 @@ function check(options: Options = {}) { ); // The authority file itself: es5 swaps both field rows for TS2340 // and keeps the static row at TS2576. - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let source = - std::fs::read_to_string(root.join( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", - )) - .unwrap(); + let source = authority_source("superAccess.ts"); let oracle_es5 = checker_codes(&check_text_with( &source, ProgramCheckOptions::standard().with_target(Some("es5")), @@ -6408,12 +6373,7 @@ function check(options: Options = {}) { /// directive-stripped source); the count is the pinned oracle. #[test] fn super_field_via_super_matches_baseline_count() { - let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let source = std::fs::read_to_string(root.join(concat!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", - "checkSuperCallBeforeThisAccess.ts" - ))) - .unwrap(); + let source = authority_source("checkSuperCallBeforeThisAccess.ts"); let count = checker_codes(&check_text(&source)) .into_iter() .filter(|code| *code == SUPER_FIELD_VIA_SUPER.as_str()) @@ -6423,12 +6383,8 @@ function check(options: Options = {}) { // reaches `this` inside the super() arguments, which evaluate before // the call completes. for variant in ["2", "5", "8"] { - let variant_source = std::fs::read_to_string( - root.join(format!( - "target/authority/typescript-7.0.2-tests/tests/cases/compiler/checkSuperCallBeforeThisAccessing{variant}.ts" - )), - ) - .unwrap(); + let variant_source = + authority_source(&format!("checkSuperCallBeforeThisAccessing{variant}.ts")); let codes = checker_codes(&check_text(&variant_source)); assert_eq!( codes @@ -6504,6 +6460,10 @@ function check(options: Options = {}) { assert!(codes.contains(&SUPER_FIELD_VIA_SUPER.as_str())); } + /// TS17009/TS17011: a derived constructor's `this` and `super.x` + /// accesses before a guaranteed `super()` call, with the oracle's + /// flow shapes: arrows are exempt (deferred `this`), a conditional + /// super() covers only its branch, and loops/try never guarantee. #[test] fn super_before_this_flow_matrix() { fn codes(text: &str) -> Vec<&'static str> { @@ -6550,6 +6510,20 @@ function check(options: Options = {}) { ), vec![SUPER_BEFORE_THIS.as_str(), SUPER_BEFORE_THIS.as_str()] ); + // Every continuing branch has called super, including when the + // other branch exits before the merge. + assert_eq!( + codes( + "class A extends Object { constructor(c) { if (c) super(); else super(); this; } }" + ), + Vec::<&str>::new() + ); + assert_eq!( + codes( + "class A extends Object { constructor(c) { if (c) return {}; else super(); this; } }" + ), + Vec::<&str>::new() + ); // A super() inside a loop guarantees nothing after it. assert_eq!( codes( diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 2609c30..617834c 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -8916,7 +8916,7 @@ impl<'src> Binder<'src> { let else_ok = else_exits || matches!(else_flow, SuperFlow::Tracking { called: true }); SuperFlow::Tracking { - called: entry && then_ok && else_ok, + called: entry || (then_ok && else_ok), } } (suspended, _, _) => suspended, diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index 11e835f..e6d9dc5 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -1695,12 +1695,6 @@ impl<'a> Rewriter<'a> { let iterable = self.rewrite_expr(&for_of.iterable); let zero = self.number_expr("0"); let counter_decl = self.make_declarator(counter.clone(), Some(zero), range); - let source_decl = self.make_declarator(source.clone(), Some(iterable), range); - let initializer = ForInitializer::Variable(VariableDeclaration { - range, - kind: VariableKind::Var, - declarations: vec![counter_decl, source_decl], - }); let counter_expr = self.node(counter.range(), Expression::Identifier(counter.clone())); let length = self.member_ident(&source, "length", range); let test = self.node( @@ -1741,7 +1735,6 @@ impl<'a> Rewriter<'a> { declaration.declarations = vec![lowered]; self.node(range, Statement::Variable(declaration)) } else { - let iterable = for_of.iterable.as_ref().clone(); let body = self.rewrite_single_statement(&for_of.body); return vec![self.node( range, @@ -1758,7 +1751,6 @@ impl<'a> Rewriter<'a> { // verbatim statement below keeps the shape (and any // applicable diagnostic) instead of dropping it. target @ ForBinding::Target(_) => { - let iterable = for_of.iterable.as_ref().clone(); let body = self.rewrite_single_statement(&for_of.body); return vec![self.node( range, @@ -1771,6 +1763,12 @@ impl<'a> Rewriter<'a> { )]; } }; + let source_decl = self.make_declarator(source.clone(), Some(iterable), range); + let initializer = ForInitializer::Variable(VariableDeclaration { + range, + kind: VariableKind::Var, + declarations: vec![counter_decl, source_decl], + }); // The binding declaration must itself lower (pattern bindings, // defaults) - route it through the statement rewriter. let lowered_binding = self.rewrite_statements(&[binding_statement]); @@ -1813,18 +1811,37 @@ impl<'a> Rewriter<'a> { ); } if !Self::needs(LanguageFeature::Destructuring, self.options) { - let declarations = declaration - .declarations - .iter() - .map(|declarator| self.rewrite_declarator_initializer(declarator)) - .collect(); - return vec![self.node( + let outer_key_prelude = std::mem::take(&mut self.key_prelude); + let mut statements = Vec::new(); + let mut declarations = Vec::with_capacity(declaration.declarations.len()); + for declarator in &declaration.declarations { + let rewritten = self.rewrite_declarator_initializer(declarator); + let prelude = std::mem::take(&mut self.key_prelude); + if !prelude.is_empty() { + if !declarations.is_empty() { + statements.push(self.node( + statement.range(), + Statement::Variable(VariableDeclaration { + range: declaration.range, + kind: declaration.kind, + declarations: std::mem::take(&mut declarations), + }), + )); + } + statements.extend(prelude); + } + declarations.push(rewritten); + } + self.key_prelude = outer_key_prelude; + statements.push(self.node( statement.range(), Statement::Variable(VariableDeclaration { + range: declaration.range, + kind: declaration.kind, declarations, - ..declaration.clone() }), - )]; + )); + return statements; } // A suspending binding (yield in a default or computed key) cannot // live in a ternary: the machine must split the default selection @@ -9966,6 +9983,21 @@ mod tests { assert!(!code.contains("static x"), "{code}"); } + #[test] + fn native_declaration_emits_suspending_class_key_prelude_in_order() { + let output = emit_at( + "function* g() { const left = before(), C = class { [yield key()]() {} }, right = after(); }\n", + ScriptTarget::Es2015, + ); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let code = javascript(&output); + let positions = ["before()", "key()", "C =", "after()"].map(|needle| { + assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); + code.find(needle).expect("emitted expression") + }); + assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); + } + #[test] fn static_this_and_super_are_rebound_to_the_constructor() { let output = emit_at( @@ -10568,6 +10600,17 @@ console.log(JSON.stringify([bar, bar4, log])); ); } + #[test] + fn es5_for_of_fallback_keeps_rewritten_iterable() { + let output = emit_at( + "async function f() { let x; for (x of await values) {} }\n", + ScriptTarget::Es5, + ); + let code = javascript(&output); + assert!(!code.contains("await values"), "{code}"); + assert!(code.contains("yield values"), "{code}"); + } + #[test] fn node_executes_lowered_compound_exponentiation_with_single_evaluations() { let output = emit_at( From 3c8884f89b8788d25180f735376e3acef1fedc5f Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:49:23 +0000 Subject: [PATCH 39/42] Fix remaining PR #199 review findings --- .github/pr199-review-fixes.part-00.patch | 105 ------------------ .github/pr199-review-fixes.part-01.patch | 106 ------------------ .github/pr199-review-fixes.part-02.patch | 103 ----------------- .github/pr199-review-fixes.part-03.patch | 108 ------------------ .github/pr199-review-fixes.part-04.patch | 96 ---------------- .github/pr199-review-fixes.part-05.patch | 88 --------------- .github/workflows/ci.yml | 5 + .github/workflows/pr199-review-fix.yml | 100 ----------------- crates/bamts-compiler/src/checker.rs | 105 +++++++++++++++++- crates/bamts-compiler/src/checker/binder.rs | 37 +++++-- crates/bamts-compiler/src/emitter.rs | 117 ++++++++++++++++++++ crates/bamts-compiler/src/parser.rs | 76 ++++++++++++- 12 files changed, 324 insertions(+), 722 deletions(-) delete mode 100644 .github/pr199-review-fixes.part-00.patch delete mode 100644 .github/pr199-review-fixes.part-01.patch delete mode 100644 .github/pr199-review-fixes.part-02.patch delete mode 100644 .github/pr199-review-fixes.part-03.patch delete mode 100644 .github/pr199-review-fixes.part-04.patch delete mode 100644 .github/pr199-review-fixes.part-05.patch delete mode 100644 .github/workflows/pr199-review-fix.yml diff --git a/.github/pr199-review-fixes.part-00.patch b/.github/pr199-review-fixes.part-00.patch deleted file mode 100644 index 810a2cc..0000000 --- a/.github/pr199-review-fixes.part-00.patch +++ /dev/null @@ -1,105 +0,0 @@ -diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml ---- a/.github/workflows/ci.yml -+++ b/.github/workflows/ci.yml -@@ -50,3 +50,8 @@ -+ - name: Fetch pinned TypeScript test fixtures -+ run: >- -+ cargo run --locked -p bamts-verification -- source fetch -+ typescript-primary-tests --dest target/authority/typescript-7.0.2-tests -+ - - name: Test workspace - run: cargo test --workspace --locked - -diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs ---- a/crates/bamts-compiler/src/checker.rs -+++ b/crates/bamts-compiler/src/checker.rs -@@ -5983,7 +5983,19 @@ -- /// TS17009/TS17011: a derived constructor's `this` and `super.x` -- /// accesses before a guaranteed `super()` call, with the oracle's -- /// flow shapes: arrows are exempt (deferred `this`), a conditional -- /// super() covers only its branch, and loops/try never guarantee. -+ fn authority_source(relative: &str) -> String { -+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) -+ .join("../..") -+ .join("target/authority/typescript-7.0.2-tests/tests/cases/compiler") -+ .join(relative); -+ std::fs::read_to_string(&path).unwrap_or_else(|error| { -+ panic!( -+ "authority fixture {} is unavailable: {error}; run \ -+ `cargo run --locked -p bamts-verification -- source fetch \ -+ typescript-primary-tests --dest target/authority/typescript-7.0.2-tests` \ -+ from the repository root first", -+ path.display() -+ ) -+ }) -+ } -+ - /// The superAccess es2015 baseline carries one TS2576 (static S1 via - /// super) and two TS2855 (fields S2 and f); the es5 variant's TS2340 - /// flavor is a separate target-conditional slice. -@@ -5991,8 +6003,3 @@ - fn super_static_member_matches_superaccess_baseline() { -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let source = -- std::fs::read_to_string(root.join( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", -- )) -- .unwrap(); -+ let source = authority_source("superAccess.ts"); - let codes = checker_codes(&check_text(&source)); -@@ -6019,8 +6026,3 @@ - fn enum_used_before_declaration_matches_baseline() { -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let source = std::fs::read_to_string(root.join(concat!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", -- "blockScopedEnumVariablesUseBeforeDef.ts" -- ))) -- .unwrap(); -+ let source = authority_source("blockScopedEnumVariablesUseBeforeDef.ts"); - let codes = checker_codes(&check_text(&source)); -@@ -6037,20 +6039,3 @@ -- /// The superAccess es5 baseline swaps each TS2855 field row for the -- /// single pre-fields rule TS2340; the static row stays TS2576 at both -- /// targets. -- #[test] -- fn zz_enumbasics3_probe() { -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let source = -- std::fs::read_to_string(root.join( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/enumBasics3.ts", -- )) -- .unwrap(); -- eprintln!("ZZ eb3: {:?}", checker_codes(&check_text(&source))); -- } -- -- /// TS2371: only an implementation may carry parameter defaults. The -- /// oracle (defaultValueInConstructorOverload1) is exactly one row on a -- /// constructor overload signature. -- /// TS1114: a label redeclared in one function (sibling or nested). -+ /// TS1114: a label redeclared while it still encloses the current statement. - /// TS1116: a labeled break may only target an enclosing label. The - /// oracles are duplicateLabel2 (one row) and breakTarget6 (one row). -@@ -6067,5 +6052,5 @@ -- // Sibling redeclaration in the same function is also a duplicate. -+ // A completed sibling label is no longer active and may be reused. - assert_eq!( - checker_codes(&check_text("a: {} a: {}")), -- vec![DUPLICATE_LABEL.as_str()] -+ Vec::<&str>::new() - ); -@@ -6103,8 +6088,3 @@ - // Authority oracles: one row each. -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let duplicate = std::fs::read_to_string(root.join(concat!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", -- "duplicateLabel2.ts" -- ))) -- .unwrap(); -+ let duplicate = authority_source("duplicateLabel2.ts"); - assert_eq!( -@@ -6115,7 +6095,3 @@ -- let jump = std::fs::read_to_string(root.join(concat!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", -- "breakTarget6.ts" -- ))) -- .unwrap(); diff --git a/.github/pr199-review-fixes.part-01.patch b/.github/pr199-review-fixes.part-01.patch deleted file mode 100644 index 99e59e0..0000000 --- a/.github/pr199-review-fixes.part-01.patch +++ /dev/null @@ -1,106 +0,0 @@ -+ let jump = authority_source("breakTarget6.ts"); - assert_eq!( - checker_codes(&check_text(&jump)), -@@ -6126,7 +6102,3 @@ - // TS1107: the label exists, but in an enclosing function. -- let crosses = std::fs::read_to_string(root.join(concat!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", -- "breakTarget5.ts" -- ))) -- .unwrap(); -+ let crosses = authority_source("breakTarget5.ts"); - assert_eq!( -@@ -6146,3 +6118,6 @@ -+ /// TS2371: only an implementation may carry parameter defaults. The -+ /// oracle (defaultValueInConstructorOverload1) is exactly one row on a -+ /// constructor overload signature. - #[test] - fn parameter_initializer_only_in_implementation() { - assert_eq!( -@@ -6165,8 +6140,3 @@ -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let source = std::fs::read_to_string(root.join(concat!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", -- "defaultValueInConstructorOverload1.ts" -- ))) -- .unwrap(); -+ let source = authority_source("defaultValueInConstructorOverload1.ts"); - assert_eq!( - checker_codes(&check_text(&source)), -@@ -6177,3 +6147,6 @@ -+ /// The superAccess es5 baseline swaps each TS2855 field row for the -+ /// single pre-fields rule TS2340; the static row stays TS2576 at both -+ /// targets. - #[test] - fn super_field_flavor_splits_by_target() { - let source = "class MyBase { -@@ -6241,9 +6214,4 @@ - // The authority file itself: es5 swaps both field rows for TS2340 - // and keeps the static row at TS2576. -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let source = -- std::fs::read_to_string(root.join( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/superAccess.ts", -- )) -- .unwrap(); -+ let source = authority_source("superAccess.ts"); - let oracle_es5 = checker_codes(&check_text_with( -@@ -6411,8 +6379,3 @@ - fn super_field_via_super_matches_baseline_count() { -- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); -- let source = std::fs::read_to_string(root.join(concat!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/", -- "checkSuperCallBeforeThisAccess.ts" -- ))) -- .unwrap(); -+ let source = authority_source("checkSuperCallBeforeThisAccess.ts"); - let count = checker_codes(&check_text(&source)) -@@ -6428,8 +6391,5 @@ - for variant in ["2", "5", "8"] { -- let variant_source = std::fs::read_to_string( -- root.join(format!( -- "target/authority/typescript-7.0.2-tests/tests/cases/compiler/checkSuperCallBeforeThisAccessing{variant}.ts" -- )), -- ) -- .unwrap(); -+ let variant_source = authority_source(&format!( -+ "checkSuperCallBeforeThisAccessing{variant}.ts" -+ )); - let codes = checker_codes(&check_text(&variant_source)); -@@ -6519,3 +6479,7 @@ -+ /// TS17009/TS17011: a derived constructor's `this` and `super.x` -+ /// accesses before a guaranteed `super()` call, with the oracle's -+ /// flow shapes: arrows are exempt (deferred `this`), a conditional -+ /// super() covers only its branch, and loops/try never guarantee. - #[test] - fn super_before_this_flow_matrix() { - fn codes(text: &str) -> Vec<&'static str> { -@@ -6574,3 +6538,78 @@ -+ #[test] -+ fn pr199_super_flow_joins_every_continuing_path() { -+ for (body, safe) in [ -+ ("if (c) { super(); } else { super(); }", true), -+ ("if (c) { throw 0; } else { super(); }", true), -+ ("if (c) { super(); } else { throw 0; }", true), -+ ("if (c) { return { method() {} }; } else { super(); }", true), -+ ("super(); if (c) {} else {}", true), -+ ("if (c) { if (c) { super(); } else { super(); } } else { super(); }", true), -+ ("if (c) { super(); }", false), -+ ("if (c) {} else { super(); }", false), -+ ] { -+ let source = format!( -+ "class B {{ method() {{}} }} \ -+ class D extends B {{ constructor(c: boolean) {{ \ -+ {body} this; super.method(); }} }}" -+ ); -+ let mut codes = checker_codes(&check_text(&source)) -+ .into_iter() -+ .filter(|code| { -+ *code == SUPER_BEFORE_THIS.as_str() -+ || *code == SUPER_BEFORE_SUPER_PROPERTY.as_str() -+ }) -+ .collect::>(); -+ codes.sort_unstable(); -+ let expected = if safe { -+ Vec::new() -+ } else { diff --git a/.github/pr199-review-fixes.part-02.patch b/.github/pr199-review-fixes.part-02.patch deleted file mode 100644 index 4108c7f..0000000 --- a/.github/pr199-review-fixes.part-02.patch +++ /dev/null @@ -1,103 +0,0 @@ -+ vec![SUPER_BEFORE_THIS.as_str(), SUPER_BEFORE_SUPER_PROPERTY.as_str()] -+ }; -+ assert_eq!(codes, expected, "{body}"); -+ } -+ } -+ -+ #[test] -+ fn pr199_label_frames_and_nearest_targets() { -+ let cases = [ -+ ("L: {} L: {}", None), -+ ("L: { L: {} }", Some(DUPLICATE_LABEL.as_str())), -+ ("L: { function f() { L: { break L; } } break L; }", None), -+ ("L: { const f = () => { L: { break L; } }; break L; }", None), -+ ("L: { class C { constructor() { L: { break L; } } } break L; }", None), -+ ("L: { function f() { break L; } break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str())), -+ ("L: { const f = () => { break L; }; break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str())), -+ ("L: { class C { constructor() { break L; } } break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str())), -+ ("const f = () => { local: {} }; local: {}", None), -+ ("class C { constructor() { local: {} } } local: {}", None), -+ ("L: {} break L;", Some(BREAK_TARGET_NOT_ENCLOSING.as_str())), -+ ("while (true) { break missing; }", Some(BREAK_TARGET_NOT_ENCLOSING.as_str())), -+ ]; -+ for (source, expected) in cases { -+ let codes = checker_codes(&check_text(source)) -+ .into_iter() -+ .filter(|code| { -+ *code == DUPLICATE_LABEL.as_str() -+ || *code == BREAK_TARGET_NOT_ENCLOSING.as_str() -+ || *code == BREAK_TARGET_CROSSES_FUNCTION.as_str() -+ }) -+ .collect::>(); -+ assert_eq!(codes, expected.into_iter().collect::>(), "{source}"); -+ } -+ } -+ -+ #[test] -+ fn pr199_missing_authority_fixture_explains_the_prerequisite() { -+ let panic = std::panic::catch_unwind(|| { -+ authority_source("__pr199_fixture_that_must_not_exist__.ts") -+ }) -+ .expect_err("a missing authority fixture must fail rather than skip"); -+ let message = panic.downcast_ref::().expect("formatted panic"); -+ assert!(message.contains("__pr199_fixture_that_must_not_exist__.ts")); -+ assert!(message.contains("source fetch typescript-primary-tests")); -+ assert!(message.contains("--dest target/authority/typescript-7.0.2-tests")); -+ } -+ - #[test] - fn super_call_context_matrix() { - const SUPER_CODES: [&str; 4] = [ -diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs ---- a/crates/bamts-compiler/src/checker/binder.rs -+++ b/crates/bamts-compiler/src/checker/binder.rs -@@ -8918,6 +8918,6 @@ - let else_ok = - else_exits || matches!(else_flow, SuperFlow::Tracking { called: true }); - SuperFlow::Tracking { -- called: entry && then_ok && else_ok, -+ called: entry || (then_ok && else_ok), - } - } -@@ -9120,4 +9120,4 @@ - let label = self.identifier_text(&statement.label).into_owned(); -- // TS1114: sibling and nested redeclarations in one function -- // are both duplicate labels. -+ // TS1114 concerns active labels, not earlier sibling statements. -+ let label_mark = self.label_declarations.len(); - let function_labels_start = self.label_scope_marks.last().copied().unwrap_or(0); -@@ -9135,8 +9135,8 @@ - self.label_ancestors.push((label, self.label_frame)); - self.resolve_statement(&statement.body, scope); - self.label_ancestors.pop(); -+ self.label_declarations.truncate(label_mark); - } - Statement::Break(jump) => { -- // TS1116: a labeled break may only target an enclosing -- // label. (TS1107, crossing a function boundary, needs the -- // declaring function's labels and is banked.) -+ // Retain outer frames to distinguish TS1107 from TS1116, -+ // but select the nearest enclosing label first. -@@ -9149,3 +9149,4 @@ - .label_ancestors - .iter() -+ .rev() - .find(|(ancestor, _)| *ancestor == label.as_ref()); -@@ -9735,3 +9736,17 @@ -+ fn push_label_scope(&mut self) { -+ self.label_scope_marks.push(self.label_declarations.len()); -+ self.label_frame += 1; -+ } -+ -+ fn pop_label_scope(&mut self) { -+ let mark = self -+ .label_scope_marks -+ .pop() -+ .expect("label scope must be entered before it is left"); -+ self.label_declarations.truncate(mark); -+ self.label_frame -= 1; -+ } -+ - fn resolve_function( - &mut self, - function: &'src FunctionLike, diff --git a/.github/pr199-review-fixes.part-03.patch b/.github/pr199-review-fixes.part-03.patch deleted file mode 100644 index 2172f6a..0000000 --- a/.github/pr199-review-fixes.part-03.patch +++ /dev/null @@ -1,108 +0,0 @@ -@@ -9752,4 +9767,3 @@ - self.super_call_guarantees = true; -- self.label_scope_marks.push(self.label_declarations.len()); -- self.label_frame += 1; -+ self.push_label_scope(); - self.bind_implicit_function_values(&function.parameters, scope); -@@ -9889,7 +9903,4 @@ - self.super_flow = outer_super_flow; - self.super_call_guarantees = outer_guarantees; -- if let Some(mark) = self.label_scope_marks.pop() { -- self.label_declarations.truncate(mark); -- } -- self.label_frame -= 1; -+ self.pop_label_scope(); - debug_assert_eq!(popped_home, Some(member_home)); -@@ -11894,4 +11905,5 @@ - let child = self.new_scope(ScopeKind::Function, Some(scope)); -+ self.push_label_scope(); - let new_target_marker = self.new_target_contexts.len(); - self.new_target_contexts.push(true); - self.bind_implicit_function_values(&constructor.parameters, child); -@@ -11979,6 +11991,7 @@ - self.new_target_contexts.truncate(new_target_marker); - self.super_call_contexts.pop(); - let popped_home = self.super_member_homes.pop(); -+ self.pop_label_scope(); - debug_assert_eq!(popped_home, Some(SuperMemberHome::ClassMember { derived })); - } - ClassMember::Property(property) => { -@@ -12106,3 +12119,4 @@ - Expression::Arrow(arrow) => { - let child = self.new_scope(ScopeKind::Function, Some(scope)); -+ self.push_label_scope(); - // Arrows capture `this` but never inherit super-call legality. -@@ -12218,5 +12232,6 @@ - if self.node_types.insert(expression.id(), type_id).is_none() { - self.typed_expressions.push((expression.range(), type_id)); - } -+ self.pop_label_scope(); - } - Expression::Call(call) => { -diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs ---- a/crates/bamts-compiler/src/emitter.rs -+++ b/crates/bamts-compiler/src/emitter.rs -@@ -6773,3 +6773,111 @@ -+ #[test] -+ fn pr199_for_of_fallbacks_keep_the_rewritten_iterable() { -+ for (source, recovery_declarators) in [ -+ ("var target; for (target of [2 ** 3]) {}", None), -+ ("for (var first, second of [2 ** 3]) {}", Some(2)), -+ ] { -+ let parsed = crate::parser::parse(crate::scanner::scan( -+ SourceId::new(0), -+ ScriptKind::TypeScript, -+ Arc::new(SourceText::new(source).expect("fits")), -+ )); -+ // The malformed declaration is intentional recovery-AST coverage. -+ // Assert the binding shape so this cannot accidentally test a for-loop. -+ let statement = parsed.product().statements().last().expect("loop"); -+ let crate::syntax::Statement::ForOf(for_of) = statement.data() else { -+ panic!("expected a for-of AST for {source}"); -+ }; -+ match (&for_of.binding, recovery_declarators) { -+ (crate::syntax::ForBinding::Target(_), None) => { -+ assert!(parsed.diagnostics().is_empty()); -+ } -+ (crate::syntax::ForBinding::Variable(declaration), Some(count)) => { -+ assert_eq!(declaration.declarations.len(), count); -+ } -+ _ => panic!("wrong fallback binding for {source}"), -+ } -+ let output = emit_output( -+ parsed.product(), -+ &EmitOptions { -+ target: ScriptTarget::Es5, -+ no_emit_helpers: true, -+ ..EmitOptions::default() -+ }, -+ ); -+ let code = &javascript(&output).code; -+ assert!(code.contains(" of "), "existing native fallback is retained: {code}"); -+ assert_eq!(code.matches("Math.pow(2, 3)").count(), 1, "{code}"); -+ assert!(!code.contains("**"), "original iterable was restored: {code}"); -+ } -+ } -+ -+ fn pr199_emit_at(input: &str, target: ScriptTarget) -> EmitOutput { -+ let parsed = crate::parser::parse(crate::scanner::scan( -+ SourceId::new(0), -+ ScriptKind::TypeScript, -+ Arc::new(SourceText::new(input).expect("fits")), -+ )); -+ assert!(parsed.diagnostics().is_empty(), "{:?}", parsed.diagnostics()); -+ emit_output( -+ parsed.product(), -+ &EmitOptions { -+ target, -+ no_emit_helpers: true, -+ ..EmitOptions::default() -+ }, -+ ) -+ } -+ -+ #[test] -+ fn pr199_native_declarations_emit_keys_in_declarator_order() { -+ let source = "declare function before(): number; \ -+ declare function key(): string; \ -+ declare function between(): number; \ diff --git a/.github/pr199-review-fixes.part-04.patch b/.github/pr199-review-fixes.part-04.patch deleted file mode 100644 index 79c17a2..0000000 --- a/.github/pr199-review-fixes.part-04.patch +++ /dev/null @@ -1,96 +0,0 @@ -+ declare function laterKey(): string; \ -+ declare function after(): number; \ -+ declare function finish(): void; \ -+ function* g() { \ -+ const left = before(), C = class { [yield key()]() {} }, \ -+ middle = between(), D = class { [yield laterKey()]() {} }, \ -+ right = after(); \ -+ finish(); return [left, C, middle, D, right]; \ -+ }"; -+ for target in [ScriptTarget::Es2015, ScriptTarget::Es2016] { -+ let output = pr199_emit_at(source, target); -+ assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); -+ let code = &javascript(&output).code; -+ let positions = [ -+ "before()", "key()", "C =", "between()", "laterKey()", "D =", -+ "after()", "finish()", -+ ] -+ .map(|needle| { -+ assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); -+ code.find(needle).expect("one occurrence") -+ }); -+ assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); -+ } -+ } -+ -+ #[test] -+ fn pr199_async_native_declarations_do_not_lose_key_awaits() { -+ let source = "declare function before(): number; \ -+ declare function key(): string; declare function after(): number; \ -+ async function g() { \ -+ const left = before(), C = class { [await key()]() {} }, right = after(); \ -+ return [left, C, right]; \ -+ }"; -+ let output = pr199_emit_at(source, ScriptTarget::Es2015); -+ assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); -+ let code = &javascript(&output).code; -+ let positions = ["before()", "key()", "C =", "after()"].map(|needle| { -+ assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); -+ code.find(needle).expect("one occurrence") -+ }); -+ assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); -+ assert!(code.contains("yield"), "await must survive as a suspension: {code}"); -+ assert!(!code.contains("await key()"), "raw await leaked: {code}"); -+ } -+ - #[test] - fn es5_async_for_update_increment_lowers() { - // Synthetic probe pinning the walker/machine contract: a for-update -diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs ---- a/crates/bamts-compiler/src/emitter/transforms.rs -+++ b/crates/bamts-compiler/src/emitter/transforms.rs -@@ -1696,9 +1696,3 @@ - let zero = self.number_expr("0"); - let counter_decl = self.make_declarator(counter.clone(), Some(zero), range); -- let source_decl = self.make_declarator(source.clone(), Some(iterable), range); -- let initializer = ForInitializer::Variable(VariableDeclaration { -- range, -- kind: VariableKind::Var, -- declarations: vec![counter_decl, source_decl], -- }); - let counter_expr = self.node(counter.range(), Expression::Identifier(counter.clone())); -@@ -1743,4 +1737,3 @@ - } else { -- let iterable = for_of.iterable.as_ref().clone(); - let body = self.rewrite_single_statement(&for_of.body); - return vec![self.node( -@@ -1760,4 +1753,3 @@ - target @ ForBinding::Target(_) => { -- let iterable = for_of.iterable.as_ref().clone(); - let body = self.rewrite_single_statement(&for_of.body); - return vec![self.node( -@@ -1774,3 +1766,9 @@ - }; -+ let source_decl = self.make_declarator(source.clone(), Some(iterable), range); -+ let initializer = ForInitializer::Variable(VariableDeclaration { -+ range, -+ kind: VariableKind::Var, -+ declarations: vec![counter_decl, source_decl], -+ }); - // The binding declaration must itself lower (pattern bindings, - // defaults) - route it through the statement rewriter. -@@ -1817,15 +1815,36 @@ - if !Self::needs(LanguageFeature::Destructuring, self.options) { -- let declarations = declaration -- .declarations -- .iter() -- .map(|declarator| self.rewrite_declarator_initializer(declarator)) -- .collect(); -- return vec![self.node( -+ let outer_key_prelude = std::mem::take(&mut self.key_prelude); -+ let mut statements = Vec::new(); -+ let mut declarations = Vec::with_capacity(declaration.declarations.len()); -+ for declarator in &declaration.declarations { -+ let rewritten = self.rewrite_declarator_initializer(declarator); -+ let prelude = std::mem::take(&mut self.key_prelude); -+ if !prelude.is_empty() { diff --git a/.github/pr199-review-fixes.part-05.patch b/.github/pr199-review-fixes.part-05.patch deleted file mode 100644 index 13068d9..0000000 --- a/.github/pr199-review-fixes.part-05.patch +++ /dev/null @@ -1,88 +0,0 @@ -+ // Earlier initializers must run before this class's keys. -+ // Keep declarations grouped when no prelude separates them. -+ if !declarations.is_empty() { -+ statements.push(self.node( -+ statement.range(), -+ Statement::Variable(VariableDeclaration { -+ range: declaration.range, -+ kind: declaration.kind, -+ declarations: std::mem::take(&mut declarations), -+ }), -+ )); -+ } -+ statements.extend(prelude); -+ } -+ declarations.push(rewritten); -+ } -+ self.key_prelude = outer_key_prelude; -+ statements.push(self.node( - statement.range(), - Statement::Variable(VariableDeclaration { -+ range: declaration.range, -+ kind: declaration.kind, - declarations, -- ..declaration.clone() - }), -- )]; -+ )); -+ return statements; - } - // A suspending binding (yield in a default or computed key) cannot -diff --git a/crates/bamts-compiler/src/parser.rs b/crates/bamts-compiler/src/parser.rs ---- a/crates/bamts-compiler/src/parser.rs -+++ b/crates/bamts-compiler/src/parser.rs -@@ -2156,8 +2156,14 @@ - && self.is_constructor_name(&name) - && self.at(TokenKind::LParen) - { -- let parameters = self.parse_parameter_list(); -+ let keyword_context = KeywordContext { -+ in_function: true, -+ await_reserved: false, -+ yield_reserved: false, -+ }; -+ let parameters = -+ self.with_keyword_context(keyword_context, |this| this.parse_parameter_list()); - let return_type = self.parse_optional_type_annotation(); - if self.at(TokenKind::LBrace) { -- let body = self.parse_block(); -+ let body = self.with_keyword_context(keyword_context, Self::parse_block); - let _ = return_type; -@@ -6758,3 +6764,37 @@ -+ #[test] -+ fn pr199_constructor_keyword_context_is_local() { -+ for (source, expected, keyword) in [ -+ ("/* 🦀 */ class C { constructor() { await value; } }", "BAMTS-P018", "await"), -+ ("async function f() { class C { constructor() { await value; } } await value; }", "BAMTS-P018", "await"), -+ ("function* f() { class C { constructor() { yield 1; } } yield 2; }", "BAMTS-P017", "yield"), -+ ("async function f() { class C { constructor(x = await value) {} } await value; }", "BAMTS-P018", "await"), -+ ("function* f() { class C { constructor(x = yield 1) {} } yield 2; }", "BAMTS-P017", "yield"), -+ ("class C { constructor(x = await value); constructor(x) {} }", "BAMTS-P018", "await"), -+ ] { -+ let parsed = parse_text(source, ScriptKind::TypeScript); -+ let diagnostics = errors(&parsed) -+ .into_iter() -+ .filter(|diagnostic| { -+ matches!(diagnostic.code().as_str(), "BAMTS-P017" | "BAMTS-P018") -+ }) -+ .collect::>(); -+ assert_eq!(diagnostics.len(), 1, "{source}: {diagnostics:?}"); -+ assert_eq!(diagnostics[0].code().as_str(), expected, "{source}"); -+ let byte_start = source.find(keyword).expect("keyword in fixture"); -+ let utf16_start = source[..byte_start].encode_utf16().count(); -+ assert_eq!(diagnostics[0].range().start().get(), utf16_start, "{source}"); -+ } -+ for source in [ -+ "class C { constructor() { const f = async () => await value; const g = function*() { yield 1; }; } }", -+ "async function f() { class C { constructor() {} } await value; }", -+ "function* f() { class C { constructor() {} } yield 1; }", -+ "class C { constructor() {} } await value;", -+ ] { -+ let parsed = parse_text(source, ScriptKind::TypeScript); -+ assert!(errors(&parsed).is_empty(), "{source}: {:?}", errors(&parsed)); -+ } -+ } -+ - fn assert_clean(text: &str) -> Recovered { - let recovered = parse_ts(text); - let errs = errors(&recovered); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54c18a9..35e11c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,11 @@ jobs: - name: Clippy run: cargo clippy --workspace --all-targets --locked -- -D warnings + - name: Fetch pinned TypeScript test fixtures + run: >- + cargo run --locked -p bamts-verification -- source fetch + typescript-primary-tests --dest target/authority/typescript-7.0.2-tests + - name: Test workspace run: cargo test --workspace --locked diff --git a/.github/workflows/pr199-review-fix.yml b/.github/workflows/pr199-review-fix.yml deleted file mode 100644 index 82910ca..0000000 --- a/.github/workflows/pr199-review-fix.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: PR 199 review-fix bootstrap - -on: - pull_request: - types: [synchronize] - -permissions: - contents: read - -jobs: - validate: - if: >- - github.event.pull_request.number == 199 && - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' - runs-on: macos-15 - timeout-minutes: 60 - steps: - - name: Checkout exact PR head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - - - name: Reassemble and validate patch - run: | - set -euo pipefail - cat .github/pr199-review-fixes.part-*.patch > /tmp/pr199-review-fixes.patch - git apply --check /tmp/pr199-review-fixes.patch - git apply /tmp/pr199-review-fixes.patch - git diff --check - printf '%s\n' \ - .github/workflows/ci.yml \ - crates/bamts-compiler/src/checker.rs \ - crates/bamts-compiler/src/checker/binder.rs \ - crates/bamts-compiler/src/emitter.rs \ - crates/bamts-compiler/src/emitter/transforms.rs \ - crates/bamts-compiler/src/parser.rs | sort > /tmp/expected-paths - git diff --name-only | sort > /tmp/actual-paths - diff -u /tmp/expected-paths /tmp/actual-paths - - - name: Install Rust toolchain - run: | - TOOLCHAIN=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust-toolchain.toml) - rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt,clippy - rustup default "$TOOLCHAIN" - - - name: Check formatting - run: cargo fmt --all --check - - - name: Focused review regressions - run: cargo test --locked -p bamts-compiler --lib pr199_ - - - name: Fetch pinned TypeScript test fixtures - run: >- - cargo run --locked -p bamts-verification -- source fetch - typescript-primary-tests --dest target/authority/typescript-7.0.2-tests - - - name: Compiler regression suite - run: cargo test --locked -p bamts-compiler --lib - - - name: Compiler clippy - run: cargo clippy --locked -p bamts-compiler --all-targets -- -D warnings - - apply: - needs: validate - if: >- - github.event.pull_request.number == 199 && - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.head.ref == 'diamond/walker-lockstep-pins' - runs-on: macos-15 - timeout-minutes: 10 - permissions: - contents: write - steps: - - name: Checkout exact validated PR head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - persist-credentials: true - - - name: Apply validated patch and remove bootstrap - run: | - set -euo pipefail - cat .github/pr199-review-fixes.part-*.patch > /tmp/pr199-review-fixes.patch - git apply --check /tmp/pr199-review-fixes.patch - git apply /tmp/pr199-review-fixes.patch - git diff --check - git rm .github/pr199-review-fixes.part-*.patch .github/workflows/pr199-review-fix.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add .github/workflows/ci.yml \ - crates/bamts-compiler/src/checker.rs \ - crates/bamts-compiler/src/checker/binder.rs \ - crates/bamts-compiler/src/emitter.rs \ - crates/bamts-compiler/src/emitter/transforms.rs \ - crates/bamts-compiler/src/parser.rs - git commit -m 'Fix remaining PR #199 review findings' - git push origin "HEAD:refs/heads/${{ github.event.pull_request.head.ref }}" diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index e8357ca..d4bd933 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -6036,7 +6036,7 @@ function check(options: Options = {}) { ); } - /// TS1114: a label redeclared in one function (sibling or nested). + /// TS1114: a label redeclared while it still encloses the current statement. /// TS1116: a labeled break may only target an enclosing label. The /// oracles are duplicateLabel2 (one row) and breakTarget6 (one row). #[test] @@ -6047,10 +6047,10 @@ function check(options: Options = {}) { )), vec![DUPLICATE_LABEL.as_str()] ); - // Sibling redeclaration in the same function is also a duplicate. + // A completed sibling label is no longer active and may be reused. assert_eq!( checker_codes(&check_text("a: {} a: {}")), - vec![DUPLICATE_LABEL.as_str()] + Vec::<&str>::new() ); // A nested function owns its labels; reuse across the boundary is // legal. @@ -6128,6 +6128,9 @@ function check(options: Options = {}) { ); } + /// The superAccess es5 baseline swaps each TS2855 field row for the + /// single pre-fields rule TS2340; the static row stays TS2576 at both + /// targets. #[test] fn super_field_flavor_splits_by_target() { let source = "class MyBase { @@ -6545,6 +6548,102 @@ function check(options: Options = {}) { ); } + #[test] + fn pr199_super_flow_joins_every_continuing_path() { + for (body, safe) in [ + ("if (c) { super(); } else { super(); }", true), + ("if (c) { throw 0; } else { super(); }", true), + ("if (c) { super(); } else { throw 0; }", true), + ("if (c) { return { method() {} }; } else { super(); }", true), + ("super(); if (c) {} else {}", true), + ( + "if (c) { if (c) { super(); } else { super(); } } else { super(); }", + true, + ), + ("if (c) { super(); }", false), + ("if (c) {} else { super(); }", false), + ] { + let source = format!( + "class B {{ method() {{}} }} \ + class D extends B {{ constructor(c: boolean) {{ \ + {body} this; super.method(); }} }}" + ); + let mut codes = checker_codes(&check_text(&source)) + .into_iter() + .filter(|code| { + *code == SUPER_BEFORE_THIS.as_str() + || *code == SUPER_BEFORE_SUPER_PROPERTY.as_str() + }) + .collect::>(); + codes.sort_unstable(); + let expected = if safe { + Vec::new() + } else { + vec![ + SUPER_BEFORE_THIS.as_str(), + SUPER_BEFORE_SUPER_PROPERTY.as_str(), + ] + }; + assert_eq!(codes, expected, "{body}"); + } + } + + #[test] + fn pr199_label_frames_and_nearest_targets() { + let cases = [ + ("L: {} L: {}", None), + ("L: { L: {} }", Some(DUPLICATE_LABEL.as_str())), + ("L: { function f() { L: { break L; } } break L; }", None), + ("L: { const f = () => { L: { break L; } }; break L; }", None), + ( + "L: { class C { constructor() { L: { break L; } } } break L; }", + None, + ), + ( + "L: { function f() { break L; } break L; }", + Some(BREAK_TARGET_CROSSES_FUNCTION.as_str()), + ), + ( + "L: { const f = () => { break L; }; break L; }", + Some(BREAK_TARGET_CROSSES_FUNCTION.as_str()), + ), + ( + "L: { class C { constructor() { break L; } } break L; }", + Some(BREAK_TARGET_CROSSES_FUNCTION.as_str()), + ), + ("const f = () => { local: {} }; local: {}", None), + ("class C { constructor() { local: {} } } local: {}", None), + ("L: {} break L;", Some(BREAK_TARGET_NOT_ENCLOSING.as_str())), + ( + "while (true) { break missing; }", + Some(BREAK_TARGET_NOT_ENCLOSING.as_str()), + ), + ]; + for (source, expected) in cases { + let codes = checker_codes(&check_text(source)) + .into_iter() + .filter(|code| { + *code == DUPLICATE_LABEL.as_str() + || *code == BREAK_TARGET_NOT_ENCLOSING.as_str() + || *code == BREAK_TARGET_CROSSES_FUNCTION.as_str() + }) + .collect::>(); + assert_eq!(codes, expected.into_iter().collect::>(), "{source}"); + } + } + + #[test] + fn pr199_missing_authority_fixture_explains_the_prerequisite() { + let panic = std::panic::catch_unwind(|| { + authority_source("__pr199_fixture_that_must_not_exist__.ts") + }) + .expect_err("a missing authority fixture must fail rather than skip"); + let message = panic.downcast_ref::().expect("formatted panic"); + assert!(message.contains("__pr199_fixture_that_must_not_exist__.ts")); + assert!(message.contains("source fetch typescript-primary-tests")); + assert!(message.contains("--dest target/authority/typescript-7.0.2-tests")); + } + #[test] fn super_call_context_matrix() { const SUPER_CODES: [&str; 4] = [ diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index 617834c..a491ca9 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -9125,8 +9125,8 @@ impl<'src> Binder<'src> { } Statement::Labeled(statement) => { let label = self.identifier_text(&statement.label).into_owned(); - // TS1114: sibling and nested redeclarations in one function - // are both duplicate labels. + // TS1114 concerns active labels, not earlier sibling statements. + let label_mark = self.label_declarations.len(); let function_labels_start = self.label_scope_marks.last().copied().unwrap_or(0); if self.label_declarations[function_labels_start..].contains(&label) { self.emit_with_message( @@ -9140,16 +9140,17 @@ impl<'src> Binder<'src> { self.label_ancestors.push((label, self.label_frame)); self.resolve_statement(&statement.body, scope); self.label_ancestors.pop(); + self.label_declarations.truncate(label_mark); } Statement::Break(jump) => { - // TS1116: a labeled break may only target an enclosing - // label. (TS1107, crossing a function boundary, needs the - // declaring function's labels and is banked.) + // Retain outer frames to distinguish TS1107 from TS1116, + // but select the nearest enclosing label first. if let Some(label_node) = &jump.label { let label = self.identifier_text(label_node); let hit = self .label_ancestors .iter() + .rev() .find(|(ancestor, _)| *ancestor == label.as_ref()); match hit { Some((_, frame)) if *frame == self.label_frame => {} @@ -9729,6 +9730,20 @@ impl<'src> Binder<'src> { self.types.generator_return_type(annotation) } + fn push_label_scope(&mut self) { + self.label_scope_marks.push(self.label_declarations.len()); + self.label_frame += 1; + } + + fn pop_label_scope(&mut self) { + let mark = self + .label_scope_marks + .pop() + .expect("label scope must be entered before it is left"); + self.label_declarations.truncate(mark); + self.label_frame -= 1; + } + fn resolve_function( &mut self, function: &'src FunctionLike, @@ -9751,8 +9766,7 @@ impl<'src> Binder<'src> { self.super_flow = SuperFlow::Suspended; let outer_guarantees = self.super_call_guarantees; self.super_call_guarantees = true; - self.label_scope_marks.push(self.label_declarations.len()); - self.label_frame += 1; + self.push_label_scope(); self.bind_implicit_function_values(&function.parameters, scope); let function_symbol = function.name.as_ref().map(|name| { let symbol_scope = if is_declaration { parent } else { scope }; @@ -9888,10 +9902,7 @@ impl<'src> Binder<'src> { let popped_home = self.super_member_homes.pop(); self.super_flow = outer_super_flow; self.super_call_guarantees = outer_guarantees; - if let Some(mark) = self.label_scope_marks.pop() { - self.label_declarations.truncate(mark); - } - self.label_frame -= 1; + self.pop_label_scope(); debug_assert_eq!(popped_home, Some(member_home)); } @@ -11907,6 +11918,7 @@ impl<'src> Binder<'src> { ); let derived = self.class_derived_stack.last().copied().unwrap_or(false); let child = self.new_scope(ScopeKind::Function, Some(scope)); + self.push_label_scope(); let new_target_marker = self.new_target_contexts.len(); self.new_target_contexts.push(true); self.bind_implicit_function_values(&constructor.parameters, child); @@ -11979,6 +11991,7 @@ impl<'src> Binder<'src> { self.new_target_contexts.truncate(new_target_marker); self.super_call_contexts.pop(); let popped_home = self.super_member_homes.pop(); + self.pop_label_scope(); debug_assert_eq!(popped_home, Some(SuperMemberHome::ClassMember { derived })); } ClassMember::Property(property) => { @@ -12105,6 +12118,7 @@ impl<'src> Binder<'src> { } Expression::Arrow(arrow) => { let child = self.new_scope(ScopeKind::Function, Some(scope)); + self.push_label_scope(); // Arrows capture `this` but never inherit super-call legality. self.super_call_contexts .push(SuperCallContext::NonConstructor); @@ -12199,6 +12213,7 @@ impl<'src> Binder<'src> { if self.node_types.insert(expression.id(), type_id).is_none() { self.typed_expressions.push((expression.range(), type_id)); } + self.pop_label_scope(); } Expression::Call(call) => { let is_super_call = matches!(call.callee.data(), Expression::Super); diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 2a0e22c..21803e4 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -6770,6 +6770,123 @@ var c = () => 1; ); } + #[test] + fn pr199_for_of_fallbacks_keep_the_rewritten_iterable() { + let source = "var target; for (target of source(2 ** 3)) {}"; + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(source).expect("fits")), + )); + assert!(parsed.diagnostics().is_empty()); + let statement = parsed.product().statements().last().expect("loop"); + let crate::syntax::Statement::ForOf(for_of) = statement.data() else { + panic!("expected a for-of AST for {source}"); + }; + assert!(matches!( + for_of.binding, + crate::syntax::ForBinding::Target(_) + )); + let output = emit_output( + parsed.product(), + &EmitOptions { + target: ScriptTarget::Es5, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ); + let code = &javascript(&output).code; + assert!( + code.contains(" of "), + "existing native fallback is retained: {code}" + ); + assert_eq!(code.matches("Math.pow(2, 3)").count(), 1, "{code}"); + assert!( + !code.contains("**"), + "original iterable was restored: {code}" + ); + } + + fn pr199_emit_at(input: &str, target: ScriptTarget) -> EmitOutput { + let parsed = crate::parser::parse(crate::scanner::scan( + SourceId::new(0), + ScriptKind::TypeScript, + Arc::new(SourceText::new(input).expect("fits")), + )); + assert!( + parsed.diagnostics().is_empty(), + "{:?}", + parsed.diagnostics() + ); + emit_output( + parsed.product(), + &EmitOptions { + target, + no_emit_helpers: true, + ..EmitOptions::default() + }, + ) + } + + #[test] + fn pr199_native_declarations_emit_keys_in_declarator_order() { + let source = "declare function before(): number; \ + declare function key(): string; \ + declare function between(): number; \ + declare function laterKey(): string; \ + declare function after(): number; \ + declare function finish(): void; \ + function* g() { \ + const left = before(), C = class { [yield key()]() {} }, \ + middle = between(), D = class { [yield laterKey()]() {} }, \ + right = after(); \ + finish(); return [left, C, middle, D, right]; \ + }"; + for target in [ScriptTarget::Es2015, ScriptTarget::Es2016] { + let output = pr199_emit_at(source, target); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let code = &javascript(&output).code; + let positions = [ + "before()", + "key()", + "C =", + "between()", + "laterKey()", + "D =", + "after()", + "finish()", + ] + .map(|needle| { + assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); + code.find(needle).expect("one occurrence") + }); + assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); + } + } + + #[test] + fn pr199_async_native_declarations_do_not_lose_key_awaits() { + let source = "declare function before(): number; \ + declare function key(): string; declare function after(): number; \ + async function g() { \ + const left = before(), C = class { [await key()]() {} }, right = after(); \ + return [left, C, right]; \ + }"; + let output = pr199_emit_at(source, ScriptTarget::Es2015); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let code = &javascript(&output).code; + let positions = ["before()", "key()", "C =", "after()"].map(|needle| { + assert_eq!(code.matches(needle).count(), 1, "{needle}: {code}"); + code.find(needle).expect("one occurrence") + }); + assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{code}"); + assert!( + code.contains("yield"), + "await must survive as a suspension: {code}" + ); + assert!(!code.contains("await key()"), "raw await leaked: {code}"); + } + #[test] fn es5_async_for_update_increment_lowers() { // Synthetic probe pinning the walker/machine contract: a for-update diff --git a/crates/bamts-compiler/src/parser.rs b/crates/bamts-compiler/src/parser.rs index 071ded5..0bef966 100644 --- a/crates/bamts-compiler/src/parser.rs +++ b/crates/bamts-compiler/src/parser.rs @@ -2156,10 +2156,16 @@ impl Parser { && self.is_constructor_name(&name) && self.at(TokenKind::LParen) { - let parameters = self.parse_parameter_list(); + let keyword_context = KeywordContext { + in_function: true, + await_reserved: false, + yield_reserved: false, + }; + let parameters = + self.with_keyword_context(keyword_context, |this| this.parse_parameter_list()); let return_type = self.parse_optional_type_annotation(); if self.at(TokenKind::LBrace) { - let body = self.parse_block(); + let body = self.with_keyword_context(keyword_context, Self::parse_block); let _ = return_type; return self.node( start, @@ -6755,6 +6761,72 @@ mod tests { ); } + #[test] + fn pr199_constructor_keyword_context_is_local() { + for (source, expected, keyword) in [ + ( + "/* 🦀 */ class C { constructor() { await value; } }", + "BAMTS-P018", + "await", + ), + ( + "async function f() { class C { constructor() { await value; } } await value; }", + "BAMTS-P018", + "await", + ), + ( + "function* f() { class C { constructor() { yield 1; } } yield 2; }", + "BAMTS-P017", + "yield", + ), + ( + "async function f() { class C { constructor(x = await value) {} } await value; }", + "BAMTS-P018", + "await", + ), + ( + "function* f() { class C { constructor(x = yield 1) {} } yield 2; }", + "BAMTS-P017", + "yield", + ), + ( + "class C { constructor(x = await value); constructor(x) {} }", + "BAMTS-P018", + "await", + ), + ] { + let parsed = parse_text(source, ScriptKind::TypeScript); + let diagnostics = errors(&parsed) + .into_iter() + .filter(|diagnostic| { + matches!(diagnostic.code().as_str(), "BAMTS-P017" | "BAMTS-P018") + }) + .collect::>(); + assert_eq!(diagnostics.len(), 1, "{source}: {diagnostics:?}"); + assert_eq!(diagnostics[0].code().as_str(), expected, "{source}"); + let byte_start = source.find(keyword).expect("keyword in fixture"); + let utf16_start = source[..byte_start].encode_utf16().count(); + assert_eq!( + diagnostics[0].range().start().get(), + utf16_start, + "{source}" + ); + } + for source in [ + "class C { constructor() { const f = async () => await value; const g = function*() { yield 1; }; } }", + "async function f() { class C { constructor() {} } await value; }", + "function* f() { class C { constructor() {} } yield 1; }", + "class C { constructor() {} } await value;", + ] { + let parsed = parse_text(source, ScriptKind::TypeScript); + assert!( + errors(&parsed).is_empty(), + "{source}: {:?}", + errors(&parsed) + ); + } + } + fn assert_clean(text: &str) -> Recovered { let recovered = parse_ts(text); let errs = errors(&recovered); From 7a662c75402791fa8a119b9d9249437e4b629854 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:51:40 +0000 Subject: [PATCH 40/42] Isolate label scopes inside class static blocks --- crates/bamts-compiler/src/checker.rs | 5 +++++ crates/bamts-compiler/src/checker/binder.rs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/crates/bamts-compiler/src/checker.rs b/crates/bamts-compiler/src/checker.rs index d4bd933..4e9f079 100644 --- a/crates/bamts-compiler/src/checker.rs +++ b/crates/bamts-compiler/src/checker.rs @@ -6611,8 +6611,13 @@ function check(options: Options = {}) { "L: { class C { constructor() { break L; } } break L; }", Some(BREAK_TARGET_CROSSES_FUNCTION.as_str()), ), + ( + "L: { class C { static { break L; } } break L; }", + Some(BREAK_TARGET_CROSSES_FUNCTION.as_str()), + ), ("const f = () => { local: {} }; local: {}", None), ("class C { constructor() { local: {} } } local: {}", None), + ("class C { static { local: {} } } local: {}", None), ("L: {} break L;", Some(BREAK_TARGET_NOT_ENCLOSING.as_str())), ( "while (true) { break missing; }", diff --git a/crates/bamts-compiler/src/checker/binder.rs b/crates/bamts-compiler/src/checker/binder.rs index a491ca9..2470780 100644 --- a/crates/bamts-compiler/src/checker/binder.rs +++ b/crates/bamts-compiler/src/checker/binder.rs @@ -12041,8 +12041,10 @@ impl<'src> Binder<'src> { let derived = self.class_derived_stack.last().copied().unwrap_or(false); self.super_member_homes .push(SuperMemberHome::ClassMember { derived }); + self.push_label_scope(); self.bind_statements(&block.data().statements, child); self.resolve_statements(&block.data().statements, child); + self.pop_label_scope(); let popped_home = self.super_member_homes.pop(); debug_assert_eq!(popped_home, Some(SuperMemberHome::ClassMember { derived })); self.new_target_contexts.truncate(new_target_marker); From 2135d4c410aaa8a081326a9dce760f29e2467e0d Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:25:25 +0000 Subject: [PATCH 41/42] Lower ES5 assignment-target for-of and order computed destructuring keys --- crates/bamts-compiler/src/emitter.rs | 17 ++- .../bamts-compiler/src/emitter/transforms.rs | 101 +++++++++++------- 2 files changed, 67 insertions(+), 51 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 21803e4..36c9a82 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -6771,8 +6771,8 @@ var c = () => 1; } #[test] - fn pr199_for_of_fallbacks_keep_the_rewritten_iterable() { - let source = "var target; for (target of source(2 ** 3)) {}"; + fn pr199_for_of_assignment_targets_lower_to_indexed_loops() { + let source = "var target; for (target of source(2 ** 3)) { use(target); }"; let parsed = crate::parser::parse(crate::scanner::scan( SourceId::new(0), ScriptKind::TypeScript, @@ -6796,15 +6796,12 @@ var c = () => 1; }, ); let code = &javascript(&output).code; - assert!( - code.contains(" of "), - "existing native fallback is retained: {code}" - ); + assert!(!code.contains(" of "), "native for-of survived ES5: {code}"); assert_eq!(code.matches("Math.pow(2, 3)").count(), 1, "{code}"); - assert!( - !code.contains("**"), - "original iterable was restored: {code}" - ); + assert!(!code.contains("**"), "{code}"); + let assign = code.find("target = ").expect("indexed assignment"); + let use_call = code.find("use(target)").expect("body"); + assert!(assign < use_call, "{code}"); } fn pr199_emit_at(input: &str, target: ScriptTarget) -> EmitOutput { diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index e6d9dc5..de57967 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -1747,20 +1747,23 @@ impl<'a> Rewriter<'a> { )]; } } - // Assignment-target bindings have no lowering here; the - // verbatim statement below keeps the shape (and any - // applicable diagnostic) instead of dropping it. - target @ ForBinding::Target(_) => { - let body = self.rewrite_single_statement(&for_of.body); - return vec![self.node( + // `target = source[counter];` - destructuring targets lower + // through the statement rewriter like any assignment. + ForBinding::Target(target) => { + let assignment = self.node( range, - Statement::ForOf(ForOfStatement { - mode: for_of.mode, - binding: target.clone(), - iterable: Box::new(iterable), - body: Box::new(body), + Expression::Assignment(AssignmentExpression { + operator: AssignmentOperator::Assign, + left: target.clone(), + right: Box::new(element), + }), + ); + self.node( + range, + Statement::Expression(ExpressionStatement { + expression: Box::new(assignment), }), - )]; + ) } }; let source_decl = self.make_declarator(source.clone(), Some(iterable), range); @@ -2391,21 +2394,18 @@ impl<'a> Rewriter<'a> { out: &mut Vec, ) -> Vec { let mut rest_keys = Vec::new(); + // Properties evaluate in source order: a computed key runs after + // the reads and defaults of the properties before it, so its temp + // is a declarator in sequence rather than a hoisted prelude. for property in &object.properties { - let PropertyName::Computed(key) = &property.name else { - continue; - }; - let temp = self.temp_ident(); - let value = self.rewrite_expr(key); - let declaration = self.make_temp_declaration(temp.clone(), value, range); - self.key_prelude.push(declaration); - rest_keys.push(RestExcludeKey::Computed(temp.clone())); - let reference = self.node(temp.range(), Expression::Identifier(temp.clone())); - let member = self.member_computed(rhs, &reference, range); - self.lower_property_binding(property, member, range, out); - } - for property in &object.properties { - if let PropertyName::Computed(_) = &property.name { + if let PropertyName::Computed(key) = &property.name { + let temp = self.temp_ident(); + let value = self.rewrite_expr(key); + out.push(self.make_declarator(temp.clone(), Some(value), range)); + rest_keys.push(RestExcludeKey::Computed(temp.clone())); + let reference = self.node(temp.range(), Expression::Identifier(temp.clone())); + let member = self.member_computed(rhs, &reference, range); + self.lower_property_binding(property, member, range, out); continue; } if let BindingPattern::Rest(rest) = property.binding.data() { @@ -10438,21 +10438,22 @@ console.log(JSON.stringify([bar, bar4, log])); let code = javascript(&output); // ES5 temps are `var` (a `let` inside cloned machine bodies is a // SyntaxError at ES5); ES2015+ keeps the tighter `let` binding. - let (kind, key_temp_at) = code - .find("var _t") - .map(|at| ("var", at)) - .or_else(|| code.find("let _t").map(|at| ("let", at))) - .expect("key temp declared"); - assert_eq!(kind, "var", "ES5 temp kind: {code}"); - let digits: String = code[key_temp_at + "var _t".len()..] + assert!(code.contains("var _t"), "ES5 temp kind: {code}"); + assert!(!code.contains("let _t"), "ES5 temp kind: {code}"); + let key_read_at = code.find(" = key").expect("key temp declared"); + let temp_name: String = code[..key_read_at] .chars() - .take_while(|character| character.is_ascii_digit()) + .rev() + .take_while(|character| character.is_ascii_alphanumeric() || *character == '_') + .collect::>() + .into_iter() + .rev() .collect(); - let temp_name = format!("_t{digits}"); - let declaration_form = format!("var {temp_name} = key;"); - assert!( - code.contains(&declaration_form), - "key evaluates once into a temp ({declaration_form:?}): {code}" + assert!(temp_name.starts_with("_t"), "key temp name: {code}"); + assert_eq!( + code.matches(" = key").count(), + 1, + "key evaluates once into a temp: {code}" ); assert!( code.contains(&format!("[{temp_name}]")), @@ -10601,14 +10602,32 @@ console.log(JSON.stringify([bar, bar4, log])); } #[test] - fn es5_for_of_fallback_keeps_rewritten_iterable() { + fn es5_for_of_assignment_target_keeps_rewritten_iterable() { let output = emit_at( "async function f() { let x; for (x of await values) {} }\n", ScriptTarget::Es5, ); let code = javascript(&output); assert!(!code.contains("await values"), "{code}"); - assert!(code.contains("yield values"), "{code}"); + assert!(!code.contains(" of "), "{code}"); + assert!(code.contains("/*yield*/, values]"), "{code}"); + assert!(code.contains("x = _t1[_t0]"), "{code}"); + } + + #[test] + fn computed_destructuring_keys_evaluate_after_earlier_properties() { + let output = emit_at( + "var { a = first(), [second()]: b, c = third() } = source();\n", + ScriptTarget::Es5, + ); + let code = javascript(&output); + let position = |needle: &str| { + code.find(needle) + .unwrap_or_else(|| panic!("{needle}: {code}")) + }; + assert!(position("source()") < position("first()"), "{code}"); + assert!(position("first()") < position("second()"), "{code}"); + assert!(position("second()") < position("third()"), "{code}"); } #[test] From 28a08152366c710b4119fe776f7ee5e7a94f7fa7 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:30:12 +0000 Subject: [PATCH 42/42] Rewrite member assignment targets in lowered ES5 for-of heads --- crates/bamts-compiler/src/emitter.rs | 7 ++- .../bamts-compiler/src/emitter/transforms.rs | 44 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/crates/bamts-compiler/src/emitter.rs b/crates/bamts-compiler/src/emitter.rs index 36c9a82..84c6843 100644 --- a/crates/bamts-compiler/src/emitter.rs +++ b/crates/bamts-compiler/src/emitter.rs @@ -6772,7 +6772,7 @@ var c = () => 1; #[test] fn pr199_for_of_assignment_targets_lower_to_indexed_loops() { - let source = "var target; for (target of source(2 ** 3)) { use(target); }"; + let source = "var target = {}; for (target[2 ** 1] of source(2 ** 3)) { use(target); }"; let parsed = crate::parser::parse(crate::scanner::scan( SourceId::new(0), ScriptKind::TypeScript, @@ -6798,8 +6798,11 @@ var c = () => 1; let code = &javascript(&output).code; assert!(!code.contains(" of "), "native for-of survived ES5: {code}"); assert_eq!(code.matches("Math.pow(2, 3)").count(), 1, "{code}"); + assert_eq!(code.matches("Math.pow(2, 1)").count(), 1, "{code}"); assert!(!code.contains("**"), "{code}"); - let assign = code.find("target = ").expect("indexed assignment"); + let assign = code + .find("target[Math.pow(2, 1)] = ") + .expect("indexed assignment"); let use_call = code.find("use(target)").expect("body"); assert!(assign < use_call, "{code}"); } diff --git a/crates/bamts-compiler/src/emitter/transforms.rs b/crates/bamts-compiler/src/emitter/transforms.rs index de57967..5abf133 100644 --- a/crates/bamts-compiler/src/emitter/transforms.rs +++ b/crates/bamts-compiler/src/emitter/transforms.rs @@ -1750,11 +1750,17 @@ impl<'a> Rewriter<'a> { // `target = source[counter];` - destructuring targets lower // through the statement rewriter like any assignment. ForBinding::Target(target) => { + let left = match target.data() { + AssignmentTarget::Member(member) => { + self.rewrite_member_target(member, target.range()) + } + _ => target.clone(), + }; let assignment = self.node( range, Expression::Assignment(AssignmentExpression { operator: AssignmentOperator::Assign, - left: target.clone(), + left, right: Box::new(element), }), ); @@ -6956,20 +6962,7 @@ impl<'a> Rewriter<'a> { // Convert awaits inside the target too, so a // suspending target becomes a visible machine refusal // instead of a live `await` in ES5 output. - let object = self.rewrite_expr(&member.object); - let property = match &member.property { - MemberProperty::Computed(key) => { - MemberProperty::Computed(Box::new(self.rewrite_expr(key))) - } - other => other.clone(), - }; - self.node( - assignment.left.range(), - AssignmentTarget::Member(AssignmentMemberTarget { - object: Box::new(object), - property, - }), - ) + self.rewrite_member_target(member, assignment.left.range()) } else { assignment.left.clone() }; @@ -6993,6 +6986,27 @@ impl<'a> Rewriter<'a> { } } + fn rewrite_member_target( + &mut self, + member: &AssignmentMemberTarget, + range: TextRange, + ) -> AssignmentTargetNode { + let object = self.rewrite_expr(&member.object); + let property = match &member.property { + MemberProperty::Computed(key) => { + MemberProperty::Computed(Box::new(self.rewrite_expr(key))) + } + other => other.clone(), + }; + self.node( + range, + AssignmentTarget::Member(AssignmentMemberTarget { + object: Box::new(object), + property, + }), + ) + } + /// `Math.pow(base, exponent)` — the ES2016 exponentiation downlevel form. fn math_pow(&mut self, base: Expr, exponent: Expr, range: TextRange) -> Expr { let math = self.ident("Math");