Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,26 @@ UTF-8 by default. Same owned/borrowed distinction as Rust, but inferred:
; Destructuring:
[let [x y] [get-point]]
[let {name age} user]
```

; Vector/tuple patterns in match:
[match v
#[a b] => [+ a b] ; matches a vector/tuple of EXACTLY length 2
#[a b c] => [+ a [+ b c]]
_ => "other"]
```

**Sequence-pattern ruling (2026-07).** A `#[p1 … pn]` (or `(p1, …, pn)`)
pattern in `match` matches when the scrutinee is a vector or tuple of
*exactly* length n (Rust slice-pattern prior); element subpatterns match
recursively, and a non-match falls through to the next arm. A destructuring
`let` (`[let [x y] v]`, `[let #[x y] v]`) requires a vector or tuple with *at
least* as many elements as binders — extra elements are allowed (Clojure
prior) — and anything shorter, or a non-sequence value, is a **loud runtime
error** on every backend ("destructuring expected at least N elements, got
M" / "destructuring requires a vector or tuple"). Clojure would bind `nil`
for missing elements; Loon has no nil, and silently binding `()` is the
worst failure mode for an agent-first language, so the divergence is
deliberate and loud.

### Closures

Expand Down
13 changes: 13 additions & 0 deletions crates/loon-cli/tests/prior-alignment/clojure-vec-destructure.oo
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
; prior: Clojure — `(let [[x y] [7 8]] (+ x y))` binds positionally; extra
; elements are ignored: `(let [[a b] [1 2 3]] ...)` is fine.
; expectation: positional let destructuring binds elements; extra elements
; are allowed. (Divergence: Clojure binds nil for MISSING elements; Loon has
; no nil and errors loudly instead — DESIGN.md §6 sequence-pattern ruling.)
; principle: silent () binds are the worst failure mode (agent-first.md).
; expect-stdout: 15
; expect-stdout: 3
[fn main []
[let [x y] #[7 8]]
[println [+ x y]]
[let [a b] #[1 2 3 4]]
[println [+ a b]]]
15 changes: 15 additions & 0 deletions crates/loon-cli/tests/prior-alignment/rust-match-vec-pattern.oo
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
; prior: Rust — slice patterns: `match v[..] { [a, b] => a + b, _ => 0 }`
; matches on exact length and binds the elements.
; expectation: `#[a b]` in match matches a vector of exactly length 2 and
; binds a and b; a wrong-length vector falls through to the next arm.
; principle: patterns that look like they bind must bind (issue #17 — they
; used to silently produce () on the default backend).
; expect-stdout: 3
; expect-stdout: fallthrough
[fn main []
[println [match #[1 2]
#[a b] [str [+ a b]]
_ "no match"]]
[println [match #[1 2 3]
#[a b] [str [+ a b]]
_ "fallthrough"]]]
80 changes: 73 additions & 7 deletions crates/loon-lang/src/check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3779,12 +3779,56 @@ impl Checker {
let form_span = Span::new(binding.span.start, args[val_idx].span.end);
self.add_definition(name, binding.span, form_span);
}
// Positional destructuring: [let [x y] v] / [let #[x y] v] /
// [let (x, y) v] — bind each element with its inferred type.
ExprKind::List(_) | ExprKind::Vec(_) | ExprKind::Tuple(_) => {
self.bind_destructure_vars(binding, &val_ty);
}
// Map destructuring: [let {a b} m] — bind the names (value
// types are not derived from the map type yet).
ExprKind::Map(pairs) => {
for (k, _) in pairs {
if let ExprKind::Symbol(name) = &k.kind {
let t = self.subst.fresh();
self.env.set(name.clone(), Scheme::mono(t));
}
}
}
_ => {}
}

val_ty
}

/// Bind the variables of a positional `let` destructure (`[x y]`,
/// `#[x y]`, `(x, y)`, possibly nested) against the bound value's type:
/// vector elements get the element type, tuple elements their positional
/// type, anything else a fresh var.
fn bind_destructure_vars(&mut self, binding: &Expr, val_ty: &Type) {
match &binding.kind {
ExprKind::Symbol(name) if name != "_" => {
let t = self.subst.resolve(val_ty);
self.env.set(name.clone(), Scheme::mono(t.clone()));
self.type_of.insert(binding.id, t);
self.add_definition(name, binding.span, binding.span);
}
ExprKind::List(items) | ExprKind::Vec(items) | ExprKind::Tuple(items) => {
let resolved = self.subst.resolve(val_ty);
for (i, item) in items.iter().enumerate() {
let elem_ty = match &resolved {
Type::Con(name, params) if name == "Vec" && params.len() == 1 => {
params[0].clone()
}
Type::Tuple(ts) if i < ts.len() => ts[i].clone(),
_ => self.subst.fresh(),
};
self.bind_destructure_vars(item, &elem_ty);
}
}
_ => {}
}
}

fn infer_if(&mut self, args: &[Expr], span: Span) -> Type {
if args.len() < 2 {
return Type::Unit;
Expand Down Expand Up @@ -4565,23 +4609,45 @@ impl Checker {
}
}

/// Bind variables from a match pattern into the current scope.
fn bind_pattern_vars(&mut self, pattern: &Expr, _scrutinee_ty: &Type) {
/// Bind variables from a match pattern into the current scope, with the
/// type the pattern is matched against.
fn bind_pattern_vars(&mut self, pattern: &Expr, scrutinee_ty: &Type) {
match &pattern.kind {
ExprKind::Symbol(s) if s != "_" && !s.starts_with(char::is_uppercase) => {
let t = self.subst.fresh();
self.env.set(s.clone(), Scheme::mono(t));
ExprKind::Symbol(s)
if s != "_" && !s.starts_with(char::is_uppercase) && !s.starts_with(':') =>
{
let t = self.subst.resolve(scrutinee_ty);
self.env.set(s.clone(), Scheme::mono(t.clone()));
self.type_of.insert(pattern.id, t);
}
ExprKind::List(items) if !items.is_empty() => {
// Constructor pattern: [Ok x] — bind the field vars
// Constructor pattern: [Ok x] — bind the field vars. Field
// types are not derived from the constructor signature yet,
// so each gets a fresh var.
if let ExprKind::Symbol(ctor) = &items[0].kind {
if ctor.starts_with(char::is_uppercase) {
for field in &items[1..] {
self.bind_pattern_vars(field, _scrutinee_ty);
let t = self.subst.fresh();
self.bind_pattern_vars(field, &t);
}
}
}
}
// Vector/tuple pattern: #[a b] or (a, b) — bind each element
// subpattern with its inferred element type.
ExprKind::Vec(items) | ExprKind::Tuple(items) => {
let resolved = self.subst.resolve(scrutinee_ty);
for (i, item) in items.iter().enumerate() {
let elem_ty = match &resolved {
Type::Con(name, params) if name == "Vec" && params.len() == 1 => {
params[0].clone()
}
Type::Tuple(ts) if i < ts.len() => ts[i].clone(),
_ => self.subst.fresh(),
};
self.bind_pattern_vars(item, &elem_ty);
}
}
_ => {}
}
}
Expand Down
79 changes: 74 additions & 5 deletions crates/loon-lang/src/eir/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1103,14 +1103,44 @@ impl<'a> Lower<'a> {

let val = self.lower_expr(val_expr);

// Bind the name
if let ExprKind::Symbol(name) = &binding.kind {
self.bind(name, val);
}
// TODO: destructuring patterns
self.lower_let_binding(binding, val);
val
}

/// Bind a `let` binding form to an already-lowered value: a plain name,
/// or a positional vector/tuple destructure (`[let [x y] v]`,
/// `[let #[x y] v]`). Destructuring emits a runtime guard that errors
/// loudly when the value is not a vector/tuple with at least as many
/// elements as binders (extra elements are allowed, Clojure-style) —
/// never a silent `()` bind. Map destructuring is still TODO here.
fn lower_let_binding(&mut self, binding: &Expr, val: Reg) {
match &binding.kind {
ExprKind::Symbol(name) => {
if name != "_" {
self.bind(name, val);
}
}
ExprKind::List(items) | ExprKind::Vec(items) | ExprKind::Tuple(items) => {
let span = binding.span;
let expected = self.reg();
self.emit(Op::Lit(expected, Lit::Int(items.len() as i64), span));
let guard = self.reg();
self.emit(Op::Builtin(
guard,
Built::DestructureCheck,
vec![val, expected],
span,
));
for (i, item) in items.iter().enumerate() {
let r = self.reg();
self.emit(Op::Field(r, val, Selector::Index(i as u16), item.span));
self.lower_let_binding(item, r);
}
}
_ => {} // TODO: map destructuring patterns
}
}

fn lower_if(&mut self, args: &[Expr], span: Span) -> Reg {
if args.len() < 2 {
let r = self.reg();
Expand Down Expand Up @@ -1427,6 +1457,32 @@ impl<'a> Lower<'a> {
None
}

// Vector/tuple pattern: #[a b] or (a, b) — matches a vector or
// tuple of exactly that length (Rust slice-pattern prior), with
// element subpatterns matched recursively.
ExprKind::Vec(items) | ExprKind::Tuple(items) => {
let len_r = self.reg();
self.emit(Op::Builtin(len_r, Built::SeqLen, vec![scrutinee], span));
let expected = self.reg();
self.emit(Op::Lit(expected, Lit::Int(items.len() as i64), span));
let mut cond = self.reg();
self.emit(Op::Bin(cond, BinOp::Eq, len_r, expected, span));
// Element subtests are side-effect free and safe to evaluate
// eagerly even when the length test fails (out-of-range Field
// yields unit, which simply fails the comparison), so a flat
// non-short-circuit And chain is correct.
for (i, item) in items.iter().enumerate() {
let elem = self.reg();
self.emit(Op::Field(elem, scrutinee, Selector::Index(i as u16), span));
if let Some(sub) = self.compile_pattern_test(item, elem, span) {
let both = self.reg();
self.emit(Op::Bin(both, BinOp::And, cond, sub, span));
cond = both;
}
}
Some(cond)
}

_ => None,
}
}
Expand Down Expand Up @@ -1458,6 +1514,19 @@ impl<'a> Lower<'a> {
let _ = ctor; // used for tag check in full implementation
}
}
ExprKind::Vec(items) | ExprKind::Tuple(items) => {
// Vector/tuple pattern: bind each element subpattern.
for (i, item) in items.iter().enumerate() {
let r = self.reg();
self.emit(Op::Field(
r,
scrutinee,
Selector::Index(i as u16),
item.span,
));
self.bind_pattern(item, r);
}
}
_ => {
// Literal pattern — value equality check
// For now, bind nothing (handled by match semantics)
Expand Down
9 changes: 9 additions & 0 deletions crates/loon-lang/src/eir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,13 @@ pub enum Built {
SomeP,
/// `none?` — true for None/unit (complement of `some?`).
NoneP,
/// Internal (not name-resolvable): length of a vector or tuple, or -1
/// for any other value. Emitted by pattern compilation so `#[a b]` can
/// test "is a sequence of length 2" in one comparison.
SeqLen,
/// Internal (not name-resolvable): destructuring guard. Args are
/// (value, expected-binder-count). Errors loudly unless the value is a
/// vector/tuple with at least that many elements — a silent `()` bind
/// here is the worst failure mode for an agent-first language.
DestructureCheck,
}
48 changes: 48 additions & 0 deletions crates/loon-lang/src/eir/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,9 @@ impl Vm {
(Some(Obj::Tuple(fields)), Selector::Index(i)) => {
fields.get(*i as usize).copied().unwrap_or(Val::UNIT)
}
(Some(Obj::Vec(items)), Selector::Index(i)) => {
items.get(*i as usize).copied().unwrap_or(Val::UNIT)
}
(Some(Obj::Map(map)), Selector::Name(sid)) => {
// Try symbol key first, then string key
let sym_key = Val::sym(sid.0);
Expand Down Expand Up @@ -1416,6 +1419,41 @@ impl Vm {
};
Ok(Val::int(len))
}
Built::SeqLen => {
let v = args.first().copied().unwrap_or(Val::UNIT);
let len = match self.get_obj(v) {
Some(Obj::Vec(items)) => items.len() as i64,
Some(Obj::Tuple(items)) => items.len() as i64,
_ => -1,
};
Ok(Val::int(len))
}
Built::DestructureCheck => {
let v = args.first().copied().unwrap_or(Val::UNIT);
let expected = args
.get(1)
.copied()
.filter(|e| e.is_int())
.map(|e| e.as_int())
.unwrap_or(0);
let len = match self.get_obj(v) {
Some(Obj::Vec(items)) => items.len() as i64,
Some(Obj::Tuple(items)) => items.len() as i64,
_ => {
return Err(VmError::new(VmErrorKind::DestructureMismatch(
"destructuring requires a vector or tuple".to_string(),
))
.with_span(self.current_span));
}
};
if len < expected {
return Err(VmError::new(VmErrorKind::DestructureMismatch(format!(
"destructuring expected at least {expected} elements, got {len}"
)))
.with_span(self.current_span));
}
Ok(Val::UNIT)
}
Built::Get => {
let coll = args.first().copied().unwrap_or(Val::UNIT);
let key = args.get(1).copied().unwrap_or(Val::UNIT);
Expand Down Expand Up @@ -3049,6 +3087,10 @@ pub enum VmErrorKind {
/// here would let programs believe they got a valid quotient. The `&str`
/// names the operation ("division" or "modulo") for the diagnostic.
DivideByZero(&'static str),
/// A destructuring `let` (or handler binding) was given a value that is
/// not a vector/tuple or has too few elements. Silently binding `()`
/// here would let programs proceed with wrong data.
DestructureMismatch(String),
}

/// Structured detail for a replay divergence, so `loon verify` can classify
Expand Down Expand Up @@ -3078,6 +3120,7 @@ impl VmErrorKind {
VmErrorKind::ReplayDivergence(_) => "replay-divergence",
VmErrorKind::UnhandledEffect(_) => "unhandled-effect",
VmErrorKind::DivideByZero(_) => "divide-by-zero",
VmErrorKind::DestructureMismatch(_) => "destructure-mismatch",
}
}
}
Expand Down Expand Up @@ -3124,6 +3167,11 @@ impl std::fmt::Display for VmError {
// "modulo by zero" so both backends fail the same way.
write!(f, "{kind} by zero")
}
VmErrorKind::DestructureMismatch(msg) => {
// Wording matches the interpreter's destructuring errors so
// both backends fail the same way.
write!(f, "{msg}")
}
}
}
}
Expand Down
Loading
Loading