mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 00:02:18 +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
368 lines
7.1 KiB
TOML
368 lines
7.1 KiB
TOML
# Error and warning diagnostics from all pipeline stages
|
|
|
|
# Type errors
|
|
|
|
[[test]]
|
|
name = "type error: string minus number"
|
|
expression = "a - b"
|
|
input = '{"a": "hello", "b": 5}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "type error: not on number"
|
|
expression = "not a"
|
|
input = '{"a": 5}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
# Type warnings
|
|
|
|
[[test]]
|
|
name = "type warning: always false comparison"
|
|
expression = "a == b"
|
|
input = '{"a": "hello", "b": 5}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "warning" },
|
|
]
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "warning" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "type warning: always true not-equal"
|
|
expression = "a != b"
|
|
input = '{"a": "hello", "b": 5}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "warning" },
|
|
]
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "warning" },
|
|
]
|
|
|
|
# Lexer errors
|
|
|
|
[[test]]
|
|
name = "lexer error: invalid symbol"
|
|
expression = "@invalid"
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "lexer", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "lexer", severity = "error" },
|
|
]
|
|
|
|
# Parser errors
|
|
|
|
[[test]]
|
|
name = "parser error: incomplete expression"
|
|
expression = "a +"
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "parser error: unmatched paren"
|
|
expression = "(a + b"
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "unknown method call causes parse error"
|
|
expression = "name.upper()"
|
|
input = '{"name": "John"}'
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error" },
|
|
]
|
|
|
|
# Invalid member access on typed objects
|
|
|
|
[[test]]
|
|
name = "invalid member on typed object"
|
|
expression = "customer.nonExistent"
|
|
input = '{"customer": {"name": "John", "age": 30}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "valid member on typed object no error"
|
|
expression = "customer.name"
|
|
input = '{"customer": {"name": "John", "age": 30}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "invalid nested member"
|
|
expression = "order.customer.nonExistent"
|
|
input = '{"order": {"customer": {"name": "John"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "closure alias invalid member"
|
|
expression = "map(items as item, item.nonExistent)"
|
|
input = '{"items": [{"price": 10, "name": "a"}]}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array":"Any"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array":"Any"}'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "closure alias valid member no error"
|
|
expression = "map(items as item, item.price)"
|
|
input = '{"items": [{"price": 10, "name": "a"}]}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array":"Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array":"Number"}'
|
|
|
|
[[test]]
|
|
name = "closure callback ref invalid member"
|
|
expression = "map(items, #.nonExistent)"
|
|
input = '{"items": [{"price": 10, "name": "a"}]}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array":"Any"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array":"Any"}'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "nested closure invalid inner member"
|
|
expression = "map(groups as g, sum(map(g.items as i, i.nonExistent)))"
|
|
input = '{"groups": [{"items": [{"value": 1}]}]}'
|
|
|
|
[test.loose]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "empty object allows any member even in strict"
|
|
expression = "data.anything"
|
|
input = '{"data": {}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "conditional string vs number merges to any"
|
|
expression = "flag ? a : b"
|
|
input = '{"flag": true, "a": "hello", "b": 123}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "member on conditional any result is permissive in strict"
|
|
expression = "(flag ? obj1 : obj2).name"
|
|
input = '{"flag": true, "obj1": {"name": "John"}, "obj2": {"age": 30}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "slice with non-number from index"
|
|
expression = "arr[bad:2]"
|
|
input = '{"arr": [1, 2, 3], "bad": "x"}'
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "slice with non-number to index"
|
|
expression = "arr[0:bad]"
|
|
input = '{"arr": [1, 2, 3], "bad": "x"}'
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "valid slice has no diagnostics"
|
|
expression = "arr[1:2]"
|
|
input = '{"arr": [1, 2, 3]}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array":"Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array":"Number"}'
|
|
|
|
# Diagnostic codes
|
|
|
|
[[test]]
|
|
name = "code: unknown member"
|
|
expression = "customer.nme"
|
|
input = '{"customer": {"name": "x"}}'
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error", code = "type.unknown-member", args = { name = "nme" } },
|
|
]
|
|
|
|
[[test]]
|
|
name = "code: invalid enum member"
|
|
expression = "status == 'pending'"
|
|
input = '{"Object":{"status":{"Enum":["status",["open","closed"]]}}}'
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error", code = "type.invalid-enum-member", args = { value = "pending", enum = "status" } },
|
|
]
|
|
|
|
[[test]]
|
|
name = "code: function argument mismatch"
|
|
expression = "upper(5)"
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error", code = "type.mismatch", args = { got = "number" } },
|
|
]
|
|
|
|
[[test]]
|
|
name = "code: expected property"
|
|
expression = "customer."
|
|
input = '{"customer": {"name": "x"}}'
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error", code = "expr.expected-property" },
|
|
]
|
|
|
|
|
|
[[test]]
|
|
name = "code: missing value after operator"
|
|
expression = "a *"
|
|
input = '{"a": 1}'
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "parser", severity = "error", code = "expr.missing-value", args = { operator = "*" } },
|
|
]
|