@holmes-lab/holmes-kit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,14 @@ exports.FUNCTION_TYPES = new Set([
12
12
  'function_definition', 'lambda',
13
13
  // @implements A-SPEC-519.1 — Go: methods and function literals (closures/goroutine bodies).
14
14
  'method_declaration', 'func_literal',
15
+ // @implements A-SPEC-520.1 — Java: constructors, lambdas and initializer blocks.
16
+ 'constructor_declaration', 'lambda_expression', 'static_initializer',
17
+ 'instance_initializer', 'compact_constructor_declaration',
18
+ // @implements A-SPEC-521.1 — C#: local functions, anonymous methods, operators, accessors.
19
+ 'local_function_statement', 'anonymous_method_expression', 'destructor_declaration',
20
+ 'operator_declaration', 'conversion_operator_declaration', 'accessor_declaration',
21
+ // @implements A-SPEC-522.1 — Rust: named functions and closures.
22
+ 'function_item', 'closure_expression',
15
23
  ]);
16
24
  const TS_SIMPLE = new Set([
17
25
  'expression_statement', 'lexical_declaration', 'variable_declaration', 'debugger_statement',
@@ -69,8 +77,102 @@ const GO_RULES = {
69
77
  ]),
70
78
  block: 'block',
71
79
  };
