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:
@@ -1313,8 +1313,13 @@ func (p *Parser) parseIf() *ast.IfStmt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
endPos := p.current.Pos
|
endPos := p.current.Pos
|
||||||
if !p.match(token.ENDIF) {
|
if !p.match(token.ENDIF) && !p.match(token.END) {
|
||||||
p.match(token.END) // alternative
|
// 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()
|
p.expectEndOfStmt()
|
||||||
|
|
||||||
@@ -1399,8 +1404,9 @@ func (p *Parser) parseDoWhile() *ast.DoWhileStmt {
|
|||||||
body := p.parseStmtBlock(token.ENDDO, token.END)
|
body := p.parseStmtBlock(token.ENDDO, token.END)
|
||||||
|
|
||||||
endPos := p.current.Pos
|
endPos := p.current.Pos
|
||||||
if !p.match(token.ENDDO) {
|
if !p.match(token.ENDDO) && !p.match(token.END) {
|
||||||
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()
|
p.expectEndOfStmt()
|
||||||
|
|
||||||
@@ -1438,8 +1444,9 @@ func (p *Parser) parseFor() ast.Stmt {
|
|||||||
|
|
||||||
body := p.parseStmtBlock(token.NEXT, token.END)
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
||||||
nextPos := p.current.Pos
|
nextPos := p.current.Pos
|
||||||
if !p.match(token.NEXT) {
|
if !p.match(token.NEXT) && !p.match(token.END) {
|
||||||
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)
|
// Skip optional counter variable after NEXT (e.g. NEXT nVar)
|
||||||
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
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)
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
||||||
nextPos := p.current.Pos
|
nextPos := p.current.Pos
|
||||||
if !p.match(token.NEXT) {
|
if !p.match(token.NEXT) && !p.match(token.END) {
|
||||||
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
|
// Skip optional counter variable after NEXT
|
||||||
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
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
|
endPos := p.current.Pos
|
||||||
if !p.match(token.ENDCASE) {
|
if !p.match(token.ENDCASE) && !p.match(token.END) {
|
||||||
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()
|
p.expectEndOfStmt()
|
||||||
|
|
||||||
@@ -1569,10 +1578,9 @@ func (p *Parser) parseSwitch() *ast.SwitchStmt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
endPos := p.current.Pos
|
endPos := p.current.Pos
|
||||||
if !p.match(token.ENDSWITCH) {
|
if !p.match(token.ENDSWITCH) && !p.match(token.ENDCASE) && !p.match(token.END) {
|
||||||
if !p.match(token.ENDCASE) {
|
p.error(fmt.Sprintf("expected ENDSWITCH or END to close SWITCH at %s, got %v %q",
|
||||||
p.match(token.END)
|
switchPos, p.current.Kind, p.current.Literal))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
p.expectEndOfStmt()
|
p.expectEndOfStmt()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user