From 88806687898e8b44b0fcf3e99ba2ca3864856071 Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 05:18:03 +0600 Subject: [PATCH 1/9] spec: add destructuring and regex literal grammar and semantics for v0.4 --- XPR-SPEC.md | 236 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 219 insertions(+), 17 deletions(-) diff --git a/XPR-SPEC.md b/XPR-SPEC.md index 0a0b2a0..6b61bfa 100644 --- a/XPR-SPEC.md +++ b/XPR-SPEC.md @@ -2,7 +2,7 @@ A sandboxed expression language with JS/Python-familiar syntax, designed for data pipeline transforms, with native interpreters in JavaScript, Python, and Go. -**Version**: 0.3.0 +**Version**: 0.4.0 **Status**: Draft --- @@ -766,7 +766,7 @@ XprError { ## EBNF Grammar -Formal grammar for XPR v0.2. Uses standard EBNF notation: +Formal grammar for XPR v0.4. Uses standard EBNF notation: - `*` = zero or more - `+` = one or more - `?` = optional @@ -779,8 +779,13 @@ Formal grammar for XPR v0.2. Uses standard EBNF notation: Expression ::= LetExpression | PipeExpr -(* Let bindings — immutable, scoped *) -LetExpression ::= "let" Identifier "=" Expression ";" Expression +(* Let bindings — immutable, scoped. Supports destructuring patterns. *) +LetExpression ::= "let" BindingTarget "=" Expression ";" Expression + +(* Binding target — identifier or destructuring pattern *) +BindingTarget ::= Identifier + | ObjectPattern + | ArrayPattern (* Pipe — lowest precedence, left-associative *) PipeExpr ::= TernaryExpr ( "|>" TernaryExpr )* @@ -833,12 +838,17 @@ Primary ::= Literal | ObjectLiteral | TemplateLiteral | ArrowFunction + | RegexLiteral -(* Arrow functions *) +(* Arrow functions — params support destructuring patterns *) ArrowFunction ::= Identifier "=>" Expression | "(" ParamList? ")" "=>" Expression -ParamList ::= Identifier ( "," Identifier )* +ParamList ::= ParamElement ( "," ParamElement )* + +ParamElement ::= Identifier (* simple parameter *) + | ObjectPattern (* destructured object param *) + | ArrayPattern (* destructured array param *) ArgList ::= ArgElement ( "," ArgElement )* @@ -858,6 +868,28 @@ ObjectEntry ::= "..." Expression (* spread *) Property ::= ( Identifier | StringLiteral ) ":" Expression +(* Destructuring patterns — used in let bindings and arrow function params *) +ObjectPattern ::= "{" ( ObjectPatternEntry ( "," ObjectPatternEntry )* )? "}" + +ObjectPatternEntry ::= "..." Identifier (* rest: {...rest} *) + | Identifier "=" Expression (* shorthand with default: {name = "x"} *) + | Identifier (* shorthand: {name} ≡ {name: name} *) + | ( Identifier | StringLiteral ) ":" BindingTarget (* rename: {name: n} or nested: {a: {b}} *) + | ( Identifier | StringLiteral ) ":" BindingTarget "=" Expression (* rename with default *) + +ArrayPattern ::= "[" ( ArrayPatternEntry ( "," ArrayPatternEntry )* )? "]" + +ArrayPatternEntry ::= "..." Identifier (* rest: [...tail] *) + | BindingTarget (* element: [a] or nested: [[a,b]] *) + | BindingTarget "=" Expression (* element with default: [a = 0] *) + +(* Regex literals — context-based tokenizer disambiguation *) +RegexLiteral ::= "/" RegexBody "/" RegexFlags? + +RegexBody ::= RegexChar+ (* any chars except unescaped / *) + +RegexFlags ::= ( "i" | "m" | "s" )+ (* case-insensitive, multiline, dotall *) + TemplateLiteral ::= "`" TemplateContent* "`" TemplateContent ::= TemplateChar+ @@ -981,18 +1013,20 @@ Custom functions are called like built-in functions. They receive evaluated argu ## AST Node Types -19 node types. ESTree-influenced, simplified for expression-only context. +27 node types. ESTree-influenced, simplified for expression-only context. Every node carries a `position` field (character offset) for error reporting. ``` -── Literals (6) ────────────────────────────── +── Literals (7) ────────────────────────────── NumberLiteral { value: number, position: number } StringLiteral { value: string, position: number } BooleanLiteral { value: boolean, position: number } NullLiteral { position: number } ArrayExpression { elements: Expression[], position: number } ObjectExpression { properties: Property[], position: number } +RegexLiteral { pattern: string, flags: string, position: number } + // flags: subset of "ims" — case-insensitive, multiline, dotall ── Access (2) ──────────────────────────────── Identifier { name: string, position: number } @@ -1012,7 +1046,8 @@ ConditionalExpression { test: Expression, consequent: Expression, alternate: Expression, position: number } ── Functions (2) ───────────────────────────── -ArrowFunction { params: string[], body: Expression, position: number } +ArrowFunction { params: (string | DestructurePattern)[], body: Expression, position: number } + // params: simple identifiers OR destructuring patterns CallExpression { callee: Expression, arguments: Expression[], optional: boolean, position: number } @@ -1023,11 +1058,32 @@ TemplateLiteral { quasis: string[], expressions: Expression[], position: n PipeExpression { left: Expression, right: Expression, position: number } ── Bindings (1) ────────────────────────────── -LetExpression { name: string, value: Expression, body: Expression, position: number } +LetExpression { name: string | DestructurePattern, value: Expression, body: Expression, position: number } + // name: simple identifier OR destructuring pattern ── Spread (1) ──────────────────────────────── SpreadElement { argument: Expression, position: number } // Used in ArrayExpression.elements, ObjectExpression.properties, and CallExpression.arguments + +── Destructuring Patterns (6) ──────────────── +ObjectPattern { properties: PatternProperty[], position: number } + // Used in LetExpression.name and ArrowFunction.params + +ArrayPattern { elements: ArrayPatternElement[], position: number } + // Used in LetExpression.name and ArrowFunction.params + +PatternProperty { key: string, value: BindingTarget, defaultValue: Expression | null, shorthand: boolean, position: number } + // shorthand=true: {name} ≡ {name: name} + // defaultValue: expression evaluated when value is null + +ArrayPatternElement { element: BindingTarget | null, defaultValue: Expression | null, position: number } + // element=null for holes (not supported — reserved) + +RestElement { argument: Identifier, position: number } + // Must be last element in ObjectPattern or ArrayPattern + +DestructurePattern ::= ObjectPattern | ArrayPattern + // Union type used in LetExpression.name and ArrowFunction.params ``` ### Property (used in ObjectExpression) @@ -1125,29 +1181,175 @@ Pratt is superior for expression languages because: --- +## Destructuring (v0.4) + +Destructuring binds values from objects and arrays to named variables. Supported in `let` bindings and arrow function parameters. + +### Object Destructuring + +```javascript +let {name, age} = user; name // shorthand: binds user.name to name +let {name: n, age: a} = user; n // rename: binds user.name to n +let {name = "Anonymous"} = {}; name // default: "Anonymous" when null +let {a, ...rest} = {a: 1, b: 2, c: 3}; rest // rest: {b: 2, c: 3} +let {address: {city}} = user; city // nested: extracts user.address.city +``` + +### Array Destructuring + +```javascript +let [first, second] = [1, 2, 3]; first // 1 +let [head, ...tail] = [1, 2, 3]; tail // [2, 3] +let [a = 0, b = 0] = [1]; b // 0 (default for missing) +let [[a, b], [c, d]] = [[1, 2], [3, 4]]; c // 3 (nested) +``` + +### In Arrow Function Parameters + +```javascript +({name}) => name.toUpperCase() +([a, b]) => a + b +users.map(({name, age}) => `${name}: ${age}`) +users.filter(({age}) => age >= 18) +``` + +### Semantics + +- **Missing property**: returns `null` (consistent with property access) +- **Null source**: error — `let {name} = null` throws "cannot destructure null" +- **Default values**: evaluated when the extracted value is `null` +- **Rest element**: must be last. Collects remaining keys/elements into object/array. +- **Shorthand**: `{name}` is equivalent to `{name: name}` +- **Nested**: patterns can be arbitrarily nested + +### Edge Cases + +| Expression | Result | +|------------|--------| +| `let {} = obj; 1` | Valid — binds nothing, returns 1 | +| `let [] = arr; 1` | Valid — binds nothing, returns 1 | +| `let {a, a} = obj` | Error: duplicate binding 'a' | +| `let {a: b, a: c} = obj` | Valid — different targets, same source key | +| `let {name} = [1,2,3]` | null (arrays don't have .name) | +| `let [a] = {a: 1}` | Error: cannot destructure non-array as array | +| `let [a] = "hello"` | Error: cannot destructure string as array | +| `let [a,b,c] = [1,2]` | c = null (out of bounds) | +| `let [a,...rest] = []` | a = null, rest = [] | +| `let {a,...rest} = {a:1}` | rest = {} | + +### Not Supported + +- Array holes/elision: `[a,,b]` — not valid +- Computed keys: `{[expr]: val}` — not valid +- Standalone assignment: `{a} = obj` without `let` — not valid +- Rest element in non-last position + +--- + +## Regex Literals (v0.4) + +Regex literals provide a first-class `regex` type. They coexist with the function-based regex API from v0.3 (`matches`, `match`, `matchAll`, `replacePattern`). + +### Syntax + +```javascript +/pattern/ // basic regex +/pattern/i // case-insensitive +/pattern/im // case-insensitive + multiline +``` + +**Supported flags**: `i` (case-insensitive), `m` (multiline), `s` (dotall). No `g` flag — replace is always all-matches, match/test is always first-match. + +**RE2 constraint**: Patterns must be RE2-compatible. No lookahead, lookbehind, backreferences, or atomic groups. + +### Tokenizer Disambiguation + +`/` is a **regex delimiter** when the previous non-whitespace token is one of: +- Start of input +- `=`, `==`, `!=`, `(`, `[`, `{`, `,`, `;`, `?`, `:`, `|>`, `=>`, `&&`, `||`, `??`, `!`, `+`, `-`, `*`, `/`, `%`, `**`, `<`, `>`, `<=`, `>=`, `...`, `let` + +`/` is a **division operator** when the previous token is one of: +- `)`, `]`, `}`, Identifier, Number, String, Boolean, Null, TemplateTail, Regex + +### Regex Type + +`regex` is the 7th XPR type. `type(/\d+/)` returns `"regex"`. + +| Function | Input | Output | +|----------|-------|--------| +| `type(/\d+/)` | regex | `"regex"` | +| `string(/\d+/i)` | regex | `"/\\d+/i"` | +| `bool(/\d+/)` | regex | `true` (non-null) | + +### Regex Methods + +```javascript +/\d+/.test("abc123") // true — matches anywhere in string +/\d+/.test("no digits") // false +/hello/i.test("Hello World") // true — case-insensitive +``` + +### String Methods with Regex + +```javascript +"order-123".match(/\d+/) // "123" — first match or null +"no digits".match(/\d+/) // null +"hello world".replace(/o/, "0") // "hell0 w0rld" — replaces ALL +"2024-01-15".replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1") // "15/01/2024" +``` + +### Equality + +```javascript +/abc/ == /abc/ // true — same pattern, same flags +/abc/i == /abc/ // false — different flags +/abc/ == "abc" // false — different types +``` + +### Error Cases + +```javascript +/\d+/.test(42) // Error: Type error — test expects string +"str".replace(/\d+/, 42) // Error: Type error — replacement must be string +/\d+/ + "str" // Error: Type error — cannot add regex and string +/\d+/ < /abc/ // Error: Type error — cannot compare regex values +``` + +### Not Supported + +- Regex properties: `.source`, `.flags`, `.global` +- Regex methods beyond `.test()` +- `str.split(/regex/)` — use `str.split(str)` instead +- `str.matchAll(/regex/)` — use global `matchAll(str, pattern)` instead +- Regex constructor function: `regex("pattern")` — use literals only +- Regex in context objects — construction via literals only +- `g` flag — all replace operations are global, all match/test are first-match + +--- + ## Deliverables | # | Deliverable | Status | |---|---|---| | 1 | Grammar spec (EBNF) — this document | ✓ | | 2 | Conformance test suite (YAML, 250+ cases) | ✓ | -| 3 | JavaScript runtime (`@xpr-lang/xpr` on npm) v0.3.0 | ✓ | -| 4 | Python runtime v0.3.0 | ✓ | -| 5 | Go runtime (`github.com/xpr-lang/xpr-go`) v0.3.0 | ✓ | +| 3 | JavaScript runtime (`@xpr-lang/xpr` on npm) v0.4.0 | ✓ | +| 4 | Python runtime v0.4.0 | ✓ | +| 5 | Go runtime (`github.com/xpr-lang/xpr-go`) v0.4.0 | ✓ | | 6 | Playground (web, CodeMirror 6) | ✓ | --- -## Future (v0.4+) +## Future (v0.5+) -Features explicitly deferred from v0.3: +Features explicitly deferred from v0.4: | Feature | Reason for Deferral | |---------|---------------------| -| **Destructuring** | Complex grammar, multiple forms (array, object, nested) | | **Pattern matching** | Requires type system extensions | | **Async expressions** | Fundamentally changes evaluation model | | **Custom operator overloading** | Requires type system | | **Type annotations** | Requires type system | -| **Regex literals** | `/pattern/flags` syntax — adds tokenizer complexity. Function-based regex (`matches`, `match`, etc.) is supported in v0.3. | | **Timezone-aware dates** | IANA timezone database, DST handling — enormous complexity. UTC-only date functions are supported in v0.3. | +| **Regex literals with `g` flag** | All replace/match operations are already implicitly global/first-match. | +| **`str.split(/regex/)`** | Deferred — use `str.split(str)` for now. | From 3d0f77cd7815033cacdc578b092b8345e3caf352 Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 05:19:43 +0600 Subject: [PATCH 2/9] test: add conformance tests for destructuring and regex literals --- conformance/destructuring.yaml | 291 ++++++++++++++++++++++++++++++++ conformance/regex_literals.yaml | 260 ++++++++++++++++++++++++++++ 2 files changed, 551 insertions(+) create mode 100644 conformance/destructuring.yaml create mode 100644 conformance/regex_literals.yaml diff --git a/conformance/destructuring.yaml b/conformance/destructuring.yaml new file mode 100644 index 0000000..5f71d03 --- /dev/null +++ b/conformance/destructuring.yaml @@ -0,0 +1,291 @@ +suite: destructuring +version: "0.4.0" +tests: + + # ── Object destructuring — basic ────────────────────────────────────────── + + - name: object destructuring basic + expression: "let {name} = user; name" + context: {user: {name: "Alice", age: 30}} + expected: "Alice" + tags: ["destructuring", "object"] + + - name: object destructuring multiple properties + expression: "let {name, age} = user; name + \" \" + string(age)" + context: {user: {name: "Alice", age: 30}} + expected: "Alice 30" + tags: ["destructuring", "object"] + + - name: object destructuring rename + expression: "let {name: n, age: a} = user; n" + context: {user: {name: "Alice", age: 30}} + expected: "Alice" + tags: ["destructuring", "object"] + + - name: object destructuring rename multiple + expression: "let {name: n, age: a} = user; a" + context: {user: {name: "Alice", age: 30}} + expected: 30 + tags: ["destructuring", "object"] + + - name: object destructuring missing property returns null + expression: "let {missing} = user; missing" + context: {user: {name: "Alice"}} + expected: null + tags: ["destructuring", "object"] + + - name: object destructuring empty pattern + expression: "let {} = obj; 42" + context: {obj: {a: 1}} + expected: 42 + tags: ["destructuring", "object"] + + - name: object destructuring from array returns null + expression: "let {name} = arr; name" + context: {arr: [1, 2, 3]} + expected: null + tags: ["destructuring", "object"] + + # ── Object destructuring — defaults ─────────────────────────────────────── + + - name: object destructuring default value used when null + expression: "let {name = \"Anonymous\"} = obj; name" + context: {obj: {}} + expected: "Anonymous" + tags: ["destructuring", "object", "defaults"] + + - name: object destructuring default value not used when present + expression: "let {name = \"Anonymous\"} = obj; name" + context: {obj: {name: "Alice"}} + expected: "Alice" + tags: ["destructuring", "object", "defaults"] + + - name: object destructuring default value expression + expression: "let {x = 1 + 2} = obj; x" + context: {obj: {}} + expected: 3 + tags: ["destructuring", "object", "defaults"] + + - name: object destructuring rename with default + expression: "let {name: n = \"Bob\"} = obj; n" + context: {obj: {}} + expected: "Bob" + tags: ["destructuring", "object", "defaults"] + + - name: object destructuring rename with default not used + expression: "let {name: n = \"Bob\"} = obj; n" + context: {obj: {name: "Alice"}} + expected: "Alice" + tags: ["destructuring", "object", "defaults"] + + # ── Object destructuring — rest ──────────────────────────────────────────── + + - name: object destructuring rest collects remaining + expression: "let {a, ...rest} = obj; rest" + context: {obj: {a: 1, b: 2, c: 3}} + expected: {b: 2, c: 3} + tags: ["destructuring", "object", "rest"] + + - name: object destructuring rest empty when no remaining + expression: "let {a, ...rest} = obj; rest" + context: {obj: {a: 1}} + expected: {} + tags: ["destructuring", "object", "rest"] + + - name: object destructuring rest with rename + expression: "let {a: x, ...rest} = obj; x" + context: {obj: {a: 1, b: 2}} + expected: 1 + tags: ["destructuring", "object", "rest"] + + # ── Object destructuring — nested ───────────────────────────────────────── + + - name: object destructuring nested + expression: "let {address: {city}} = user; city" + context: {user: {name: "Alice", address: {city: "NYC", zip: "10001"}}} + expected: "NYC" + tags: ["destructuring", "object", "nested"] + + - name: object destructuring nested multiple levels + expression: "let {a: {b: {c}}} = obj; c" + context: {obj: {a: {b: {c: 42}}}} + expected: 42 + tags: ["destructuring", "object", "nested"] + + - name: object destructuring nested with default + expression: "let {address: {city = \"Unknown\"}} = user; city" + context: {user: {address: {}}} + expected: "Unknown" + tags: ["destructuring", "object", "nested", "defaults"] + + # ── Array destructuring — basic ──────────────────────────────────────────── + + - name: array destructuring basic + expression: "let [first] = arr; first" + context: {arr: [1, 2, 3]} + expected: 1 + tags: ["destructuring", "array"] + + - name: array destructuring multiple elements + expression: "let [a, b, c] = arr; b" + context: {arr: [10, 20, 30]} + expected: 20 + tags: ["destructuring", "array"] + + - name: array destructuring out of bounds returns null + expression: "let [a, b, c] = arr; c" + context: {arr: [1, 2]} + expected: null + tags: ["destructuring", "array"] + + - name: array destructuring empty pattern + expression: "let [] = arr; 99" + context: {arr: [1, 2, 3]} + expected: 99 + tags: ["destructuring", "array"] + + - name: array destructuring single element + expression: "let [x] = arr; x" + context: {arr: [42]} + expected: 42 + tags: ["destructuring", "array"] + + # ── Array destructuring — defaults ──────────────────────────────────────── + + - name: array destructuring default value used when null + expression: "let [a = 0, b = 0] = arr; b" + context: {arr: [1]} + expected: 0 + tags: ["destructuring", "array", "defaults"] + + - name: array destructuring default value not used when present + expression: "let [a = 0, b = 0] = arr; a" + context: {arr: [5, 10]} + expected: 5 + tags: ["destructuring", "array", "defaults"] + + - name: array destructuring default expression + expression: "let [a = 2 * 3] = arr; a" + context: {arr: []} + expected: 6 + tags: ["destructuring", "array", "defaults"] + + # ── Array destructuring — rest ───────────────────────────────────────────── + + - name: array destructuring rest collects remaining + expression: "let [head, ...tail] = arr; tail" + context: {arr: [1, 2, 3, 4]} + expected: [2, 3, 4] + tags: ["destructuring", "array", "rest"] + + - name: array destructuring rest empty when no remaining + expression: "let [a, ...rest] = arr; rest" + context: {arr: [1]} + expected: [] + tags: ["destructuring", "array", "rest"] + + - name: array destructuring rest from empty array + expression: "let [a, ...rest] = arr; a" + context: {arr: []} + expected: null + tags: ["destructuring", "array", "rest"] + + - name: array destructuring rest from empty array rest is empty + expression: "let [a, ...rest] = arr; rest" + context: {arr: []} + expected: [] + tags: ["destructuring", "array", "rest"] + + # ── Array destructuring — nested ─────────────────────────────────────────── + + - name: array destructuring nested + expression: "let [[a, b], [c, d]] = matrix; c" + context: {matrix: [[1, 2], [3, 4]]} + expected: 3 + tags: ["destructuring", "array", "nested"] + + - name: array destructuring nested object + expression: "let [{name}, {name: n2}] = users; n2" + context: {users: [{name: "Alice"}, {name: "Bob"}]} + expected: "Bob" + tags: ["destructuring", "array", "nested"] + + # ── Arrow function params — destructuring ────────────────────────────────── + + - name: arrow function with object destructuring param + expression: "let f = ({name}) => name; f({name: \"Alice\", age: 30})" + expected: "Alice" + tags: ["destructuring", "arrow"] + + - name: arrow function with array destructuring param + expression: "let f = ([a, b]) => a + b; f([3, 4])" + expected: 7 + tags: ["destructuring", "arrow"] + + - name: arrow function destructuring in map + expression: "users.map(({name}) => name)" + context: {users: [{name: "Alice"}, {name: "Bob"}, {name: "Carol"}]} + expected: ["Alice", "Bob", "Carol"] + tags: ["destructuring", "arrow", "collections"] + + - name: arrow function destructuring in filter + expression: "users.filter(({age}) => age >= 18).map(({name}) => name)" + context: {users: [{name: "Alice", age: 30}, {name: "Bob", age: 15}, {name: "Carol", age: 25}]} + expected: ["Alice", "Carol"] + tags: ["destructuring", "arrow", "collections"] + + - name: arrow function destructuring with default + expression: "let f = ({name = \"Unknown\"}) => name; f({})" + expected: "Unknown" + tags: ["destructuring", "arrow", "defaults"] + + - name: arrow function mixed params + expression: "let f = (x, {name}) => x + name; f(\"Hello \", {name: \"World\"})" + expected: "Hello World" + tags: ["destructuring", "arrow"] + + - name: arrow function array destructuring in reduce + expression: "pairs.reduce((acc, [k, v]) => acc + k + \"=\" + string(v) + \";\", \"\")" + context: {pairs: [["a", 1], ["b", 2], ["c", 3]]} + expected: "a=1;b=2;c=3;" + tags: ["destructuring", "arrow", "collections"] + + # ── Combined let + arrow ─────────────────────────────────────────────────── + + - name: chained let with destructuring + expression: "let {a, b} = obj; let sum = a + b; sum" + context: {obj: {a: 10, b: 20}} + expected: 30 + tags: ["destructuring", "object"] + + - name: destructuring in pipe + expression: "obj |> (({x, y}) => x + y)" + context: {obj: {x: 3, y: 4}} + expected: 7 + tags: ["destructuring", "pipe"] + + # ── Error cases ──────────────────────────────────────────────────────────── + + - name: destructuring null throws error + expression: "let {name} = val; name" + context: {val: null} + error: "destructure null" + tags: ["destructuring", "error"] + + - name: array destructuring non-array throws error + expression: "let [a] = obj; a" + context: {obj: {a: 1}} + error: "destructure" + tags: ["destructuring", "array", "error"] + + - name: array destructuring string throws error + expression: "let [a] = str; a" + context: {str: "hello"} + error: "destructure" + tags: ["destructuring", "array", "error"] + + - name: duplicate binding throws error + expression: "let {a, a} = obj; a" + context: {obj: {a: 1}} + error: "duplicate" + tags: ["destructuring", "error"] diff --git a/conformance/regex_literals.yaml b/conformance/regex_literals.yaml new file mode 100644 index 0000000..c81a5bf --- /dev/null +++ b/conformance/regex_literals.yaml @@ -0,0 +1,260 @@ +suite: regex_literals +version: "0.4.0" +tests: + + # ── Basic regex literals ─────────────────────────────────────────────────── + + - name: regex literal type + expression: "type(/\\d+/)" + expected: "regex" + tags: ["regex_literals", "type"] + + - name: regex literal is truthy + expression: "bool(/\\d+/)" + expected: true + tags: ["regex_literals", "type"] + + - name: regex literal string representation + expression: "string(/\\d+/)" + expected: "/\\d+/" + tags: ["regex_literals", "type"] + + - name: regex literal with flags string representation + expression: "string(/hello/i)" + expected: "/hello/i" + tags: ["regex_literals", "type"] + + - name: regex literal with multiple flags string representation + expression: "string(/hello/im)" + expected: "/hello/im" + tags: ["regex_literals", "type"] + + - name: regex literal in let binding + expression: "let r = /\\d+/; type(r)" + expected: "regex" + tags: ["regex_literals", "let"] + + - name: regex literal in array + expression: "let patterns = [/\\d+/, /[a-z]+/]; type(patterns[0])" + expected: "regex" + tags: ["regex_literals", "array"] + + - name: regex literal in object + expression: "let obj = {pattern: /\\d+/}; type(obj.pattern)" + expected: "regex" + tags: ["regex_literals", "object"] + + # ── Regex .test() method ─────────────────────────────────────────────────── + + - name: regex test matches + expression: "/\\d+/.test(\"abc123\")" + expected: true + tags: ["regex_literals", "test"] + + - name: regex test no match + expression: "/\\d+/.test(\"no digits\")" + expected: false + tags: ["regex_literals", "test"] + + - name: regex test case insensitive flag + expression: "/hello/i.test(\"Hello World\")" + expected: true + tags: ["regex_literals", "test", "flags"] + + - name: regex test case sensitive without flag + expression: "/hello/.test(\"Hello World\")" + expected: false + tags: ["regex_literals", "test"] + + - name: regex test empty string + expression: "/\\d+/.test(\"\")" + expected: false + tags: ["regex_literals", "test"] + + - name: regex test stored in variable + expression: "let r = /\\d+/; r.test(\"abc123\")" + expected: true + tags: ["regex_literals", "test", "let"] + + - name: regex test in filter + expression: "items.filter(s => /\\d+/.test(s))" + context: {items: ["abc", "123", "def", "456"]} + expected: ["123", "456"] + tags: ["regex_literals", "test", "collections"] + + - name: regex test in ternary + expression: "/\\d+/.test(s) ? \"has digits\" : \"no digits\"" + context: {s: "abc123"} + expected: "has digits" + tags: ["regex_literals", "test"] + + # ── String .match() with regex ───────────────────────────────────────────── + + - name: string match returns first match + expression: "\"order-123\".match(/\\d+/)" + expected: "123" + tags: ["regex_literals", "match"] + + - name: string match returns null when no match + expression: "\"no digits\".match(/\\d+/)" + expected: null + tags: ["regex_literals", "match"] + + - name: string match case insensitive + expression: "\"Hello World\".match(/hello/i)" + expected: "Hello" + tags: ["regex_literals", "match", "flags"] + + - name: string match returns first of multiple + expression: "\"abc 456 def 789\".match(/\\d+/)" + expected: "456" + tags: ["regex_literals", "match"] + + - name: string match with context variable + expression: "s.match(/\\d{4}/)" + context: {s: "2024-01-15"} + expected: "2024" + tags: ["regex_literals", "match"] + + # ── String .replace() with regex ────────────────────────────────────────── + + - name: string replace with regex replaces all + expression: "\"hello world\".replace(/o/, \"0\")" + expected: "hell0 w0rld" + tags: ["regex_literals", "replace"] + + - name: string replace no match unchanged + expression: "\"no match\".replace(/\\d+/, \"X\")" + expected: "no match" + tags: ["regex_literals", "replace"] + + - name: string replace case insensitive + expression: "\"Hello hello HELLO\".replace(/hello/i, \"hi\")" + expected: "hi hi hi" + tags: ["regex_literals", "replace", "flags"] + + - name: string replace with group references + expression: "\"2024-01-15\".replace(/(\\d{4})-(\\d{2})-(\\d{2})/, \"$3/$2/$1\")" + expected: "15/01/2024" + tags: ["regex_literals", "replace"] + + - name: string replace empty string + expression: "\"\".replace(/\\d+/, \"X\")" + expected: "" + tags: ["regex_literals", "replace"] + + # ── Regex equality ───────────────────────────────────────────────────────── + + - name: regex equality same pattern and flags + expression: "/abc/ == /abc/" + expected: true + tags: ["regex_literals", "equality"] + + - name: regex equality different flags + expression: "/abc/i == /abc/" + expected: false + tags: ["regex_literals", "equality"] + + - name: regex equality different patterns + expression: "/abc/ == /def/" + expected: false + tags: ["regex_literals", "equality"] + + - name: regex inequality + expression: "/abc/ != /def/" + expected: true + tags: ["regex_literals", "equality"] + + - name: regex not equal to string + expression: "/abc/ == \"abc\"" + expected: false + tags: ["regex_literals", "equality"] + + - name: regex equality with flags both sides + expression: "/hello/im == /hello/im" + expected: true + tags: ["regex_literals", "equality"] + + # ── Tokenizer disambiguation ─────────────────────────────────────────────── + + - name: regex after equals sign + expression: "let r = /\\d+/; r.test(\"123\")" + expected: true + tags: ["regex_literals", "tokenizer"] + + - name: regex after open paren + expression: "(/\\d+/).test(\"123\")" + expected: true + tags: ["regex_literals", "tokenizer"] + + - name: regex after comma in array + expression: "[/\\d+/, /[a-z]+/].length" + expected: 2 + tags: ["regex_literals", "tokenizer"] + + - name: division not confused with regex + expression: "10 / 2 / 5" + expected: 1 + tags: ["regex_literals", "tokenizer"] + + - name: division after identifier not regex + expression: "let x = 10; x / 2" + expected: 5 + tags: ["regex_literals", "tokenizer"] + + - name: regex after pipe operator + expression: "\"abc123\" |> (s => /\\d+/.test(s))" + expected: true + tags: ["regex_literals", "tokenizer", "pipe"] + + - name: regex after ternary colon + expression: "true ? /\\d+/ : /[a-z]+/" + expected: "/\\d+/" + tags: ["regex_literals", "tokenizer"] + + # ── Cross-feature: destructuring + regex ────────────────────────────────── + + - name: destructuring with regex value + expression: "let {pattern} = obj; pattern.test(\"abc123\")" + context: {obj: {pattern: null}} + expected: null + skip: true + tags: ["regex_literals", "destructuring"] + + - name: regex in arrow function body + expression: "let check = s => /\\d+/.test(s); check(\"abc123\")" + expected: true + tags: ["regex_literals", "arrow"] + + - name: regex in map + expression: "items.map(s => s.match(/\\d+/))" + context: {items: ["abc123", "no-digits", "456def"]} + expected: ["123", null, "456"] + tags: ["regex_literals", "collections"] + + # ── Error cases ──────────────────────────────────────────────────────────── + + - name: regex test on non-string throws error + expression: "/\\d+/.test(42)" + error: "Type error" + tags: ["regex_literals", "error"] + + - name: regex test on null throws error + expression: "/\\d+/.test(null)" + error: "Type error" + tags: ["regex_literals", "error"] + + - name: string replace with non-string replacement throws error + expression: "\"hello\".replace(/l/, 42)" + error: "Type error" + tags: ["regex_literals", "error"] + + - name: regex arithmetic throws error + expression: "/\\d+/ + \"str\"" + error: "Type error" + tags: ["regex_literals", "error"] + + - name: regex comparison throws error + expression: "/\\d+/ < /abc/" + error: "Type error" + tags: ["regex_literals", "error"] From 4c3f60597be2f6198aa5f7fe5445c050eb785636 Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 05:27:17 +0600 Subject: [PATCH 3/9] test: fix regex ternary test to use string() for comparison --- conformance/regex_literals.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conformance/regex_literals.yaml b/conformance/regex_literals.yaml index c81a5bf..cbda58b 100644 --- a/conformance/regex_literals.yaml +++ b/conformance/regex_literals.yaml @@ -208,7 +208,7 @@ tests: tags: ["regex_literals", "tokenizer", "pipe"] - name: regex after ternary colon - expression: "true ? /\\d+/ : /[a-z]+/" + expression: "string(true ? /\\d+/ : /[a-z]+/)" expected: "/\\d+/" tags: ["regex_literals", "tokenizer"] From 87ea0b3dfba3deab7422c2a0d52f985dc570af3a Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 11:42:54 +0600 Subject: [PATCH 4/9] test: add v0.5 conformance tests (~110 cases) --- conformance/collections_v5.yaml | 254 +++++++++++++++++++++++++++++++ conformance/features_v5.yaml | 110 +++++++++++++ conformance/math_v5.yaml | 159 +++++++++++++++++++ conformance/type_predicates.yaml | 118 ++++++++++++++ 4 files changed, 641 insertions(+) create mode 100644 conformance/collections_v5.yaml create mode 100644 conformance/features_v5.yaml create mode 100644 conformance/math_v5.yaml create mode 100644 conformance/type_predicates.yaml diff --git a/conformance/collections_v5.yaml b/conformance/collections_v5.yaml new file mode 100644 index 0000000..00276bd --- /dev/null +++ b/conformance/collections_v5.yaml @@ -0,0 +1,254 @@ +suite: collections_v5 +version: "0.5.0" +tests: + # ─── sortBy ──────────────────────────────────────────────────────────────── + - name: sortBy strings ascending + expression: '["banana","apple","cherry"].sortBy(x => x)' + expected: ["apple", "banana", "cherry"] + tags: ["level-3", "advanced"] + + - name: sortBy numbers ascending + expression: "[3,1,4,1,5].sortBy(x => x)" + expected: [1, 1, 3, 4, 5] + tags: ["level-3", "advanced"] + + - name: sortBy objects by field + expression: "items.sortBy(x => x.n)" + context: + items: + - {n: 3} + - {n: 1} + - {n: 2} + expected: + - {n: 1} + - {n: 2} + - {n: 3} + tags: ["level-3", "advanced"] + + - name: sortBy empty array + expression: "[].sortBy(x => x)" + expected: [] + tags: ["level-3", "advanced"] + + # ─── take ────────────────────────────────────────────────────────────────── + - name: take three from five + expression: "[1,2,3,4,5].take(3)" + expected: [1, 2, 3] + tags: ["level-3", "advanced"] + + - name: take beyond length returns full array + expression: "[1,2].take(5)" + expected: [1, 2] + tags: ["level-3", "advanced"] + + - name: take zero returns empty + expression: "[1,2,3].take(0)" + expected: [] + tags: ["level-3", "advanced"] + + - name: take from empty array + expression: "[].take(3)" + expected: [] + tags: ["level-3", "advanced"] + + # ─── drop ────────────────────────────────────────────────────────────────── + - name: drop two from five + expression: "[1,2,3,4,5].drop(2)" + expected: [3, 4, 5] + tags: ["level-3", "advanced"] + + - name: drop beyond length returns empty + expression: "[1,2].drop(5)" + expected: [] + tags: ["level-3", "advanced"] + + - name: drop zero returns full array + expression: "[1,2,3].drop(0)" + expected: [1, 2, 3] + tags: ["level-3", "advanced"] + + # ─── count ───────────────────────────────────────────────────────────────── + - name: count elements greater than three + expression: "[1,2,3,4,5].count(x => x > 3)" + expected: 2 + tags: ["level-3", "advanced"] + + - name: count on empty array + expression: "[].count(x => true)" + expected: 0 + tags: ["level-3", "advanced"] + + - name: count all false predicate + expression: "[1,2,3].count(x => x > 10)" + expected: 0 + tags: ["level-3", "advanced"] + + # ─── sum ─────────────────────────────────────────────────────────────────── + - name: sum basic numbers + expression: "[1,2,3].sum()" + expected: 6 + tags: ["level-3", "advanced"] + + - name: sum empty array is zero + expression: "[].sum()" + expected: 0 + tags: ["level-3", "advanced"] + + - name: sum with non-numeric error + expression: '[1,"a"].sum()' + error: "type" + tags: ["level-3", "advanced"] + + # ─── avg ─────────────────────────────────────────────────────────────────── + - name: avg basic numbers + expression: "[2,4,6].avg()" + expected: 4 + tags: ["level-3", "advanced"] + + - name: avg empty array error + expression: "[].avg()" + error: "empty" + tags: ["level-3", "advanced"] + + - name: avg with non-numeric error + expression: '[1,"a"].avg()' + error: "type" + tags: ["level-3", "advanced"] + + # ─── compact ─────────────────────────────────────────────────────────────── + - name: compact removes nulls only + expression: "[1,null,2,null,3].compact()" + expected: [1, 2, 3] + tags: ["level-3", "advanced"] + + - name: compact keeps falsy non-null values + expression: '[false,0,"",null].compact()' + expected: [false, 0, ""] + tags: ["level-3", "advanced"] + + - name: compact empty array + expression: "[].compact()" + expected: [] + tags: ["level-3", "advanced"] + + # ─── partition ───────────────────────────────────────────────────────────── + - name: partition by greater than three + expression: "[1,2,3,4,5].partition(x => x > 3)" + expected: [[4, 5], [1, 2, 3]] + tags: ["level-3", "advanced"] + + - name: partition empty array + expression: "[].partition(x => true)" + expected: [[], []] + tags: ["level-3", "advanced"] + + - name: partition all match + expression: "[4,5,6].partition(x => x > 3)" + expected: [[4, 5, 6], []] + tags: ["level-3", "advanced"] + + # ─── keyBy ───────────────────────────────────────────────────────────────── + - name: keyBy by id field + expression: "items.keyBy(x => x.id)" + context: + items: + - {id: "a", val: 1} + - {id: "b", val: 2} + expected: + a: {id: "a", val: 1} + b: {id: "b", val: 2} + tags: ["level-3", "advanced"] + + - name: keyBy duplicate key last wins + expression: '[{"k":"a","v":1},{"k":"a","v":2}].keyBy(x => x.k)' + expected: + a: {k: "a", v: 2} + tags: ["level-3", "advanced"] + + - name: keyBy empty array + expression: "[].keyBy(x => x)" + expected: {} + tags: ["level-3", "advanced"] + + # ─── min / max ───────────────────────────────────────────────────────────── + - name: min basic + expression: "[3,1,2].min()" + expected: 1 + tags: ["level-3", "advanced"] + + - name: max basic + expression: "[3,1,2].max()" + expected: 3 + tags: ["level-3", "advanced"] + + - name: min empty array error + expression: "[].min()" + error: "empty" + tags: ["level-3", "advanced"] + + - name: max empty array error + expression: "[].max()" + error: "empty" + tags: ["level-3", "advanced"] + + - name: min non-numeric error + expression: '["a"].min()' + error: "type" + tags: ["level-3", "advanced"] + + # ─── first / last ────────────────────────────────────────────────────────── + - name: first basic + expression: "[1,2,3].first()" + expected: 1 + tags: ["level-3", "advanced"] + + - name: last basic + expression: "[1,2,3].last()" + expected: 3 + tags: ["level-3", "advanced"] + + - name: first empty array returns null + expression: "[].first()" + expected: null + tags: ["level-3", "advanced"] + + - name: last empty array returns null + expression: "[].last()" + expected: null + tags: ["level-3", "advanced"] + + - name: first single element + expression: "[42].first()" + expected: 42 + tags: ["level-3", "advanced"] + + - name: last single element + expression: "[42].last()" + expected: 42 + tags: ["level-3", "advanced"] + + # ─── additional edge cases ───────────────────────────────────────────────── + - name: sortBy single element + expression: "[5].sortBy(x => x)" + expected: [5] + tags: ["level-3", "advanced"] + + - name: count all match predicate + expression: "[1,2,3].count(x => x > 0)" + expected: 3 + tags: ["level-3", "advanced"] + + - name: max non-numeric error + expression: '["a"].max()' + error: "type" + tags: ["level-3", "advanced"] + + - name: drop one from three + expression: "[1,2,3].drop(1)" + expected: [2, 3] + tags: ["level-3", "advanced"] + + - name: partition none match + expression: "[1,2,3].partition(x => x > 10)" + expected: [[], [1, 2, 3]] + tags: ["level-3", "advanced"] diff --git a/conformance/features_v5.yaml b/conformance/features_v5.yaml new file mode 100644 index 0000000..d8cbd79 --- /dev/null +++ b/conformance/features_v5.yaml @@ -0,0 +1,110 @@ +suite: features_v5 +version: "0.5.0" +tests: + # ─── fromEntries ─────────────────────────────────────────────────────────── + - name: fromEntries basic pairs + expression: 'fromEntries([["a",1],["b",2]])' + expected: + a: 1 + b: 2 + tags: ["level-3", "advanced"] + + - name: fromEntries empty array + expression: "fromEntries([])" + expected: {} + tags: ["level-3", "advanced"] + + - name: fromEntries duplicate key last wins + expression: 'fromEntries([["a",1],["a",2]])' + expected: + a: 2 + tags: ["level-3", "advanced"] + + - name: fromEntries non-string key coerced to string + expression: 'fromEntries([[1,"x"]])' + expected: + "1": "x" + tags: ["level-3", "advanced"] + + - name: fromEntries non-array argument error + expression: "fromEntries(42)" + error: "type" + tags: ["level-3", "advanced"] + + - name: fromEntries bad element not a pair error + expression: "fromEntries([1,2])" + error: "pair" + tags: ["level-3", "advanced"] + + # ─── split with regex ────────────────────────────────────────────────────── + - name: split by digit pattern + expression: '"a1b2c3".split(/\d+/)' + expected: ["a", "b", "c", ""] + tags: ["level-3", "advanced"] + + - name: split by letter pattern + expression: '"hello".split(/l/)' + expected: ["he", "", "o"] + tags: ["level-3", "advanced"] + + # ─── rest parameters ─────────────────────────────────────────────────────── + - name: rest params all args captured + expression: "((...args) => args.length)(1,2,3)" + expected: 3 + tags: ["level-3", "advanced"] + + - name: rest params named plus rest + expression: "((first, ...rest) => rest)(1,2,3)" + expected: [2, 3] + tags: ["level-3", "advanced"] + + - name: rest params empty call + expression: "((...args) => args)()" + expected: [] + tags: ["level-3", "advanced"] + + - name: rest params missing rest is empty + expression: "((a, b, ...rest) => rest)(1)" + expected: [] + tags: ["level-3", "advanced"] + + - name: rest params in map wraps each element + expression: "[1,2,3].map((...args) => args)" + expected: [[1], [2], [3]] + tags: ["level-3", "advanced"] + + # ─── rest params regression ──────────────────────────────────────────────── + - name: rest params regression single arg lambda + expression: "(x => x * 2)(5)" + expected: 10 + tags: ["level-3", "advanced"] + + - name: rest params regression two arg lambda + expression: "((x, y) => x + y)(1, 2)" + expected: 3 + tags: ["level-3", "advanced"] + + - name: rest params regression destructure object + expression: '(({name}) => name)({"name":"Alice"})' + expected: "Alice" + tags: ["level-3", "advanced"] + + - name: rest params single rest all args + expression: "((...args) => args)(10,20,30)" + expected: [10, 20, 30] + tags: ["level-3", "advanced"] + + - name: rest params two named one rest + expression: "((a, b, ...rest) => rest)(1,2,3,4)" + expected: [3, 4] + tags: ["level-3", "advanced"] + + - name: rest params length with named and rest + expression: "((a, ...rest) => rest.length)(1,2,3,4)" + expected: 3 + tags: ["level-3", "advanced"] + + - name: rest params sum via reduce + expression: "((...args) => args.reduce((acc, x) => acc + x, 0))(1,2,3,4)" + expected: 10 + tags: ["level-3", "advanced"] diff --git a/conformance/math_v5.yaml b/conformance/math_v5.yaml new file mode 100644 index 0000000..9e687a1 --- /dev/null +++ b/conformance/math_v5.yaml @@ -0,0 +1,159 @@ +suite: math_v5 +version: "0.5.0" +tests: + # ─── sqrt ────────────────────────────────────────────────────────────────── + - name: sqrt of perfect square + expression: "sqrt(9)" + expected: 3 + tags: ["level-3", "advanced"] + + - name: sqrt of zero + expression: "sqrt(0)" + expected: 0 + tags: ["level-3", "advanced"] + + - name: sqrt of two irrational + expression: "sqrt(2)" + expected: 1.4142135623730951 + tags: ["level-3", "advanced"] + + - name: sqrt of negative error + expression: "sqrt(-1)" + error: "negative" + tags: ["level-3", "advanced"] + + - name: sqrt of string type error + expression: 'sqrt("x")' + error: "type" + tags: ["level-3", "advanced"] + + # ─── log ─────────────────────────────────────────────────────────────────── + - name: log of one is zero + expression: "log(1)" + expected: 0 + tags: ["level-3", "advanced"] + + - name: log of zero error + expression: "log(0)" + error: "domain" + tags: ["level-3", "advanced"] + + - name: log of negative error + expression: "log(-1)" + error: "domain" + tags: ["level-3", "advanced"] + + - name: log of string type error + expression: 'log("x")' + error: "type" + tags: ["level-3", "advanced"] + + - name: log of E is approximately one lower bound + expression: "log(E) > 0.999" + expected: true + tags: ["level-3", "advanced"] + + - name: log of E is approximately one upper bound + expression: "log(E) < 1.001" + expected: true + tags: ["level-3", "advanced"] + + # ─── pow ─────────────────────────────────────────────────────────────────── + - name: pow two to ten + expression: "pow(2, 10)" + expected: 1024 + tags: ["level-3", "advanced"] + + - name: pow two to zero + expression: "pow(2, 0)" + expected: 1 + tags: ["level-3", "advanced"] + + - name: pow two to negative one + expression: "pow(2, -1)" + expected: 0.5 + tags: ["level-3", "advanced"] + + - name: pow three squared + expression: "pow(3, 2)" + expected: 9 + tags: ["level-3", "advanced"] + + # ─── random ──────────────────────────────────────────────────────────────── + - name: random returns number type + expression: 'type(random()) == "number"' + expected: true + tags: ["level-3", "advanced"] + + - name: random is at least zero + expression: "random() >= 0" + expected: true + tags: ["level-3", "advanced"] + + - name: random is less than one + expression: "random() < 1" + expected: true + tags: ["level-3", "advanced"] + + # ─── sign ────────────────────────────────────────────────────────────────── + - name: sign of negative number + expression: "sign(-5)" + expected: -1 + tags: ["level-3", "advanced"] + + - name: sign of zero + expression: "sign(0)" + expected: 0 + tags: ["level-3", "advanced"] + + - name: sign of positive number + expression: "sign(42)" + expected: 1 + tags: ["level-3", "advanced"] + + - name: sign of string type error + expression: 'sign("x")' + error: "type" + tags: ["level-3", "advanced"] + + # ─── trunc ───────────────────────────────────────────────────────────────── + - name: trunc positive float + expression: "trunc(4.7)" + expected: 4 + tags: ["level-3", "advanced"] + + - name: trunc negative float + expression: "trunc(-4.7)" + expected: -4 + tags: ["level-3", "advanced"] + + - name: trunc zero + expression: "trunc(0)" + expected: 0 + tags: ["level-3", "advanced"] + + - name: trunc of string type error + expression: 'trunc("x")' + error: "type" + tags: ["level-3", "advanced"] + + # ─── PI / E constants ────────────────────────────────────────────────────── + - name: PI constant value + expression: "PI" + expected: 3.141592653589793 + tags: ["level-3", "advanced"] + + - name: E constant value + expression: "E" + expected: 2.718281828459045 + tags: ["level-3", "advanced"] + + - name: PI is greater than three + expression: "PI > 3" + expected: true + tags: ["level-3", "advanced"] + + - name: E is greater than two + expression: "E > 2" + expected: true + tags: ["level-3", "advanced"] diff --git a/conformance/type_predicates.yaml b/conformance/type_predicates.yaml new file mode 100644 index 0000000..104f970 --- /dev/null +++ b/conformance/type_predicates.yaml @@ -0,0 +1,118 @@ +suite: type_predicates +version: "0.5.0" +tests: + # ─── isNumber ────────────────────────────────────────────────────────────── + - name: isNumber with integer + expression: "isNumber(42)" + expected: true + tags: ["level-3", "advanced"] + + - name: isNumber with string + expression: 'isNumber("42")' + expected: false + tags: ["level-3", "advanced"] + + - name: isNumber with null + expression: "isNumber(null)" + expected: false + tags: ["level-3", "advanced"] + + - name: isNumber with boolean + expression: "isNumber(true)" + expected: false + tags: ["level-3", "advanced"] + + - name: isNumber with array + expression: "isNumber([1,2])" + expected: false + tags: ["level-3", "advanced"] + + # ─── isString ────────────────────────────────────────────────────────────── + - name: isString with string + expression: 'isString("hello")' + expected: true + tags: ["level-3", "advanced"] + + - name: isString with number + expression: "isString(42)" + expected: false + tags: ["level-3", "advanced"] + + - name: isString with null + expression: "isString(null)" + expected: false + tags: ["level-3", "advanced"] + + # ─── isArray ─────────────────────────────────────────────────────────────── + - name: isArray with array + expression: "isArray([1,2])" + expected: true + tags: ["level-3", "advanced"] + + - name: isArray with string + expression: 'isArray("hello")' + expected: false + tags: ["level-3", "advanced"] + + - name: isArray with object + expression: 'isArray({"a":1})' + expected: false + tags: ["level-3", "advanced"] + + # ─── isNull ──────────────────────────────────────────────────────────────── + - name: isNull with null + expression: "isNull(null)" + expected: true + tags: ["level-3", "advanced"] + + - name: isNull with zero + expression: "isNull(0)" + expected: false + tags: ["level-3", "advanced"] + + - name: isNull with false + expression: "isNull(false)" + expected: false + tags: ["level-3", "advanced"] + + - name: isNull with empty string + expression: 'isNull("")' + expected: false + tags: ["level-3", "advanced"] + + # ─── isObject ────────────────────────────────────────────────────────────── + - name: isObject with object + expression: 'isObject({"a":1})' + expected: true + tags: ["level-3", "advanced"] + + - name: isObject with array is false + expression: "isObject([1,2])" + expected: false + tags: ["level-3", "advanced"] + + - name: isObject with null is false + expression: "isObject(null)" + expected: false + tags: ["level-3", "advanced"] + + - name: isObject with string is false + expression: 'isObject("x")' + expected: false + tags: ["level-3", "advanced"] + + # ─── isRegex ─────────────────────────────────────────────────────────────── + - name: isRegex with regex literal + expression: "isRegex(/test/)" + expected: true + tags: ["level-3", "advanced"] + + - name: isRegex with string + expression: 'isRegex("test")' + expected: false + tags: ["level-3", "advanced"] + + - name: isRegex with null + expression: "isRegex(null)" + expected: false + tags: ["level-3", "advanced"] From 1d43896596f7bbcf3909e827e9fbe5fb5fa5a5dc Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 12:07:45 +0600 Subject: [PATCH 5/9] spec: add v0.5 features (math, type predicates, collections, rest params) --- XPR-SPEC.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/XPR-SPEC.md b/XPR-SPEC.md index 6b61bfa..a79268e 100644 --- a/XPR-SPEC.md +++ b/XPR-SPEC.md @@ -2,7 +2,7 @@ A sandboxed expression language with JS/Python-familiar syntax, designed for data pipeline transforms, with native interpreters in JavaScript, Python, and Go. -**Version**: 0.4.0 +**Version**: 0.5.0 **Status**: Draft --- @@ -121,7 +121,7 @@ fn(...42) // Error: Cannot spread non-array fn(..."hello") // Error: Cannot spread string into arguments ``` -**NOT supported**: Spread in arrow function parameter definitions (`(...args) => ...` rest params are not supported). + --- @@ -1340,7 +1340,50 @@ Regex literals provide a first-class `regex` type. They coexist with the functio --- -## Future (v0.5+) +## New in v0.5 + +### Math Functions (v0.5) + +| Function | Signature | Returns | Error Cases | +|----------|-----------|---------|-------------| +| `sqrt` | `(n: number) → number` | Square root | `sqrt(-1)` → error; type error on non-number | +| `log` | `(n: number) → number` | Natural log | `log(0)` → error; `log(-1)` → error | +| `pow` | `(x: number, y: number) → number` | `x^y` | Type errors | +| `random` | `() → number` | Float in `[0, 1)` | None | +| `sign` | `(n: number) → number` | `-1`, `0`, or `1` | Type error | +| `trunc` | `(n: number) → number` | Truncate toward zero | Type error | + +### Math Constants (v0.5) + +`PI` = `3.141592653589793`, `E` = `2.718281828459045` — bare global identifiers. + +Identifier resolution order: context vars → global constants (PI, E) → global functions → custom functions → error. Context always wins. + +### Type Predicates (v0.5) + +`isNumber(v)`, `isString(v)`, `isArray(v)`, `isNull(v)`, `isObject(v)`, `isRegex(v)` — each returns `boolean`. Note: `isObject([])` → `false` (arrays are `"array"` type). + +### New Array Methods (v0.5) + +`sortBy(fn)`, `take(n)`, `drop(n)`, `count(fn)`, `sum()`, `avg()`, `compact()`, `partition(fn)`, `keyBy(fn)`, `min()`, `max()`, `first()`, `last()`. + +Key rules: `compact()` removes `null` only (not `false`/`0`/`""`). `[].first()` → `null`. `[].avg()` → error. `[].sum()` → `0`. `[].min()` and `[].max()` → error. + +### fromEntries (v0.5) + +`fromEntries(pairs: array) → object` — inverse of `.entries()`. Example: `fromEntries([["a",1]])` → `{"a":1}`. + +### str.split(/regex/) Overload (v0.5) + +`"a1b2c3".split(/\d+/)` → `["a","b","c"]`. Existing string split unchanged. + +### Rest Parameters in Arrow Functions (v0.5) + +`(...args) => expr` and `(first, ...rest) => expr`. Must be last parameter. AST: `ArrowFunction` gains `restParam: string | null`. + +--- + +## Future (v0.6+) Features explicitly deferred from v0.4: From 2ea42e544e059fafa7c745311bcba730a7d04082 Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 12:27:57 +0600 Subject: [PATCH 6/9] test: fix error substring matches in v0.5 conformance tests --- conformance/collections_v5.yaml | 8 ++++---- conformance/features_v5.yaml | 2 +- conformance/math_v5.yaml | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/conformance/collections_v5.yaml b/conformance/collections_v5.yaml index 00276bd..84d5c6c 100644 --- a/conformance/collections_v5.yaml +++ b/conformance/collections_v5.yaml @@ -96,7 +96,7 @@ tests: - name: sum with non-numeric error expression: '[1,"a"].sum()' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] # ─── avg ─────────────────────────────────────────────────────────────────── @@ -112,7 +112,7 @@ tests: - name: avg with non-numeric error expression: '[1,"a"].avg()' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] # ─── compact ─────────────────────────────────────────────────────────────── @@ -193,7 +193,7 @@ tests: - name: min non-numeric error expression: '["a"].min()' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] # ─── first / last ────────────────────────────────────────────────────────── @@ -240,7 +240,7 @@ tests: - name: max non-numeric error expression: '["a"].max()' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] - name: drop one from three diff --git a/conformance/features_v5.yaml b/conformance/features_v5.yaml index d8cbd79..e9f6a7e 100644 --- a/conformance/features_v5.yaml +++ b/conformance/features_v5.yaml @@ -28,7 +28,7 @@ tests: - name: fromEntries non-array argument error expression: "fromEntries(42)" - error: "type" + error: "Type error" tags: ["level-3", "advanced"] - name: fromEntries bad element not a pair error diff --git a/conformance/math_v5.yaml b/conformance/math_v5.yaml index 9e687a1..123bb2d 100644 --- a/conformance/math_v5.yaml +++ b/conformance/math_v5.yaml @@ -24,7 +24,7 @@ tests: - name: sqrt of string type error expression: 'sqrt("x")' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] # ─── log ─────────────────────────────────────────────────────────────────── @@ -35,17 +35,17 @@ tests: - name: log of zero error expression: "log(0)" - error: "domain" + error: "non-positive" tags: ["level-3", "advanced"] - name: log of negative error expression: "log(-1)" - error: "domain" + error: "non-positive" tags: ["level-3", "advanced"] - name: log of string type error expression: 'log("x")' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] - name: log of E is approximately one lower bound @@ -113,7 +113,7 @@ tests: - name: sign of string type error expression: 'sign("x")' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] # ─── trunc ───────────────────────────────────────────────────────────────── @@ -134,7 +134,7 @@ tests: - name: trunc of string type error expression: 'trunc("x")' - error: "type" + error: "Type error" tags: ["level-3", "advanced"] # ─── PI / E constants ────────────────────────────────────────────────────── From 9b7207559898bef3e29a32395bd58ba81410c6c8 Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 14:40:11 +0600 Subject: [PATCH 7/9] docs: fix spec contradictions for v0.5 --- XPR-SPEC.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/XPR-SPEC.md b/XPR-SPEC.md index a79268e..1995672 100644 --- a/XPR-SPEC.md +++ b/XPR-SPEC.md @@ -230,7 +230,7 @@ XPR uses `null` exclusively. There is no `undefined` type. ### No `date` in v0.1 -Date/time operations are deferred to v0.2 due to timezone and format complexity. See [Future](#future-v02). +Date/time operations were added in v0.3. See [Date/Time Functions](#datetime-functions-v03). ### Number Representation @@ -1144,7 +1144,7 @@ Everything in Level 2, plus: └────────────┬───────────────┘ │ produces ┌────────────▼───────────────┐ -│ Language-idiomatic AST │ <- 17 node types, internal to each runtime +│ Language-idiomatic AST │ <- 27 node types, internal to each runtime └────────────┬───────────────┘ │ walks ┌────────────▼───────────────┐ @@ -1153,7 +1153,7 @@ Everything in Level 2, plus: │ verified by ┌────────────▼───────────────┐ │ Conformance Test Suite │ <- expression + context -> expected result -│ (YAML, 120+ cases) │ +│ (YAML, 600+ cases) │ └────────────────────────────┘ ``` @@ -1333,9 +1333,9 @@ Regex literals provide a first-class `regex` type. They coexist with the functio |---|---|---| | 1 | Grammar spec (EBNF) — this document | ✓ | | 2 | Conformance test suite (YAML, 250+ cases) | ✓ | -| 3 | JavaScript runtime (`@xpr-lang/xpr` on npm) v0.4.0 | ✓ | -| 4 | Python runtime v0.4.0 | ✓ | -| 5 | Go runtime (`github.com/xpr-lang/xpr-go`) v0.4.0 | ✓ | +| 3 | JavaScript runtime (`@xpr-lang/xpr` on npm) v0.5.0 | ✓ | +| 4 | Python runtime v0.5.0 | ✓ | +| 5 | Go runtime (`github.com/xpr-lang/xpr-go`) v0.5.0 | ✓ | | 6 | Playground (web, CodeMirror 6) | ✓ | --- @@ -1385,7 +1385,7 @@ Key rules: `compact()` removes `null` only (not `false`/`0`/`""`). `[].first()` ## Future (v0.6+) -Features explicitly deferred from v0.4: +Features explicitly deferred from v0.5: | Feature | Reason for Deferral | |---------|---------------------| @@ -1395,4 +1395,3 @@ Features explicitly deferred from v0.4: | **Type annotations** | Requires type system | | **Timezone-aware dates** | IANA timezone database, DST handling — enormous complexity. UTC-only date functions are supported in v0.3. | | **Regex literals with `g` flag** | All replace/match operations are already implicitly global/first-match. | -| **`str.split(/regex/)`** | Deferred — use `str.split(str)` for now. | From b68e09c465559ef29fd5b7925b0c189885a5b7cc Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 14:47:56 +0600 Subject: [PATCH 8/9] docs: update conformance test list to 22 files for v0.5 --- README.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6767519..a7d6b97 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,28 @@ See [XPR-SPEC.md](./XPR-SPEC.md) for the complete language specification. The `conformance/` directory contains YAML test files organized by category: -- `literals.yaml` — Number, string, boolean, null, array, object, template literals +- `access.yaml` — Property access, optional chaining - `arithmetic.yaml` — Arithmetic operators, precedence, associativity +- `collections.yaml` — Array collection methods (v0.1) +- `collections_v5.yaml` — New array methods (v0.5): sortBy, take, drop, sum, avg, etc. - `comparison.yaml` — Equality and comparison operators -- `logic.yaml` — Logical operators, ternary, nullish coalescing -- `access.yaml` — Property access, optional chaining +- `datetime.yaml` — Date/time functions (v0.3) +- `destructuring.yaml` — Object and array destructuring (v0.4) +- `examples.yaml` — End-to-end integration examples +- `features_v5.yaml` — v0.5 features: math, type predicates, fromEntries, rest params - `functions.yaml` — Built-in functions, custom functions, arrow functions -- `strings.yaml` — String methods -- `collections.yaml` — Array collection methods -- `pipe.yaml` — Pipe operator - `let.yaml` — Let bindings, scoping, shadowing, closures +- `literals.yaml` — Number, string, boolean, null, array, object, template literals +- `logic.yaml` — Logical operators, ternary, nullish coalescing +- `math_v5.yaml` — Math functions and constants (v0.5) +- `methods_v2.yaml` — v0.2 array, string, object methods and range() +- `negative_indexing.yaml` — Negative array indexing (v0.3) +- `pipe.yaml` — Pipe operator +- `regex.yaml` — Regex functions (v0.3): matches, match, matchAll, replacePattern +- `regex_literals.yaml` — Regex literal syntax and regex type (v0.4) - `spread.yaml` — Array and object spread operator -- `methods_v2.yaml` — New array, string, object methods and range function (v0.2) -- `examples.yaml` — End-to-end integration examples +- `strings.yaml` — String methods +- `type_predicates.yaml` — Type predicate functions (v0.5) See [conformance/SCHEMA.md](./conformance/SCHEMA.md) for the test format specification. From 496f8e9d861373a215807b8eb8b0873feabccc74 Mon Sep 17 00:00:00 2001 From: Shahadul Haider Date: Sun, 15 Mar 2026 16:55:57 +0600 Subject: [PATCH 9/9] docs: link to live docs site --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7d6b97..8e396d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A sandboxed expression language with JS/Python-familiar syntax, designed for dat | [xpr-lang/xpr-js](https://github.com/xpr-lang/xpr-js) | JavaScript/TypeScript runtime | | [xpr-lang/xpr-python](https://github.com/xpr-lang/xpr-python) | Python runtime | | [xpr-lang/xpr-go](https://github.com/xpr-lang/xpr-go) | Go runtime | -| [xpr-lang/xpr-docs](https://github.com/xpr-lang/xpr-docs) | Documentation site | +| [xpr-lang/xpr-docs](https://xpr-lang.github.io/xpr-docs/) | Documentation site | | [xpr-lang/xpr-playground](https://github.com/xpr-lang/xpr-playground) | Interactive playground | ## Spec