diff --git a/plans/agent-briefings/primitives-loop.md b/plans/agent-briefings/primitives-loop.md index 2b088851..47b47f6b 100644 --- a/plans/agent-briefings/primitives-loop.md +++ b/plans/agent-briefings/primitives-loop.md @@ -182,7 +182,7 @@ simulate sum types. A native `define-type` + `match` form eliminates this everyw `(define-type Name (Ctor1 field...) (Ctor2 field...) ...)` Creates constructor functions `Ctor1`, `Ctor2` + predicate `Name?`. -- [ ] Spec: implement `match` special form: +- [x] Spec: implement `match` special form: `(match expr ((Ctor1 a b) body) ((Ctor2 x) body) (else body))` Exhaustiveness warning if not all constructors covered and no `else`. @@ -686,6 +686,7 @@ Brief each language's loop agent (or do inline) after rebasing their branch onto _Newest first._ +- 2026-04-26: Phase 6 Spec match done — ADT case added to match-pattern in spec/evaluator.sx: checks (list? pattern)+(symbol? first)+(dict? value)+(get value :_adt), then matches :_ctor+arity and recursively binds field patterns. No-clause error now uses make-cek-value+raise-eval-frame so guard can catch it. 20 new match tests pass; 40/40 total ADT tests green. Zero regressions. - 2026-04-26: Phase 6 Spec define-type done — sf-define-type registered via register-special-form! in spec/evaluator.sx; AdtValue as {:_adt true :_type "..." :_ctor "..." :_fields (list ...)}; ctor fns + arity checking + Name?/Ctor? predicates + Ctor-field accessors; *adt-registry* dict populated per define-type call. 20/20 JS tests pass in spec/tests/test-adt.sx. OCaml define-type is next task. - 2026-04-26: Phase 6 Design done — plans/designs/sx-adt.md written. Covers define-type/match syntax, AdtValue CEK runtime, stepSfDefineType+MatchFrame dispatch, exhaustiveness warnings, recursive types, nested patterns, wildcard _. 3-phase impl plan. Next fire: Spec implement define-type. - 2026-04-26: Phase 5 complete — string buffer fully landed (d98b5fa2). 17 tests, 17/17 OCaml+JS. Phase 6 (ADTs) next. diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 48adc939..413c071c 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -31,7 +31,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-04-26T17:41:33Z"; + var SX_VERSION = "2026-04-26T18:15:33Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -2558,7 +2558,12 @@ PRIMITIVES["match-find-clause"] = matchFindClause; var matchPattern = function(pattern, value, env) { return (isSxTruthy(sxEq(pattern, new Symbol("_"))) ? true : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(sxEq(len(pattern), 2)) && sxEq(first(pattern), new Symbol("?")))) ? (function() { var pred = evalExpr(nth(pattern, 1), env); return cekCall(pred, [value]); -})() : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(!isSxTruthy(isEmpty(pattern))) && sxEq(first(pattern), new Symbol("quote")))) ? sxEq(value, nth(pattern, 1)) : (isSxTruthy(symbol_p(pattern)) ? (envBind(env, symbolName(pattern), value), true) : (isSxTruthy((isSxTruthy(isDict(pattern)) && isDict(value))) ? isEvery(function(k) { return matchPattern(get(pattern, k), get(value, k), env); }, keys(pattern)) : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(isList(value)) && contains(pattern, new Symbol("&rest")))) ? (function() { +})() : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(!isSxTruthy(isEmpty(pattern))) && sxEq(first(pattern), new Symbol("quote")))) ? sxEq(value, nth(pattern, 1)) : (isSxTruthy(symbol_p(pattern)) ? (envBind(env, symbolName(pattern), value), true) : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(!isSxTruthy(isEmpty(pattern))) && isSxTruthy(symbol_p(first(pattern))) && isSxTruthy(isDict(value)) && get(value, "_adt"))) ? (function() { + var ctorName = symbolName(first(pattern)); + var fieldPatterns = rest(pattern); + var fields = get(value, "_fields"); + return (isSxTruthy(sxEq(get(value, "_ctor"), ctorName)) && isSxTruthy(sxEq(len(fieldPatterns), len(fields))) && isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, zip(fieldPatterns, fields))); +})() : (isSxTruthy((isSxTruthy(isDict(pattern)) && isDict(value))) ? isEvery(function(k) { return matchPattern(get(pattern, k), get(value, k), env); }, keys(pattern)) : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(isList(value)) && contains(pattern, new Symbol("&rest")))) ? (function() { var restIdx = indexOf_(pattern, new Symbol("&rest")); return (isSxTruthy((len(value) >= restIdx)) && isSxTruthy(isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, zip(slice(pattern, 0, restIdx), slice(value, 0, restIdx)))) && (function() { var restName = nth(pattern, (restIdx + 1)); @@ -2568,7 +2573,7 @@ PRIMITIVES["match-find-clause"] = matchFindClause; })() : (isSxTruthy((isSxTruthy(isList(pattern)) && isList(value))) ? (isSxTruthy(!isSxTruthy(sxEq(len(pattern), len(value)))) ? false : (function() { var pairs = zip(pattern, value); return isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, pairs); -})()) : sxEq(pattern, value)))))))); }; +})()) : sxEq(pattern, value))))))))); }; PRIMITIVES["match-pattern"] = matchPattern; // step-sf-match @@ -2577,7 +2582,7 @@ PRIMITIVES["match-pattern"] = matchPattern; var clauses = rest(args); return (function() { var result = matchFindClause(val, clauses, env); - return (isSxTruthy(isNil(result)) ? error((String("match: no clause matched ") + String(inspect(val)))) : makeCekState(nth(result, 1), first(result), kont)); + return (isSxTruthy(isNil(result)) ? makeCekValue((String("match: no clause matched ") + String(inspect(val))), env, kontPush(makeRaiseEvalFrame(env, false), kont)) : makeCekState(nth(result, 1), first(result), kont)); })(); })(); }; PRIMITIVES["step-sf-match"] = stepSfMatch; diff --git a/spec/evaluator.sx b/spec/evaluator.sx index be7f9a10..9d3407ea 100644 --- a/spec/evaluator.sx +++ b/spec/evaluator.sx @@ -2736,6 +2736,17 @@ (= value (nth pattern 1)) (symbol? pattern) (do (env-bind! env (symbol-name pattern) value) true) + (and (list? pattern) (not (empty? pattern)) (symbol? (first pattern)) (dict? value) (get value :_adt)) + (let + ((ctor-name (symbol-name (first pattern))) + (field-patterns (rest pattern)) + (fields (get value :_fields))) + (and + (= (get value :_ctor) ctor-name) + (= (len field-patterns) (len fields)) + (every? + (fn (pair) (match-pattern (first pair) (nth pair 1) env)) + (zip field-patterns fields)))) (and (dict? pattern) (dict? value)) (every? (fn (k) (match-pattern (get pattern k) (get value k) env)) @@ -2780,7 +2791,7 @@ ((result (match-find-clause val clauses env))) (if (nil? result) - (error (str "match: no clause matched " (inspect val))) + (make-cek-value (str "match: no clause matched " (inspect val)) env (kont-push (make-raise-eval-frame env false) kont)) (make-cek-state (nth result 1) (first result) kont)))))) (define diff --git a/spec/tests/test-adt.sx b/spec/tests/test-adt.sx index 68e00b56..bceb0f7a 100644 --- a/spec/tests/test-adt.sx +++ b/spec/tests/test-adt.sx @@ -146,4 +146,133 @@ (define-type P1 (P1-ctor value)) (define-type P2 (P2-ctor value)) (assert= 10 (P1-ctor-value (P1-ctor 10))) - (assert= 20 (P2-ctor-value (P2-ctor 20)))))) + (assert= 20 (P2-ctor-value (P2-ctor 20))))) + (deftest + "match dispatches on first matching constructor" + (do + (define-type Color (Red) (Green) (Blue)) + (assert= "red" (match (Red) ((Red) "red") ((Green) "green") ((Blue) "blue"))) + (assert= "green" (match (Green) ((Red) "red") ((Green) "green") ((Blue) "blue"))) + (assert= "blue" (match (Blue) ((Red) "red") ((Green) "green") ((Blue) "blue"))))) + (deftest + "match binds field to variable" + (do + (define-type Wrapper (Wrap val)) + (assert= 42 (match (Wrap 42) ((Wrap v) v))) + (assert= "hi" (match (Wrap "hi") ((Wrap v) v))))) + (deftest + "match zero-arg constructor" + (do + (define-type Signal (On) (Off)) + (assert= "on" (match (On) ((On) "on") ((Off) "off"))) + (assert= "off" (match (Off) ((On) "on") ((Off) "off"))))) + (deftest + "match multi-field constructor binds all fields" + (do + (define-type Vec2 (V2 x y)) + (let ((v (V2 3 4))) + (assert= 7 (match v ((V2 a b) (+ a b))))))) + (deftest + "match with else clause" + (do + (define-type Opt2 (Some2 val) (None2)) + (assert= 10 (match (Some2 10) ((Some2 v) v) (else 0))) + (assert= 0 (match (None2) ((Some2 v) v) (else 0))))) + (deftest + "match else catches non-adt values" + (do + (assert= "other" (match 42 ((else) "other") (else "other"))) + (assert= "other" (match "str" (else "other"))))) + (deftest + "match returns body expression value" + (do + (define-type Num (Num-of n)) + (assert= 100 (match (Num-of 10) ((Num-of n) (* n n)))))) + (deftest + "match second arm fires when first does not match" + (do + (define-type Either (Left val) (Right val)) + (assert= "left-1" (match (Left 1) ((Left v) (str "left-" v)) ((Right v) (str "right-" v)))) + (assert= "right-2" (match (Right 2) ((Left v) (str "left-" v)) ((Right v) (str "right-" v)))))) + (deftest + "match wildcard _ in constructor pattern" + (do + (define-type Pair3 (Pair3-of a b)) + (assert= 5 (match (Pair3-of 5 99) ((Pair3-of x _) x))) + (assert= 99 (match (Pair3-of 5 99) ((Pair3-of _ y) y))))) + (deftest + "match nested adt constructor pattern" + (do + (define-type Tree2 (Leaf2) (Node2 left val right)) + (let ((t (Node2 (Leaf2) 7 (Leaf2)))) + (assert= 7 (match t ((Node2 _ v _) v))) + (assert= true (match t ((Node2 (Leaf2) _ _) true) (else false)))))) + (deftest + "match literal pattern" + (do + (assert= "zero" (match 0 (0 "zero") (else "nonzero"))) + (assert= "hello" (match "hello" ("hello" "hello") (else "other"))))) + (deftest + "match symbol binding pattern" + (do + (assert= 42 (match 42 (x x))))) + (deftest + "match no matching clause raises error" + (do + (define-type AB (A-val) (B-val)) + (let ((ok false)) + (guard (exn (else (set! ok true))) + (match (A-val) ((B-val) "b"))) + (assert ok)))) + (deftest + "match result used in further computation" + (do + (define-type Num2 (N v)) + (assert= 30 + (+ + (match (N 10) ((N v) v)) + (match (N 20) ((N v) v)))))) + (deftest + "match with define" + (do + (define-type Tag (Tagged label value)) + (define get-label (fn (t) (match t ((Tagged lbl _) lbl)))) + (define get-value (fn (t) (match t ((Tagged _ val) val)))) + (let ((t (Tagged "name" 99))) + (assert= "name" (get-label t)) + (assert= 99 (get-value t))))) + (deftest + "match three-field constructor" + (do + (define-type Triple2 (T3 a b c)) + (assert= 6 (match (T3 1 2 3) ((T3 a b c) (+ a b c)))))) + (deftest + "match clauses tried in order" + (do + (define-type Expr2 (Lit n) (Add l r) (Mul l r)) + (define eval-expr2 (fn (e) + (match e + ((Lit n) n) + ((Add l r) (+ (eval-expr2 l) (eval-expr2 r))) + ((Mul l r) (* (eval-expr2 l) (eval-expr2 r)))))) + (assert= 7 (eval-expr2 (Add (Lit 3) (Lit 4)))) + (assert= 12 (eval-expr2 (Mul (Lit 3) (Lit 4)))) + (assert= 11 (eval-expr2 (Add (Lit 2) (Mul (Lit 3) (Lit 3))))))) + (deftest + "match else binding captures value" + (do + (define-type Coin2 (Heads2) (Tails2)) + (assert= "Tails2" (match (Tails2) ((Heads2) "Heads2") (x (get x :_ctor)))))) + (deftest + "match on adt with string field" + (do + (define-type Msg (Hello name) (Bye name)) + (assert= "Hello, Alice" (match (Hello "Alice") ((Hello n) (str "Hello, " n)) ((Bye n) (str "Bye, " n)))) + (assert= "Bye, Bob" (match (Bye "Bob") ((Hello n) (str "Hello, " n)) ((Bye n) (str "Bye, " n)))))) + (deftest + "match nested pattern with variable binding" + (do + (define-type Box2 (Box2-of v)) + (define-type Inner (Inner-of n)) + (assert= 5 (match (Box2-of (Inner-of 5)) ((Box2-of (Inner-of n)) n))))) +)