mirror of
https://github.com/gorules/zen.git
synced 2026-10-05 00:02:26 +00:00
* 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
265 lines
6.6 KiB
TOML
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" }]
|