From 483866d85050c71cda9a989edf1459b0b173ae00 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sun, 19 Apr 2026 16:57:15 +0000 Subject: [PATCH] Sentinel break/continue in try bodies (#87, #92) --- internal/luatest/try_break_continue_test.go | 194 +++++++++++++ internal/transpiler/loops.go | 9 + internal/transpiler/statements.go | 107 +++---- internal/transpiler/transpiler.go | 298 ++++++++++++++------ 4 files changed, 466 insertions(+), 142 deletions(-) create mode 100644 internal/luatest/try_break_continue_test.go diff --git a/internal/luatest/try_break_continue_test.go b/internal/luatest/try_break_continue_test.go new file mode 100644 index 0000000..3d9b72a --- /dev/null +++ b/internal/luatest/try_break_continue_test.go @@ -0,0 +1,194 @@ +package luatest + +import ( + "testing" + + "github.com/realcoldfry/tslua/internal/transpiler" +) + +// TestTry_BreakContinueAcrossPcall covers break/continue statements inside a +// try/catch/finally body. The transpiler wraps the try body in a pcall(function() +// ... end), so a direct break or goto would cross the Lua function boundary. +// The fix is a sentinel-flag assignment plus return, with dispatch after the +// pcall returns and finally has run. +// +// Regression: +// - https://github.com/RealColdFry/tslua/issues/87 +// - https://github.com/RealColdFry/tslua/issues/92 +func TestTry_BreakContinueAcrossPcall(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + needGoto bool + }{ + { + "unlabeled continue inside try/finally", + `const log: number[] = []; + for (let i = 0; i < 10; i++) { + try { + if (i === 2) { i = 7; continue; } + } finally {} + log.push(i); + } + return log;`, + `{0, 1, 8, 9}`, + false, + }, + { + "unlabeled break inside try/catch", + `const log: number[] = []; + for (let i = 0; i < 10; i++) { + try { + if (i === 3) break; + } catch {} + log.push(i); + } + return log;`, + `{0, 1, 2}`, + false, + }, + { + "continue inside try/finally runs finally", + `const log: number[] = []; + for (let i = 0; i < 3; i++) { + try { + if (i === 1) continue; + log.push(i); + } finally { + log.push(100 + i); + } + } + return log;`, + `{0, 100, 101, 2, 102}`, + false, + }, + { + "labeled break inside try targets outer loop", + `const log: number[] = []; + outer: for (let i = 0; i < 5; i++) { + for (let j = 0; j < 5; j++) { + try { + if (j === 1 && i === 2) break outer; + } finally {} + log.push(i * 10 + j); + } + } + return log;`, + `{0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 20}`, + true, + }, + { + "labeled continue inside try targets outer loop", + `const log: number[] = []; + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + try { + if (j === 1) continue outer; + } finally {} + log.push(i * 10 + j); + } + } + return log;`, + `{0, 10, 20}`, + true, + }, + { + "break inside try inside inner loop only exits inner", + `const log: number[] = []; + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 5; j++) { + try { + if (j === 2) break; + } finally {} + log.push(i * 10 + j); + } + } + return log;`, + `{0, 1, 10, 11, 20, 21}`, + false, + }, + } + + targets := []struct { + name string + target transpiler.LuaTarget + }{ + {"LuaJIT", transpiler.LuaTargetLuaJIT}, + {"5.1", transpiler.LuaTargetLua51}, + {"5.2", transpiler.LuaTargetLua52}, + {"5.3", transpiler.LuaTargetLua53}, + {"5.4", transpiler.LuaTargetLua54}, + {"5.5", transpiler.LuaTargetLua55}, + } + + for _, c := range cases { + for _, tgt := range targets { + if c.needGoto && !tgt.target.SupportsGoto() { + continue + } + t.Run(c.name+" ["+tgt.name+"]", func(t *testing.T) { + t.Parallel() + ExpectFunction(t, c.body, c.want, Opts{LuaTarget: tgt.target}) + }) + } + } +} + +// TestAsyncTry_LabeledBreakContinue covers labeled break/continue inside an +// async try body. Previously the labeled branch short-circuited before the +// try-scope check, emitting a goto that crossed the __TS__AsyncAwaiter function +// boundary. See https://github.com/RealColdFry/tslua/issues/92. +func TestAsyncTry_LabeledBreakContinue(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "labeled break outer inside async try", + `const log: number[] = []; + async function run() { + outer: for (let i = 0; i < 3; i++) { + try { + await Promise.resolve(); + if (i === 1) break outer; + log.push(i); + } catch {} + } + } + run(); + return log;`, + `{0}`, + }, + { + "labeled continue outer inside async try", + `const log: number[] = []; + async function run() { + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + try { + await Promise.resolve(); + if (j === 1) continue outer; + log.push(i * 10 + j); + } catch {} + } + } + } + run(); + return log;`, + `{0, 10, 20}`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, c.body, c.want, Opts{LuaTarget: transpiler.LuaTargetLuaJIT}) + }) + } +} diff --git a/internal/transpiler/loops.go b/internal/transpiler/loops.go index 7ed9428..8422f7d 100644 --- a/internal/transpiler/loops.go +++ b/internal/transpiler/loops.go @@ -129,6 +129,9 @@ func (t *Transpiler) transformLabeledStatement(node *ast.Node) []lua.Statement { if t.continueLabelMap == nil { t.continueLabelMap = make(map[string]string) } + if t.labelScopeDepths == nil { + t.labelScopeDepths = make(map[string]int) + } if hasBreak { t.breakLabels[tsLabel] = breakLabel @@ -137,6 +140,11 @@ func (t *Transpiler) transformLabeledStatement(node *ast.Node) []lua.Statement { t.continueLabelMap[tsLabel] = continueLabel t.activeLabeledContinue = continueLabel } + // Record the scope-stack length at registration. The inner iteration/block + // pushes its own scope at this index, so a break/continue inside that scope + // targeting this label whose scope-stack index is below this length has + // crossed any Try/Catch scope in between. + t.labelScopeDepths[tsLabel] = len(t.scopeStack) // Transform the inner statement (it will use the registered labels) stmts := t.transformStatementWithComments(ls.Statement) @@ -144,6 +152,7 @@ func (t *Transpiler) transformLabeledStatement(node *ast.Node) []lua.Statement { // Clean up labels delete(t.breakLabels, tsLabel) delete(t.continueLabelMap, tsLabel) + delete(t.labelScopeDepths, tsLabel) t.activeLabeledContinue = "" if hasBreak { diff --git a/internal/transpiler/statements.go b/internal/transpiler/statements.go index f81d520..3b8aeec 100644 --- a/internal/transpiler/statements.go +++ b/internal/transpiler/statements.go @@ -733,8 +733,8 @@ func (t *Transpiler) transformReturnStatement(node *ast.Node) []lua.Statement { if t.asyncDepth > 0 { // If we're inside an async try/catch, defer the return to post-check // logic after the awaiter chain resolves. Ported from TSTL PR #1706. - if tryScope := t.findAsyncTryScope(); tryScope != nil { - tryScope.AsyncTryHasReturn = true + if tryScope := t.findTryScopeInStack(); tryScope != nil { + tryScope.TryHasReturn = true result := []lua.Statement{ lua.Assign([]lua.Expression{lua.Ident("____hasReturned")}, []lua.Expression{lua.Bool(true)}), } @@ -1332,9 +1332,11 @@ func (t *Transpiler) transformTryStatement(node *ast.Node) []lua.Statement { return t.transformBlock(ts.TryBlock) } - // Track that we're inside a try block (return → return true, value) + // Track that we're inside a try block (return → return true, value) and + // push a ScopeTry so nested break/continue can detect they'd cross the + // pcall function boundary. t.tryDepth++ - tryBlockStmts := t.transformBlock(ts.TryBlock) + tryBlockStmts, tryScope := t.transformBlockInNewScope(ts.TryBlock, ScopeTry) tryHasReturn := containsReturnInBlock(ts.TryBlock) t.tryDepth-- @@ -1366,9 +1368,16 @@ func (t *Transpiler) transformTryStatement(node *ast.Node) []lua.Statement { } t.tryDepth++ - catchBlockStmts := t.transformBlock(cc.Block) + var catchBlockStmts []lua.Statement + var catchScope *Scope + catchBlockStmts, catchScope = t.transformBlockInNewScope(cc.Block, ScopeCatch) catchHasReturn := containsReturnInBlock(cc.Block) t.tryDepth-- + // Merge catch's deferred transfers into the try scope so the single + // post-check after finally handles both try and catch body transfers. + for _, xfer := range catchScope.TryDeferredTransfers { + tryScope.addDeferredTransfer(xfer) + } var catchParams []*lua.Identifier if hasCatchVar { @@ -1479,6 +1488,25 @@ func (t *Transpiler) transformTryStatement(node *ast.Node) []lua.Statement { )) } + // Break/continue propagation: if the try/catch body had to defer a jump + // because it would have crossed the pcall function boundary, declare the + // sentinel flags before the pcall and dispatch them after finally. + if len(tryScope.TryDeferredTransfers) > 0 { + flagDecls := make([]*lua.Identifier, 0, len(tryScope.TryDeferredTransfers)) + for _, xfer := range tryScope.TryDeferredTransfers { + flagDecls = append(flagDecls, lua.Ident(xfer.FlagName)) + } + // Prepend the flag declaration so the pcall/catch closures can assign it. + result = append([]lua.Statement{lua.LocalDecl(flagDecls, nil)}, result...) + for _, xfer := range tryScope.TryDeferredTransfers { + result = append(result, lua.If( + lua.Ident(xfer.FlagName), + &lua.Block{Statements: xfer.Dispatch}, + nil, + )) + } + } + return []lua.Statement{lua.Do(result...)} } @@ -1570,28 +1598,26 @@ func (t *Transpiler) transformAsyncTry(ts *ast.TryStatement) []lua.Statement { } // Collect deferred-control-flow flags from try and catch scopes. - hasReturn := tryScope.AsyncTryHasReturn - hasBreak := tryScope.AsyncTryHasBreak - hasContinue := tryScope.AsyncTryHasContinue + hasReturn := tryScope.TryHasReturn if catchScope != nil { - hasReturn = hasReturn || catchScope.AsyncTryHasReturn - hasBreak = hasBreak || catchScope.AsyncTryHasBreak - hasContinue = hasContinue || catchScope.AsyncTryHasContinue + hasReturn = hasReturn || catchScope.TryHasReturn + // Merge catch's deferred transfers into the try scope. + for _, xfer := range catchScope.TryDeferredTransfers { + tryScope.addDeferredTransfer(xfer) + } } + transfers := tryScope.TryDeferredTransfers var result []lua.Statement // Declare flag locals before the awaiter so the inner closures can assign them. - if hasReturn || hasBreak || hasContinue { + if hasReturn || len(transfers) > 0 { var flagDecls []*lua.Identifier if hasReturn { flagDecls = append(flagDecls, lua.Ident("____hasReturned"), lua.Ident("____returnValue")) } - if hasBreak { - flagDecls = append(flagDecls, lua.Ident("____hasBroken")) - } - if hasContinue { - flagDecls = append(flagDecls, lua.Ident("____hasContinued")) + for _, xfer := range transfers { + flagDecls = append(flagDecls, lua.Ident(xfer.FlagName)) } result = append(result, lua.LocalDecl(flagDecls, nil)) } @@ -1628,56 +1654,17 @@ func (t *Transpiler) transformAsyncTry(ts *ast.TryStatement) []lua.Statement { }}, }) } - if hasBreak { - // if ____hasBroken then break end + // Dispatch each deferred break/continue transfer captured during body transforms. + for _, xfer := range transfers { result = append(result, &lua.IfStatement{ - Condition: lua.Ident("____hasBroken"), - IfBlock: &lua.Block{Statements: []lua.Statement{lua.Break()}}, - }) - } - if hasContinue { - // if ____hasContinued then end - continueStmts := t.buildContinueStatements() - result = append(result, &lua.IfStatement{ - Condition: lua.Ident("____hasContinued"), - IfBlock: &lua.Block{Statements: continueStmts}, + Condition: lua.Ident(xfer.FlagName), + IfBlock: &lua.Block{Statements: xfer.Dispatch}, }) } return result } -// buildContinueStatements emits the target-appropriate continue sequence for -// the innermost enclosing loop (native continue, goto label, or repeat-break -// fallback). Used by the async try post-check when a continue inside the try -// needs to propagate to the outer loop. -func (t *Transpiler) buildContinueStatements() []lua.Statement { - if t.luaTarget.HasNativeContinue() { - var stmts []lua.Statement - if n := len(t.forLoopPreContinue); n > 0 { - stmts = append(stmts, t.forLoopPreContinue[n-1]...) - } - if n := len(t.forLoopIncrementors); n > 0 { - if inc := t.forLoopIncrementors[n-1]; inc != nil { - stmts = append(stmts, inc...) - } - } - return append(stmts, lua.Continue()) - } - label := "__continue" - if len(t.continueLabels) > 0 { - label = t.continueLabels[len(t.continueLabels)-1] - } - if t.luaTarget.SupportsGoto() { - return []lua.Statement{lua.Goto(label)} - } - // Lua 5.0/5.1: set continue flag and break out of repeat loop - return []lua.Statement{ - lua.Assign([]lua.Expression{lua.Ident(label)}, []lua.Expression{lua.Bool(true)}), - lua.Break(), - } -} - // throw expr → error(expr, 0) func (t *Transpiler) transformThrowStatement(node *ast.Node) []lua.Statement { ts := node.AsThrowStatement() diff --git a/internal/transpiler/transpiler.go b/internal/transpiler/transpiler.go index 6a0ba99..4c7c727 100644 --- a/internal/transpiler/transpiler.go +++ b/internal/transpiler/transpiler.go @@ -76,12 +76,34 @@ type Scope struct { FunctionDefs map[SymbolID]*FunctionDefinitionInfo ImportStatements []lua.Statement // import statements to hoist to top - // Flags set when a return/break/continue inside an async try/catch needs - // to be deferred to post-check logic after the awaiter chain. Ported from - // TSTL PR #1706. - AsyncTryHasReturn bool - AsyncTryHasBreak bool - AsyncTryHasContinue bool + // Flags set when a return/break/continue inside a try/catch needs to be + // deferred to post-check logic after the pcall (sync) or awaiter chain + // (async). Break/continue inside the pcall/awaiter function body can't + // use goto/break directly because Lua forbids crossing function boundaries + // with either, so we assign a sentinel flag and return, then dispatch + // after the try wrapper. + TryHasReturn bool + TryDeferredTransfers []DeferredTransfer +} + +// TransferKind identifies the kind of deferred control-flow transfer out of a +// try/catch body. +type TransferKind int + +const ( + TransferBreak TransferKind = iota + TransferContinue +) + +// DeferredTransfer describes a break/continue that had to be replaced with a +// sentinel-flag assignment + return because the jump would have crossed the +// pcall/awaiter function boundary. The transformTryStatement visitor consumes +// these to emit flag declarations and post-pcall dispatch. +type DeferredTransfer struct { + Kind TransferKind + TSLabel string // "" for unlabeled + FlagName string // Lua identifier for the sentinel flag + Dispatch []lua.Statement } // Transpiler holds the state needed for transpiling a single source file. @@ -112,6 +134,7 @@ type Transpiler struct { loopVarRenames map[*ast.Symbol]string // for-loop let symbols renamed when the outer counter is separated from the per-iteration binding breakLabels map[string]string // TS label name → Lua break label name (for labeled break) continueLabelMap map[string]string // TS label name → Lua continue label name (for labeled continue) + labelScopeDepths map[string]int // TS label name → scope-stack length at label registration (target loop/block index) activeLabeledContinue string // Lua label to emit at continue point of next loop (set by transformLabeledStatement) destructuredParamNames map[*ast.Node]string // maps binding pattern nodes to their generated temp names bindingPatternCount int // per-function counter for ____bindingPatternN naming @@ -1083,77 +1106,18 @@ func (t *Transpiler) transformStatement(node *ast.Node) (result []lua.Statement) return t.transformThrowStatement(node) case ast.KindBreakStatement: bs := node.AsBreakStatement() + tsLabel := "" if bs.Label != nil { - tsLabel := bs.Label.Text() - if luaLabel, ok := t.breakLabels[tsLabel]; ok { - if t.luaTarget.SupportsGoto() { - return []lua.Statement{lua.Goto(luaLabel)} - } - return []lua.Statement{ - lua.Assign([]lua.Expression{lua.Ident(luaLabel)}, []lua.Expression{lua.Bool(true)}), - lua.Break(), - } - } - } - if t.asyncDepth > 0 { - if tryScope := t.findAsyncTryScopeBeforeLoop(); tryScope != nil { - tryScope.AsyncTryHasBreak = true - return []lua.Statement{ - lua.Assign([]lua.Expression{lua.Ident("____hasBroken")}, []lua.Expression{lua.Bool(true)}), - lua.Return(), - } - } + tsLabel = bs.Label.Text() } - return []lua.Statement{lua.Break()} + return t.buildBreakDispatch(tsLabel) case ast.KindContinueStatement: - if t.asyncDepth > 0 { - if tryScope := t.findAsyncTryScopeBeforeLoop(); tryScope != nil { - tryScope.AsyncTryHasContinue = true - return []lua.Statement{ - lua.Assign([]lua.Expression{lua.Ident("____hasContinued")}, []lua.Expression{lua.Bool(true)}), - lua.Return(), - } - } - } - if t.luaTarget.HasNativeContinue() { - // For C-style for-loops, the incrementor must run before continue. - // Duplicate it here since native continue skips to the loop condition. - var stmts []lua.Statement - if n := len(t.forLoopPreContinue); n > 0 { - stmts = append(stmts, t.forLoopPreContinue[n-1]...) - } - if n := len(t.forLoopIncrementors); n > 0 { - if inc := t.forLoopIncrementors[n-1]; inc != nil { - stmts = append(stmts, inc...) - } - } - return append(stmts, lua.Continue()) - } cs := node.AsContinueStatement() + tsLabel := "" if cs.Label != nil { - tsLabel := cs.Label.Text() - if luaLabel, ok := t.continueLabelMap[tsLabel]; ok { - if t.luaTarget.SupportsGoto() { - return []lua.Statement{lua.Goto(luaLabel)} - } - return []lua.Statement{ - lua.Assign([]lua.Expression{lua.Ident(luaLabel)}, []lua.Expression{lua.Bool(true)}), - lua.Break(), - } - } - } - label := "__continue" - if len(t.continueLabels) > 0 { - label = t.continueLabels[len(t.continueLabels)-1] - } - if t.luaTarget.SupportsGoto() { - return []lua.Statement{lua.Goto(label)} - } - // Lua 5.1: set continue flag and break out of repeat loop - return []lua.Statement{ - lua.Assign([]lua.Expression{lua.Ident(label)}, []lua.Expression{lua.Bool(true)}), - lua.Break(), + tsLabel = cs.Label.Text() } + return t.buildContinueDispatch(tsLabel) case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration: // Type-only declarations — erased in Lua output return nil @@ -1202,11 +1166,11 @@ func (t *Transpiler) peekScope() *Scope { return t.scopeStack[len(t.scopeStack)-1] } -// findAsyncTryScope walks up the scope stack looking for an enclosing Try/Catch -// scope inside the current async function. Returns nil if a Function scope is -// encountered first (i.e. the try belongs to an outer function). Ported from -// TSTL PR #1706 findAsyncTryScopeInStack. -func (t *Transpiler) findAsyncTryScope() *Scope { +// findTryScopeInStack walks up the scope stack looking for an enclosing +// Try/Catch scope. Returns nil if a Function scope is encountered first (i.e. +// the try belongs to an outer function, so a return inside our function body +// doesn't cross its pcall/awaiter boundary). Used by return handling. +func (t *Transpiler) findTryScopeInStack() *Scope { for i := len(t.scopeStack) - 1; i >= 0; i-- { s := t.scopeStack[i] if s.Type == ScopeFunction { @@ -1219,13 +1183,14 @@ func (t *Transpiler) findAsyncTryScope() *Scope { return nil } -// findAsyncTryScopeBeforeLoop is like findAsyncTryScope but stops at Loop -// boundaries so that break/continue in an inner loop don't target the outer -// async try. Ported from TSTL PR #1706 findAsyncTryScopeBeforeLoop. -func (t *Transpiler) findAsyncTryScopeBeforeLoop() *Scope { - for i := len(t.scopeStack) - 1; i >= 0; i-- { +// findTryScopeAbove returns the innermost Try/Catch scope above the given +// scope-stack index, or nil if none (or a Function scope intervenes). Used by +// break/continue handling to detect whether the jump crosses a pcall/awaiter +// function boundary. +func (t *Transpiler) findTryScopeAbove(targetDepth int) *Scope { + for i := len(t.scopeStack) - 1; i > targetDepth; i-- { s := t.scopeStack[i] - if s.Type == ScopeFunction || s.Type == ScopeLoop { + if s.Type == ScopeFunction { return nil } if s.Type == ScopeTry || s.Type == ScopeCatch { @@ -1235,6 +1200,175 @@ func (t *Transpiler) findAsyncTryScopeBeforeLoop() *Scope { return nil } +// findInnermostBreakTargetDepth returns the scope-stack index of the innermost +// Loop or Switch scope inside the current function. -1 if none. +func (t *Transpiler) findInnermostBreakTargetDepth() int { + for i := len(t.scopeStack) - 1; i >= 0; i-- { + s := t.scopeStack[i] + if s.Type == ScopeFunction { + return -1 + } + if s.Type == ScopeLoop || s.Type == ScopeSwitch { + return i + } + } + return -1 +} + +// findInnermostLoopDepth returns the scope-stack index of the innermost Loop +// scope inside the current function. -1 if none. +func (t *Transpiler) findInnermostLoopDepth() int { + for i := len(t.scopeStack) - 1; i >= 0; i-- { + s := t.scopeStack[i] + if s.Type == ScopeFunction { + return -1 + } + if s.Type == ScopeLoop { + return i + } + } + return -1 +} + +// addDeferredTransfer appends a transfer to the try scope's list, deduping by +// flag name. +func (s *Scope) addDeferredTransfer(xfer DeferredTransfer) { + for _, existing := range s.TryDeferredTransfers { + if existing.FlagName == xfer.FlagName { + return + } + } + s.TryDeferredTransfers = append(s.TryDeferredTransfers, xfer) +} + +// buildBreakDispatch emits the Lua statements for a TS break statement, +// routing through the sentinel-flag path when the break would cross a +// pcall/awaiter function boundary from within a try/catch body. +func (t *Transpiler) buildBreakDispatch(tsLabel string) []lua.Statement { + targetDepth := -1 + if tsLabel != "" { + if d, ok := t.labelScopeDepths[tsLabel]; ok { + targetDepth = d - 1 + } + } else { + targetDepth = t.findInnermostBreakTargetDepth() + } + + normal := t.normalBreakStatements(tsLabel) + + if tryScope := t.findTryScopeAbove(targetDepth); tryScope != nil { + flag := "____hasBroken" + if tsLabel != "" { + flag = "____hasBroken_" + tsLabel + } + tryScope.addDeferredTransfer(DeferredTransfer{ + Kind: TransferBreak, + TSLabel: tsLabel, + FlagName: flag, + Dispatch: normal, + }) + return []lua.Statement{ + lua.Assign([]lua.Expression{lua.Ident(flag)}, []lua.Expression{lua.Bool(true)}), + lua.Return(), + } + } + + return normal +} + +// buildContinueDispatch emits the Lua statements for a TS continue statement, +// routing through the sentinel-flag path when the continue would cross a +// pcall/awaiter function boundary from within a try/catch body. +func (t *Transpiler) buildContinueDispatch(tsLabel string) []lua.Statement { + targetDepth := -1 + if tsLabel != "" { + if d, ok := t.labelScopeDepths[tsLabel]; ok { + targetDepth = d - 1 + } + } else { + targetDepth = t.findInnermostLoopDepth() + } + + normal := t.normalContinueStatements(tsLabel) + + if tryScope := t.findTryScopeAbove(targetDepth); tryScope != nil { + flag := "____hasContinued" + if tsLabel != "" { + flag = "____hasContinued_" + tsLabel + } + tryScope.addDeferredTransfer(DeferredTransfer{ + Kind: TransferContinue, + TSLabel: tsLabel, + FlagName: flag, + Dispatch: normal, + }) + return []lua.Statement{ + lua.Assign([]lua.Expression{lua.Ident(flag)}, []lua.Expression{lua.Bool(true)}), + lua.Return(), + } + } + + return normal +} + +// normalBreakStatements returns the Lua statements for a break (labeled or +// unlabeled) assuming the jump doesn't cross a pcall/awaiter boundary. +func (t *Transpiler) normalBreakStatements(tsLabel string) []lua.Statement { + if tsLabel != "" { + if luaLabel, ok := t.breakLabels[tsLabel]; ok { + if t.luaTarget.SupportsGoto() { + return []lua.Statement{lua.Goto(luaLabel)} + } + return []lua.Statement{ + lua.Assign([]lua.Expression{lua.Ident(luaLabel)}, []lua.Expression{lua.Bool(true)}), + lua.Break(), + } + } + } + return []lua.Statement{lua.Break()} +} + +// normalContinueStatements returns the Lua statements for a continue (labeled +// or unlabeled) assuming the jump doesn't cross a pcall/awaiter boundary. +func (t *Transpiler) normalContinueStatements(tsLabel string) []lua.Statement { + if tsLabel != "" { + if luaLabel, ok := t.continueLabelMap[tsLabel]; ok { + if t.luaTarget.SupportsGoto() { + return []lua.Statement{lua.Goto(luaLabel)} + } + return []lua.Statement{ + lua.Assign([]lua.Expression{lua.Ident(luaLabel)}, []lua.Expression{lua.Bool(true)}), + lua.Break(), + } + } + } + if t.luaTarget.HasNativeContinue() { + // For C-style for-loops, the incrementor must run before continue. + // Duplicate it here since native continue skips to the loop condition. + var stmts []lua.Statement + if n := len(t.forLoopPreContinue); n > 0 { + stmts = append(stmts, t.forLoopPreContinue[n-1]...) + } + if n := len(t.forLoopIncrementors); n > 0 { + if inc := t.forLoopIncrementors[n-1]; inc != nil { + stmts = append(stmts, inc...) + } + } + return append(stmts, lua.Continue()) + } + label := "__continue" + if len(t.continueLabels) > 0 { + label = t.continueLabels[len(t.continueLabels)-1] + } + if t.luaTarget.SupportsGoto() { + return []lua.Statement{lua.Goto(label)} + } + return []lua.Statement{ + lua.Assign([]lua.Expression{lua.Ident(label)}, []lua.Expression{lua.Bool(true)}), + lua.Break(), + } +} + // isInsideFunction returns true if any scope in the stack is a function scope. // Used to check if code is inside a function body vs at module/file level. func (t *Transpiler) isInsideFunction() bool {