feat(parser): keyword-as-identifier at stmt-block boundaries

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 16:56:44 +09:00
parent 0a5482b6aa
commit d3c4447198

View File

@@ -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
}
}