Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/reference/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,12 @@ appear in error chains for diagnostic purposes. Strings, runes, and
comments are not rewritten — `?.` written inside `"..."` or a
comment is preserved verbatim.

A nil-coalescing `??` operator is **not** provided. The combination
of `?.` / `?[`, operand-returning `||`, and `try(value, default)`
covers the same use cases. When the LHS is a meaningful falsey value
that should be kept (`0`, `""`, `[]`), use `try(x, default)`
explicitly.

## Helpful errors

expr annotates "not found" errors with a short hint drawn from the
Expand Down
81 changes: 51 additions & 30 deletions higher_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,45 +28,66 @@ type itEnv struct {
// special form so evalCall can dispatch via a single map lookup.
type higherOrderForm func(*Program, context.Context, *ast.CallExpr, any, int) (any, error)

// higherOrderForms maps builtin names to their special-form
// userForm describes one user-visible higher-order special form. It
// is the single source of truth that drives the dispatch table, the
// suggester's "did you mean" candidate set, and the "is a special
// form" hint shown for bare-identifier references. Adding a form
// means appending one entry here; the three derived tables stay in
// lockstep automatically.
type userForm struct {
// name is the spelling users type. For `map` this differs from
// the dispatch key because Go's parser reserves `map`; the
// engine rewrites the spelling to mapFormName before parsing.
name string
// internal is the key under which fn is registered in
// higherOrderForms. Equal to name for every form except `map`.
internal string
// callHint is the signature shown in the "is a special form"
// suggester message, e.g. `map(xs, predicate)`.
callHint string
fn higherOrderForm
}

// userForms enumerates every user-visible special form. Order is
// stable for deterministic iteration in the suggester. Populated in
// init for the same reason as higherOrderForms: the fn references
// reach back through the evaluator into specialFormHint, which reads
// this slice — Go's initialization-cycle check flags that as a
// direct cycle.
var userForms []userForm

// higherOrderForms maps dispatch keys to their special-form
// evaluators. Unlike ordinary functions in the engine's funcs map,
// these receive the raw *ast.CallExpr so they can re-evaluate the
// predicate argument per element with `it`/`index` in scope.
//
// These names are active by default but are not reserved: a
// user-registered function or an env entry of the same name shadows
// the form, matching the identifier-resolution order used everywhere
// else in expr. See the dispatch in evalCall.
//
// Populated in init rather than as a var initializer because the
// function bodies transitively reach p.evalCall, which reads this
// map — Go's initialization-cycle check flags that as a direct cycle.
// User-visible form names are not reserved: a user-registered
// function or an env entry of the same name shadows the form,
// matching the identifier-resolution order used everywhere else in
// expr. See the dispatch in evalCall.
var higherOrderForms map[string]higherOrderForm

func init() {
higherOrderForms = map[string]higherOrderForm{
// map is rewritten by preprocessSource to the internal
// identifier before parsing because `map` is a Go keyword.
mapFormName: formMap,
"filter": formFilter,
"any": formAny,
"all": formAll,
"find": formFind,
"count": formCount,
"try": formTry,
// Sentinel forms emitted by the optaccess pre-parse rewrite.
// Users do not type these names; they appear only as the
// callee of synthesized CallExpr nodes.
trySelectFormName: formTrySelect,
tryIndexFormName: formTryIndex,
}
userForms = []userForm{
{name: "map", internal: mapFormName, callHint: "map(xs, predicate)", fn: formMap},
{name: "filter", internal: "filter", callHint: "filter(xs, predicate)", fn: formFilter},
{name: "any", internal: "any", callHint: "any(xs, predicate)", fn: formAny},
{name: "all", internal: "all", callHint: "all(xs, predicate)", fn: formAll},
{name: "find", internal: "find", callHint: "find(xs, predicate)", fn: formFind},
{name: "count", internal: "count", callHint: "count(xs, predicate)", fn: formCount},
{name: "try", internal: "try", callHint: "try(value, default)", fn: formTry},
}
higherOrderForms = make(map[string]higherOrderForm, len(userForms)+2)
for _, f := range userForms {
higherOrderForms[f.internal] = f.fn
}
// Sentinel forms emitted by the optaccess pre-parse rewrite.
// Users do not type these names; they appear only as the
// callee of synthesized CallExpr nodes.
higherOrderForms[trySelectFormName] = formTrySelect
higherOrderForms[tryIndexFormName] = formTryIndex
}

// higherOrderNames is the user-visible list of special-form builtins.
// It drives "did you mean" hints so users see the spelling they would
// actually type (`map`, not the internal rewritten name).
var higherOrderNames = []string{"map", "filter", "any", "all", "find", "count", "try"}

// iterItems evaluates collExpr and converts the result to a []any for
// predicate iteration. nil is treated as an empty list so
// `map(nil, it)` / `filter(nil, it > 0)` return empty without error.
Expand Down
4 changes: 4 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ config?.feature?.enabled
The rewrite is token-level and skips strings, runes, and comments,
so `"obj?.field"` and `// obj?.field` survive untouched.

There is no `??` operator. Use `?.` / `?[` with operand-returning
`||` for nil-or-fallback, and `try(x, default)` when the LHS may be
a meaningful falsey value you want to keep.

## Custom functions

```go
Expand Down
26 changes: 8 additions & 18 deletions suggest.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,12 @@ func identHint(env any, funcs map[string]any, name string, fieldTags *structTagC
// matches the form's actual arity so users see the correct shape
// (`try(value, default)` vs `count(xs, predicate)`).
func specialFormHint(name string) (string, bool) {
sig, ok := formCallHints[name]
if !ok {
return "", false
for _, f := range userForms {
if f.name == name {
return fmt.Sprintf(" (%q is a special form, did you mean to call %s?)", name, f.callHint), true
}
}
return fmt.Sprintf(" (%q is a special form, did you mean to call %s?)", name, sig), true
}

// formCallHints maps each higher-order form to its call signature for
// use in "is a special form" hints. Kept in lockstep with
// higherOrderForms in higher_order.go.
var formCallHints = map[string]string{
"map": "map(xs, predicate)",
"filter": "filter(xs, predicate)",
"any": "any(xs, predicate)",
"all": "all(xs, predicate)",
"find": "find(xs, predicate)",
"count": "count(xs, predicate)",
"try": "try(value, default)",
return "", false
}

// fieldHint is identHint's counterpart for selector and index
Expand Down Expand Up @@ -178,7 +166,9 @@ func availableIdents(env any, funcs map[string]any, fieldTags *structTagConfig)
}
// Include the higher-order special forms under their user-visible
// names so a typo of `map` or `filter` suggests the right thing.
names = append(names, higherOrderNames...)
for _, f := range userForms {
names = append(names, f.name)
}
return names
}

Expand Down
Loading