From 3fbb1a48b6e926b4e43076449418ed6ecee1e224 Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Wed, 27 May 2026 15:57:56 +0900 Subject: [PATCH] fix(parser): report missing ENDIF/ENDDO/NEXT/ENDCASE/ENDSWITCH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- compiler/parser/parser.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index d2498a6..98a72b2 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -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()