mirror of
https://github.com/gorules/zen.git
synced 2026-10-03 16:02:27 +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
311 lines
5.8 KiB
TOML
311 lines
5.8 KiB
TOML
# Built-in function type inference and dependency tracking
|
|
|
|
[[test]]
|
|
name = "sum function"
|
|
expression = "sum([a, b, c])"
|
|
input = '{"a": 1, "b": 2, "c": 3}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
{ type = "direct", path = ["c"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "len function"
|
|
expression = "len(items)"
|
|
input = '{"items": [1, 2, 3]}'
|
|
reads = [
|
|
{ type = "direct", path = ["items"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "max function"
|
|
expression = "max([a, b])"
|
|
input = '{"a": 10, "b": 20}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "min function"
|
|
expression = "min([a, b])"
|
|
input = '{"a": 10, "b": 20}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "floor function"
|
|
expression = "floor(x)"
|
|
input = '{"x": 5.7}'
|
|
reads = [
|
|
{ type = "direct", path = ["x"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "ceil function"
|
|
expression = "ceil(x)"
|
|
input = '{"x": 5.2}'
|
|
reads = [
|
|
{ type = "direct", path = ["x"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "abs function"
|
|
expression = "abs(x)"
|
|
input = '{"x": -5}'
|
|
reads = [
|
|
{ type = "direct", path = ["x"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "upper function"
|
|
expression = "upper(name)"
|
|
input = '{"name": "hello"}'
|
|
reads = [
|
|
{ type = "direct", path = ["name"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "lower function"
|
|
expression = "lower(name)"
|
|
input = '{"name": "HELLO"}'
|
|
reads = [
|
|
{ type = "direct", path = ["name"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "contains function"
|
|
expression = "contains(text, 'world')"
|
|
input = '{"text": "hello world"}'
|
|
reads = [
|
|
{ type = "direct", path = ["text"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "startsWith function"
|
|
expression = "startsWith(text, 'hello')"
|
|
input = '{"text": "hello world"}'
|
|
reads = [
|
|
{ type = "direct", path = ["text"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "endsWith function"
|
|
expression = "endsWith(text, 'world')"
|
|
input = '{"text": "hello world"}'
|
|
reads = [
|
|
{ type = "direct", path = ["text"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "merge of an any-typed array"
|
|
expression = "merge([a, b])"
|
|
input = '{"Object":{"a":"Any","b":"Any"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge of empty array literal"
|
|
expression = "merge([])"
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge of scalar array errors"
|
|
expression = "merge([1, 2])"
|
|
|
|
[test.loose]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "merge nullable object with object"
|
|
expression = "merge([a, {y: 1}])"
|
|
input = '{"Object": {"a": {"Nullable": {"Object": {"x": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Object": {"x": "Number", "y": "Number"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Object": {"x": "Number", "y": "Number"}}'
|
|
|
|
[[test]]
|
|
name = "merge null literal with object"
|
|
expression = "merge([null, {y: 1}])"
|
|
|
|
[test.loose]
|
|
return_type = '{"Object": {"y": "Number"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Object": {"y": "Number"}}'
|
|
|
|
[[test]]
|
|
name = "mergeDeep nullable object with object"
|
|
expression = "mergeDeep([a, {y: 1}])"
|
|
input = '{"Object": {"a": {"Nullable": {"Object": {"x": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Object": {"x": "Number", "y": "Number"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Object": {"x": "Number", "y": "Number"}}'
|
|
|
|
[[test]]
|
|
name = "merge of non-array argument errors"
|
|
expression = "merge(a)"
|
|
input = '{"Object": {"a": {"Object": {"x": "Number"}}}}'
|
|
|
|
[test.loose]
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "mergeDeep of scalar array errors"
|
|
expression = "mergeDeep([1, 2])"
|
|
|
|
[test.loose]
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "date comparison methods return bool"
|
|
expression = "d(a).isSameOrAfter(d(b))"
|
|
input = '{"a": "2023-10-15", "b": "2023-10-14"}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "date comparison with unit returns bool"
|
|
expression = "d(a).isBefore(d(b), 'day')"
|
|
input = '{"a": "2023-10-15", "b": "2023-10-14"}'
|
|
reads = [
|
|
{ type = "direct", path = ["a"] },
|
|
{ type = "direct", path = ["b"] },
|
|
]
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "failed overload keeps its return type"
|
|
expression = "len(tags)"
|
|
input = '{"Object": {"tags": {"Nullable": {"Array": "String"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
{ source = "type_check", severity = "error" },
|
|
]
|