fix(gengo): SWITCH edge cases — empty body, OTHERWISE-only, EXIT semantics

Three SWITCH codegen bugs surfaced by harbour-core/tests/switch.prg:

1. Empty SWITCH (`SWITCH x ENDSWITCH`) — legal Harbour, produced by
   conditional-compile files like switch.prg:13. Previous code
   emitted `_sw := t.Pop2()` followed by `}` with no matching `{`,
   closing the enclosing procedure body and producing "syntax error:
   non-declaration statement outside function body".

2. OTHERWISE-only (no CASE arms) — emitted `} else {` with no opening
   if, same "unexpected keyword else" category.

3. `EXIT` inside a CASE should break out of the SWITCH — but Five
   lowers SWITCH to an if/else-if chain, so the generated `break`
   had nowhere to land ("break is not in a loop, switch, or select").

Fix all three by wrapping every SWITCH in a one-iteration `for`
loop. `break` inside a case targets the wrapper, matching Harbour
semantics. Empty / OTHERWISE-only bodies still emit valid Go
because the for-loop provides the scope boundary regardless of
whether any if-chain opened. A trailing `break` keeps the loop
one-shot.

Also:
- `_ = _sw` silences unused-var for empty SWITCH.
- Conditionally emit the if-chain closing `}` only when at least
  one CASE ran.

All 15 SWITCH blocks in harbour-core/tests/switch.prg now build
and run to completion. FiveSql2 43/43, Harbour compat 56/56,
Go test ALL PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 17:11:47 +09:00
parent 4b629f7e7a
commit 65b2edc906

View File

@@ -2077,8 +2077,17 @@ func hasLoopStmt(s ast.Stmt) bool {
func (g *Generator) emitSwitch(s *ast.SwitchStmt, locals localMap) {
// Wrap the whole thing in a one-iteration `for` so:
// 1. `_sw` stays scoped to the switch.
// 2. `EXIT` inside a CASE emits `break` and targets this loop,
// matching Harbour SWITCH semantics (EXIT terminates SWITCH).
// 3. Empty SWITCH (`SWITCH x ENDSWITCH`, common in conditional-
// compile test files) stays valid Go.
g.writeln("for {")
g.indent++
g.emitExpr(s.Expr)
g.writeln("_sw := t.Pop2()")
g.writeln("_ = _sw") // silence unused-var warning when no cases reference it
first := true
for _, c := range s.Cases {
if first {
@@ -2096,13 +2105,28 @@ func (g *Generator) emitSwitch(s *ast.SwitchStmt, locals localMap) {
g.indent--
}
if len(s.Otherwise) > 0 {
g.writeln("} else {")
g.indent++
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
if first {
// No CASE arms — emit the OTHERWISE body as-is, no if/else.
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
} else {
g.writeln("} else {")
g.indent++
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
}
g.indent--
} else if !first {
// Had CASE arms, no OTHERWISE — close the if/else-if chain.
g.writeln("}")
}
// Always break out of our one-iteration `for` wrapper, regardless
// of which (or no) case ran.
g.writeln("break")
g.indent--
g.writeln("}")
}