Skip to content

Commit 2eb2c0c

Browse files
Pattern matching: match expression with patterns / ranges / type tags
Adds `match scrutinee { pat => stmt, ... }` to OMC. Single biggest ergonomic gap closed since dicts. Patterns supported: - literals (int, float, string, bool, null) - wildcard `_` - bind (any bare identifier — captures the value) - alternation `a | b | c` (reuses existing `|` BitOr token) - inclusive numeric range `0..9` - inclusive 1-char string range `"0".."9"` - type tag (`int`, `string`, `dict`, etc — same names as type_of) Lexer: new `=>` (FatArrow) and `..` (DotDot) tokens. `match` joins the keyword set. AST: Statement::Match { scrutinee, arms } with MatchArm { pattern, body }, Pattern enum. Interpreter: pattern_matches() helper threads bindings through recursive match — alternation snapshots the bindings vec and rolls back on a failed alt so partial bindings don't leak. The matched arm's bindings are installed via set_var into the surrounding scope (matching `if`'s no-extra-scope semantics). VM: compiles to Op::ExecStmt — same tree-walk fallback try/catch uses. A native lowering would emit guarded jump tables; it's straightforward (~50 lines per pattern variant) but match isn't in any hot path yet, so deferred. The existing vm_take_return machinery handles `return` inside a matched arm cleanly. Bindings now make this idiomatic in OMC: fn parse_atom(c) { match c { "(" => { return "open"; } ")" => { return "close"; } "0".."9" => { return "digit"; } _ => { return "ident"; } } } 41/41 functional examples produce identical output under tree-walk and VM. 92/92 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 30dfddc commit 2eb2c0c

5 files changed

Lines changed: 346 additions & 0 deletions

File tree

omnimcode-core/src/ast.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,57 @@ pub enum Statement {
6262
err_var: String,
6363
handler: Vec<Statement>,
6464
},
65+
/// `match expr { pat => stmts, ... }`. First arm whose pattern
66+
/// accepts the scrutinee runs; remaining arms are skipped.
67+
/// A wildcard or bare-identifier arm at the end is the default.
68+
/// If no arm matches, the whole match is a no-op (no error).
69+
Match {
70+
scrutinee: Expression,
71+
arms: Vec<MatchArm>,
72+
},
73+
}
74+
75+
/// A single arm in a `match` statement. Patterns can:
76+
/// - match literals (number, float, string, bool, null)
77+
/// - match a wildcard (`_`) or bind a variable (any bare ident)
78+
/// - match a range (numeric `1..10` or single-char string `"a".."z"`)
79+
/// - alternate via `|` (`1 | 2 | 3`)
80+
/// - dispatch on type name (`int`, `string`, `dict`, etc.)
81+
///
82+
/// Body is a sequence of statements (block or single `=> stmt;` arm).
83+
#[derive(Clone, Debug, PartialEq)]
84+
pub struct MatchArm {
85+
pub pattern: Pattern,
86+
pub body: Vec<Statement>,
87+
}
88+
89+
#[derive(Clone, Debug, PartialEq)]
90+
pub enum Pattern {
91+
/// Matches anything; binds nothing.
92+
Wildcard,
93+
/// Matches anything; binds the value to `name` in the arm body.
94+
Bind(String),
95+
/// Matches by structural equality with the literal.
96+
LitInt(i64),
97+
LitFloat(f64),
98+
LitString(String),
99+
LitBool(bool),
100+
LitNull,
101+
/// Numeric range, inclusive on both ends. `lo..=hi`. Stored as
102+
/// inclusive because that's the common case for digit/letter
103+
/// dispatch (`'0'..='9'`, `'a'..='z'`).
104+
RangeInt(i64, i64),
105+
/// Single-char string range, inclusive. Each side must be a
106+
/// 1-char string at parse time. Matches a 1-char string whose
107+
/// codepoint falls in [lo, hi]. Useful for the JSON-parser
108+
/// `is_digit` style dispatch.
109+
RangeStr(char, char),
110+
/// Alternation: any of the inner patterns matches.
111+
Or(Vec<Pattern>),
112+
/// Match by type tag — same names as the `type_of` builtin.
113+
/// E.g. `int`, `float`, `string`, `bool`, `array`, `dict`,
114+
/// `function`, `null`.
115+
Type(String),
65116
}
66117

