From e4cc3a5745481218861f10e945e8657f07d3fbc5 Mon Sep 17 00:00:00 2001 From: Curtis Myzie Date: Fri, 30 Oct 2026 17:57:39 -0400 Subject: [PATCH 1/2] fix(check): report a registered function used as a value Check typed a bare function name (`first.id`, `[upper]`) as Any, so it passed and then failed at run time on the function value. It now reports the name. An env entry with the same name still wins. Co-Authored-By: Claude Opus 5.5 --- check.go | 3 +++ check_test.go | 2 ++ llms.txt | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/check.go b/check.go index 7226b1e..5b3f272 100644 --- a/check.go +++ b/check.go @@ -264,6 +264,9 @@ func (c *checker) ident(n *ast.Ident, sc *scope) *Type { return orAny(t) } if _, ok := c.p.funcs[name]; ok { + // A function used as a value fails wherever Run uses it: a field + // read, a comparison, a condition, or the result a host stores. + c.report(n, "%q is a function, not a value; call it", name) return Any } names := make([]string, 0, len(c.env)) diff --git a/check_test.go b/check_test.go index 6f554b9..ef2b140 100644 --- a/check_test.go +++ b/check_test.go @@ -76,6 +76,8 @@ func TestCheckRejects(t *testing.T) { `map(findings.items, it.titel)`: `no field "titel"`, `findings.items["x"]`: "list index must be a number", `classify?.missing`: `no field "missing"`, + `first.id`: `"first" is a function, not a value; call it`, + `[upper, 1]`: `"upper" is a function, not a value; call it`, } { _, err := checkSrc(t, src) require.Error(t, err, src) diff --git a/llms.txt b/llms.txt index cab241b..0de81bd 100644 --- a/llms.txt +++ b/llms.txt @@ -473,8 +473,8 @@ t, err := p.Check(env, expr.WithFuncType("concat", concatType)) Types: `expr.Any`, `Null()`, `Bool()`, `Number()`, `String()`, `EnumOf(...)`, `ListOf(t)`, `ObjectOf(fields)` (closed), `OpenObject(fields)`, `t.OrNil()`, -`t.NonNil()`, `Join(a, b)`. Check catches unknown names and fields, -reads of maybe-nil values without `?.`/`try`, wrong-kind operations, +`t.NonNil()`, `Join(a, b)`. Check catches unknown names and fields, a +registered function used as a value instead of called, reads of maybe-nil values without `?.`/`try`, wrong-kind operations, builtin calls with the wrong arguments, and comparisons that are never true (including against an enum). A nil `*Type` means `Any`. From d63a461cfb673cdc526fd328bbf1d0892f3761a8 Mon Sep 17 00:00:00 2001 From: Curtis Myzie Date: Fri, 30 Oct 2026 20:43:20 -0400 Subject: [PATCH 2/2] docs(check): list the function-as-value rule and name it as the Check/Run exception Add the rule to the spec's list of Check problems, state in the spec and CLAUDE.md that it is the one intended disagreement with Run, narrow the check.go comment to the cases where Run fails, and rewrap llms.txt. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 ++ check.go | 5 +++-- docs/reference/spec.md | 7 +++++++ llms.txt | 7 ++++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f355bcf..4ba96ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,8 @@ parameter interpolation. form accepts at run time, change `check.go` to match (`builtinSigs` for builtins), and add the case to `TestCheckMatchesRun`, which runs every rejected expression to confirm `Run` fails too. + The one intended exception: Check rejects a registered function used as a + value (`[upper, 1]`), which `Run` still evaluates. - When changing language behavior, update `docs/reference/spec.md` in the same change; if the change affects how expressions *look*, update `docs/guides/examples.md` and `llms.txt` too. - Guide pages under `docs/guides/` have runnable companions under `examples/` and diff --git a/check.go b/check.go index 5b3f272..d3002cf 100644 --- a/check.go +++ b/check.go @@ -264,8 +264,9 @@ func (c *checker) ident(n *ast.Ident, sc *scope) *Type { return orAny(t) } if _, ok := c.p.funcs[name]; ok { - // A function used as a value fails wherever Run uses it: a field - // read, a comparison, a condition, or the result a host stores. + // Run returns the function itself, which fails on a field read + // and when a host stores the result. Run accepts it in a + // condition or a list literal; Check rejects it there too. c.report(n, "%q is a function, not a value; call it", name) return Any } diff --git a/docs/reference/spec.md b/docs/reference/spec.md index ae8b886..fb90555 100644 --- a/docs/reference/spec.md +++ b/docs/reference/spec.md @@ -761,6 +761,13 @@ Check reports, as a `*TypeError` wrapping `ErrType`, every: - `==`/`!=` that can never be true: different kinds, or a literal outside the other side's enum (`kind == "bugfix"` when kind is `"bug_fix" | "feature"`). +- registered function name used as a value instead of called (`first.id`, + `[upper, 1]`). An env entry or bound name with the same name wins. + +Check otherwise agrees with Run: what it reports, Run rejects too. A +function used as a value is the one intended exception. Run returns the +function itself, which fails on a field read or when a host stores the +result, but Run accepts it in a condition or a list literal. `&&` and `||` return one of their operands (see Logical), so they type as the join of their operand types: `bool` only when both sides are. Joining diff --git a/llms.txt b/llms.txt index 0de81bd..d4c7706 100644 --- a/llms.txt +++ b/llms.txt @@ -474,9 +474,10 @@ t, err := p.Check(env, expr.WithFuncType("concat", concatType)) Types: `expr.Any`, `Null()`, `Bool()`, `Number()`, `String()`, `EnumOf(...)`, `ListOf(t)`, `ObjectOf(fields)` (closed), `OpenObject(fields)`, `t.OrNil()`, `t.NonNil()`, `Join(a, b)`. Check catches unknown names and fields, a -registered function used as a value instead of called, reads of maybe-nil values without `?.`/`try`, wrong-kind operations, -builtin calls with the wrong arguments, and comparisons that are never -true (including against an enum). A nil `*Type` means `Any`. +registered function used as a value instead of called, reads of +maybe-nil values without `?.`/`try`, wrong-kind operations, builtin +calls with the wrong arguments, and comparisons that are never true +(including against an enum). A nil `*Type` means `Any`. `Template.Check(env)` checks every `${...}`. `WithFuncType(name, fn)` types a registered function: `fn(args []*expr.Type) (*expr.Type, error)` returns