match-expr ::= '(' 'match' expr clause+ ')'
clause ::= case-clause | default-clause
case-clause ::= '(' 'case' pattern guard? expr ')'
default-clause ::= '(' 'default' expr ')'
guard ::= '(' 'if' expr ')'
pattern ::= literal-pat | wildcard-pat | symbol-pat | array-pat | object-pat | or-pat
literal-pat ::= number | string | boolean | 'null'
or-pat ::= '(' '|' literal-pat+ ')'
wildcard-pat ::= '_'
symbol-pat ::= identifier
array-pat ::= '[' (pattern (',' pattern)* (',' '&' symbol-pat)?)? ']'
object-pat ::= '{' (key-binding (',' key-binding)*)? '}'
key-binding ::= identifier ':' pattern
[[match e c1 c2 ... cn]] =
let v = [[e]]
[[c1]]v || [[c2]]v || ... || [[cn]]v || throw "No matching pattern for value: <v>"
Where [[ci]]v means "evaluate clause ci with value v".
[[(case p r)]]v =
if matches(v, p) then
let bindings = extract(v, p)
with bindings: [[r]]
else
fail
[[(case p (if g) r)]]v =
if matches(v, p) then
let bindings = extract(v, p)
with bindings:
if [[g]] then [[r]] else fail
else
fail
[[(default r)]]v = [[r]]
matches(v, literal) = (v === literal)
extract(v, literal) = {}
matches(v, null) = (v === null)
extract(v, null) = {}
matches(v, (| p1 p2 ... pn)) = (v === p1) || (v === p2) || ... || (v === pn)
extract(v, (| p1 p2 ... pn)) = {}
Or-patterns do not produce bindings. They compare the value against each alternative using ===.
matches(v, _) = true
extract(v, _) = {}
matches(v, x) = true
extract(v, x) = {x: v}
matches(v, [p1, p2, ..., pn]) =
Array.isArray(v) &&
v.length === n
extract(v, [p1, p2, ..., pn]) =
JS destructuring: let [p1, p2, ..., pn] = v
Note: The condition checks Array.isArray and exact length. Binding uses JS array destructuring via an IIFE parameter.
matches(v, [p1, ..., pk, & r]) =
Array.isArray(v) &&
v.length >= k
extract(v, [p1, ..., pk, & r]) =
JS destructuring: let [p1, ..., pk, ...r] = v
matches(v, {k1: p1, k2: p2, ..., kn: pn}) =
typeof v === "object" &&
v !== null &&
!Array.isArray(v) &&
k1 in v &&
k2 in v &&
... &&
kn in v
Object pattern matching uses the __hql_match_obj runtime helper which checks that the value is a non-null, non-array object and that all specified keys exist (via the in operator).
extract(v, {k1: p1, k2: p2, ..., kn: pn}) =
JS destructuring: let {k1: p1, k2: p2, ..., kn: pn} = v
Binding uses JS object destructuring via an IIFE parameter. If a key exists but has value undefined, the binding receives undefined.
The match macro binds the value to a gensym variable (using auto-gensym val#) and dispatches to __match_impl__:
compile(match e c1 ... cn) =
(let (val# e)
(__match_impl__ val# c1 ... cn))
__match_impl__ processes clauses recursively. For each case clause:
compile-clause((case p r), val, rest) =
condition(p, val) ?
body(p, val, r) :
compile-clause(rest[0], val, rest[1:])
compile-clause((case p (if g) r), val, rest) =
condition(p, val) ?
guarded-body(p, val, g, r, rest) :
compile-clause(rest[0], val, rest[1:])
compile-clause((default r), val, _) = r
condition(literal, val) = (=== val literal)
condition(null, val) = (=== val null)
condition(_, val) = true
condition(symbol, val) = true
condition((| p1 ... pn), val) = (__match_or_cond__ val p1 ... pn)
condition({...}, val) = (__hql_match_obj val (quote pattern))
condition([p1...pn], val) = (and (Array.isArray val) (=== (js-get val "length") n))
condition([p1...pk & r], val) = (and (Array.isArray val) (>= (js-get val "length") k))
body(literal, val, r) = r
body(null, val, r) = r
body(_, val, r) = r
body(symbol, val, r) = (let (symbol val) r)
body(array-pat, val, r) = ((fn [array-pat] r) val)
body(object-pat, val, r) = ((fn [object-pat] r) val)
body((| ...), val, r) = r
guarded-body(p, val, g, r, rest) =
body(p, val, (if g then r else compile-clause(rest[0], val, rest[1:])))
When condition is true (wildcard, symbol binding), the if wrapper is omitted:
// Instead of: (if true body fallback)
// Emits: body
Pattern matching generates the following runtime checks:
| Pattern | Condition Check |
|---|---|
null | === null |
[...] | Array.isArray(v) && v.length === n |
[... & r] | Array.isArray(v) && v.length >= k |
{...} | __hql_match_obj(v, pattern) (typeof object, not null, not array, all keys exist) |
| `( | ...)` |
| literal | === literal |
| symbol | (none - always matches) |
_ | (none - always matches) |
Variables bound by patterns are scoped to:
For symbol bindings, scope is created via let. For array/object patterns, scope is created via IIFE destructuring parameter.
(match x
(case [a, b] // a, b bound here
(if (> a b)) // a, b available in guard
(+ a b))) // a, b available in result
The pattern matching is implemented as three macros in src/hql/lib/macro/core.hql:
match Macro(macro match [value & clauses]
`(let (val# ~value)
(__match_impl__ val# ~@clauses)))
Uses auto-gensym (val#) for hygienic variable binding.
__match_impl__ MacroInternal recursive macro that:
case or default)case: classifies pattern type (literal, symbol, wildcard, array, object, or-pattern)(if ...))__match_impl__ call__match_or_cond__ MacroHelper for or-patterns that builds chained === checks recursively:
(macro __match_or_cond__ [val-sym & pats]
(if (%empty? pats)
false
(if (=== (%length pats) 1)
`(=== ~val-sym ~(%first pats))
`(|| (=== ~val-sym ~(%first pats))
(__match_or_cond__ ~val-sym ~@(%rest pats))))))