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 7226b1e..d3002cf 100644 --- a/check.go +++ b/check.go @@ -264,6 +264,10 @@ func (c *checker) ident(n *ast.Ident, sc *scope) *Type { return orAny(t) } if _, ok := c.p.funcs[name]; ok { + // 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 } 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/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 cab241b..d4c7706 100644 --- a/llms.txt +++ b/llms.txt @@ -473,10 +473,11 @@ 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, -builtin calls with the wrong arguments, and comparisons that are never -true (including against an enum). A nil `*Type` means `Any`. +`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`. `Template.Check(env)` checks every `${...}`. `WithFuncType(name, fn)` types a registered function: `fn(args []*expr.Type) (*expr.Type, error)` returns