80
+ // @implements A-SPEC-520.1 — Java, classified from the census over gson+junit4 (8,376
81
+ // functions, 20 statement types). Java rides the TS chassis: if/while/for/do/labeled/break/
82
+ // continue/return/throw and the try-clause names are the SAME probed shapes. What is Java's own:
83
+ // one switch node carrying two styles, a try body spelled `block`, try-with-resources, and
84
+ // synchronized as a body wrapper.
85
+ const JAVA_SIMPLE = new Set([
86
+ 'expression_statement', 'local_variable_declaration', 'class_declaration',
87
+ 'record_declaration', 'assert_statement', 'empty_statement',
88
+ ]);
89
+ const JAVA_RULES = {
90
+ simple: JAVA_SIMPLE,
91
+ handled: new Set([
92
+ ...JAVA_SIMPLE,
93
+ 'block', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
94
+ 'enhanced_for_statement', 'switch_expression', 'try_statement',
95
+ 'try_with_resources_statement', 'synchronized_statement', 'labeled_statement',
96
+ 'break_statement', 'continue_statement', 'return_statement', 'throw_statement',
97
+ 'yield_statement',
98
+ ]),
99
+ block: 'block',
100
+ };
101
+ // @implements A-SPEC-521.1 — C#, classified from the census over newtonsoft+restsharp (13,642
102
+ // functions, 27 statement types). Same face as Java, opposite semantics in three places: switch
103
+ // fallthrough is ILLEGAL (sections isolate), `yield` is the iterator protocol (return passes
104
+ // through, break exits), and the preprocessor puts ALTERNATIVES in statement position. `goto`
105
+ // refuses (one corpus occurrence). Files where conditional compilation crosses syntax refuse at
106
+ // L1 (51 of 1,149 — the honest limit, recorded).
107
+ const CSHARP_SIMPLE = new Set([
108
+ 'expression_statement', 'local_declaration_statement', 'local_function_statement',
109
+ 'empty_statement', 'preproc_region', 'preproc_endregion', 'preproc_pragma',
110
+ ]);
111
+ const CSHARP_RULES = {
112
+ simple: CSHARP_SIMPLE,
113
+ handled: new Set([
114
+ ...CSHARP_SIMPLE,
115
+ 'block', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
116
+ 'foreach_statement', 'switch_statement', 'try_statement', 'using_statement',
117
+ 'lock_statement', 'checked_statement', 'preproc_if',
118
+ 'break_statement', 'continue_statement', 'return_statement', 'throw_statement',
119
+ 'yield_statement',
120
+ ]),
121
+ block: 'block',
122
+ };
123
+ // @implements A-SPEC-522.1 — Rust, where control flow is an EXPRESSION. The simple set
124
+ // enumerates the value expressions and declarations the corpus census actually put in statement
125
+ // or trailing position (36 kinds over ripgrep+serde); anything new refuses, keeping fail-closed
126
+ // meaningful. Control expressions are lowered as control wherever they stand as a statement or a
127
+ // trailing value — flattening them would produce the zero-violation HOLLOW graph this slice's
128
+ // corpus gate (branch edges > 0) exists to catch.
129
+ const RUST_SIMPLE = new Set([
130
+ 'let_declaration', 'call_expression', 'macro_invocation', 'identifier', 'self',
131
+ 'scoped_identifier', 'field_expression', 'reference_expression', 'binary_expression',
132
+ 'unary_expression', 'assignment_expression', 'compound_assignment_expr', 'struct_expression',
133
+ 'tuple_expression', 'array_expression', 'index_expression', 'await_expression',
134
+ 'type_cast_expression', 'range_expression', 'closure_expression', 'parenthesized_expression',
135
+ 'integer_literal', 'float_literal', 'string_literal', 'raw_string_literal', 'char_literal',
136
+ 'boolean_literal', 'unit_expression', 'try_expression', 'method_call_expression',
137
+ 'use_declaration', 'struct_item', 'enum_item', 'impl_item', 'function_item', 'const_item',
138
+ 'static_item', 'type_item', 'mod_item', 'trait_item', 'macro_definition', 'attribute_item',
139
+ 'inner_attribute_item', 'empty_statement', 'foreign_mod_item', 'extern_crate_declaration',
140
+ // expression_statement lands here when its inner is NOT a control expression (the unwrap
141
+ // check runs first), so a plain `g();` is one point.
142
+ 'expression_statement',
143
+ ]);
144
+ const RUST_RULES = {
145
+ simple: RUST_SIMPLE,
146
+ handled: new Set([
147
+ ...RUST_SIMPLE,
148
+ 'block', 'expression_statement', 'if_expression', 'match_expression', 'for_expression',
149
+ 'while_expression', 'loop_expression', 'return_expression', 'break_expression',
150
+ 'continue_expression',
151
+ ]),
152
+ block: 'block',
153
+ };
154
+ // @implements A-SPEC-523.1 — C++, classified from three corpora. Its first fact is the PARSER:
155
+ // 25-34% of template/macro-heavy real files refuse at L1 (measured on nlohmann/fmt/leveldb) —
156
+ // the matrix's ast basis discloses this. What parses rides the TS chassis (else_clause wrapper,
157
+ // C fallthrough switch, same try-clause names). Labels and goto refuse (zero corpus occurrences).
158
+ const CPP_SIMPLE = new Set([
159
+ 'expression_statement', 'declaration', 'alias_declaration', 'static_assert_declaration',
160
+ 'enum_specifier', 'using_declaration', 'type_definition', 'struct_specifier',
161
+ 'preproc_call', 'preproc_def', 'preproc_function_def', 'preproc_include',
162
+ ]);
163
+ const CPP_RULES = {
164
+ simple: CPP_SIMPLE,
165
+ handled: new Set([
166
+ ...CPP_SIMPLE,
167
+ 'compound_statement', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
168
+ 'for_range_loop', 'switch_statement', 'try_statement', 'throw_statement',
169
+ 'break_statement', 'continue_statement', 'return_statement', 'preproc_if', 'preproc_ifdef',
170
+ ]),
171
+ block: 'compound_statement',
172
+ };
72
173
  const LANG_RULES = {
73
- typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES,
174
+ typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES, java: JAVA_RULES,
175
+ csharp: CSHARP_RULES, rust: RUST_RULES, cpp: CPP_RULES,
74
176
  };
75
177
  // @implements A-SPEC-510.6