67118
#[derive(Clone, Debug, PartialEq)]

omnimcode-core/src/compiler.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,14 @@ impl Compiler {
880880
// itself, fall back to the AST walker for try/catch.
881881
self.emit(Op::ExecStmt(Box::new(s.clone())));
882882
}
883+
Statement::Match { .. } => {
884+
// Same fallback strategy as Try. A native lowering
885+
// would compile each arm into a guarded Jump and
886+
// emit the bindings as Op::StoreVar — straightforward
887+
// but adds 50+ lines of Rust per pattern variant.
888+
// Defer until benchmarks show match in a hot path.
889+
self.emit(Op::ExecStmt(Box::new(s.clone())));
890+
}
883891
}
884892
Ok(())
885893
}

omnimcode-core/src/formatter.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,45 @@ fn format_stmt(stmt: &Statement, level: usize, out: &mut String) {
181181
indent_to(level, out);
182182
out.push_str("}\n");
183183
}
184+
Statement::Match { scrutinee, arms } => {
185+
out.push_str("match ");
186+
format_expr(scrutinee, out);
187+
out.push_str(" {\n");
188+
for arm in arms {
189+
indent_to(level + 1, out);
190+
format_pattern(&arm.pattern, out);
191+
out.push_str(" => {\n");
192+
for s in &arm.body { format_stmt(s, level + 2, out); }
193+
indent_to(level + 1, out);
194+
out.push_str("}\n");
195+
}
196+
indent_to(level, out);
197+
out.push_str("}\n");
198+
}
199+
}
200+
}
201+
202+
fn format_pattern(pat: &crate::ast::Pattern, out: &mut String) {
203+
use crate::ast::Pattern;
204+
match pat {
205+
Pattern::Wildcard => out.push('_'),
206+
Pattern::Bind(n) => out.push_str(n),
207+
Pattern::LitInt(n) => out.push_str(&n.to_string()),
208+
Pattern::LitFloat(f) => out.push_str(&format!("{}", f)),
209+
Pattern::LitString(s) => out.push_str(&format!("{:?}", s)),
210+
Pattern::LitBool(b) => out.push_str(if *b { "true" } else { "false" }),
211+
Pattern::LitNull => out.push_str("null"),
212+
Pattern::RangeInt(lo, hi) => out.push_str(&format!("{}..{}", lo, hi)),
213+
Pattern::RangeStr(lo, hi) => {
214+
out.push_str(&format!("\"{}\"..\"{}\"", lo, hi));
215+
}
216+
Pattern::Or(alts) => {
217+
for (i, p) in alts.iter().enumerate() {
218+
if i > 0 { out.push_str(" | "); }
219+
format_pattern(p, out);
220+
}
221+
}
222+
Pattern::Type(name) => out.push_str(name),
184223
}
185224
}
186225

omnimcode-core/src/interpreter.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,24 @@ impl Interpreter {
677677
// the caller reaches them via dotted-call syntax.
678678
self.import_module_with_alias(module, alias.as_deref())
679679
}
680+
Statement::Match { scrutinee, arms } => {
681+
let value = self.eval_expr(scrutinee)?;
682+
for arm in arms {
683+
let mut bindings: Vec<(String, Value)> = Vec::new();
684+
if pattern_matches(&arm.pattern, &value, &mut bindings) {
685+
// Apply the bindings as plain set_var into the
686+
// current scope, then run the arm body. The
687+
// scope IS the surrounding block — match isn't
688+
// its own scope, matching `if`'s behavior.
689+
for (n, v) in bindings {
690+
self.set_var(n, v);
691+
}
692+
return self.execute_block(&arm.body);
693+
}
694+
}
695+
// No arm matched — silent no-op.
696+
Ok(())
697+
}
680698
Statement::Try { body, err_var, handler } => {
681699
// Run the body; if anything inside returns Err, jump to
682700
// the handler with err_var bound to the message string.
@@ -3599,6 +3617,9 @@ impl Interpreter {
35993617
for s in body { visit(s, fns); }
36003618
for s in handler { visit(s, fns); }
36013619
}
3620+
Statement::Match { arms, .. } => {
3621+
for arm in arms { for s in &arm.body { visit(s, fns); } }
3622+
}
36023623
_ => {}
36033624
}
36043625
}
@@ -3829,6 +3850,95 @@ fn vm_fast_dispatch(name: &str, args: &[Value]) -> Option<Result<Value, String>>
38293850
}
38303851
}
38313852

