fix(parser): report missing ENDIF/ENDDO/NEXT/ENDCASE/ENDSWITCH

A missing terminator on IF/WHILE/FOR/FOR EACH/DO CASE/SWITCH used to
be silently accepted: parseStmtBlock kept consuming tokens past where
the block should have closed, and the final p.match(token.ENDIF) etc.
returned false without registering an error. The build then "succeeded"
with a near-empty generated .go because every subsequent top-level
FUNCTION had been swallowed into the unterminated block's body and
re-parsed as an expression statement (FUNCTION name(...) reads as two
function calls). At run time vm.Run("MAIN") panicked with "function
not found" — by the time the symptom showed up the cause was three
preprocessing stages away.

Now each branch reports

    expected ENDIF or END to close IF at file:line:col, got ... ""

and the build fails with a useful parse error pointing at the missing
terminator's intended position.

BEGIN SEQUENCE intentionally left alone — END without SEQUENCE is the
documented short form and not an error.

Tested minimum repro (8-line IF without ENDIF) now errors cleanly.
Full regression: go test ./compiler/... ./hbrt/... ./hbrtl/... pass,
compat 56/56, std.ch 17/17, FRB 7/7, FiveSql2 43/43.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 15:57:56 +09:00
parent c7ac4044f7
commit 3fbb1a48b6

View File

@@ -1313,8 +1313,13 @@ func (p *Parser) parseIf() *ast.IfStmt {
}
endPos := p.current.Pos
if !p.match(token.ENDIF) {
p.match(token.END) // alternative
if !p.match(token.ENDIF) && !p.match(token.END) {
// Without this, a missing ENDIF lets parseStmtBlock above
// swallow every subsequent top-level FUNCTION as part of the
// IF body — the build then "succeeds" with a near-empty
// prg_*.go and dies at runtime with "function not found".
p.error(fmt.Sprintf("expected ENDIF or END to close IF at %s, got %v %q",
ifPos, p.current.Kind, p.current.Literal))
}
p.expectEndOfStmt()
@@ -1399,8 +1404,9 @@ func (p *Parser) parseDoWhile() *ast.DoWhileStmt {
body := p.parseStmtBlock(token.ENDDO, token.END)
endPos := p.current.Pos
if !p.match(token.ENDDO) {
p.match(token.END)
if !p.match(token.ENDDO) && !p.match(token.END) {
p.error(fmt.Sprintf("expected ENDDO or END to close WHILE at %s, got %v %q",
doPos, p.current.Kind, p.current.Literal))
}
p.expectEndOfStmt()
@@ -1438,8 +1444,9 @@ func (p *Parser) parseFor() ast.Stmt {
body := p.parseStmtBlock(token.NEXT, token.END)
nextPos := p.current.Pos
if !p.match(token.NEXT) {
p.match(token.END)
if !p.match(token.NEXT) && !p.match(token.END) {
p.error(fmt.Sprintf("expected NEXT or END to close FOR at %s, got %v %q",
forPos, p.current.Kind, p.current.Literal))
}
// Skip optional counter variable after NEXT (e.g. NEXT nVar)
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
@@ -1476,8 +1483,9 @@ func (p *Parser) parseForEach(forPos token.Position) *ast.ForEachStmt {
body := p.parseStmtBlock(token.NEXT, token.END)
nextPos := p.current.Pos
if !p.match(token.NEXT) {
p.match(token.END)
if !p.match(token.NEXT) && !p.match(token.END) {
p.error(fmt.Sprintf("expected NEXT or END to close FOR EACH at %s, got %v %q",
forPos, p.current.Kind, p.current.Literal))
}
// Skip optional counter variable after NEXT
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
@@ -1531,8 +1539,9 @@ func (p *Parser) parseDoCase() *ast.IfStmt {
}
endPos := p.current.Pos
if !p.match(token.ENDCASE) {
p.match(token.END)
if !p.match(token.ENDCASE) && !p.match(token.END) {
p.error(fmt.Sprintf("expected ENDCASE or END to close DO CASE at %s, got %v %q",
doPos, p.current.Kind, p.current.Literal))
}
p.expectEndOfStmt()
@@ -1569,10 +1578,9 @@ func (p *Parser) parseSwitch() *ast.SwitchStmt {
}
endPos := p.current.Pos
if !p.match(token.ENDSWITCH) {
if !p.match(token.ENDCASE) {
p.match(token.END)
}
if !p.match(token.ENDSWITCH) && !p.match(token.ENDCASE) && !p.match(token.END) {
p.error(fmt.Sprintf("expected ENDSWITCH or END to close SWITCH at %s, got %v %q",
switchPos, p.current.Kind, p.current.Literal))
}
p.expectEndOfStmt()