From ec6ea600ad3fa27995806cb3b559dd0c0469ab73 Mon Sep 17 00:00:00 2001 From: Brian Bentow Date: Sun, 2 Aug 2026 13:24:55 -0700 Subject: [PATCH] :string:starts_with: evaluate the pattern argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit starts_with evaluates its first argument through the substitution but takes its second raw, so the pattern must be a string literal written into the program text. A prefix computed from data — fn:string:concat(Token, "/") — is rejected, which makes a common class of prefix join inexpressible. Evaluates the second argument the same way the first already is. Literal patterns are unaffected; computed ones now work. Verified against a versioned-identifier join: with activation tokens 'soc2' and 'pci-dss' and frameworks 'soc2/v0.1' and 'pci-dss/v0.1', resolves/2 yields both pairs, while a token with no matching framework correctly yields nothing. --- builtin/builtin.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/builtin/builtin.go b/builtin/builtin.go index 93622fb..3cd1257 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -471,9 +471,15 @@ func match(pattern ast.Atom, subst *unionfind.UnionFind) (bool, *unionfind.Union if len(pattern.Args) != 2 { return false, nil, fmt.Errorf("wrong number of arguments for built-in predicate ':string:starts_with': %v", pattern.Args) } - pat, ok := pattern.Args[1].(ast.Constant) + // Evaluate the pattern through the substitution, as the scrutinee already is, + // so a prefix computed from data works and not only a program literal. + evaluatedPat, err := functional.EvalExpr(pattern.Args[1], subst) + if err != nil { + return false, nil, err + } + pat, ok := evaluatedPat.(ast.Constant) if !ok || pat.Type != ast.StringType { - return false, nil, fmt.Errorf("2nd arguments must be string constant for ':string:starts_with': %v", pattern) + return false, nil, fmt.Errorf("2nd argument must evaluate to a string for ':string:starts_with': %v", pattern) } str, ok := evaluatedArg.(ast.Constant) if !ok || str.Type != ast.StringType {