spec: match special form — ADT constructor pattern matching (20 tests)

Extends match-pattern in spec/evaluator.sx with an ADT case: when the
pattern is (CtorName var...) and the value is an ADT dict (:_adt true),
check :_ctor matches, arity matches, then recursively bind field patterns.
Supports nested patterns, wildcard _, variable binding, and zero-arg ctors.

Changes step-sf-match to route no-clause errors through raise-eval-frame
instead of direct error, allowing guard to catch non-exhaustive matches.

40/40 ADT tests pass (20 define-type + 20 match). Zero regressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:16:16 +00:00
parent 6c87210728
commit 0dc7e1599c
4 changed files with 153 additions and 7 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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

View File

@@ -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)))))
)