76
178
  /**
@@ -115,6 +217,7 @@ function cfgOf(ast, fn, source) {
115
217
  const rules = LANG_RULES[ast.lang];
116
218
  const isPython = ast.lang === 'python';
117
219
  const isGo = ast.lang === 'go';
220
+ const isRust = ast.lang === 'rust';
118
221
  const kids = childrenIndex(ast);
119
222
  // Points: one per lowered statement (block ids after coalescing). 0=ENTRY, 1=EXIT.
120
223
  const stmtsOf = [[], []];
@@ -155,7 +258,9 @@ function cfgOf(ast, fn, source) {
155
258
  }
156
259
  return { entry, exits };
157
260
  };
158
- const stmtChildren = (i) => kids.of(i).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'comment');
261
+ const stmtChildren = (i) =>
262
+ // `comment` (TS/PY/Go) and Java's line_comment/block_comment are all named nodes.
263
+ kids.of(i).filter((k) => ast.nodes[k].named && !ast.nodes[k].type.includes('comment'));
159
264
  const wireException = (first, targetProvider) => {
160
265
  // Conservative: every point created inside [first, now) can throw into the handler.
161
266
  const target = targetProvider();
@@ -168,14 +273,54 @@ function cfgOf(ast, fn, source) {
168
273
  throw new Unsupported(t);
169
274
  const label = ctx.pendingLabel;
170
275
  ctx.pendingLabel = null;
276
+ // @implements A-SPEC-522.1 — Rust control lives in expressions. A statement whose sole
277
+ // named child is a control expression lowers as that control (flattening it makes the
278
+ // hollow graph); a `?` anywhere in a point's subtree adds the implicit early-return edge;
279
+ // a let-else is a branch whose false arm is the (compiler-enforced) diverging else block.
280
+ const RUST_CONTROL = new Set(['if_expression', 'match_expression', 'for_expression', 'while_expression', 'loop_expression', 'return_expression', 'break_expression', 'continue_expression']);
281
+ if (isRust && t === 'expression_statement') {
282
+ const inner = stmtChildren(i);
283
+ if (inner.length === 1 && RUST_CONTROL.has(ast.nodes[inner[0]].type))
284
+ return lowerStmt(inner[0], ctx);
285
+ }
286
+ const rustQuestion = (p, root) => {
287
+ if (!isRust)
288
+ return;
289
+ const stack = [root];
290
+ while (stack.length) {
291
+ const n = stack.pop();
292
+ if (n !== root && exports.FUNCTION_TYPES.has(ast.nodes[n].type))
293
+ continue; // closures own their `?`
294
+ if (ast.nodes[n].type === 'try_expression') {
295
+ link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
296
+ return;
297
+ }
298
+ for (const c of kids.of(n))
299
+ stack.push(c);
300
+ }
301
+ };
302
+ if (isRust && t === 'let_declaration') {
303
+ const elseBlock = kids.of(i).some((k) => !ast.nodes[k].named && source !== undefined
304
+ && source.slice(ast.nodes[k].start, ast.nodes[k].end) === 'else')
305
+ ? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(-1) : undefined;
306
+ if (elseBlock !== undefined) {
307
+ const p = newPoint(i);
308
+ rustQuestion(p, i);
309
+ const ef = lowerStmt(elseBlock, ctx);
310
+ link(p, ef.entry, 'false');
311
+ return { entry: p, exits: [{ from: p, kind: 'true' }, ...ef.exits] };
312
+ }
313
+ }
171
314
  if (rules.simple.has(t)) {
172
315
  const p = newPoint(i);
316
+ rustQuestion(p, i);
173
317
  return { entry: p, exits: [{ from: p, kind: 'seq' }] };
174
318
  }
175
319
  switch (t) {
176
320
  case 'statement_block':
177
321
  case 'block':
178
322
  case 'statement_list':
323
+ case 'compound_statement':
179
324
  // Go's block carries one extra statement_list layer (probed shape) — both are containers.
180
325
  return lowerSeq(stmtChildren(i), ctx);
181
326
  case 'labeled_statement': {
@@ -209,9 +354,29 @@ function cfgOf(ast, fn, source) {
209
354
  : stmtChildren(clause).find((k) => ast.nodes[k].type === 'block');
210
355
  }
211
356
  }
357
+ else if (ast.lang === 'java' || ast.lang === 'csharp') {
358
+ // @implements A-SPEC-520.1 / A-SPEC-521.1 — Java and C# have NO else_clause
359
+ // wrapper: named[2] IS the
360
+ // alternative (a block, or the next if_statement of an else-if chain). Unwrapping it
361
+ // like TS lowered an else-if's CONDITION as a statement — the corpus caught it.
362
+ alt = named[2];
363
+ }
212
364
  else {
213
365
  alt = named[2] !== undefined ? stmtChildren(named[2])[0] : undefined;
214
366
  }
367
+ if (cons === undefined || !rules.handled.has(ast.nodes[cons].type)) {
368
+ // `if (c()) ;` — an empty then-arm: both edges leave the header directly.
369
+ const exits0 = [{ from: p, kind: 'true' }];
370
+ if (alt !== undefined && rules.handled.has(ast.nodes[alt].type)) {
371
+ const af0 = lowerStmt(alt, ctx);
372
+ link(p, af0.entry, 'false');
373
+ exits0.push(...af0.exits);
374
+ }
375
+ else {
376
+ exits0.push({ from: p, kind: 'false' });
377
+ }
378
+ return { entry: p, exits: exits0 };
379
+ }
215
380
  const cf = lowerStmt(cons, ctx);
216
381
  link(p, cf.entry, 'true');
217
382
  const exits = [...cf.exits];
@@ -240,6 +405,17 @@ function cfgOf(ast, fn, source) {
240
405
  const bodyIdx = (isPython || isGo)
241
406
  ? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(0)
242
407
  : stmtChildren(i).at(-1);
408
+ // Found by the Java corpus, latent for TS too: `for (; ; ) ;` has NO named children —
409
+ // empty clauses and an empty-statement body. The loop is a bare spin: header with a
410
+ // self loop-back and only the (over-approximate) exhaustion exit.
411
+ if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
412
+ // Latent for TS, found by the Java corpus: an EMPTY body (`while (c()) ;`,
413
+ // `for (; ; ) ;`) leaves no statement child, so `.at(-1)` lands on the condition
414
+ // itself. The loop is a bare spin: header with a self loop-back.
415
+ const header = newPoint(i);
416
+ link(header, header, 'loop-back');
417
+ return { entry: header, exits: [{ from: header, kind: 'false' }] };
418
+ }
243
419
  // @implements A-SPEC-519.1 — a bare Go `for {}` (no named non-block child) is INFINITE:
244
420
  // no exhaustion edge. `for { select {…} }` is the daemon idiom, and a phantom false edge
245
421
  // would hang spurious control dependence on everything after the loop. A for_clause with
@@ -273,6 +449,10 @@ function cfgOf(ast, fn, source) {
273
449
  case 'do_statement': {
274
450
  const header = newPoint(i); // condition point (the `do` node)
275
451
  const bodyIdx = stmtChildren(i)[0];
452
+ if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
453
+ link(header, header, 'loop-back');
454
+ return { entry: header, exits: [{ from: header, kind: 'false' }] };
455
+ }
276
456
  const myBreak = { label, target: -1 };
277
457
  ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
278
458
  ctx.contT.push({ label, target: header }, { label: null, target: header });
@@ -368,6 +548,238 @@ function cfgOf(ast, fn, source) {
368
548
  exits.push({ from: b.point, kind: 'break' });
369
549
  return { entry: p, exits };
370
550
  }
551
+ // @implements A-SPEC-520.1 — Java's enhanced for: the header owns the binding; body
552
+ // loops back; exhaustion exits false — same skeleton as Go's range.
553
+ case 'enhanced_for_statement':
554
+ case 'foreach_statement':
555
+ case 'for_range_loop': {
556
+ const header = newPoint(i);
557
+ const bodyIdx = kids.of(i).find((k) => ast.nodes[k].type === rules.block)
558
+ ?? stmtChildren(i).at(-1);
559
+ const myBreak = { label, target: -1 };
560
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
561
+ ctx.contT.push({ label, target: header }, { label: null, target: header });
562
+ const bf = lowerStmt(bodyIdx, ctx);
563
+ ctx.breakT.splice(-2);
564
+ ctx.contT.splice(-2);
565
+ link(header, bf.entry, 'true');
566
+ for (const e of bf.exits)
567
+ link(e.from, header, 'loop-back');
568
+ const exits = [{ from: header, kind: 'false' }];
569
+ for (const b of claimBreaks(myBreak))
570
+ exits.push({ from: b.point, kind: 'break' });
571
+ return { entry: header, exits };
572
+ }
573
+ // @implements A-SPEC-520.1 — synchronized: a body wrapper for control flow (the lock is
574
+ // a use the DDG reads off the header point); lock semantics are declared out of scope.
575
+ case 'synchronized_statement':
576
+ case 'lock_statement':
577
+ case 'checked_statement': {
578
+ const p = newPoint(i);
579
+ const body = kids.of(i).find((k) => ast.nodes[k].type === 'block');
580
+ if (body === undefined)
581
+ throw new Unsupported(t + ' without a block');
582
+ const bf = lowerStmt(body, ctx);
583
+ link(p, bf.entry, 'seq');
584
+ return { entry: p, exits: bf.exits };
585
+ }
586
+ // @implements A-SPEC-521.1 — the preprocessor puts ALTERNATIVES in statement position.
587
+ // Sequencing the arms would fabricate flow no build configuration ever runs; each arm is
588
+ // a branch and both rejoin after #endif (a may-analysis over every configuration).
589
+ case 'preproc_if':
590
+ case 'preproc_ifdef': {
591
+ const p = newPoint(i);
592
+ const named = stmtChildren(i);
593
+ const elseArm = named.find((k) => ['preproc_else', 'preproc_elif'].includes(ast.nodes[k].type));
594
+ const ifStmts = named.filter((k) => rules.handled.has(ast.nodes[k].type));
595
+ const exits = [];
596
+ const f1 = lowerSeq(ifStmts, ctx);
597
+ link(p, f1.entry, 'true');
598
+ exits.push(...f1.exits);
599
+ if (elseArm !== undefined) {
600
+ if (ast.nodes[elseArm].type === 'preproc_elif')
601
+ throw new Unsupported('preproc_elif');
602
+ const elseStmts = stmtChildren(elseArm).filter((k) => rules.handled.has(ast.nodes[k].type));
603
+ const f2 = lowerSeq(elseStmts, ctx);
604
+ link(p, f2.entry, 'false');
605
+ exits.push(...f2.exits);
606
+ }
607
+ else {
608
+ exits.push({ from: p, kind: 'false' });
609
+ }
610
+ return { entry: p, exits };
611
+ }
612
+ // @implements A-SPEC-521.1 — using: a wrapper whose header owns the resource defs
613
+ // (variable_declarator does the DDG work); the implicit Dispose is covered by the
614
+ // enclosing try wiring, same posture as Java's try-with-resources.
615
+ case 'using_statement': {
616
+ const p = newPoint(i);
617
+ const body = kids.of(i).find((k) => ast.nodes[k].type === 'block')
618
+ ?? stmtChildren(i).filter((k) => rules.handled.has(ast.nodes[k].type)).at(-1);
619
+ if (body === undefined)
620
+ return { entry: p, exits: [{ from: p, kind: 'seq' }] };
621
+ const bf = lowerStmt(body, ctx);
622
+ link(p, bf.entry, 'seq');
623
+ return { entry: p, exits: bf.exits };
624
+ }
625
+ // @implements A-SPEC-520.1 — ONE node, TWO styles. Old-style groups fall through into
626
+ // each other (TS switch semantics); arrow rules never touch (Go case semantics). A
627
+ // `yield` leaves the switch like a break-with-value. Statement position only — an
628
+ // expression-position switch stays inside its statement's point, like a ternary.
629
+ case 'switch_expression': {
630
+ const p = newPoint(i);
631
+ const myBreak = { label, target: -1 };
632
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
633
+ ctx.yieldT.push(myBreak);
634
+ const swBlock = kids.of(i).find((k) => ast.nodes[k].type === 'switch_block');
635
+ const members = swBlock === undefined ? [] : kids.of(swBlock).filter((k) => ['switch_block_statement_group', 'switch_rule'].includes(ast.nodes[k].type));
636
+ let sawDefault = false;
637
+ let prevFall = [];
638
+ const exits = [];
639
+ for (const m of members) {
640
+ const labelNode = kids.of(m).find((k) => ast.nodes[k].type === 'switch_label');
641
+ if (labelNode !== undefined && source !== undefined
642
+ && source.slice(ast.nodes[labelNode].start, ast.nodes[labelNode].end).trim().startsWith('default'))
643
+ sawDefault = true;
644
+ const stmts = stmtChildren(m).filter((k) => ast.nodes[k].type !== 'switch_label');
645
+ const cf = lowerSeq(stmts, ctx);
646
+ link(p, cf.entry, 'case');
647
+ if (ast.nodes[m].type === 'switch_block_statement_group') {
648
+ for (const e of prevFall)
649
+ link(e.from, cf.entry, 'seq'); // implicit fallthrough
650
+ prevFall = cf.exits;
651
+ }
652
+ else {
653
+ exits.push(...cf.exits); // arrow rule: isolated
654
+ }
655
+ }
656
+ exits.push(...prevFall);
657
+ if (!sawDefault)
658
+ exits.push({ from: p, kind: 'default' });
659
+ ctx.breakT.splice(-2);
660
+ ctx.yieldT.pop();
661
+ for (const b of claimBreaks(myBreak))
662
+ exits.push({ from: b.point, kind: 'break' });
663
+ return { entry: p, exits };
664
+ }
665
+ case 'yield_statement': {
666
+ const p = newPoint(i);
667
+ // @implements A-SPEC-521.1 — the SAME node name, two protocols. Java: a value-carrying
668
+ // switch escape. C#: the iterator protocol — `yield return v` (has a named child)
669
+ // CONTINUES executing; `yield break` (bare) leaves the function.
670
+ if (ast.lang === 'csharp') {
671
+ if (stmtChildren(i).length > 0)
672
+ return { entry: p, exits: [{ from: p, kind: 'seq' }] };
673
+ link(p, EXIT, 'return');
674
+ return { entry: p, exits: [] };
675
+ }
676
+ const owner = ctx.yieldT.at(-1);
677
+ if (owner === undefined)
678
+ throw new Unsupported('yield outside switch');
679
+ pendingBreaks.push({ point: p, owner });
680
+ return { entry: p, exits: [] };
681
+ }
682
+ // @implements A-SPEC-522.1 — Rust's if EXPRESSION: [cond, block, else_clause?[block|if]].
683
+ case 'if_expression': {
684
+ const p = newPoint(i);
685
+ rustQuestion(p, i === undefined ? i : kids.of(i)[0] ?? i);
686
+ const consR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
687
+ const elseClause = kids.of(i).find((k) => ast.nodes[k].type === 'else_clause');
688
+ const altR = elseClause === undefined ? undefined
689
+ : kids.of(elseClause).find((k) => ['block', 'if_expression'].includes(ast.nodes[k].type));
690
+ const cf = lowerStmt(consR, ctx);
691
+ link(p, cf.entry, 'true');
692
+ const exits = [...cf.exits];
693
+ if (altR !== undefined) {
694
+ const af = lowerStmt(altR, ctx);
695
+ link(p, af.entry, 'false');
696
+ exits.push(...af.exits);
697
+ }
698
+ else {
699
+ exits.push({ from: p, kind: 'false' });
700
+ }
701
+ return { entry: p, exits };
702
+ }
703
+ // @implements A-SPEC-522.1 — match: arms isolated, EXHAUSTIVE (rustc enforces it, so a
704
+ // "no arm matched" escape would be flow no execution takes — the opposite of Python's
705
+ // match, deliberately). Arm values may be blocks or bare expressions.
706
+ case 'match_expression': {
707
+ const p = newPoint(i);
708
+ const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
709
+ const exits = [];
710
+ for (const arm of mb === undefined ? [] : kids.of(mb)) {
711
+ if (ast.nodes[arm].type !== 'match_arm')
712
+ continue;
713
+ const val = kids.of(arm).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'match_pattern').at(-1);
714
+ if (val === undefined)
715
+ continue;
716
+ const vf = rules.handled.has(ast.nodes[val].type)
717
+ ? lowerStmt(val, ctx)
718
+ : (() => { const vp = newPoint(val); rustQuestion(vp, val); return { entry: vp, exits: [{ from: vp, kind: 'seq' }] }; })();
719
+ link(p, vf.entry, 'case');
720
+ exits.push(...vf.exits);
721
+ }
722
+ return { entry: p, exits };
723
+ }
724
+ // @implements A-SPEC-522.1 — for/while/loop expressions ride the loop skeleton; a bare
725
+ // `loop` is INFINITE (no exhaustion edge — Go's bare-for call, same reasoning).
726
+ case 'for_expression':
727
+ case 'while_expression':
728
+ case 'loop_expression': {
729
+ const header = newPoint(i);
730
+ rustQuestion(header, i);
731
+ const bodyR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
732
+ const myBreak = { label, target: -1 };
733
+ const loopLabel = (() => {
734
+ const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
735
+ return ln !== undefined && source !== undefined
736
+ ? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : label;
737
+ })();
738
+ const owner = { label: loopLabel, target: -1 };
739
+ ctx.breakT.push({ label: loopLabel, owner }, { label: null, owner });
740
+ ctx.contT.push({ label: loopLabel, target: header }, { label: null, target: header });
741
+ const bf = lowerStmt(bodyR, ctx);
742
+ ctx.breakT.splice(-2);
743
+ ctx.contT.splice(-2);
744
+ link(header, bf.entry, 'true');
745
+ for (const e of bf.exits)
746
+ link(e.from, header, 'loop-back');
747
+ const exits = [];
748
+ if (t !== 'loop_expression')
749
+ exits.push({ from: header, kind: 'false' });
750
+ for (const b of claimBreaks(owner))
751
+ exits.push({ from: b.point, kind: 'break' });
752
+ void myBreak;
753
+ return { entry: header, exits };
754
+ }
755
+ case 'return_expression': {
756
+ const p = newPoint(i);
757
+ rustQuestion(p, i);
758
+ link(p, EXIT, 'return');
759
+ return { entry: p, exits: [] };
760
+ }
761
+ case 'break_expression': {
762
+ const p = newPoint(i);
763
+ const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
764
+ const lbl = ln !== undefined && source !== undefined
765
+ ? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
766
+ const bentry = nearestBreak(ctx.breakT, lbl);
767
+ if (!bentry)
768
+ throw new Unsupported('break outside breakable');
769
+ pendingBreaks.push({ point: p, owner: bentry.owner });
770
+ return { entry: p, exits: [] };
771
+ }
772
+ case 'continue_expression': {
773
+ const p = newPoint(i);
774
+ const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
775
+ const lbl = ln !== undefined && source !== undefined
776
+ ? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
777
+ const centry = nearestCont(ctx.contT, lbl);
778
+ if (!centry || centry.target < 0)
779
+ throw new Unsupported('continue outside loop');
780
+ link(p, centry.target, 'continue');
781
+ return { entry: p, exits: [] };
782
+ }
371
783
  // @implements A-SPEC-519.1 — select: each communication case is a branch off the header;
372
784
  // WITHOUT a default the statement BLOCKS until some case fires, so there is no "no arm
373
785
  // matched" escape (the opposite of Python's match).
@@ -420,6 +832,67 @@ function cfgOf(ast, fn, source) {
420
832
  return { entry: p, exits: [] };
421
833
  }
422
834
  case 'switch_statement': {
835
+ // @implements A-SPEC-521.1 — C# shares the node NAME with TS and inverts the
836
+ // semantics: implicit fallthrough is a compile error, so sections are ISOLATED.
837
+ // (The suite caught the first attempt — a duplicate case label silently shadowed the
838
+ // TS lowering and broke its fallthrough pin.)
839
+ if (ast.lang === 'csharp') {
840
+ const p = newPoint(i);
841
+ const myBreak = { label, target: -1 };
842
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
843
+ const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'switch_body');
844
+ let sawDefault = false;
845
+ const exits = [];
846
+ for (const sec of swBody === undefined ? [] : kids.of(swBody)) {
847
+ if (ast.nodes[sec].type !== 'switch_section')
848
+ continue;
849
+ const children = stmtChildren(sec);
850
+ const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
851
+ const hasPattern = children.some((k) => ast.nodes[k].type.includes('pattern'));
852
+ if (!hasPattern)
853
+ sawDefault = true; // a default section carries no pattern
854
+ const cf = lowerSeq(stmts, ctx);
855
+ link(p, cf.entry, hasPattern ? 'case' : 'default');
856
+ exits.push(...cf.exits); // isolated: nothing flows to a sibling
857
+ }
858
+ if (!sawDefault)
859
+ exits.push({ from: p, kind: 'default' });
860
+ ctx.breakT.splice(-2);
861
+ for (const b of claimBreaks(myBreak))
862
+ exits.push({ from: b.point, kind: 'break' });
863
+ return { entry: p, exits };
864
+ }
865
+ // @implements A-SPEC-523.1 — C++: flat case_statement children inside a
866
+ // compound_statement, with C's implicit fallthrough (a value-less case is default).
867
+ if (ast.lang === 'cpp') {
868
+ const p = newPoint(i);
869
+ const myBreak = { label, target: -1 };
870
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
871
+ const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'compound_statement');
872
+ let sawDefault = false;
873
+ let prevFall = [];
874
+ const exits = [];
875
+ for (const c of swBody === undefined ? [] : kids.of(swBody)) {
876
+ if (ast.nodes[c].type !== 'case_statement')
877
+ continue;
878
+ const children = stmtChildren(c);
879
+ const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
880
+ if (stmts.length === children.length)
881
+ sawDefault = true; // no label value → default
882
+ const cf = lowerSeq(stmts, ctx);
883
+ link(p, cf.entry, stmts.length === children.length ? 'default' : 'case');
884
+ for (const e of prevFall)
885
+ link(e.from, cf.entry, 'seq'); // C fallthrough
886
+ prevFall = cf.exits;
887
+ }
888
+ exits.push(...prevFall);
889
+ if (!sawDefault)
890
+ exits.push({ from: p, kind: 'default' });
891
+ ctx.breakT.splice(-2);
892
+ for (const b of claimBreaks(myBreak))
893
+ exits.push({ from: b.point, kind: 'break' });
894
+ return { entry: p, exits };
895
+ }
423
896
  const p = newPoint(i);
424
897
  const body = stmtChildren(i).at(-1); // switch_body
425
898
  const myBreak = { label, target: -1 };
@@ -478,13 +951,17 @@ function cfgOf(ast, fn, source) {
478
951
  link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
479
952
  return { entry: p, exits: [] };
480
953
  }
954
+ // @implements A-SPEC-520.1 — try-with-resources joins the try case: same clause wiring,
955
+ // plus a point for the resource_specification so its defs (`var r = open()`) have a home.
956
+ case 'try_with_resources_statement':
481
957
  case 'try_statement': {
482
958
  const named = stmtChildren(i);
483
959
  // @implements A-SPEC-510.7 — Python spells the same shape with `block` / `except_clause`
484
960
  // / `finally_clause`, PLUS an `else_clause` that runs ONLY when no exception was raised.
961
+ // @implements A-SPEC-520.1 — Java spells the try body `block`; TS `statement_block`.
485
962
  const body = isPython
486
963
  ? kids.of(i).find((k) => ast.nodes[k].type === 'block')
487
- : named.find((k) => ast.nodes[k].type === 'statement_block');
964
+ : named.find((k) => ['statement_block', 'block', 'compound_statement'].includes(ast.nodes[k].type));
488
965
  const handler = isPython
489
966
  ? kids.of(i).find((k) => ast.nodes[k].type === 'except_clause')
490
967
  : named.find((k) => ast.nodes[k].type === 'catch_clause');
@@ -538,6 +1015,12 @@ function cfgOf(ast, fn, source) {
538
1015
  else {
539
1016
  exits.push(...after);
540
1017
  }
1018
+ const resSpec = kids.of(i).find((k) => ast.nodes[k].type === 'resource_specification');
1019
+ if (resSpec !== undefined) {
1020
+ const rp = newPoint(resSpec);
1021
+ link(rp, bf.entry, 'seq');
1022
+ return { entry: rp, exits };
1023
+ }
541
1024
  return { entry: bf.entry, exits };
542
1025
  }
543
1026
  default:
@@ -569,7 +1052,7 @@ function cfgOf(ast, fn, source) {
569
1052
  }
570
1053
  return undefined;
571
1054
  };
572
- const ctx = { breakT: [], contT: [], catchT: [], pendingLabel: null };
1055
+ const ctx = { breakT: [], contT: [], catchT: [], yieldT: [], pendingLabel: null };
573
1056
  const bodyNode = ast.nodes[fn.bodyIndex];
574
1057
  const flow = bodyNode.type === rules.block
575
1058
  ? lowerSeq(stmtChildren(fn.bodyIndex), ctx)