Files
zen/core/expression/tests/data/analysis/closures.toml
T
stefan-gorules ffdfd807d1 feat: implement slots instead of natural language (#527)
* feat: implement slots instead of natural language

Replace the natural-language projection (`nl`) with engine-owned slot
completion: for a cursor position the engine classifies the slot being
edited (operand, operator, literal, member access, ...) and returns typed
options, literal facts and the expected type, for policies and graphs.

- zen-expression: new `slot` module (classification, literals, operators)
  and a lenient lexer mode for incomplete input; `nl` removed
- Intellisense: typed diagnostic codes with arguments, richer inspect for
  hover, dedicated parser error variants (messages unchanged)
- Engine: `Workspace::cursor_scope`, `slot`, `facts`; `rename_from` and
  `references_from`; editor spans in UTF-16 units; renames reach imported
  policies, `$nodes` reads, child graphs and output column heads; no `$root`
  completion in policies
- Node bindings: `slot`, `facts`, `cursorScope`, `slotBatch` replace
  `nl`, `nlTokenize`, `nlEncodeString`, `nlTokenizeBatch`

No change to expression evaluation: the VM, compiler, functions and the
default lexer path are untouched, and parser error messages keep their text.

* fix: harden slot classification, scopes and graph expectations

- restore master execution order; slot scopes hide writes of the edited
  block and its dependents instead of relying on block ranking
- token-start editing gated by slot state; operator, list and operand
  spans no longer splice into neighbouring tokens
- interval-aware bracket pairing for half-open and reversed intervals
- lenient lexer recovers from unknown characters (slots only)
- graph expected type follows switch and pass-through nodes and merges
  compatible output schemas
- sibling inference caps distinct values and analyses lazily
- deterministic overload return type display

* fix: order policy blocks by entity reads through closures and relationships

- record reads for `#.field` in closures so entity fields read through
  lists become dependencies
- link reads through derived lists (filter/map results) to the entity
  fields they carry and order the list after those fields
- evaluate entity blocks on single relationships, and demand entity
  paths for plain reads through relationships
- slot scopes keep the top-level variable when hiding written fields
2026-09-28 21:47:40 +02:00

265 lines
6.6 KiB
TOML

# Closure and iteration dependency tracking
# # = current iteration item (CallbackReference / Node::Pointer)
# Closures produce Iteration reads with nested inner reads
# Alias reads (e.g. item.price) appear as inner direct reads — this is the key
# information for cross-entity dependency resolution
[[test]]
name = "filter with callback ref"
expression = "filter(items, # > 10)"
input = '{"items": [5, 15, 20]}'
reads = [
{ type = "iteration", collection = ["items"], reads = [] },
]
[test.loose]
[test.strict]
[[test]]
name = "filter with alias"
expression = "filter(items as item, item.price > threshold)"
input = '{"items": [{"price": 5}], "threshold": 10}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "price"] },
{ type = "direct", path = ["threshold"] },
] },
]
[test.loose]
[test.strict]
[[test]]
name = "map with callback ref"
expression = "map(items, # * 2)"
input = '{"items": [1, 2, 3]}'
reads = [
{ type = "iteration", collection = ["items"], reads = [] },
]
[test.loose]
return_type = '{"Array":"Number"}'
[test.strict]
return_type = '{"Array":"Number"}'
[[test]]
name = "map with alias"
expression = "map(orders as order, order.total)"
input = '{"orders": [{"total": 100}]}'
reads = [
{ type = "iteration", collection = ["orders"], alias = "order", reads = [
{ type = "direct", path = ["order", "total"] },
] },
]
[test.loose]
[test.strict]
[[test]]
name = "some with alias"
expression = "some(items as item, item.active)"
input = '{"items": [{"active": true}]}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "active"] },
] },
]
[test.loose]
return_type = '"Bool"'
[test.strict]
return_type = '"Bool"'
[[test]]
name = "all with alias"
expression = "all(items as item, item.valid)"
input = '{"items": [{"valid": true}]}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "valid"] },
] },
]
[test.loose]
return_type = '"Bool"'
[test.strict]
return_type = '"Bool"'
[[test]]
name = "none with alias"
expression = "none(items as item, item.expired)"
input = '{"items": [{"expired": false}]}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "expired"] },
] },
]
[test.loose]
return_type = '"Bool"'
[test.strict]
return_type = '"Bool"'
[[test]]
name = "count with alias"
expression = "count(items as item, item.active)"
input = '{"items": [{"active": true}]}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "active"] },
] },
]
[test.loose]
return_type = '"Number"'
[test.strict]
return_type = '"Number"'
[[test]]
name = "one with alias"
expression = "one(items as item, item.special)"
input = '{"items": [{"special": true}]}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "special"] },
] },
]
[test.loose]
return_type = '"Bool"'
[test.strict]
return_type = '"Bool"'
[[test]]
name = "closure with external reference"
expression = "filter(items as item, item.price > minPrice)"
input = '{"items": [{"price": 5}], "minPrice": 3}'
reads = [
{ type = "iteration", collection = ["items"], alias = "item", reads = [
{ type = "direct", path = ["item", "price"] },
{ type = "direct", path = ["minPrice"] },
] },
]
[test.loose]
[test.strict]
[[test]]
name = "nested closure"
expression = "map(groups as group, filter(group.items as item, item.active))"
input = '{"groups": [{"items": [{"active": true}]}]}'
reads = [
{ type = "iteration", collection = ["groups"], alias = "group", reads = [
{ type = "iteration", collection = ["group", "items"], alias = "item", reads = [
{ type = "direct", path = ["item", "active"] },
] },
] },
]
[test.loose]
[test.strict]
[[test]]
name = "callback ref member access in closure"
expression = "map(items, #.price)"
input = '{"items": [{"price": 10}]}'
reads = [
{ type = "iteration", collection = ["items"], reads = [
{ type = "direct", path = ["items", "price"], via_index = true },
] },
]
[test.loose]
[test.strict]
[[test]]
name = "flatMap with alias"
expression = "flatMap(groups as group, group.items)"
input = '{"groups": [{"items": [1, 2]}]}'
reads = [
{ type = "iteration", collection = ["groups"], alias = "group", reads = [
{ type = "direct", path = ["group", "items"] },
] },
]
# flatMap(coll, body) = flatten(map(coll, body)). Since `group.items` is
# Array<Number>, flatMap returns Array<Number>.
[test.loose]
return_type = '{"Array":"Number"}'
[test.strict]
return_type = '{"Array":"Number"}'
# A closure whose collection is itself a `filter(...)` binds the outer alias
# through the filter to the underlying collection, so `x.price` surfaces as a
# read of `items.price` (via the alias) rather than leaking an unbound `x`.
[[test]]
name = "alias bound through filter collection"
expression = "map(filter(items as i, i.active) as x, x.price)"
input = '{"items": [{"active": true, "price": 10}]}'
reads = [
{ type = "iteration", collection = ["items"], alias = "i", reads = [
{ type = "direct", path = ["i", "active"] },
] },
{ type = "iteration", collection = ["items"], alias = "x", reads = [
{ type = "direct", path = ["x", "price"] },
] },
]
[test.loose]
[test.strict]
# A map whose collection is `coll ?? []` binds the alias to `coll` so aliased
# reads in the body resolve through it instead of leaking an unbound alias.
[[test]]
name = "alias bound through nullish-coalesced collection"
expression = "map(items ?? [] as i, i.price)"
input = '{"items": [{"price": 10}]}'
reads = [
{ type = "direct", path = ["items"] },
{ type = "iteration", collection = ["items"], alias = "i", reads = [
{ type = "direct", path = ["i", "price"] },
] },
]
[test.loose]
return_type = '{"Array":"Number"}'
[test.strict]
return_type = '{"Array":"Number"}'
[[test]]
name = "callback ref over any collection"
expression = "map(x, # > 1)"
input = '{"Object": {"x": "Any"}}'
[test.loose]
return_type = '{"Array": "Bool"}'
[test.strict]
return_type = '{"Array": "Bool"}'
[[test]]
name = "callback ref misuse over number collection"
expression = "map(x, # + '')"
input = '{"Object": {"x": {"Array": "Number"}}}'
[test.loose]
diagnostics = [{ source = "type_check", severity = "error" }]
[test.strict]
diagnostics = [{ source = "type_check", severity = "error" }]