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
54 changes: 46 additions & 8 deletions crates/loon-lang/src/check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4018,15 +4018,13 @@ impl Checker {
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).
// Map destructuring: `[let {name age} m]` binds each key name to
// the map's value type (`Map Keyword v` → each binding has type
// `v`), not a bare fresh var — so uses of the bound names type-check
// against real element types and any per-key default expression is
// unified against the same type.
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));
}
}
self.bind_map_destructure(pairs, &val_ty);
}
_ => {}
}
Expand Down Expand Up @@ -4063,6 +4061,46 @@ impl Checker {
}
}

/// Bind the names introduced by a `[let {k default ...} m]` map
/// destructuring pattern. Each key symbol is bound to the map's value
/// type; a distinct default expression is inferred and unified against it.
fn bind_map_destructure(&mut self, pairs: &[(Expr, Expr)], val_ty: &Type) {
// Recover the element type from `Map Keyword v`; otherwise fall back to
// a fresh var and constrain the source to be a keyword-keyed map.
let elem_ty = match self.subst.resolve(val_ty) {
Type::Con(ref name, ref targs) if name == "Map" && targs.len() == 2 => targs[1].clone(),
_ => {
let elem = self.subst.fresh();
let map_ty = Type::Con("Map".to_string(), vec![Type::Keyword, elem.clone()]);
let _ = unify(&mut self.subst, val_ty, &map_ty);
elem
}
};

for (k, v) in pairs {
let name = match &k.kind {
ExprKind::Symbol(s) => s,
_ => continue,
};
// A value symbol identical to the key is shorthand (no default);
// anything else is a default expression, whose type must match the
// element type.
let has_default = !matches!(&v.kind, ExprKind::Symbol(vs) if vs == name);
if has_default {
let default_ty = self.infer(v);
if let Err(e) = unify(&mut self.subst, &elem_ty, &default_ty) {
self.push_unify_error(e, v.span);
}
}
if name != "_" {
let scheme = generalize(&self.env, &self.subst, &elem_ty);
self.env.set(name.clone(), scheme);
self.type_of.insert(k.id, elem_ty.clone());
self.add_definition(name, k.span, k.span);
}
}
}

fn infer_if(&mut self, args: &[Expr], span: Span) -> Type {
if args.len() < 2 {
return Type::Unit;
Expand Down
87 changes: 81 additions & 6 deletions crates/loon-lang/src/eir/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1207,11 +1207,12 @@ impl<'a> Lower<'a> {
}

/// 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.
/// a positional vector/tuple destructure (`[let [x y] v]`, `[let #[x y]
/// v]`), or a map destructure (`[let {name age} m]`). Positional
/// 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 binds each key from the map (see `lower_map_destructure`).
fn lower_let_binding(&mut self, binding: &Expr, val: Reg) {
match &binding.kind {
ExprKind::Symbol(name) => {
Expand All @@ -1236,7 +1237,81 @@ impl<'a> Lower<'a> {
self.lower_let_binding(item, r);
}
}
_ => {} // TODO: map destructuring patterns
// Map destructuring: `[let {name age} m]` binds each key from the
// map. A shorthand entry `{name name}` binds `name` to `m[:name]`;
// an entry with a distinct value `{name default}` uses `default`
// as the fallback when the key is absent. Matches the legacy
// interpreter's `Param::MapDestructure` in `bind_param`: present
// key → its value, missing key → the default expr (or unit).
ExprKind::Map(pairs) => {
self.lower_map_destructure(pairs, val, binding.span);
}
_ => {}
}
}

/// Lower `[let {k default ...} map]` map-destructuring bindings.
///
/// For each `(key, val)` pair the key symbol is bound to `map[:key]`. When
/// the pair's value is a symbol identical to the key it is treated as a
/// shorthand with no default; otherwise the value is a default expression
/// evaluated (lazily, only on the miss path) when the key is absent. A
/// missing key with no default binds unit — mirroring the interpreter.
fn lower_map_destructure(&mut self, pairs: &[(Expr, Expr)], map_reg: Reg, span: Span) {
for (k, v) in pairs {
let name = match &k.kind {
ExprKind::Symbol(s) => s.clone(),
_ => continue,
};
let has_default = !matches!(&v.kind, ExprKind::Symbol(vs) if *vs == name);
let fid = self.intern(&name);

if !has_default {
// No default: `Op::Field` yields the value, or unit if absent
// (the VM returns unit for a missing map key), matching the
// interpreter's `None => Value::Unit` fallthrough.
let r = self.reg();
self.emit(Op::Field(r, map_reg, Selector::Name(fid), span));
self.bind(&name, r);
continue;
}

// Default present: branch on key presence so the default is only
// evaluated (and its side effects only run) when the key is
// missing — `Op::Field` alone cannot distinguish an absent key
// from a present unit value.
let key_reg = self.reg();
self.emit(Op::Lit(key_reg, Lit::Keyword(fid), span));
let cond = self.reg();
self.emit(Op::Builtin(
cond,
Built::Contains,
vec![map_reg, key_reg],
span,
));

let then_b = self.new_block();
let else_b = self.new_block();
let merge_b = self.new_block();
self.seal(End::Br(cond, then_b, else_b));

// Present: read the field.
self.switch_to(then_b);
let field_r = self.reg();
self.emit(Op::Field(field_r, map_reg, Selector::Name(fid), span));
self.seal(End::Jmp(merge_b, vec![field_r]));

// Absent: evaluate the default expression.
self.switch_to(else_b);
let default_r = self.lower_expr(v);
self.seal(End::Jmp(merge_b, vec![default_r]));

// Merge: the bound value is the block parameter.
self.switch_to(merge_b);
let result = self.reg();
let func = &mut self.module.funcs[self.cur_func.unwrap()];
func.blocks[merge_b.0 as usize].params.push(result);
self.bind(&name, result);
}
}

Expand Down
25 changes: 25 additions & 0 deletions crates/loon-lang/tests/backend_parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,31 @@ const CORPUS: &[(&str, &str)] = &[
"map-eq-order-independent",
"[fn main [] [println [= {:a 1 :b 2} {:b 2 :a 1}]]]",
),
// map destructuring in let: `{name name}` shorthand binds a key to its
// value; both backends must bind the names (the EIR VM used to drop the
// pattern silently and bind nothing). Present keys read through.
(
"let-map-destructure-shorthand",
"[fn main [] [let m {:name 42 :age 7}] \
[let {name name age age} m] \
[println name] [println age]]",
),
// a per-key default expression is used only when the key is ABSENT; a
// present key keeps its value and the default is not evaluated.
(
"let-map-destructure-default",
"[fn main [] [let m {:name 42}] \
[let {name name missing 99} m] \
[println name] [println missing]]",
),
// a missing key with NO default binds unit on both backends (matching the
// interpreter's `None => Value::Unit` fallthrough) — printed as ().
(
"let-map-destructure-missing-no-default",
"[fn main [] [let m {:a 1}] \
[let {gone gone} m] \
[println gone]]",
),
// control / pattern
("when", "[fn main [] [when [> 3 2] [println \"yes\"]]]"),
("nested-let", "[fn main [] [let a 2] [let b [* a 3]] [let c [+ a b]] [println c]]"),
Expand Down
Loading