3853+
/// Test whether `pattern` accepts `value`. On success, appends any
3854+
/// `Pattern::Bind(name)` matches into `bindings` (ordered) so the
3855+
/// caller can install them in the arm's scope.
3856+
///
3857+
/// Pure / side-effect-free aside from the bindings vec — same
3858+
/// helper is used by both tree-walk and VM (via vm_match_helper).
3859+
pub(crate) fn pattern_matches(
3860+
pattern: &crate::ast::Pattern,
3861+
value: &Value,
3862+
bindings: &mut Vec<(String, Value)>,
3863+
) -> bool {
3864+
use crate::ast::Pattern;
3865+
match pattern {
3866+
Pattern::Wildcard => true,
3867+
Pattern::Bind(n) => {
3868+
bindings.push((n.clone(), value.clone()));
3869+
true
3870+
}
3871+
Pattern::LitInt(n) => match value {
3872+
Value::HInt(h) => h.value == *n,
3873+
Value::HFloat(f) => *f == *n as f64,
3874+
_ => false,
3875+
},
3876+
Pattern::LitFloat(f) => match value {
3877+
Value::HFloat(g) => g == f,
3878+
Value::HInt(h) => (h.value as f64) == *f,
3879+
_ => false,
3880+
},
3881+
Pattern::LitString(s) => matches!(value, Value::String(v) if v == s),
3882+
Pattern::LitBool(b) => match value {
3883+
Value::Bool(v) => v == b,
3884+
// OMC's int-as-bool convention: 0/1 ints commonly stand
3885+
// in for false/true. Accept matches against literal bool
3886+
// patterns so `match flag { true => ..., false => ... }`
3887+
// works on the int-coded values too.
3888+
Value::HInt(h) => (h.value != 0) == *b,
3889+
_ => false,
3890+
},
3891+
Pattern::LitNull => matches!(value, Value::Null),
3892+
Pattern::RangeInt(lo, hi) => {
3893+
let n = match value {
3894+
Value::HInt(h) => h.value,
3895+
Value::HFloat(f) => *f as i64,
3896+
_ => return false,
3897+
};
3898+
n >= *lo && n <= *hi
3899+
}
3900+
Pattern::RangeStr(lo, hi) => {
3901+
if let Value::String(s) = value {
3902+
let chars: Vec<char> = s.chars().collect();
3903+
if chars.len() == 1 {
3904+
let c = chars[0];
3905+
return c >= *lo && c <= *hi;
3906+
}
3907+
}
3908+
false
3909+
}
3910+
Pattern::Or(alts) => {
3911+
// Try each alt with a snapshot of bindings; first match wins.
3912+
// We don't allow bindings to differ between alts (same as Rust's
3913+
// requirement that all alts bind the same names) — for v1 we
3914+
// simply propagate whatever the matching alt produced.
3915+
for p in alts {
3916+
let snapshot_len = bindings.len();
3917+
if pattern_matches(p, value, bindings) {
3918+
return true;
3919+
}
3920+
bindings.truncate(snapshot_len);
3921+
}
3922+
false
3923+
}
3924+
Pattern::Type(tag) => {
3925+
let actual = match value {
3926+
Value::HInt(_) => "int",
3927+
Value::HFloat(_) => "float",
3928+
Value::String(_) => "string",
3929+
Value::Bool(_) => "bool",
3930+
Value::Array(_) => "array",
3931+
Value::Dict(_) => "dict",
3932+
Value::Function { .. } => "function",
3933+
Value::Null => "null_t",
3934+
Value::Singularity { .. } => "singularity",
3935+
_ => "unknown",
3936+
};
3937+
actual == tag
3938+
}
3939+
}
3940+
}
3941+
38323942
fn values_equal(a: &Value, b: &Value) -> bool {
38333943
match (a, b) {
38343944
(Value::String(x), Value::String(y)) => x == y,

0 commit comments

Comments
 (0)