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
1065 lines
26 KiB
TOML
1065 lines
26 KiB
TOML
# Nullable type system: merge behavior, member access, operators, strict mode
|
|
#
|
|
# Tests use [test.loose] and [test.strict] sections to verify both modes in one entry.
|
|
|
|
# =============================================================================
|
|
# Merge via ternary: T | null => Nullable(T)
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "merge: string | null"
|
|
expression = "flag ? name : null"
|
|
input = '{"Object": {"flag": "Bool", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "merge: null | string"
|
|
expression = "flag ? null : name"
|
|
input = '{"Object": {"flag": "Bool", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "merge: number | null"
|
|
expression = "flag ? age : null"
|
|
input = '{"Object": {"flag": "Bool", "age": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "merge: null | number"
|
|
expression = "flag ? null : age"
|
|
input = '{"Object": {"flag": "Bool", "age": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "merge: bool | null"
|
|
expression = "flag ? active : null"
|
|
input = '{"Object": {"flag": "Bool", "active": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Bool"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Bool"}'
|
|
|
|
[[test]]
|
|
name = "merge: date | null"
|
|
expression = "flag ? d : null"
|
|
input = '{"Object": {"flag": "Bool", "d": "Date"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Date"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Date"}'
|
|
|
|
[[test]]
|
|
name = "merge: null | null => Null"
|
|
expression = "flag ? null : null"
|
|
input = '{"Object": {"flag": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Null"'
|
|
|
|
[test.strict]
|
|
return_type = '"Null"'
|
|
|
|
[[test]]
|
|
name = "merge: same type no nullable"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": "String", "b": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "merge: same number no nullable"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": "Number", "b": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "merge: same bool no nullable"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": "Bool", "b": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
# =============================================================================
|
|
# Merge via ternary: Const / Enum interactions
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "merge: const | null => Nullable(Const)"
|
|
expression = "flag ? 'hello' : null"
|
|
input = '{"Object": {"flag": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": {"Const": "hello"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": {"Const": "hello"}}'
|
|
|
|
[[test]]
|
|
name = "merge: null | const => Nullable(Const)"
|
|
expression = "flag ? null : 'world'"
|
|
input = '{"Object": {"flag": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": {"Const": "world"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": {"Const": "world"}}'
|
|
|
|
[[test]]
|
|
name = "merge: const | const => Enum"
|
|
expression = "flag ? 'hello' : 'world'"
|
|
input = '{"Object": {"flag": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Enum": [null, ["hello", "world"]]}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Enum": [null, ["hello", "world"]]}'
|
|
|
|
[[test]]
|
|
name = "merge: same const => Const"
|
|
expression = "flag ? 'same' : 'same'"
|
|
input = '{"Object": {"flag": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Const": "same"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Const": "same"}'
|
|
|
|
[[test]]
|
|
name = "merge: const | string => String"
|
|
expression = "flag ? 'literal' : name"
|
|
input = '{"Object": {"flag": "Bool", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "merge: string | const => String"
|
|
expression = "flag ? name : 'literal'"
|
|
input = '{"Object": {"flag": "Bool", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
# =============================================================================
|
|
# Merge via ternary: incompatible types
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "merge: string | number => Any"
|
|
expression = "flag ? name : age"
|
|
input = '{"Object": {"flag": "Bool", "name": "String", "age": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge: bool | string => Any"
|
|
expression = "flag ? active : name"
|
|
input = '{"Object": {"flag": "Bool", "active": "Bool", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge: number | bool => Any"
|
|
expression = "flag ? age : active"
|
|
input = '{"Object": {"flag": "Bool", "age": "Number", "active": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge: any | T => Any"
|
|
expression = "flag ? x : name"
|
|
input = '{"Object": {"flag": "Bool", "x": "Any", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge: T | any => Any"
|
|
expression = "flag ? name : x"
|
|
input = '{"Object": {"flag": "Bool", "name": "String", "x": "Any"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
# =============================================================================
|
|
# Merge via ternary: Nullable inputs
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "merge: Nullable(String) | Nullable(String)"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}, "b": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "merge: Nullable(Number) | Number"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Nullable": "Number"}, "b": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "merge: Number | Nullable(Number)"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": "Number", "b": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "merge: Nullable(String) | null"
|
|
expression = "flag ? a : null"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "merge: null | Nullable(Bool)"
|
|
expression = "flag ? null : a"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Nullable": "Bool"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Bool"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Bool"}'
|
|
|
|
[[test]]
|
|
name = "merge: Nullable(String) | Number => Any (incompatible inner)"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}, "b": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "merge: Any | Nullable(String) => Any"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": "Any", "b": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
# =============================================================================
|
|
# Merge via ternary: arrays
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "merge: array | null => Nullable(Array)"
|
|
expression = "flag ? items : null"
|
|
input = '{"Object": {"flag": "Bool", "items": {"Array": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": {"Array": "Number"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": {"Array": "Number"}}'
|
|
|
|
[[test]]
|
|
name = "merge: array | array => Array"
|
|
expression = "flag ? a : b"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Array": "String"}, "b": {"Array": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "String"}'
|
|
|
|
# =============================================================================
|
|
# Nullable member access
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "member: nullable object property"
|
|
expression = "user.name"
|
|
input = '{"Object": {"user": {"Nullable": {"Object": {"name": "String"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "member: nullable object nested"
|
|
expression = "user.address"
|
|
input = '{"Object": {"user": {"Nullable": {"Object": {"address": {"Object": {"city": "String"}}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Object": {"city": "String"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": {"Object": {"city": "String"}}}'
|
|
|
|
[[test]]
|
|
name = "member: nullable object nested chain"
|
|
expression = "user.address.city"
|
|
input = '{"Object": {"user": {"Nullable": {"Object": {"address": {"Object": {"city": "String"}}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "member: nullable array index"
|
|
expression = "items[0]"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": "Number"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "member: array index introduces nullable in strict"
|
|
expression = "items[0]"
|
|
input = '{"Object": {"items": {"Array": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "member: indexed property is nullable in strict"
|
|
expression = "companies[0].revenue"
|
|
input = '{"Object": {"companies": {"Array": {"Object": {"revenue": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "member: coalesce recovers from nullable index"
|
|
expression = "companies[0].revenue ?? 0"
|
|
input = '{"Object": {"companies": {"Array": {"Object": {"revenue": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "member: nullable object unknown key"
|
|
expression = "user.unknown"
|
|
input = '{"Object": {"user": {"Nullable": {"Object": {"name": "String"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "member: null type returns Null"
|
|
expression = "x.foo"
|
|
input = '{"Object": {"x": "Null"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Null"'
|
|
|
|
[test.strict]
|
|
return_type = '"Null"'
|
|
|
|
# =============================================================================
|
|
# Nullish coalescing (??)
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "coalesce: Nullable(String) ?? const"
|
|
expression = "name ?? 'default'"
|
|
input = '{"Object": {"name": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "coalesce: Nullable(Number) ?? number"
|
|
expression = "val ?? 0"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "coalesce: Null ?? number"
|
|
expression = "x ?? 42"
|
|
input = '{"Object": {"x": "Null"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Number"'
|
|
|
|
[test.strict]
|
|
return_type = '"Number"'
|
|
|
|
[[test]]
|
|
name = "coalesce: Nullable(Const) ?? const merges into an enum"
|
|
expression = "kind ?? 'world'"
|
|
input = '{"Object": {"kind": {"Nullable": {"Const": "hello"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Enum":[null,["hello","world"]]}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Enum":[null,["hello","world"]]}'
|
|
|
|
[[test]]
|
|
name = "coalesce: non-nullable passes through"
|
|
expression = "name ?? 'fallback'"
|
|
input = '{"Object": {"name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "coalesce: Nullable(Const) ?? const => Enum"
|
|
expression = "x ?? 'other'"
|
|
input = '{"Object": {"x": {"Nullable": {"Const": "hello"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Enum": [null, ["hello", "other"]]}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Enum": [null, ["hello", "other"]]}'
|
|
|
|
[[test]]
|
|
name = "coalesce: Nullable(Bool) ?? bool"
|
|
expression = "active ?? false"
|
|
input = '{"Object": {"active": {"Nullable": "Bool"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "coalesce: Any ?? string => Any"
|
|
expression = "x ?? 'hi'"
|
|
input = '{"Object": {"x": "Any"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
# =============================================================================
|
|
# Nullable in comparisons
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "compare: nullable == literal"
|
|
expression = "val == 5"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "compare: nullable != literal"
|
|
expression = "val != 5"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "compare: nullable == null"
|
|
expression = "val == null"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "compare: nullable != null"
|
|
expression = "val != null"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "compare: nullable string == const"
|
|
expression = "name == 'hello'"
|
|
input = '{"Object": {"name": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
[[test]]
|
|
name = "compare: nullable < number => error"
|
|
expression = "val < 10"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "compare: nullable > number => error"
|
|
expression = "val > 0"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
# =============================================================================
|
|
# Nullable in arithmetic (must null-check first)
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "arith: nullable + number => error"
|
|
expression = "val + 1"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "arith: nullable - number => error"
|
|
expression = "val - 1"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "arith: nullable * number => error"
|
|
expression = "val * 2"
|
|
input = '{"Object": {"val": {"Nullable": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "arith: nullable string + string => error"
|
|
expression = "name + ' world'"
|
|
input = '{"Object": {"name": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
# =============================================================================
|
|
# Nullable in logical operators
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "logical: nullable and bool => error"
|
|
expression = "a and b"
|
|
input = '{"Object": {"a": {"Nullable": "Bool"}, "b": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "logical: nullable or bool => error"
|
|
expression = "a or b"
|
|
input = '{"Object": {"a": {"Nullable": "Bool"}, "b": "Bool"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "logical: not nullable => error"
|
|
expression = "not a"
|
|
input = '{"Object": {"a": {"Nullable": "Bool"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
# =============================================================================
|
|
# Edge cases
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "edge: deeply nested ternary with nulls"
|
|
expression = "a ? (b ? x : null) : null"
|
|
input = '{"Object": {"a": "Bool", "b": "Bool", "x": "Number"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "Number"}'
|
|
|
|
[[test]]
|
|
name = "edge: chained coalesce"
|
|
expression = "a ?? b ?? 'default'"
|
|
input = '{"Object": {"a": {"Nullable": "String"}, "b": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "edge: coalesce then member"
|
|
expression = "(user ?? fallback).name"
|
|
input = '{"Object": {"user": {"Nullable": {"Object": {"name": "String"}}}, "fallback": {"Object": {"name": "String"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "edge: ternary producing Nullable then coalesce"
|
|
expression = "(flag ? name : null) ?? 'none'"
|
|
input = '{"Object": {"flag": "Bool", "name": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "edge: ternary with Any branch"
|
|
expression = "flag ? x : null"
|
|
input = '{"Object": {"flag": "Bool", "x": "Any"}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
|
|
[[test]]
|
|
name = "edge: double nullable merge stays single"
|
|
expression = "a ? (b ? x : null) : null"
|
|
input = '{"Object": {"a": "Bool", "b": "Bool", "x": "String"}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "edge: Nullable(Nullable) does not nest from merge"
|
|
expression = "flag ? a : null"
|
|
input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "edge: in operator with nullable array => error"
|
|
expression = "3 in items"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": "Number"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "edge: not in with nullable array => error"
|
|
expression = "3 not in items"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": "Number"}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[test.strict]
|
|
return_type = '"Any"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "edge: in with non-nullable array works"
|
|
expression = "3 in items"
|
|
input = '{"Object": {"items": {"Array": "Number"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
|
|
# =============================================================================
|
|
# Nullable in closures
|
|
# =============================================================================
|
|
|
|
[[test]]
|
|
name = "closure: map over nullable array"
|
|
expression = "map(items as item, item.price)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Number"}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: map over nullable array with fallback"
|
|
expression = "map(items ?? [], #.price)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[[test]]
|
|
name = "closure: map unknown property on nullable array (strict error)"
|
|
expression = "map(items as item, item.unknown)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Any"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Any"}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: filter over nullable array"
|
|
expression = "filter(items as item, item.active)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"active": "Bool"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Nullable": {"Array": {"Object": {"active": "Bool"}}}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": {"Array": {"Object": {"active": "Bool"}}}}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: some over nullable array"
|
|
expression = "some(items as item, item.active)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"active": "Bool"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"Bool"'
|
|
|
|
[test.strict]
|
|
return_type = '"Bool"'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: map over non-nullable array valid member"
|
|
expression = "map(companies as c, c.name)"
|
|
input = '{"Object": {"companies": {"Array": {"Object": {"name": "String", "revenue": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "String"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "String"}'
|
|
|
|
[[test]]
|
|
name = "closure: map over non-nullable array unknown member (strict error)"
|
|
expression = "map(companies as c, c.unknownProperty)"
|
|
input = '{"Object": {"companies": {"Array": {"Object": {"name": "String", "revenue": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Any"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Any"}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: callback ref on nullable array"
|
|
expression = "map(items, #.price)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Number"}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: callback ref unknown property (strict error)"
|
|
expression = "map(items, #.unknown)"
|
|
input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Any"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Any"}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: nested nullable array closure"
|
|
expression = "map(groups as g, map(g.items as item, item.value))"
|
|
input = '{"Object": {"groups": {"Nullable": {"Array": {"Object": {"items": {"Array": {"Object": {"value": "Number"}}}}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": {"Array": "Number"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": {"Array": "Number"}}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "closure: nested nullable array closure with fallback"
|
|
expression = "map(groups ?? [], map(#.items, #.value))"
|
|
input = '{"Object": {"groups": {"Nullable": {"Array": {"Object": {"items": {"Array": {"Object": {"value": "Number"}}}}}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": {"Array": "Number"}}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": {"Array": "Number"}}'
|
|
|
|
[[test]]
|
|
name = "string number index"
|
|
expression = "name[0]"
|
|
input = '{"name": "hello"}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "string number index concat"
|
|
expression = "name[0] + 'x'"
|
|
input = '{"name": "hello"}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
diagnostics = [
|
|
{ source = "type_check", severity = "error" },
|
|
]
|
|
|
|
[[test]]
|
|
name = "string number index coalesce concat"
|
|
expression = "(name[0] ?? '') + 'x'"
|
|
input = '{"name": "hello"}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '"String"'
|
|
|
|
[[test]]
|
|
name = "flatten nullable array strict error"
|
|
expression = "flatten(matrix)"
|
|
input = '{"Object": {"matrix": {"Nullable": {"Array": {"Array": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Number"}'
|
|
diagnostics = [{ source = "type_check", severity = "error" }]
|
|
|
|
[[test]]
|
|
name = "flatten nullable array with fallback"
|
|
expression = "flatten(matrix ?? [])"
|
|
input = '{"Object": {"matrix": {"Nullable": {"Array": {"Array": "Number"}}}}}'
|
|
|
|
[test.loose]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[test.strict]
|
|
return_type = '{"Array": "Number"}'
|
|
|
|
[[test]]
|
|
name = "const string number index"
|
|
expression = "code[0]"
|
|
input = '{"Object": {"code": {"Const": "abc"}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|
|
|
|
[[test]]
|
|
name = "enum string number index"
|
|
expression = "status[0]"
|
|
input = '{"Object": {"status": {"Enum": [null, ["active", "inactive"]]}}}'
|
|
|
|
[test.loose]
|
|
return_type = '"String"'
|
|
|
|
[test.strict]
|
|
return_type = '{"Nullable": "String"}'
|