From d3c444719826637b3184ab60dc13cab01cdc9bdd Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Sat, 18 Apr 2026 16:56:44 +0900 Subject: [PATCH] feat(parser): keyword-as-identifier at stmt-block boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harbour permits keywords (CASE, DO, WHILE, etc.) to be used as variable/array names. In most expression contexts Five already handles this via expr.go:362 which whitelists keywords when used as bare identifiers. But parseStmtBlock was stopping on any stop token unconditionally, so a line like case[ n ] := x -- 'case' is a LOCAL array terminated the enclosing stmt block at `case` and left `[ n ] := x` unparsable. Add isIdentSuffix(): peeks one ahead and reports whether the next token is something that can only follow an identifier ([, :=, +=, -=, *=, /=, %=, ^=, ++, --, :, .). parseStmtBlock now treats the stop token as a statement-start when its suffix matches, so the block keeps going. Verified with /tmp/test_kwident.prg (`case[...]` outside DO CASE, `arr[...]` inside DO CASE body), /tmp/test_kwident2.prg (both the `case case[n] == "two"` arm and `case[1] := "updated"` assignment after ENDCASE). Pathological harbour-core/tests/keywords.prg still fails — it places `case[...]` in the arm-expected position of a DO CASE block with no leading arm, which no sane parser can disambiguate. FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- compiler/parser/parser.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 144bb2c..6ca936f 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1069,6 +1069,21 @@ func (p *Parser) parseMethodDecl() *ast.MethodDecl { // --- Statement parsing --- +// isIdentSuffix reports whether the given token could follow an +// identifier, signalling that a keyword on the prior slot is being +// used as a variable name (array index, method call, assignment, +// etc.). Used by parseStmtBlock to avoid prematurely ending a +// statement block when a keyword identifier appears. +func isIdentSuffix(k token.Kind) bool { + switch k { + case token.LBRACKET, token.ASSIGN, token.PLUSEQ, token.MINUSEQ, + token.STAREQ, token.SLASHEQ, token.PERCENTEQ, token.POWEREQ, + token.INC, token.DEC, token.COLON, token.DOT: + return true + } + return false +} + // parseStmtBlock parses statements until one of the stop tokens. func (p *Parser) parseStmtBlock(stopTokens ...token.Kind) []ast.Stmt { var stmts []ast.Stmt @@ -1084,6 +1099,14 @@ func (p *Parser) parseStmtBlock(stopTokens ...token.Kind) []ast.Stmt { p.peekAt(1) == token.ASSIGN { break // continue parsing as statement } + // Don't stop at a keyword-used-as-identifier. Harbour + // allows keywords like CASE/DO as variable names, so + // `case[idx]`, `case := 1`, `case:method()` are + // expression statements, not new block arms. Peek the + // next token for identifier-ish suffixes. + if isIdentSuffix(p.peekAt(1)) { + break // treat current token as identifier, parse as stmt + } return stmts } }