@@ -139,19 +139,41 @@ function hasCatchFallback(w: Wire): boolean {
139139 ) ;
140140}
141141
142+ type DetectedControlFlow = {
143+ kind : "break" | "continue" | "throw" | "panic" ;
144+ levels : number ;
145+ } ;
146+
142147/** Check if any wire in a set has a control flow instruction (break/continue/throw/panic). */
143- function detectControlFlow (
144- wires : Wire [ ] ,
145- ) : "break" | "continue" | "throw" | "panic" | null {
148+ function detectControlFlow ( wires : Wire [ ] ) : DetectedControlFlow | null {
146149 for ( const w of wires ) {
147150 if ( "fallbacks" in w && w . fallbacks ) {
148151 for ( const fb of w . fallbacks ) {
149- if ( fb . control )
150- return fb . control . kind as "break" | "continue" | "throw" | "panic" ;
152+ if ( fb . control ) {
153+ const kind = fb . control . kind as
154+ | "break"
155+ | "continue"
156+ | "throw"
157+ | "panic" ;
158+ const levels =
159+ kind === "break" || kind === "continue"
160+ ? Math . max ( 1 , Number ( ( fb . control as any ) . levels ) || 1 )
161+ : 1 ;
162+ return { kind, levels } ;
163+ }
151164 }
152165 }
153166 if ( "catchControl" in w && w . catchControl ) {
154- return w . catchControl . kind as "break" | "continue" | "throw" | "panic" ;
167+ const kind = w . catchControl . kind as
168+ | "break"
169+ | "continue"
170+ | "throw"
171+ | "panic" ;
172+ const levels =
173+ kind === "break" || kind === "continue"
174+ ? Math . max ( 1 , Number ( ( w . catchControl as any ) . levels ) || 1 )
175+ : 1 ;
176+ return { kind, levels } ;
155177 }
156178 }
157179 return null ;
@@ -648,6 +670,12 @@ class CodegenContext {
648670 ` const __ctx = { logger: __opts?.logger ?? {}, signal: __signal };` ,
649671 ) ;
650672 lines . push ( ` const __trace = __opts?.__trace;` ) ;
673+ lines . push (
674+ ` const __isLoopCtrl = (v) => (v?.__bridgeControl === "break" || v?.__bridgeControl === "continue") && Number.isInteger(v?.levels) && v.levels > 0;` ,
675+ ) ;
676+ lines . push (
677+ ` const __nextLoopCtrl = (v) => ({ __bridgeControl: v.__bridgeControl, levels: v.levels - 1 });` ,
678+ ) ;
651679 lines . push ( ` async function __call(fn, input, toolName) {` ) ;
652680 lines . push ( ` if (__signal?.aborted) throw new __BridgeAbortError();` ) ;
653681 lines . push ( ` const start = __trace ? performance.now() : 0;` ) ;
@@ -1386,6 +1414,8 @@ class CodegenContext {
13861414 // Only check control flow on direct element wires, not sub-array element wires
13871415 const directElemWires = elemWires . filter ( ( w ) => w . to . path . length === 1 ) ;
13881416 const cf = detectControlFlow ( directElemWires ) ;
1417+ const anyCf = detectControlFlow ( elemWires ) ;
1418+ const requiresLabeledLoop = ! cf && ! ! anyCf && anyCf . levels > 1 ;
13891419 // Check if any element wire generates `await` (element-scoped tools or catch fallbacks)
13901420 const needsAsync = elemWires . some ( ( w ) => this . wireNeedsAwait ( w ) ) ;
13911421
@@ -1401,20 +1431,27 @@ class CodegenContext {
14011431 arrayIterators ,
14021432 0 ,
14031433 4 ,
1404- cf === "continue" ? "for-continue" : "break" ,
1434+ cf . kind === "continue" ? "for-continue" : "break" ,
14051435 )
14061436 : ` _result.push(${ this . buildElementBody ( elemWires , arrayIterators , 0 , 4 ) } );` ;
14071437
14081438 lines . push ( ` const _result = [];` ) ;
1409- lines . push ( ` for (const _el0 of (${ arrayExpr } ?? [])) {` ) ;
1439+ lines . push ( ` __loop0: for (const _el0 of (${ arrayExpr } ?? [])) {` ) ;
1440+ lines . push ( ` try {` ) ;
14101441 for ( const pl of preambleLines ) {
1411- lines . push ( ` ${ pl } ` ) ;
1442+ lines . push ( ` ${ pl } ` ) ;
14121443 }
1413- lines . push ( body ) ;
1444+ lines . push ( ` ${ body . trimStart ( ) } ` ) ;
1445+ lines . push ( ` } catch (_ctrl) {` ) ;
1446+ lines . push (
1447+ ` if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; }` ,
1448+ ) ;
1449+ lines . push ( ` throw _ctrl;` ) ;
1450+ lines . push ( ` }` ) ;
14141451 lines . push ( ` }` ) ;
14151452 lines . push ( ` return _result;` ) ;
14161453 this . elementLocalVars . clear ( ) ;
1417- } else if ( cf === "continue" ) {
1454+ } else if ( cf ?. kind === "continue" && cf . levels === 1 ) {
14181455 // Use flatMap — skip elements that trigger continue (sync only)
14191456 const body = this . buildElementBodyWithControlFlow (
14201457 elemWires ,
@@ -1426,18 +1463,35 @@ class CodegenContext {
14261463 lines . push ( ` return (${ arrayExpr } ?? []).flatMap((_el0) => {` ) ;
14271464 lines . push ( body ) ;
14281465 lines . push ( ` });` ) ;
1429- } else if ( cf === "break" ) {
1466+ } else if (
1467+ cf ?. kind === "break" ||
1468+ cf ?. kind === "continue" ||
1469+ requiresLabeledLoop
1470+ ) {
1471+ // Use an explicit loop for:
1472+ // - direct break/continue control
1473+ // - nested multilevel control (e.g. break 2 / continue 2) that must
1474+ // escape from sub-array IIFEs through throw/catch propagation.
14301475 // Use a loop with early break (sync)
1431- const body = this . buildElementBodyWithControlFlow (
1432- elemWires ,
1433- arrayIterators ,
1434- 0 ,
1435- 4 ,
1436- "break" ,
1437- ) ;
1476+ const body = cf
1477+ ? this . buildElementBodyWithControlFlow (
1478+ elemWires ,
1479+ arrayIterators ,
1480+ 0 ,
1481+ 4 ,
1482+ cf . kind === "continue" ? "for-continue" : "break" ,
1483+ )
1484+ : ` _result.push(${ this . buildElementBody ( elemWires , arrayIterators , 0 , 4 ) } );` ;
14381485 lines . push ( ` const _result = [];` ) ;
1439- lines . push ( ` for (const _el0 of (${ arrayExpr } ?? [])) {` ) ;
1440- lines . push ( body ) ;
1486+ lines . push ( ` __loop0: for (const _el0 of (${ arrayExpr } ?? [])) {` ) ;
1487+ lines . push ( ` try {` ) ;
1488+ lines . push ( ` ${ body . trimStart ( ) } ` ) ;
1489+ lines . push ( ` } catch (_ctrl) {` ) ;
1490+ lines . push (
1491+ ` if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; }` ,
1492+ ) ;
1493+ lines . push ( ` throw _ctrl;` ) ;
1494+ lines . push ( ` }` ) ;
14411495 lines . push ( ` }` ) ;
14421496 lines . push ( ` return _result;` ) ;
14431497 } else {
@@ -1560,6 +1614,8 @@ class CodegenContext {
15601614 // Only check control flow on direct element wires (not sub-array element wires)
15611615 const directShifted = shifted . filter ( ( w ) => w . to . path . length === 1 ) ;
15621616 const cf = detectControlFlow ( directShifted ) ;
1617+ const anyCf = detectControlFlow ( shifted ) ;
1618+ const requiresLabeledLoop = ! cf && ! ! anyCf && anyCf . levels > 1 ;
15631619 // Check if any element wire generates `await` (element-scoped tools or catch fallbacks)
15641620 const needsAsync = shifted . some ( ( w ) => this . wireNeedsAwait ( w ) ) ;
15651621 let mapExpr : string ;
@@ -1575,14 +1631,14 @@ class CodegenContext {
15751631 arrayIterators ,
15761632 0 ,
15771633 8 ,
1578- cf === "continue" ? "for-continue" : "break" ,
1634+ cf . kind === "continue" ? "for-continue" : "break" ,
15791635 )
15801636 : ` _result.push(${ this . buildElementBody ( shifted , arrayIterators , 0 , 8 ) } );` ;
15811637
15821638 const preamble = preambleLines . map ( ( l ) => ` ${ l } ` ) . join ( "\n" ) ;
1583- mapExpr = `await (async () => { const _src = ${ arrayExpr } ; if (_src == null) return null; const _result = []; for (const _el0 of _src) {\n${ preamble } \n${ asyncBody } \n } return _result; })()` ;
1639+ mapExpr = `await (async () => { const _src = ${ arrayExpr } ; if (_src == null) return null; const _result = []; __loop0: for (const _el0 of _src) {\n try {\n ${ preamble } \n${ asyncBody } \n } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n } return _result; })()` ;
15841640 this . elementLocalVars . clear ( ) ;
1585- } else if ( cf === "continue" ) {
1641+ } else if ( cf ?. kind === "continue" && cf . levels === 1 ) {
15861642 const cfBody = this . buildElementBodyWithControlFlow (
15871643 shifted ,
15881644 arrayIterators ,
@@ -1591,15 +1647,23 @@ class CodegenContext {
15911647 "continue" ,
15921648 ) ;
15931649 mapExpr = `((__s) => Array.isArray(__s) ? __s.flatMap((_el0) => {\n${ cfBody } \n }) ?? null : null)(${ arrayExpr } )` ;
1594- } else if ( cf === "break" ) {
1595- const cfBody = this . buildElementBodyWithControlFlow (
1596- shifted ,
1597- arrayIterators ,
1598- 0 ,
1599- 8 ,
1600- "break" ,
1601- ) ;
1602- mapExpr = `(() => { const _src = ${ arrayExpr } ; if (!Array.isArray(_src)) return null; const _result = []; for (const _el0 of _src) {\n${ cfBody } \n } return _result; })()` ;
1650+ } else if (
1651+ cf ?. kind === "break" ||
1652+ cf ?. kind === "continue" ||
1653+ requiresLabeledLoop
1654+ ) {
1655+ // Same rationale as root array handling above: nested multilevel
1656+ // control requires for-loop + throw/catch propagation instead of map.
1657+ const loopBody = cf
1658+ ? this . buildElementBodyWithControlFlow (
1659+ shifted ,
1660+ arrayIterators ,
1661+ 0 ,
1662+ 8 ,
1663+ cf . kind === "continue" ? "for-continue" : "break" ,
1664+ )
1665+ : ` _result.push(${ this . buildElementBody ( shifted , arrayIterators , 0 , 8 ) } );` ;
1666+ mapExpr = `(() => { const _src = ${ arrayExpr } ; if (!Array.isArray(_src)) return null; const _result = []; __loop0: for (const _el0 of _src) {\n try {\n${ loopBody } \n } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n } return _result; })()` ;
16031667 } else {
16041668 const body = this . buildElementBody ( shifted , arrayIterators , 0 , 6 ) ;
16051669 mapExpr = `((__s) => Array.isArray(__s) ? __s.map((_el0) => (${ body } )) ?? null : null)(${ arrayExpr } )` ;
@@ -1752,11 +1816,11 @@ class CodegenContext {
17521816 arrayIterators ,
17531817 depth + 1 ,
17541818 indent + 4 ,
1755- innerCf === "continue" ? "for-continue" : "break" ,
1819+ innerCf . kind === "continue" ? "for-continue" : "break" ,
17561820 )
17571821 : `${ " " . repeat ( indent + 4 ) } _result.push(${ this . buildElementBody ( shifted , arrayIterators , depth + 1 , indent + 4 ) } );` ;
1758- mapExpr = `await (async () => { const _src = ${ srcExpr } ; if (!Array.isArray(_src)) return null; const _result = []; for (const ${ innerElVar } of _src) {\n${ innerBody } \n${ " " . repeat ( indent + 2 ) } } return _result; })()` ;
1759- } else if ( innerCf === "continue" ) {
1822+ mapExpr = `await (async () => { const _src = ${ srcExpr } ; if (!Array.isArray(_src)) return null; const _result = []; __loop ${ depth + 1 } : for (const ${ innerElVar } of _src) {\n${ " " . repeat ( indent + 4 ) } try {\n ${ innerBody } \n ${ " " . repeat ( indent + 4 ) } } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n${ " " . repeat ( indent + 2 ) } } return _result; })()` ;
1823+ } else if ( innerCf ?. kind === "continue" && innerCf . levels === 1 ) {
17601824 const cfBody = this . buildElementBodyWithControlFlow (
17611825 shifted ,
17621826 arrayIterators ,
@@ -1765,15 +1829,15 @@ class CodegenContext {
17651829 "continue" ,
17661830 ) ;
17671831 mapExpr = `((__s) => Array.isArray(__s) ? __s.flatMap((${ innerElVar } ) => {\n${ cfBody } \n${ " " . repeat ( indent + 2 ) } }) ?? null : null)(${ srcExpr } )` ;
1768- } else if ( innerCf === "break" ) {
1832+ } else if ( innerCf ?. kind === "break" || innerCf ?. kind === "continue ") {
17691833 const cfBody = this . buildElementBodyWithControlFlow (
17701834 shifted ,
17711835 arrayIterators ,
17721836 depth + 1 ,
17731837 indent + 4 ,
1774- "break" ,
1838+ innerCf . kind === "continue" ? "for-continue" : "break" ,
17751839 ) ;
1776- mapExpr = `(() => { const _src = ${ srcExpr } ; if (!Array.isArray(_src)) return null; const _result = []; for (const ${ innerElVar } of _src) {\n${ cfBody } \n${ " " . repeat ( indent + 2 ) } } return _result; })()` ;
1840+ mapExpr = `(() => { const _src = ${ srcExpr } ; if (!Array.isArray(_src)) return null; const _result = []; __loop ${ depth + 1 } : for (const ${ innerElVar } of _src) {\n${ " " . repeat ( indent + 4 ) } try {\n ${ cfBody } \n ${ " " . repeat ( indent + 4 ) } } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n${ " " . repeat ( indent + 2 ) } } return _result; })()` ;
17771841 } else {
17781842 const innerBody = this . buildElementBody (
17791843 shifted ,
@@ -1840,6 +1904,21 @@ class CodegenContext {
18401904 controlWire . fallbacks ?. some (
18411905 ( fb ) => fb . type === "nullish" && fb . control != null ,
18421906 ) ?? false ;
1907+ const ctrlFromFallback = controlWire . fallbacks ?. find (
1908+ ( fb ) => fb . control != null ,
1909+ ) ?. control ;
1910+ const ctrl = ctrlFromFallback ?? controlWire . catchControl ;
1911+ const controlKind = ctrl ?. kind === "continue" ? "continue" : "break" ;
1912+ const controlLevels =
1913+ ctrl && ( ctrl . kind === "continue" || ctrl . kind === "break" )
1914+ ? Math . max ( 1 , Number ( ctrl . levels ) || 1 )
1915+ : 1 ;
1916+ const controlStatement =
1917+ controlLevels > 1
1918+ ? `throw { __bridgeControl: ${ JSON . stringify ( controlKind ) } , levels: ${ controlLevels } };`
1919+ : controlKind === "continue"
1920+ ? "continue;"
1921+ : "break;" ;
18431922
18441923 if ( mode === "continue" ) {
18451924 if ( isNullish ) {
@@ -1852,16 +1931,16 @@ class CodegenContext {
18521931 // mode === "for-continue" — same as break but uses native 'continue' keyword
18531932 if ( mode === "for-continue" ) {
18541933 if ( isNullish ) {
1855- return `${ pad } if (${ checkExpr } == null) continue; \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
1934+ return `${ pad } if (${ checkExpr } == null) ${ controlStatement } \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
18561935 }
1857- return `${ pad } if (!${ checkExpr } ) continue; \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
1936+ return `${ pad } if (!${ checkExpr } ) ${ controlStatement } \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
18581937 }
18591938
18601939 // mode === "break"
18611940 if ( isNullish ) {
1862- return `${ pad } if (${ checkExpr } == null) break; \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
1941+ return `${ pad } if (${ checkExpr } == null) ${ controlStatement } \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
18631942 }
1864- return `${ pad } if (!${ checkExpr } ) break; \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
1943+ return `${ pad } if (!${ checkExpr } ) ${ controlStatement } \n${ pad } _result.push(${ this . buildElementBody ( elemWires , arrayIterators , depth , indent ) } );` ;
18651944 }
18661945
18671946 // ── Wire → expression ────────────────────────────────────────────────────
0 commit comments