From 4b629f7e7a0a09674a8bac0f3655a9159be126ef Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Sat, 18 Apr 2026 17:07:06 +0900 Subject: [PATCH] fix(pp): #xcommand/#xtranslate patterns with paren-attached keyword MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real Harbour headers write parameterised commands with no space between the keyword and its opening paren: #xcommand MAKE_TEST( , ) => ... ParseRule stored the rule keyword as `MAKE_TEST(` (stripping only <>, [] marker wrappers), but firstToken normalised source lines by stopping the first-word scan at `(` — so `MAKE_TEST( o, 42 )` produced `MAKE_TEST` for the lookup. The two strings didn't match and the fast-path keyword check rejected every invocation, leaving the macro unexpanded and the call site as a bare undeclared identifier. Trim everything from the first `(` onward during keyword extraction so both halves agree on the dispatch key. The marker tokens inside the parens are still parsed normally by parseMarkers / matchPattern. Verified with /tmp/test_xcmd2.prg (`MAKE_TEST( o, 99 )` expands and dispatches to the object's :hVar access). FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- compiler/pp/command.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/compiler/pp/command.go b/compiler/pp/command.go index 75e2753..80b3a15 100644 --- a/compiler/pp/command.go +++ b/compiler/pp/command.go @@ -81,13 +81,19 @@ func ParseRule(directive string, isCommand, caseSens bool) *Rule { ResultTmpl: result, } - // Extract first keyword for fast matching + // Extract first keyword for fast matching. The first whitespace- + // delimited token of the pattern becomes the dispatch key; we + // strip marker wrappers and any trailing `(` so a pattern like + // `MAKE_TEST( , )` hashes on `MAKE_TEST`, matching how + // firstToken normalises source lines. words := strings.Fields(pattern) if len(words) > 0 { kw := words[0] - // Remove marker brackets kw = strings.TrimLeft(kw, "<[") kw = strings.TrimRight(kw, ">]") + if idx := strings.IndexByte(kw, '('); idx >= 0 { + kw = kw[:idx] + } if !strings.ContainsAny(kw, "!*,:") { rule.Keyword = kw }