From 2f9c9b1d2e317e9b7eb825cba93a3d218d6b2ddd Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Mon, 28 Sep 2026 09:40:03 -0700 Subject: [PATCH] feat(lang): add support for struct pattern matching --- README.md | 20 + crates/analyzer/src/navigation.rs | 15 +- crates/analyzer/src/rpc.rs | 6 +- crates/compiler/src/ast/mod.rs | 91 +++- crates/compiler/src/compile.rs | 660 ++++++++++++++++--------- crates/compiler/src/compile/result.rs | 13 +- crates/compiler/src/fingerprint.rs | 6 +- crates/compiler/src/lib.rs | 164 +++++- crates/compiler/src/nav.rs | 83 +++- crates/compiler/src/parser/grammar.rs | 54 +- crates/compiler/src/parser/mod.rs | 53 +- docs/docs/language/cells-functions.md | 9 + docs/docs/language/control-flow.md | 23 +- docs/docs/language/types-values.md | 2 +- docs/parser.md | 8 +- examples/struct_destructure/Argon.toml | 2 + examples/struct_destructure/lib.ar | 46 ++ 17 files changed, 964 insertions(+), 291 deletions(-) create mode 100644 examples/struct_destructure/Argon.toml create mode 100644 examples/struct_destructure/lib.ar diff --git a/README.md b/README.md index 23f7f12e..f0dfa3ae 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,26 @@ including on the command line: arc run --cell 'via(ViaParams { layer: "met1", size: Size { w: 100., h: 50. }, n: 1 })' ``` +A `let` takes a struct apart with a pattern that names the struct and binds its +fields. A bare `name` binds the field of that name, `field: other` binds it +under another name, and `field: _` drops it. A pattern must name every field +unless it ends in `..`, and field patterns do not nest. At the top of a cell, +each name a pattern binds is a field of the cell, like any other `let`. A +`match` on a struct takes the same patterns: + +```rust +fn area(s: Size) -> Float { + let Size { w, h: height } = s; + w * height +} + +cell via_pair(p: ViaParams) { + let ViaParams { layer, size, .. } = p; + let left = rect(layer, x0=0., y0=0., w=size.w, h=size.h); + let right = rect(layer, x0=2. * size.w, y0=0., w=size.w, h=size.h); +} +``` + Enum variants may carry values, either as a tuple or as named fields. A tuple variant is constructed like a call and taken apart by a `match` pattern that binds its payload; `_` skips an element, and a bare name or `_` arm matches diff --git a/crates/analyzer/src/navigation.rs b/crates/analyzer/src/navigation.rs index ccb500a8..1264c380 100644 --- a/crates/analyzer/src/navigation.rs +++ b/crates/analyzer/src/navigation.rs @@ -611,17 +611,12 @@ fn completion_allowed(candidate: &CompletionCandidate, site: CompletionSite) -> candidate.kind, Kind::Cell | Kind::Enum | Kind::Struct | Kind::Type ), - // A field is offered because a variant pattern binds its payload by - // name, as in `Shape::Box { w, .. }`. + // A field is offered because a struct or variant pattern binds fields + // by name, as in `Shape::Box { w, .. }`. CompletionSite::Pattern => match candidate.kind { - Kind::Enum | Kind::Variant | Kind::Module | Kind::Field => true, + Kind::Enum | Kind::Variant | Kind::Struct | Kind::Module | Kind::Field => true, Kind::Keyword => candidate.label == "_", - Kind::Function - | Kind::Cell - | Kind::Variable - | Kind::Parameter - | Kind::Struct - | Kind::Type => false, + Kind::Function | Kind::Cell | Kind::Variable | Kind::Parameter | Kind::Type => false, }, CompletionSite::ImportPath => candidate.kind == Kind::Module, CompletionSite::Keyword(keyword) => { @@ -1249,7 +1244,7 @@ mod tests { assert_eq!(labels(CompletionSite::ImportPath), ["lib"]); assert_eq!( labels(CompletionSite::Pattern), - ["Mode", "lib", "_", "Some"] + ["Mode", "Size", "lib", "_", "Some"] ); // The statement candidates, plus `else`. assert_eq!( diff --git a/crates/analyzer/src/rpc.rs b/crates/analyzer/src/rpc.rs index f954fc1c..07708c40 100644 --- a/crates/analyzer/src/rpc.rs +++ b/crates/analyzer/src/rpc.rs @@ -742,10 +742,8 @@ impl LangServer for State { let names = scope .stmts .iter() - .filter_map(|statement| match statement { - argonc::ast::Statement::LetBinding(binding) => Some(binding.name.name.as_str()), - _ => None, - }) + .flat_map(|statement| statement.let_names()) + .map(|name| name.name.as_str()) .collect::>(); let var_name = (0..) .map(|index| format!("inst{index}")) diff --git a/crates/compiler/src/ast/mod.rs b/crates/compiler/src/ast/mod.rs index 158a73f5..8c38b03d 100644 --- a/crates/compiler/src/ast/mod.rs +++ b/crates/compiler/src/ast/mod.rs @@ -244,11 +244,33 @@ pub struct Scope { #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub enum Statement { - Expr { value: Expr, semicolon: bool }, + Expr { + value: Expr, + semicolon: bool, + }, LetBinding(LetBinding), + /// `let S { f, g: name, .. } = value;` + LetPattern(LetPattern), ForLoop(ForLoop), } +impl Statement { + /// The names a `let` statement binds, in source order; empty for any + /// other statement. + pub fn let_names(&self) -> Vec<&Ident> { + match self { + Self::LetBinding(binding) => vec![&binding.name], + Self::LetPattern(binding) => binding + .pattern + .bindings() + .into_iter() + .map(|(name, _)| name) + .collect(), + Self::Expr { .. } | Self::ForLoop(_) => Vec::new(), + } + } +} + #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub struct LetBinding { pub name: Ident, @@ -259,6 +281,15 @@ pub struct LetBinding { pub span: cfgrammar::Span, } +/// A `let` that destructures its value with a pattern. Each name the pattern +/// binds carries its own metadata. +#[derive_where(Debug, Clone, Serialize, Deserialize; S)] +pub struct LetPattern { + pub pattern: Pattern, + pub value: Expr, + pub span: cfgrammar::Span, +} + #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub struct ForLoop { pub var: Ident, @@ -380,8 +411,9 @@ pub enum Pattern { fields: Vec>, span: cfgrammar::Span, }, - /// An enum variant with named fields, `E::V { f, g: name, .. }`. - StructVariant { + /// A struct, `S { f, g: name, .. }`, or an enum variant with named + /// fields, `E::V { f, g: name, .. }`. + Struct { path: IdentPath, fields: Vec>, /// Whether the pattern ended in `..`, ignoring the fields not listed. @@ -390,7 +422,7 @@ pub enum Pattern { }, } -/// One `field: pattern` entry of a [`Pattern::StructVariant`]. +/// One `field: pattern` entry of a [`Pattern::Struct`]. #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub struct FieldPattern { pub name: Ident, @@ -405,9 +437,40 @@ impl Pattern { match self { Self::Wildcard { span } => *span, Self::Binding { name, .. } => name.span, - Self::Variant { span, .. } | Self::StructVariant { span, .. } => *span, + Self::Variant { span, .. } | Self::Struct { span, .. } => *span, + } + } + + /// The bindings this pattern introduces, in source order. + /// + /// A bare name counts as a binding even where it may turn out to name a + /// unit variant, which only the type checker can decide. + pub fn bindings(&self) -> Vec<(&Ident, &T::PatternBinding)> { + let mut out = Vec::new(); + self.collect_bindings(&mut out); + out + } + + fn collect_bindings<'p>(&'p self, out: &mut Vec<(&'p Ident, &'p T::PatternBinding)>) { + match self { + Self::Wildcard { .. } => {} + Self::Binding { name, metadata } => out.push((name, metadata)), + Self::Variant { fields, .. } => { + fields.iter().for_each(|field| field.collect_bindings(out)); + } + Self::Struct { fields, .. } => { + fields + .iter() + .for_each(|field| field.pattern.collect_bindings(out)); + } } } + + /// Whether the pattern is a name or `_`, which matches any value once type + /// checking has resolved bare names that are unit variants. + pub fn is_catch_all(&self) -> bool { + matches!(self, Self::Wildcard { .. } | Self::Binding { .. }) + } } /// A binary operation expression. @@ -1065,6 +1128,7 @@ pub trait AstTransformer { semicolon: *semicolon, }, Statement::LetBinding(l) => Statement::LetBinding(self.transform_let_binding(l)), + Statement::LetPattern(l) => Statement::LetPattern(self.transform_let_pattern(l)), Statement::ForLoop(l) => Statement::ForLoop(self.transform_for_loop(l)), } } @@ -1084,6 +1148,19 @@ pub trait AstTransformer { span: input.span, } } + fn transform_let_pattern( + &mut self, + input: &LetPattern, + ) -> LetPattern { + // The value is visited first, since the names are bound after it. + let value = self.transform_expr(&input.value); + let pattern = self.transform_pattern(&input.pattern); + LetPattern { + pattern, + value, + span: input.span, + } + } fn transform_for_loop( &mut self, input: &ForLoop, @@ -1162,12 +1239,12 @@ pub trait AstTransformer { .collect(), span: *span, }, - Pattern::StructVariant { + Pattern::Struct { path, fields, rest, span, - } => Pattern::StructVariant { + } => Pattern::Struct { path: self.transform_ident_path(path), fields: fields .iter() diff --git a/crates/compiler/src/compile.rs b/crates/compiler/src/compile.rs index 2c5eb865..385ed559 100644 --- a/crates/compiler/src/compile.rs +++ b/crates/compiler/src/compile.rs @@ -39,7 +39,7 @@ use crate::workspace::WorkspaceConfig; use crate::{ ast::{ ArgDecl, Ast, AstMetadata, AstTransformer, BinOp, BinOpExpr, BoolOp, CallExpr, CellDecl, - Decl, Expr, Ident, IfExpr, LetBinding, Statement, + Decl, Expr, Ident, IfExpr, LetBinding, LetPattern, Statement, }, cellcache::{CachedCell, CellCache}, fingerprint::{ItemIndex, RebaseError, SpanRebase}, @@ -1253,21 +1253,23 @@ impl StatementView { fn extend(&mut self, typing: &CellTyping<'_>) { let stmt = self.limit; self.limit += 1; - let Statement::LetBinding(binding) = &typing.decl.scope.stmts[stmt] else { - return; - }; - let name = &binding.name.name; + let names = typing.decl.scope.stmts[stmt].let_names(); match &typing.stmts[stmt] { - Some(Statement::LetBinding(typed)) => { - self.frame - .var_bindings - .insert(name.clone(), (typed.metadata, typed.value.ty())); - self.untyped.swap_remove(name.as_str()); + Some(typed) => { + // A bare name in a pattern may have typed as a unit variant, + // so the typed statement says which names it bound. + for name in names { + self.untyped.swap_remove(name.name.as_str()); + } + for (name, id, ty) in typed_let_bindings(typed) { + self.frame.var_bindings.insert(name.name.clone(), (id, ty)); + } } - Some(_) => unreachable!("a `let` statement is typed as a `let`"), None => { - self.frame.var_bindings.swap_remove(name.as_str()); - self.untyped.insert(name.clone(), stmt); + for name in names { + self.frame.var_bindings.swap_remove(name.name.as_str()); + self.untyped.insert(name.name.clone(), stmt); + } } } } @@ -1301,6 +1303,8 @@ struct Demand { cell: VarId, /// Index of the `let` statement declaring the field. stmt: usize, + /// The field's name. + field: String, /// Where it was read, for the diagnostic if the demand cannot be met. span: cfgrammar::Span, } @@ -1590,11 +1594,35 @@ impl VariantTys { } } -/// A variant pattern's annotated path and the variant it resolved to, if any. -type ResolvedPatternPath = ( - IdentPath, - Option<(VarId, Arc)>, -); +/// A pattern's annotated path and what it resolved to, if anything. +type ResolvedPatternPath = (IdentPath, Option); + +/// The names a typed `let` statement binds, with their ids and types; empty +/// for any other statement. +pub(crate) fn typed_let_bindings( + stmt: &Statement, +) -> Vec<(&Ident, VarId, Ty)> { + match stmt { + Statement::LetBinding(binding) => { + vec![(&binding.name, binding.metadata, binding.value.ty())] + } + Statement::LetPattern(binding) => binding + .pattern + .bindings() + .into_iter() + .map(|(name, (id, ty))| (name, *id, ty.clone())) + .collect(), + Statement::Expr { .. } | Statement::ForLoop(_) => Vec::new(), + } +} + +/// The declaration a pattern's path names. +enum PatternTarget { + /// An enum variant, carrying any type arguments the path spelled out. + Variant(VarId, Arc), + /// A struct declaration, with the type arguments the path spelled out. + Struct(VarId, Arc, Option>), +} /// The checked form of a struct literal: the type it builds and, when the /// literal names an enum variant, that variant. @@ -2474,19 +2502,17 @@ impl<'a> VarIdTyPass<'a> { let mut lets: IndexMap> = IndexMap::new(); for (stmt, statement) in input.scope.stmts.iter().enumerate() { - let Statement::LetBinding(binding) = statement else { - continue; - }; - let name = &binding.name; - if RESERVED_CELL_FIELDS.contains(&name.name.as_str()) { - self.errors.push(StaticError { - span: self.span(name.span), - kind: StaticErrorKind::ReservedCellField { - name: name.name.to_string(), - }, - }); + for name in statement.let_names() { + if RESERVED_CELL_FIELDS.contains(&name.name.as_str()) { + self.errors.push(StaticError { + span: self.span(name.span), + kind: StaticErrorKind::ReservedCellField { + name: name.name.to_string(), + }, + }); + } + lets.entry(name.name.clone()).or_default().push(stmt); } - lets.entry(name.name.clone()).or_default().push(stmt); } self.cells.insert( cell_id, @@ -2717,14 +2743,11 @@ impl<'a> VarIdTyPass<'a> { || goal.attempting == Some(Unit::Stmt(demand.stmt))) }); if cyclic { - let Statement::LetBinding(binding) = &typing.decl.scope.stmts[demand.stmt] else { - unreachable!("demands name `let` statements") - }; self.errors.push(StaticError { span: self.span(demand.span), kind: StaticErrorKind::CyclicCellField { cell: typing.decl.name.name.to_string(), - field: binding.name.name.to_string(), + field: demand.field.clone(), }, }); self.poisoned.insert((demand.cell, demand.stmt)); @@ -2784,12 +2807,8 @@ impl<'a> VarIdTyPass<'a> { } let fields = stmts .iter() - .filter_map(|stmt| match stmt { - Statement::LetBinding(binding) => { - Some((binding.name.name.to_string(), binding.value.ty())) - } - _ => None, - }) + .flat_map(typed_let_bindings) + .map(|(name, _, ty)| (name.name.to_string(), ty)) .collect(); self.finished_cell_fields.insert(cell, fields); let scope = Scope { @@ -2811,12 +2830,17 @@ impl<'a> VarIdTyPass<'a> { /// Records that the current attempt read the field declared by statement /// `stmt` of `cell` before it was typed. - fn demand(&mut self, cell: VarId, stmt: usize, span: cfgrammar::Span) { + fn demand(&mut self, cell: VarId, stmt: usize, field: &str, span: cfgrammar::Span) { if self.poisoned.contains(&(cell, stmt)) { return; } if let Some(attempt) = self.attempt.as_mut() { - attempt.demands.push(Demand { cell, stmt, span }); + attempt.demands.push(Demand { + cell, + stmt, + field: field.to_string(), + span, + }); } } @@ -2830,7 +2854,7 @@ impl<'a> VarIdTyPass<'a> { let (Some(cell), Some(&stmt)) = (attempt.cell, attempt.visible_untyped.get(name)) else { return false; }; - self.demand(cell, stmt, span); + self.demand(cell, stmt, name, span); true } @@ -2894,10 +2918,16 @@ impl<'a> VarIdTyPass<'a> { return self.no_field_on_ty(field, base_ty.clone()); }; return match &typing.stmts[stmt] { - Some(Statement::LetBinding(binding)) => subst(&binding.value.ty(), &map), - Some(_) => unreachable!("`lets` indexes `let` statements"), + Some(typed) => match typed_let_bindings(typed) + .into_iter() + .find(|(binding, _, _)| binding.name == *name) + { + Some((_, _, ty)) => subst(&ty, &map), + // The name typed as a unit variant, not a binding. + None => self.no_field_on_ty(field, base_ty.clone()), + }, None => { - self.demand(def, stmt, field.span); + self.demand(def, stmt, name, field.span); Ty::Unknown } }; @@ -3895,14 +3925,14 @@ impl<'a> VarIdTyPass<'a> { } } - fn assert_ty_is_enum(&mut self, span: cfgrammar::Span, ty: &Ty) { + fn assert_ty_is_matchable(&mut self, span: cfgrammar::Span, ty: &Ty) { let ty = self.shallow(ty); - if !(matches!(ty, Ty::Enum(_) | Ty::Infer(_)) || ty.is_wildcard()) { + if !(matches!(ty, Ty::Enum(_) | Ty::Struct(_) | Ty::Infer(_)) || ty.is_wildcard()) { self.errors.push(StaticError { span: self.span(span), kind: StaticErrorKind::IncorrectTyCategory { found: self.display(&ty), - expected: "Enum".into(), + expected: "Enum or Struct".into(), }, }); } @@ -4745,60 +4775,63 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { arms: &[crate::ast::MatchArm], ) -> ::MatchExpr { let scrutinee_ty = self.shallow(&scrutinee.ty()); - self.assert_ty_is_enum(scrutinee.span(), &scrutinee_ty); + self.assert_ty_is_matchable(scrutinee.span(), &scrutinee_ty); // The scrutinee type is only known statically when it is a declared - // enum. `Any` is common in practice, because cell and instance types - // cannot be named, so arms are then checked against the enum the - // *patterns* name instead. The evaluator has no fallback for an arm set - // that does not cover the runtime variant, so the checks below are what - // keep it from reaching an unmatched value. - fn variant_ty(arm: &MatchArm) -> Option<&Ty> { + // enum or struct. `Any` is common in practice, because cell and + // instance types cannot be named, so arms are then checked against the + // type the *patterns* name instead. The evaluator has no fallback for + // an arm set that does not cover the runtime value, so the checks below + // are what keep it from reaching an unmatched value. + fn pattern_ty(arm: &MatchArm) -> Option<&Ty> { match &arm.pattern { - Pattern::Variant { path, .. } | Pattern::StructVariant { path, .. } => { + Pattern::Variant { path, .. } | Pattern::Struct { path, .. } => { Some(&path.metadata.1) } _ => None, } } - let pattern_ty = arms + let named_ty = arms .iter() - .filter_map(variant_ty) - .find(|ty| matches!(ty, Ty::Enum(_))) + .filter_map(pattern_ty) + .find(|ty| matches!(ty, Ty::Enum(_) | Ty::Struct(_))) .cloned(); let expected_ty = match scrutinee_ty { - Ty::Enum(_) => Some(scrutinee_ty.clone()), - _ => pattern_ty, + Ty::Enum(_) | Ty::Struct(_) => Some(scrutinee_ty.clone()), + _ => named_ty, }; - // Neither the scrutinee nor any arm pattern names an enum, so there is - // nothing to check the arms against: an unsayable match type has to be - // reported here or `--check` accepts a program the evaluator refuses. + // Neither the scrutinee nor any arm pattern names an enum or struct, so + // there is nothing to check the arms against: an unsayable match type + // has to be reported here or `--check` accepts a program the evaluator + // refuses. let Some(expected_ty) = expected_ty else { let already_diagnosed = matches!(scrutinee_ty, Ty::Unknown) || arms .iter() - .filter_map(variant_ty) + .filter_map(pattern_ty) .any(|ty| matches!(ty, Ty::Unknown)); if !already_diagnosed { self.errors.push(StaticError { span: self.span(scrutinee.span()), - kind: StaticErrorKind::NotAnEnum, + kind: StaticErrorKind::NotMatchable, }); } return Ty::Unknown; }; // An `Any` scrutinee leaves the patterns' type arguments unsolved; an - // inference variable scrutinee is solved to the patterns' enum. + // inference variable scrutinee is solved to the patterns' type. self.unify(&scrutinee_ty, &expected_ty); - let Ty::Enum(enum_ty) = &expected_ty else { - unreachable!("expected type is an enum") + // The enum variants no arm has covered yet. A struct has one shape, + // covered once an arm matches every value of it. + let mut remaining = match &expected_ty { + Ty::Enum(enum_ty) => self + .adt_def(enum_ty.def) + .and_then(AdtDef::as_enum) + .map(|def| def.variants.keys().cloned().collect::>()) + .unwrap_or_default(), + _ => IndexSet::from([String::new()]), }; - let mut remaining = self - .adt_def(enum_ty.def) - .and_then(AdtDef::as_enum) - .map(|def| def.variants.keys().cloned().collect::>()) - .unwrap_or_default(); let mut covered = IndexSet::new(); let mut result: Option = None; for arm in arms { @@ -4809,11 +4842,27 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { }); } else { match &arm.pattern { + // All arms must name the same type, whether or not the + // scrutinee's own type pinned that type down. + Pattern::Variant { path, .. } | Pattern::Struct { path, .. } + if matches!(expected_ty, Ty::Struct(_)) => + { + self.assert_eq_ty(arm.pattern.span(), &path.metadata.1, &expected_ty); + let catch_all = match &arm.pattern { + Pattern::Struct { fields, .. } => { + fields.iter().all(|field| field.pattern.is_catch_all()) + } + _ => false, + }; + // An arm whose path was already diagnosed counts as + // covering the struct, so that it is reported once. + if catch_all || matches!(path.metadata.1, Ty::Unknown) { + remaining.clear(); + } + } // A payload sub-pattern is a name or `_`, so an arm that // names a variant covers all of it. - Pattern::Variant { path, .. } | Pattern::StructVariant { path, .. } => { - // All arms must belong to the same enum, whether or not - // the scrutinee's own type pinned that enum down. + Pattern::Variant { path, .. } | Pattern::Struct { path, .. } => { self.assert_eq_ty(arm.pattern.span(), &path.metadata.1, &expected_ty); let variant = path.path.last().expect("paths are non-empty").name.clone(); remaining.swap_remove(variant.as_str()); @@ -5302,6 +5351,21 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { self.alloc(&name.name, ty) } + fn transform_let_pattern( + &mut self, + input: &LetPattern, + ) -> LetPattern { + let value = self.transform_expr(&input.value); + let value_ty = self.shallow(&value.ty()); + let pattern = self.type_pattern(&input.pattern, &value_ty); + self.check_let_pattern(&pattern, &value); + LetPattern { + pattern, + value, + span: input.span, + } + } + fn transform_for_loop( &mut self, input: &crate::ast::ForLoop, @@ -5359,7 +5423,7 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { } impl<'a> VarIdTyPass<'a> { - /// Types a `match` arm's pattern against the scrutinee's type, binding + /// Types a pattern against the type of the value it takes apart, binding /// each name it introduces in the current frame. A bare name is a unit /// variant pattern when it resolves to a unit variant, and a binding /// otherwise. @@ -5380,7 +5444,7 @@ impl<'a> VarIdTyPass<'a> { }; return self.type_variant_pattern( path, - Some((id, ctor)), + Some(PatternTarget::Variant(id, ctor)), &[], name.span, scrutinee_ty, @@ -5396,54 +5460,38 @@ impl<'a> VarIdTyPass<'a> { let (output, resolved) = self.resolve_pattern_path(path); self.type_variant_pattern(output, resolved, fields, *span, scrutinee_ty) } - Pattern::StructVariant { + Pattern::Struct { path, fields, rest, span, } => { let (output, resolved) = self.resolve_pattern_path(path); - self.type_struct_variant_pattern( - output, - resolved, - fields, - *rest, - *span, - scrutinee_ty, - ) + self.type_struct_pattern(output, resolved, fields, *rest, *span, scrutinee_ty) } } } - /// Resolves a variant pattern's path, reporting a name that is not a - /// variant, and returns the annotated path beside what it resolved to. + /// Resolves a pattern's path to an enum variant or a struct declaration, + /// reporting a name that is neither. fn resolve_pattern_path( &mut self, path: &IdentPath, ) -> ResolvedPatternPath { - let resolved = if path.path.len() == 1 { - match self.lookup(&path.path[0].name) { - Some((id, Ty::Ctor(ctor))) => Some((id, ctor)), - Some(_) | None => { - self.errors.push(StaticError { - span: self.span(path.span), - kind: StaticErrorKind::UndeclaredVar { - name: path.path[0].name.to_string(), - }, - }); - None - } + let binding = if path.path.len() == 1 { + let binding = self.lookup(&path.path[0].name); + if binding.is_none() { + self.errors.push(StaticError { + span: self.span(path.span), + kind: StaticErrorKind::UndeclaredVar { + name: path.path[0].name.to_string(), + }, + }); } + binding } else { match self.resolve_qualified(&path.path) { - Ok((id, Ty::Ctor(ctor))) => Some((id, ctor)), - Ok(_) => { - self.errors.push(StaticError { - span: self.span(path.span), - kind: StaticErrorKind::NotAnEnum, - }); - None - } + Ok(binding) => Some(binding), Err(error) => { self.report_qualified_error(error, path.span); None @@ -5451,6 +5499,44 @@ impl<'a> VarIdTyPass<'a> { } }; let explicit = self.explicit_args(path); + let resolved = match binding { + Some((id, Ty::Ctor(ctor))) => Some(PatternTarget::Variant( + id, + match explicit { + Some(args) => Arc::new(CtorTy { + args, + ..(*ctor).clone() + }), + None => ctor, + }, + )), + // A struct's own name is bound to its declaration's id; a variable + // of struct type is not a pattern and is reported below. + Some((id, Ty::Struct(struct_ty))) if id == struct_ty.def => { + Some(PatternTarget::Struct(id, struct_ty, explicit)) + } + None => None, + Some((_, Ty::Struct(_))) => { + self.errors.push(StaticError { + span: self.span(path.span), + kind: StaticErrorKind::NotAStruct, + }); + None + } + Some(_) => { + self.errors.push(StaticError { + span: self.span(path.span), + kind: if path.path.len() == 1 { + StaticErrorKind::UndeclaredVar { + name: path.path[0].name.to_string(), + } + } else { + StaticErrorKind::NotAnEnum + }, + }); + None + } + }; let output = IdentPath { path: path .path @@ -5464,18 +5550,6 @@ impl<'a> VarIdTyPass<'a> { metadata: (None, Ty::Unknown), span: path.span, }; - let resolved = resolved.map(|(id, ctor)| { - ( - id, - match explicit { - Some(args) => Arc::new(CtorTy { - args, - ..(*ctor).clone() - }), - None => ctor, - }, - ) - }); (output, resolved) } @@ -5573,17 +5647,26 @@ impl<'a> VarIdTyPass<'a> { fn type_variant_pattern( &mut self, mut path: IdentPath, - resolved: Option<(VarId, Arc)>, + resolved: Option, fields: &[Pattern], span: cfgrammar::Span, scrutinee_ty: &Ty, ) -> Pattern { - let Some((id, ctor)) = resolved else { - let fields = fields - .iter() - .map(|field| self.type_pattern(field, &Ty::Unknown)) - .collect(); - return Pattern::Variant { path, fields, span }; + let (id, ctor) = match resolved { + Some(PatternTarget::Variant(id, ctor)) => (id, ctor), + target => { + if let Some(PatternTarget::Struct(_, struct_ty, _)) = target { + self.errors.push(StaticError { + span: self.span(span), + kind: StaticErrorKind::StructPatternWithoutBraces(struct_ty.name.clone()), + }); + } + let fields = fields + .iter() + .map(|field| self.type_pattern(field, &Ty::Unknown)) + .collect(); + return Pattern::Variant { path, fields, span }; + } }; let def = self.adt_def(ctor.def).and_then(AdtDef::as_enum).cloned(); let Some(def) = def else { @@ -5678,12 +5761,12 @@ impl<'a> VarIdTyPass<'a> { (map, payload) } - /// Types a `E::V { f, g: name, .. }` pattern whose path resolved to - /// `resolved`, if it did. - fn type_struct_variant_pattern( + /// Types a `S { f, g: name, .. }` or `E::V { f, g: name, .. }` pattern + /// whose path resolved to `resolved`, if it did. + fn type_struct_pattern( &mut self, mut path: IdentPath, - resolved: Option<(VarId, Arc)>, + resolved: Option, fields: &[FieldPattern], rest: bool, span: cfgrammar::Span, @@ -5699,38 +5782,73 @@ impl<'a> VarIdTyPass<'a> { span: field.span, }) .collect(); - Pattern::StructVariant { + Pattern::Struct { path, fields, rest, span, } }; - let Some((id, ctor)) = resolved else { - return unchecked(self, path); - }; - let Some(def) = self.adt_def(ctor.def).and_then(AdtDef::as_enum).cloned() else { - return unchecked(self, path); - }; - let (map, payload) = - self.variant_pattern_payload(&mut path, id, &ctor, &def, span, scrutinee_ty); - let ctor_ty = Ty::Ctor(Arc::new((*ctor).clone())); - let VariantTys::Struct(payload) = payload else { - self.errors.push(StaticError { - span: self.span(span), - kind: StaticErrorKind::NotAStructVariant(format!( - "{}::{}", - ctor.enum_name, ctor.variant - )), - }); - return unchecked(self, path); + // The substitution for the declaration's type parameters, its named + // fields, and the type that names the pattern in diagnostics. + let (map, declared, pattern_ty) = match resolved { + None => return unchecked(self, path), + Some(PatternTarget::Variant(id, ctor)) => { + let Some(def) = self.adt_def(ctor.def).and_then(AdtDef::as_enum).cloned() else { + return unchecked(self, path); + }; + let (map, payload) = + self.variant_pattern_payload(&mut path, id, &ctor, &def, span, scrutinee_ty); + let VariantTys::Struct(payload) = payload else { + self.errors.push(StaticError { + span: self.span(span), + kind: StaticErrorKind::NotAStructVariant(format!( + "{}::{}", + ctor.enum_name, ctor.variant + )), + }); + return unchecked(self, path); + }; + (map, payload, Ty::Ctor(Arc::new((*ctor).clone()))) + } + Some(PatternTarget::Struct(id, struct_ty, explicit)) => { + let Some(def) = self + .adt_def(struct_ty.def) + .and_then(AdtDef::as_struct) + .cloned() + else { + return unchecked(self, path); + }; + // Like a variant pattern, a struct pattern takes the + // scrutinee's type arguments when it is this struct. + let args = match scrutinee_ty { + Ty::Struct(ty) if ty.def == struct_ty.def && explicit.is_none() => { + ty.args.clone() + } + _ => { + let map = self.instantiate(&def.params, explicit, span, &struct_ty.name); + def.params + .iter() + .map(|param| map[¶m.id].clone()) + .collect() + } + }; + let map = param_map(&def.params, &args); + let ty = Ty::Struct(Arc::new(StructTy { + def: struct_ty.def, + name: struct_ty.name.clone(), + args, + })); + path.metadata = (Some(id), ty.clone()); + (map, def.fields, ty) + } }; let mut seen = IndexSet::new(); let fields = fields .iter() .map(|field| { let name = field.name.name.as_str(); - let ty = match payload.get(name) { + let ty = match declared.get(name) { Some(ty) if seen.insert(name.to_string()) => subst(ty, &map), Some(_) => { self.errors.push(StaticError { @@ -5741,7 +5859,7 @@ impl<'a> VarIdTyPass<'a> { }); Ty::Unknown } - None => self.no_field_on_ty(&field.name, ctor_ty.clone()), + None => self.no_field_on_ty(&field.name, pattern_ty.clone()), }; FieldPattern { name: self.transform_ident(&field.name), @@ -5752,7 +5870,7 @@ impl<'a> VarIdTyPass<'a> { }) .collect(); if !rest { - let missing = payload + let missing = declared .keys() .filter(|name| !seen.contains(name.as_str())) .map(|name| format!("`{name}`")) @@ -5761,19 +5879,48 @@ impl<'a> VarIdTyPass<'a> { self.errors.push(StaticError { span: self.span(span), kind: StaticErrorKind::MissingPatternFields { - ty: self.display(&ctor_ty), + ty: self.display(&pattern_ty), fields: missing.join(", "), }, }); } } - Pattern::StructVariant { + Pattern::Struct { path, fields, rest, span, } } + + /// Checks that a typed `let` pattern matches every value of `value`'s + /// type: a struct pattern whose fields are names or `_`. + fn check_let_pattern( + &mut self, + pattern: &Pattern, + value: &Expr, + ) { + match pattern { + Pattern::Struct { path, fields, .. } if matches!(path.metadata.1, Ty::Struct(_)) => { + self.assert_eq_ty(value.span(), &value.ty(), &path.metadata.1); + for field in fields { + if !field.pattern.is_catch_all() { + self.errors.push(StaticError { + span: self.span(field.pattern.span()), + kind: StaticErrorKind::RefutableLetPattern, + }); + } + } + } + // A path that resolved to nothing was reported by resolution. + Pattern::Struct { path, .. } | Pattern::Variant { path, .. } + if matches!(path.metadata.1, Ty::Unknown) => {} + _ => self.errors.push(StaticError { + span: self.span(pattern.span()), + kind: StaticErrorKind::RefutableLetPattern, + }), + } + } } /// Replaces every inference variable in the typed nodes of one unit by its @@ -7589,34 +7736,7 @@ impl<'a> ExecPass<'a> { scope: root_scope_id, seq_num, }; - match stmt { - Statement::LetBinding(binding) => { - let value = self.visit_expr(loc, &binding.value); - self.frames - .get_mut(&fid) - .unwrap() - .bindings - .insert(binding.metadata, value); - self.cell_states - .get_mut(&cell_id) - .unwrap() - .fields - .insert(binding.name.name.to_string(), value); - self.cell_state_mut(loc.cell) - .scopes - .get_mut(&loc.scope) - .unwrap() - .bindings - .insert(loc.seq_num, (binding.name.name.to_string(), value)); - seq_num = seq_num.next(); - } - Statement::Expr { value, .. } => { - self.visit_expr(loc, value); - } - Statement::ForLoop(f) => { - self.eval_for_loop(loc, f); - } - } + seq_num = self.eval_stmt(loc, stmt, true); } while { @@ -8614,31 +8734,79 @@ impl<'a> ExecPass<'a> { }); } - fn eval_stmt(&mut self, loc: DynLoc, stmt: &Statement) { + /// Evaluates `stmt` at `loc` and returns the sequence number that follows + /// the names it binds. A top-level statement of a cell also publishes + /// those names as fields of the cell. + fn eval_stmt( + &mut self, + loc: DynLoc, + stmt: &Statement, + top_level: bool, + ) -> SeqNum { match stmt { Statement::LetBinding(binding) => { let value = self.visit_expr(loc, &binding.value); - self.frames - .get_mut(&loc.frame) - .unwrap() - .bindings - .insert(binding.metadata, value); - self.cell_state_mut(loc.cell) - .scopes - .get_mut(&loc.scope) - .unwrap() - .bindings - .insert(loc.seq_num, (binding.name.name.to_string(), value)); + self.bind_let(loc, binding.metadata, &binding.name.name, value, top_level); + loc.seq_num.next() + } + Statement::LetPattern(binding) => { + let value = self.visit_expr(loc, &binding.value); + let mut seq_num = loc.seq_num; + let Pattern::Struct { path, fields, .. } = &binding.pattern else { + return seq_num; + }; + let struct_name = match &path.metadata.1 { + Ty::Struct(ty) => Some(ty.name.clone()), + _ => None, + }; + for field in fields { + let Pattern::Binding { name, metadata } = &field.pattern else { + continue; + }; + let loc = DynLoc { seq_num, ..loc }; + let element = self.new_deferred_value(loc, |_| { + PartialEvalState::Destructure(Box::new(PartialDestructure { + base: value, + struct_name: struct_name.clone(), + field: field.name.name.to_string(), + span: field.span, + })) + }); + self.bind_let(loc, metadata.0, &name.name, element, top_level); + seq_num = seq_num.next(); + } + seq_num } Statement::Expr { value, .. } => { self.visit_expr(loc, value); + loc.seq_num } Statement::ForLoop(f) => { self.eval_for_loop(loc, f); + loc.seq_num } } } + /// Binds the `let` name `name`, with id `id`, to `value` at `loc`. + fn bind_let(&mut self, loc: DynLoc, id: VarId, name: &str, value: ValueId, field: bool) { + self.frames + .get_mut(&loc.frame) + .unwrap() + .bindings + .insert(id, value); + let state = self.cell_state_mut(loc.cell); + if field { + state.fields.insert(name.to_string(), value); + } + state + .scopes + .get_mut(&loc.scope) + .unwrap() + .bindings + .insert(loc.seq_num, (name.to_string(), value)); + } + /// Create a new execution scope. /// /// parent is the dynamic parent scope. @@ -8697,10 +8865,7 @@ impl<'a> ExecPass<'a> { scope, seq_num, }; - self.eval_stmt(loc, stmt); - if matches!(stmt, Statement::LetBinding(_)) { - seq_num = seq_num.next(); - } + seq_num = self.eval_stmt(loc, stmt, false); } let loc = DynLoc { @@ -10878,17 +11043,19 @@ impl<'a> ExecPass<'a> { MatchExprState::Scrutinee(scrutinee) => { if let Defer::Ready(val) = &self.values[&scrutinee] { // A scrutinee typed `Any` was never proven to be an - // enum value, and even a genuine enum value may belong - // to a different enum than the arms name. - let Some(value) = val.get_enum().cloned() else { + // enum or struct value, and even a genuine one may be + // of a different type than the arms name. + if !matches!(val, Value::Enum(_) | Value::Struct(_)) { let span = self.span(&vref.loc, match_.expr.scrutinee.span()); self.invalid_type(cell_id, &span); return self.poison(cell_id, vid); - }; - let arm = - match_.expr.arms.iter().find(|arm| { - pattern_matches(&arm.pattern, &Value::Enum(value.clone())) - }); + } + let value = val.clone(); + let arm = match_ + .expr + .arms + .iter() + .find(|arm| pattern_matches(&arm.pattern, &value)); let Some(arm) = arm else { let span = self.span(&vref.loc, match_.expr.scrutinee.span()); self.invalid_type(cell_id, &span); @@ -10900,7 +11067,7 @@ impl<'a> ExecPass<'a> { bindings: Default::default(), parent: Some(vref.loc.frame), }; - self.bind_pattern(&arm.pattern, scrutinee, &Value::Enum(value), &mut frame); + self.bind_pattern(&arm.pattern, scrutinee, &value, &mut frame); let fid = self.frame_id(); self.frames.insert(fid, frame); let value = self.visit_expr( @@ -11755,6 +11922,32 @@ impl<'a> ExecPass<'a> { true } } + PartialEvalState::Destructure(d) => { + if let Defer::Ready(base) = &self.values[&d.base] { + // The base may have arrived as `Any`, so it was never + // checked against the struct the pattern names. + let element = match base.as_ref() { + ValueRef::Struct(value) + if d.struct_name + .as_ref() + .is_none_or(|name| *name == value.name) => + { + value.fields.get(&d.field).cloned() + } + _ => None, + }; + let Some(element) = element else { + let span = self.span(&vref.loc, d.span); + self.invalid_type(cell_id, &span); + return self.poison(cell_id, vid); + }; + self.values.insert(vid, DeferValue::Ready(element)); + true + } else { + self.add_value_dependent(d.base, vid); + false + } + } PartialEvalState::ForLoop(f) => { if let Defer::Ready(val) = &self.values[&f.seq] { let seq = match val.as_ref() { @@ -11835,12 +12028,14 @@ impl<'a> ExecPass<'a> { self.bind_payload_element(field, element, frame); } } - Pattern::StructVariant { fields, .. } => { - let Value::Enum(value) = value else { - return; - }; - let VariantValues::Struct(values) = &value.payload else { - return; + Pattern::Struct { fields, .. } => { + let values = match value { + Value::Struct(value) => &value.fields, + Value::Enum(value) => match &value.payload { + VariantValues::Struct(values) => values, + VariantValues::Tuple(_) => return, + }, + _ => return, }; for field in fields { let Some(element) = values.get(field.name.name.as_str()) else { @@ -11852,7 +12047,8 @@ impl<'a> ExecPass<'a> { } } - /// Binds one payload element of a variant pattern, which is a name or `_`. + /// Binds one element of a variant or struct pattern, which is a name or + /// `_`. fn bind_payload_element( &mut self, pattern: &Pattern, @@ -12109,12 +12305,14 @@ fn pattern_matches(pattern: &Pattern, value: &Value) -> .zip(value.payload.values()) .all(|(field, element)| pattern_matches(field, element)) } - Pattern::StructVariant { path, fields, .. } => { - let Some(value) = enum_value(path, value) else { - return false; - }; - let VariantValues::Struct(values) = &value.payload else { - return false; + Pattern::Struct { path, fields, .. } => { + let values = match (&path.metadata.1, value) { + (Ty::Struct(ty), Value::Struct(value)) if value.name == ty.name => &value.fields, + (Ty::Struct(_), _) => return false, + _ => match enum_value(path, value).map(|value| &value.payload) { + Some(VariantValues::Struct(values)) => values, + _ => return false, + }, }; fields.iter().all(|field| { values @@ -12683,6 +12881,7 @@ enum PartialEvalState { StructLit(Box>), ForLoop(Box>), Ctor(PartialCtor), + Destructure(Box), } impl PartialEvalState { @@ -12723,10 +12922,23 @@ impl PartialEvalState { Self::StructLit(e) => e.fields.iter().copied().chain(e.base).collect(), Self::ForLoop(f) => vec![f.seq], Self::Ctor(c) => c.args.clone(), + Self::Destructure(d) => vec![d.base], } } } +/// One field read out of a struct by a `let` pattern. +#[derive(Debug, Clone)] +struct PartialDestructure { + base: ValueId, + /// The struct the pattern names, checked against a base that arrived as + /// `Any`. + struct_name: Option, + field: String, + /// The field pattern, for diagnostics. + span: cfgrammar::Span, +} + /// A tuple variant being constructed from its payload arguments. #[derive(Debug, Clone)] struct PartialCtor { diff --git a/crates/compiler/src/compile/result.rs b/crates/compiler/src/compile/result.rs index 69a12d2f..0635dee7 100644 --- a/crates/compiler/src/compile/result.rs +++ b/crates/compiler/src/compile/result.rs @@ -47,7 +47,7 @@ pub enum StaticErrorKind { /// Attempted to use an enum variant that is not declared by the enum. #[error("not a variant of the enum: {0}")] InvalidVariant(String), - /// A struct literal names something that is not a struct type. + /// A struct literal or pattern names something that is not a struct type. #[error("expected a struct type")] NotAStruct, /// A struct literal gives the same field twice. @@ -89,9 +89,18 @@ pub enum StaticErrorKind { /// with named fields accepts. #[error("variant `{0}` has no named fields")] NotAStructVariant(String), - /// A variant pattern without `..` omits declared fields. + /// A struct or variant pattern without `..` omits declared fields. #[error("missing fields {fields} in pattern of {ty}; add `..` to ignore them")] MissingPatternFields { ty: String, fields: String }, + /// A struct was matched by position, as though it were a tuple variant. + #[error("`{0}` is a struct; destructure it with braces")] + StructPatternWithoutBraces(String), + /// A `let` pattern that some value of its type would not match. + #[error("`let` requires a pattern that always matches, such as `Struct {{ field, .. }}`")] + RefutableLetPattern, + /// A `match` whose scrutinee and arms name neither an enum nor a struct. + #[error("`match` requires an enum or struct value")] + NotMatchable, /// A variant literal used `..base`, which only a struct literal accepts. #[error("`..` is not supported in a variant; every field must be given")] VariantLiteralBase, diff --git a/crates/compiler/src/fingerprint.rs b/crates/compiler/src/fingerprint.rs index 9b0f2c61..3cab2508 100644 --- a/crates/compiler/src/fingerprint.rs +++ b/crates/compiler/src/fingerprint.rs @@ -416,7 +416,7 @@ impl Builder<'_> { self.pattern(field, out); } } - Pattern::StructVariant { path, fields, .. } => { + Pattern::Struct { path, fields, .. } => { self.ident_path(path, out); for field in fields { self.pattern(&field.pattern, out); @@ -431,6 +431,10 @@ impl Builder<'_> { match stmt { Statement::Expr { value, .. } => self.expr(value, out), Statement::LetBinding(binding) => self.expr(&binding.value, out), + Statement::LetPattern(binding) => { + self.expr(&binding.value, out); + self.pattern(&binding.pattern, out); + } Statement::ForLoop(loop_) => { self.expr(&loop_.seq, out); self.scope(&loop_.body, out); diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 9e0699f6..0581f664 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -234,6 +234,7 @@ mod tests { const ARGON_OPTION: &str = concatcp!(EXAMPLES_DIR, "/option/lib.ar"); const ARGON_ENUM_PAYLOAD: &str = concatcp!(EXAMPLES_DIR, "/enum_payload/lib.ar"); const ARGON_ENUM_STRUCT_PAYLOAD: &str = concatcp!(EXAMPLES_DIR, "/enum_struct_payload/lib.ar"); + const ARGON_STRUCT_DESTRUCTURE: &str = concatcp!(EXAMPLES_DIR, "/struct_destructure/lib.ar"); const ARGON_RECURSIVE_STRUCT: &str = concatcp!(EXAMPLES_DIR, "/recursive_struct/lib.ar"); const ARGON_GENERICS_CELL: &str = concatcp!(EXAMPLES_DIR, "/generics_cell/lib.ar"); @@ -4674,7 +4675,7 @@ cell top() { // expression produced a second `expected .., found Unknown`. // // Six predicates encode "this type satisfies every check". Teaching - // only `is_eq_ty`, `assert_ty_is_cell` and `assert_ty_is_enum` about + // only `is_eq_ty`, `assert_ty_is_cell` and `assert_ty_is_matchable` about // `Unknown` left the other three still reporting a second error, so // they all go through `Ty::is_wildcard` now and all are covered here. for source in [ @@ -4705,10 +4706,10 @@ cell top() { } #[test] - fn a_match_that_names_no_enum_is_reported() { + fn a_match_that_names_no_enum_or_struct_is_reported() { // `dispatch_match_expr` returned `lub_ty.unwrap_or_default()` -- // `Ty::Unknown` -- with no diagnostic when neither the scrutinee nor - // any arm pattern resolved to an enum. That was survivable only while + // any arm pattern resolved to an enum or struct. That was survivable only while // `is_eq_ty` compared `Unknown` structurally; once `Unknown` satisfies // every check it silently suppressed the caller's checks too, and // `--check` accepted a program the evaluator refuses. @@ -4723,7 +4724,7 @@ cell top() { assert!( errors .iter() - .any(|error| matches!(error, StaticErrorKind::NotAnEnum)), + .any(|error| matches!(error, StaticErrorKind::NotMatchable)), "{errors:#?}" ); @@ -5985,6 +5986,153 @@ cell top() { )); } + #[test] + fn let_struct_patterns_are_checked() { + let source = |stmt: &str| { + format!( + "struct S {{ a: Float, b: Int, o: Option }}\n\ + struct T {{ a: Float }}\n\ + enum E {{ V {{ a: Float }}, W, }}\n\ + fn f(s: S, t: T, e: E) -> Float {{ {stmt} 0. }}" + ) + }; + for stmt in [ + "let S { a, b, o } = s;", + "let S { a: x, .. } = s;", + "let S { a: _, b: _, o: _ } = s;", + "let S { .. } = s;", + "let lib::S { a, .. } = s;", + ] { + assert!(generic_errors(&source(stmt)).is_empty(), "{stmt}"); + } + assert!(matches!( + generic_errors(&source("let S { a } = s;")).as_slice(), + [StaticErrorKind::MissingPatternFields { ty, fields }] + if ty == "S" && fields == "`b`, `o`" + )); + assert!(matches!( + generic_errors(&source("let S { c, .. } = s;")).as_slice(), + [StaticErrorKind::NoFieldOnTy { .. }] + )); + assert!(matches!( + generic_errors(&source("let S { a, a, .. } = s;")).as_slice(), + [StaticErrorKind::DuplicateStructField { field }] if field == "a" + )); + assert!(matches!( + generic_errors(&source("let T { a } = s;")).as_slice(), + [StaticErrorKind::IncorrectTy { .. }] + )); + // A pattern that some value would not match is not a `let` pattern. + assert!(matches!( + generic_errors(&source("let E::V { a } = e;")).as_slice(), + [StaticErrorKind::RefutableLetPattern] + )); + assert!(matches!( + generic_errors(&source("let S { o: None, .. } = s;")).as_slice(), + [StaticErrorKind::RefutableLetPattern] + )); + // A variable of struct type does not name a struct. + assert!(matches!( + generic_errors(&source("let v = s; let v { a, .. } = s;")).as_slice(), + [StaticErrorKind::NotAStruct] + )); + // The bindings have the field types, substituted for a generic struct. + assert!( + generic_errors( + "struct P { x: A }\n\ + fn f(p: P) -> Int { let P { x } = p; x }" + ) + .is_empty() + ); + assert!(matches!( + generic_errors( + "struct P { x: A }\n\ + fn f(p: P) -> Float { let P { x } = p; x }" + ) + .as_slice(), + [StaticErrorKind::IncorrectTy { .. }] + )); + } + + #[test] + fn match_struct_patterns_are_checked() { + let source = |arms: &str| { + format!( + "struct S {{ a: Float, o: Option }}\n\ + fn f(s: S) -> Float {{ match s {{ {arms} }} }}" + ) + }; + for arms in [ + "S { a, .. } => a,", + "S { o: None, .. } => 1., S { a, o } => a,", + "S { o: None, .. } => 1., _ => 2.,", + ] { + assert!(generic_errors(&source(arms)).is_empty(), "{arms}"); + } + assert!(matches!( + generic_errors(&source("S { a, .. } => a, _ => 2.,")).as_slice(), + [StaticErrorKind::UnreachableMatchArm] + )); + assert!(matches!( + generic_errors(&source("S { o: None, .. } => 1.,")).as_slice(), + [StaticErrorKind::MatchArmsNotComprehensive] + )); + assert!(matches!( + generic_errors(&source("S(a, o) => a,")).as_slice(), + [StaticErrorKind::StructPatternWithoutBraces(ty)] if ty == "S" + )); + // A struct scrutinee is not matched by variants. + assert!(matches!( + generic_errors(&source("None => 1., _ => 2.,")).as_slice(), + [StaticErrorKind::IncorrectTy { .. }] + )); + assert!(matches!( + generic_errors("fn f(x: Float) -> Float { match x { _ => 1., } }").as_slice(), + [StaticErrorKind::IncorrectTyCategory { .. }, ..] + )); + } + + #[test] + fn destructured_names_are_cell_fields() { + // `top` reads the fields of `pad` before `pad` is declared, so the + // pattern's statement is typed on demand. + let data = compile_source( + "struct Pad { r: Rect, via: Rect } + cell top() { + let p = inst(pad(), x=0., y=0.); + let r = rect(\"met2\", x0=0., y0=0., w=p.shape.w, h=p.via.h); + } + cell pad() { + let Pad { r: shape, via } = Pad { + r: rect(\"met1\", x0=0., y0=0., w=30., h=10.), + via: rect(\"via1\", x0=0., y0=0., w=2., h=4.), + }; + }", + "top", + Vec::new(), + ) + .unwrap_valid(); + assert_eq!(top_rect_sizes(&data), [(30., 4.)]); + let pad = data.cells.values().find(|cell| cell.name == "pad").unwrap(); + assert!(pad.fields.contains_key("shape") && pad.fields.contains_key("via")); + } + + #[test] + fn a_struct_pattern_rejects_another_struct_at_runtime() { + // A value typed `Any` reaches the pattern unchecked. + for body in ["let S { a } = v; a", "match v { S { a } => a, }"] { + let errors = run_source(&format!( + "struct S {{ a: Float }} + struct T {{ a: Float }} + fn f(v: Any) -> Float {{ {body} }} + cell top() {{ + let r = rect(\"met1\", x0=0., y0=0., w=f(T {{ a: 1. }}), h=1.); + }}" + )); + assert_reports(&errors, |error| matches!(error, ExecErrorKind::InvalidType)); + } + } + #[test] fn match_arms_after_a_catch_all_are_unreachable() { let source = |arms: &str| { @@ -6381,6 +6529,14 @@ cell top() { assert_eq!(rect_widths_of(&data, "shape"), [100., 300.]); } + #[test] + fn argon_struct_destructure() { + let data = compile_example(ARGON_STRUCT_DESTRUCTURE); + // `area(size) / w` recovers the height; the second rect's width is + // `first(p)` times the count read out of the `Some(3)`. + assert_eq!(top_rect_sizes(&data), [(100., 50.), (60., 50.)]); + } + #[test] fn argon_enum_struct_payload() { let data = compile_example(ARGON_ENUM_STRUCT_PAYLOAD); diff --git a/crates/compiler/src/nav.rs b/crates/compiler/src/nav.rs index 7beba05d..156819b9 100644 --- a/crates/compiler/src/nav.rs +++ b/crates/compiler/src/nav.rs @@ -29,7 +29,7 @@ use crate::{ }, compile::{ AdtDef, BUILTINS, RESERVED_CELL_FIELDS, Ty, TyParamTy, TypeDefs, TypedWorkspace, VarId, - VarIdTyMetadata, VariantTys, module_prefix, param_map, subst, + VarIdTyMetadata, VariantTys, module_prefix, param_map, subst, typed_let_bindings, }, }; @@ -1130,19 +1130,16 @@ impl<'a> Builder<'a> { .scope .stmts .iter() - .filter_map(|stmt| match stmt { - Statement::LetBinding(binding) => Some(binding), - _ => None, - }) + .flat_map(typed_let_bindings) .collect::>(); let fields = bindings .iter() - .map(|binding| (binding.name.name.to_string(), binding.metadata)) + .map(|(name, id, _)| (name.name.to_string(), *id)) .collect(); self.cell_fields.insert(decl.metadata.1, fields); let field_types = bindings - .iter() - .map(|binding| (binding.name.name.to_string(), binding.value.ty())) + .into_iter() + .map(|(name, _, ty)| (name.name.to_string(), ty)) .collect(); self.index .cell_field_types @@ -1597,6 +1594,9 @@ impl<'a> Builder<'a> { Statement::LetBinding(binding) => { find_param(&binding.value.ty(), name) } + Statement::LetPattern(binding) => { + find_param(&binding.value.ty(), name) + } _ => None, }) }), @@ -1890,6 +1890,11 @@ impl<'a> Builder<'a> { available_after: binding.span.end(), }); } + Statement::LetPattern(binding) => { + // The value is evaluated before the names are bound. + self.expr(&binding.value); + self.pattern(&binding.pattern, scope.span, binding.span.end()); + } Statement::ForLoop(loop_) => { self.expr(&loop_.seq); let key = DefKey::Var(loop_.metadata); @@ -1989,7 +1994,8 @@ impl<'a> Builder<'a> { Expr::Match(match_) => { self.expr(&match_.scrutinee); for arm in &match_.arms { - self.pattern(&arm.pattern, arm.expr.span()); + let body = arm.expr.span(); + self.pattern(&arm.pattern, body, body.start()); self.expr(&arm.expr); } } @@ -2121,12 +2127,13 @@ impl<'a> Builder<'a> { } } - /// Walks a `match` arm's pattern; a binding is a local scoped to the arm's - /// body. + /// Walks a pattern; a binding is a local visible in `scope` from + /// `available_after` on. fn pattern( &mut self, pattern: &'a Pattern, - body: cfgrammar::Span, + scope: cfgrammar::Span, + available_after: usize, ) { match pattern { Pattern::Wildcard { .. } => {} @@ -2151,29 +2158,30 @@ impl<'a> Builder<'a> { .push(ScopeBinding { name: name.name.to_string(), key, - scope: body, - available_after: body.start(), + scope, + available_after, }); } Pattern::Variant { path, fields, .. } => { self.ident_path(path); for field in fields { - self.pattern(field, body); + self.pattern(field, scope, available_after); } } - Pattern::StructVariant { path, fields, .. } => { + Pattern::Struct { path, fields, .. } => { self.ident_path(path); - let variant_id = path.metadata.0; + // Keyed by the variant, or by the struct for a struct pattern. + let owner_id = path.metadata.0; for field in fields { // A shorthand field is one token naming both the field and // the local it binds; the local is the more useful target. if !field.shorthand { - let target = variant_id.map_or(Target::Unresolved, |id| { + let target = owner_id.map_or(Target::Unresolved, |id| { Target::Def(DefKey::Field(id, field.name.name.to_string())) }); self.record(field.name.span, target); } - self.pattern(&field.pattern, body); + self.pattern(&field.pattern, scope, available_after); } } } @@ -2594,6 +2602,43 @@ cell top() { assert_eq!(hover("Circle {"), "Shape::Circle { radius: Float }"); } + /// A struct pattern's path jumps to the struct and its field names to the + /// fields; the names a `let` pattern binds are locals, and at the top of a + /// cell they are fields of its instances too. + #[test] + fn struct_patterns_resolve() { + check( + r#" +struct Size { width: Float, height: Float, } + +fn area(s: Size) -> Float { + let Si$0ze { wid$0th, height: tal$0l } = s; + wid$0th * tal$0l +} + +fn pick(s: Size) -> Float { + match s { + Size { heig$0ht: short, .. } => sho$0rt, + } +} + +cell pad() { + let Size { width: wi$0de, .. } = Size { width: 1., height: 2. }; + let r = rect("met1", x0=0., y0=0., w=wi$0de, h=1.); +} + +cell top() { + let p = inst(pad(), x=0., y=0.); + let q = p.wi$0de; +} +"#, + &[ + "Size#0", "width#1", "tall#0", "width#1", "tall#0", "height#0", "short#0", + "wide#0", "wide#0", "wide#0", + ], + ); + } + #[test] fn generic_types_hover_and_complete() { let source = r#" diff --git a/crates/compiler/src/parser/grammar.rs b/crates/compiler/src/parser/grammar.rs index 7bd901a0..c4d8bc77 100644 --- a/crates/compiler/src/parser/grammar.rs +++ b/crates/compiler/src/parser/grammar.rs @@ -19,8 +19,8 @@ use crate::ast::{ ArgDecl, Args, ArithOp, Ast, BinOp, BinOpExpr, BoolLiteral, BoolOp, CallExpr, CastExpr, CellDecl, ComparisonOp, ConstantDecl, Decl, EmitExpr, EnumDecl, EnumVariant, Expr, FieldAccessExpr, FieldPattern, FloatLiteral, FnDecl, ForLoop, GenericArgs, Ident, IdentPath, - IfExpr, IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, LetBinding, MatchArm, - MatchExpr, ModDecl, NilLiteral, Pattern, Scope, SeqLiteral, Statement, StringLiteral, + IfExpr, IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, LetBinding, LetPattern, + MatchArm, MatchExpr, ModDecl, NilLiteral, Pattern, Scope, SeqLiteral, Statement, StringLiteral, StructDecl, StructField, StructLitExpr, StructLitField, TupleExpr, TyParam, TySpec, TySpecKind, UnaryOp, UnaryOpExpr, UseDecl, VariantPayload, }; @@ -908,9 +908,9 @@ impl<'a> Parser<'a> { self.record_completion_site(CompletionSite::Statement); match self.cur.kind { TokenKind::KwLet => { - let lb = self.parse_let_binding(); + let stmt = self.parse_let(); self.expect(TokenKind::Semi); - stmts.push(Statement::LetBinding(lb)); + stmts.push(stmt); } TokenKind::KwFor => { stmts.push(Statement::ForLoop(self.parse_for_loop())); @@ -996,22 +996,37 @@ impl<'a> Parser<'a> { } } - /// `letBinding : LET ident (COLON tySpec)? EQ expr` (span excludes the - /// trailing SEMI, which belongs to the enclosing `statement`). - fn parse_let_binding(&mut self) -> LetBinding<&'a str, Md> { + /// `letStmt : LET (ident (COLON tySpec)? | pattern) EQ expr` (span + /// excludes the trailing SEMI, which belongs to the enclosing `statement`). + /// + /// A name followed by `{` or `::` begins a pattern, as in + /// `let geom::Size { w, .. } = s`; any other name is a plain binding. + fn parse_let(&mut self) -> Statement<&'a str, Md> { let lo = self.cur.start; self.expect(TokenKind::KwLet); + if self.at(TokenKind::Ident) + && matches!(self.nxt.kind, TokenKind::LBrace | TokenKind::PathSep) + { + let pattern = self.parse_pattern(); + self.expect(TokenKind::Eq); + let value = self.parse_expr(0); + return Statement::LetPattern(LetPattern { + pattern, + value, + span: self.finish_span(lo), + }); + } let name = self.ident(CompletionSite::NewIdentifier); let ty = self.eat(TokenKind::Colon).then(|| self.parse_ty_spec()); self.expect(TokenKind::Eq); let value = self.parse_expr(0); - LetBinding { + Statement::LetBinding(LetBinding { name, ty, value, metadata: (), span: self.finish_span(lo), - } + }) } /// `forLoop : FOR ident IN expr scope` @@ -1160,7 +1175,7 @@ impl<'a> Parser<'a> { let lo = self.cur.start; let path = self.parse_ident_path(CompletionSite::Pattern); if self.at(TokenKind::LBrace) { - return self.parse_struct_variant_pattern(lo, path); + return self.parse_struct_pattern(lo, path); } if self.eat(TokenKind::LParen) { let fields = self.separated_list(TokenKind::RParen, CompletionSite::Pattern, |p| { @@ -1191,7 +1206,7 @@ impl<'a> Parser<'a> { /// two terminators (`}` and `..`) it does not go through /// [`Self::separated_list`]; termination holds for the same reason, since /// every iteration that does not `break` consumes the separator. - fn parse_struct_variant_pattern( + fn parse_struct_pattern( &mut self, lo: u32, path: IdentPath<&'a str, Md>, @@ -1211,7 +1226,7 @@ impl<'a> Parser<'a> { } } self.expect(TokenKind::RBrace); - Pattern::StructVariant { + Pattern::Struct { path, fields, rest, @@ -1244,8 +1259,8 @@ impl<'a> Parser<'a> { } } - /// A payload element pattern: `_` or a name. Nested variant patterns are - /// not supported. + /// A payload element pattern: `_` or a name. Nested variant and struct + /// patterns are not supported. fn parse_sub_pattern(&mut self) -> Pattern<&'a str, Md> { self.record_completion_site(CompletionSite::Pattern); if self.at_wildcard() { @@ -1253,7 +1268,7 @@ impl<'a> Parser<'a> { return Pattern::Wildcard { span: self.span(t) }; } let name = self.ident(CompletionSite::Pattern); - if self.at(TokenKind::LParen) || self.at(TokenKind::PathSep) { + if self.at(TokenKind::LParen) || self.at(TokenKind::PathSep) || self.at(TokenKind::LBrace) { self.error_at( self.span(self.cur), "nested patterns are not supported; a payload element pattern is a name or `_`" @@ -1268,6 +1283,15 @@ impl<'a> Parser<'a> { p.parse_sub_pattern() }); self.expect(TokenKind::RParen); + } else if self.at(TokenKind::LBrace) { + let lo = self.cur.start; + let path = IdentPath { + path: vec![name.clone()], + generic_args: None, + metadata: (), + span: name.span, + }; + self.parse_struct_pattern(lo, path); } } Pattern::Binding { name, metadata: () } diff --git a/crates/compiler/src/parser/mod.rs b/crates/compiler/src/parser/mod.rs index b49c3087..7dde0b0d 100644 --- a/crates/compiler/src/parser/mod.rs +++ b/crates/compiler/src/parser/mod.rs @@ -564,6 +564,7 @@ mod tests { "match s { Shape::Box { w, .., } => w, _ => 0., }", // A field pattern is a name or `_`, never a nested pattern. "match s { Shape::Box { w: Some(x) } => 1., _ => 0., }", + "match s { Shape::Box { w: Size { x } } => 1., _ => 0., }", "match s { Shape::Box { : w } => 1., _ => 0., }", "match s { Shape::Box { w => 1., _ => 0., }", ]; @@ -572,6 +573,56 @@ mod tests { } } + /// A name followed by `{` or `::` after `let` begins a pattern; any other + /// name is a plain binding. + #[test] + fn let_patterns_parse() { + use crate::ast::{Decl, Pattern, Statement}; + + for body in [ + "let Size { w, h: tall } = s;", + "let Size { w, .. } = s;", + "let Size { .. } = s;", + "let geom::Size { w: _, h } = s;", + "let Pair:: { first, .. } = p;", + "let Size {} = s;", + ] { + assert!(snippet_ok(body), "should parse: `{body}`"); + } + for body in [ + "let Size { w: Size { x } } = s;", + "let Size { w } : Size = s;", + "let Size { w };", + ] { + assert!(!snippet_ok(body), "should be rejected: `{body}`"); + } + + let src = "cell c() { let Size { w, h: tall, .. } = s; let x = 1.; }"; + let ast = parse(src).unwrap(); + let Decl::Cell(cell) = &ast.ast.decls[0] else { + panic!("expected a cell"); + }; + let Statement::LetPattern(binding) = &cell.scope.stmts[0] else { + panic!("expected a let pattern"); + }; + assert_eq!( + &src[binding.span.start()..binding.span.end()], + "let Size { w, h: tall, .. } = s" + ); + let Pattern::Struct { path, rest, .. } = &binding.pattern else { + panic!("expected a struct pattern"); + }; + assert_eq!(path.path[0].name, "Size"); + assert!(rest); + let names = cell.scope.stmts[0] + .let_names() + .into_iter() + .map(|name| name.name.as_str()) + .collect::>(); + assert_eq!(names, ["w", "tall"]); + assert!(matches!(&cell.scope.stmts[1], Statement::LetBinding(_))); + } + /// A struct variant's fields keep their declared order and spans, and a /// shorthand field pattern binds the field's own name. #[test] @@ -605,7 +656,7 @@ mod tests { let Expr::Match(m) = &v.value else { panic!("expected a match"); }; - let Pattern::StructVariant { + let Pattern::Struct { path, fields, rest, diff --git a/docs/docs/language/cells-functions.md b/docs/docs/language/cells-functions.md index 8046ee7d..a9163134 100644 --- a/docs/docs/language/cells-functions.md +++ b/docs/docs/language/cells-functions.md @@ -60,4 +60,13 @@ fn inset_bounds(rect_: Rect, amount: Float) -> Rect { let bounds = bbox(child); ``` +A `let` can also take a struct apart. The pattern names the struct and binds its fields: a bare `name` binds the field of that name, `field: other` binds it under another name, and `field: _` drops it. The pattern must name every field unless it ends in `..`, and field patterns do not nest. + +```argon +let Size { w, h: height } = size; +let ViaParams { layer, .. } = params; +``` + +At the top of a cell, each name a pattern binds is a field of the cell, like any other `let`. + Top-level declarations are resolved across the whole module, so a cell can call a function declared further down the file. diff --git a/docs/docs/language/control-flow.md b/docs/docs/language/control-flow.md index c8c76426..326e078c 100644 --- a/docs/docs/language/control-flow.md +++ b/docs/docs/language/control-flow.md @@ -5,7 +5,7 @@ description: if, match, and for. # Control flow -Argon has `if`, `match` on enums, and `for` over sequences. `if` and `match` are expressions, and an `if` with no `else` is a statement. +Argon has `if`, `match` on enums and structs, and `for` over sequences. `if` and `match` are expressions, and an `if` with no `else` is a statement. ## `if` expressions @@ -142,6 +142,27 @@ literal, a variant literal in an `if` condition, a `match` scrutinee, or a `for` sequence must be parenthesized, since `name {` there begins the construct's body. +### Matching structs + +A `match` on a [struct](/language/types-values) takes the same braced patterns, +naming the struct instead of a variant. A field pattern may be a unit variant, +which matches only that variant, so an arm like the second one below is needed +to cover the rest: + +```argon +struct Via { + layer: String, + n: Option, +} + +fn count(v: Via) -> Int { + match v { + Via { n: None, .. } => 1, + Via { n, .. } => std::unwrap_or(n, 1), + } +} +``` + ## `for` loops A `for` loop walks a sequence, usually to emit geometry or instances: diff --git a/docs/docs/language/types-values.md b/docs/docs/language/types-values.md index aecd7a44..f0c7a06b 100644 --- a/docs/docs/language/types-values.md +++ b/docs/docs/language/types-values.md @@ -9,7 +9,7 @@ Argon's types fall into scalars, collections, tuples, and layout types. Write ty Structs, enums, functions, and cells may take type parameters, written in angle brackets after the name: `struct Pair`, `fn last(items: [T]) -> T`. A type parameter stands for one type per use, which is inferred from the arguments, and a value of type `T` can only be stored, passed, and returned. -A struct groups values under named fields, written `Size { w: 100., h: 50. }` and read with `.`. An enum is a fixed set of variants, each of which may carry a tuple payload or named fields of its own; see [enums and `match`](/language/control-flow#enums-and-match). +A struct groups values under named fields, written `Size { w: 100., h: 50. }` and read with `.` or taken apart with a pattern, as in `let Size { w, h } = s;` (see [bindings](/language/cells-functions#bindings-and-order)). An enum is a fixed set of variants, each of which may carry a tuple payload or named fields of its own; see [enums and `match`](/language/control-flow#enums-and-match). | Type | Example | Used for | | --- | --- | --- | diff --git a/docs/parser.md b/docs/parser.md index d4be7abf..de6b4cd5 100644 --- a/docs/parser.md +++ b/docs/parser.md @@ -443,8 +443,8 @@ non-consuming `parse_item` cannot spin. > records `CompletionSite::Pattern`. A bare name parses as a > `Pattern::Binding`, and the type checker decides whether it names a unit > variant instead. -> - **Struct variant patterns** (`E::V { f, g: name, .. }`) have two -> terminators — `}` and the `..` — so `parse_struct_variant_pattern` keeps +> - **Struct patterns** (`S { f, g: name, .. }` or `E::V { f, g: name, .. }`) +> have two terminators — `}` and the `..` — so `parse_struct_pattern` keeps > its own loop, shaped like the struct literal's. A field pattern is > `fieldPattern : ident (COLON subPattern)?`, where a bare `f` is shorthand > for `f: f`, and the `..` comes last and takes no comma after it. @@ -657,6 +657,10 @@ produce stable GUI hierarchy IDs without adding annotations to source text. The statement loop dispatches on `cur`: - `let name (: tySpec)? = expr ;` → `Statement::LetBinding` +- `let pattern = expr ;` → `Statement::LetPattern`, chosen when the name after + `let` is followed by `{` or `::`, as in `let Size { w, .. } = s;`. The + pattern is parsed by `parse_pattern`; the type checker rejects any pattern + but a struct pattern whose fields are names or `_`. - `for v in expr scope` → `Statement::ForLoop` - `if` → parsed by `parse_if` directly rather than through `parse_expr`, with the `else` **optional**. This is the only position where it may be omitted, diff --git a/examples/struct_destructure/Argon.toml b/examples/struct_destructure/Argon.toml new file mode 100644 index 00000000..0e4308ad --- /dev/null +++ b/examples/struct_destructure/Argon.toml @@ -0,0 +1,2 @@ +name = "struct_destructure" +tech = "../tech/basic.tech.toml" diff --git a/examples/struct_destructure/lib.ar b/examples/struct_destructure/lib.ar new file mode 100644 index 00000000..48f01b39 --- /dev/null +++ b/examples/struct_destructure/lib.ar @@ -0,0 +1,46 @@ +struct Size { + w: Float, + h: Float, +} + +struct Pair { + first: A, + second: B, +} + +struct Via { + layer: String, + size: Size, + n: Option, +} + +// A `let` pattern names the struct and binds its fields. `f: name` renames a +// binding, `f: _` drops one, and a trailing `..` ignores the rest. +fn area(s: Size) -> Float { + let Size { w, h: height } = s; + w * height +} + +fn first(p: Pair) -> A { + let Pair { first, .. } = p; + first +} + +// A `match` arm may name a field's unit variant; an arm that binds every +// field covers the rest. +fn count(v: Via) -> Int { + match v { + Via { n: None, .. } => 1, + Via { n, .. } => std::unwrap_or(n, 1), + } +} + +cell top() { + let size = Size { w: 100., h: 50. }; + // Each name a top-level pattern binds is a field of the cell. + let Size { w, h } = size; + let r = rect("met1", x0=0., y0=0., w=w, h=area(size) / w); + let p = Pair { first: 20., second: 3 }; + let Via { layer, n, size: _ } = Via { layer: "met2", size, n: Some(3) }; + let r2 = rect(layer, x0=0., y0=h, w=first(p) * (count(Via { layer, size, n }) as Float), h=h); +}