diff --git a/crates/loon-lang/src/check/mod.rs b/crates/loon-lang/src/check/mod.rs index f4a20c2..49bbcd8 100644 --- a/crates/loon-lang/src/check/mod.rs +++ b/crates/loon-lang/src/check/mod.rs @@ -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); } _ => {} } @@ -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; diff --git a/crates/loon-lang/src/eir/lower.rs b/crates/loon-lang/src/eir/lower.rs index 5266e76..68a69bf 100644 --- a/crates/loon-lang/src/eir/lower.rs +++ b/crates/loon-lang/src/eir/lower.rs @@ -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) => { @@ -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); } } diff --git a/crates/loon-lang/tests/backend_parity.rs b/crates/loon-lang/tests/backend_parity.rs index 2da0b4c..8def2e5 100644 --- a/crates/loon-lang/tests/backend_parity.rs +++ b/crates/loon-lang/tests/backend_parity.rs @@ -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]]"),