diff --git a/README.md b/README.md index a0112f3..23f7f12 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,10 @@ including on the command line: arc run --cell 'via(ViaParams { layer: "met1", size: Size { w: 100., h: 50. }, n: 1 })' ``` -Enum variants may carry values. 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 anything. Every variant is also an item that -`use` can import: +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 +anything. Every variant is also an item that `use` can import: ```rust enum Shape { @@ -175,6 +175,36 @@ fn width(s: Shape) -> Float { } ``` +A variant with named fields is written and constructed like a struct, with the +same shorthand, and matched by a pattern that names the fields it binds. A +pattern must name every field unless it ends in `..`, `field: name` renames a +binding, and `field: _` drops one. Unlike a struct literal, a variant takes no +`..base`, and like a struct literal it must be parenthesized in an `if` +condition, a `match` scrutinee, or a `for` sequence: + +```rust +enum Shape { + Circle { r: Float }, + Box { w: Float, h: Float }, + Empty, +} + +fn width(s: Shape) -> Float { + match s { + Shape::Circle { r } => 2. * r, + Shape::Box { w, .. } => w, + Shape::Empty => 0., + } +} + +cell top() { + let w = 300.; + let h = 20.; + let boxed = Shape::Box { w, h }; + let r = rect("met1", x0=0., y0=0., w=width(boxed), h=h); +} +``` + Structs, enums, functions, and cells take type parameters, written in angle brackets after the name as in Rust. A type parameter is opaque: a value of type `T` can be stored, passed, and returned, but not added, compared, cast, diff --git a/crates/analyzer/src/navigation.rs b/crates/analyzer/src/navigation.rs index 18cbe2f..ccb500a 100644 --- a/crates/analyzer/src/navigation.rs +++ b/crates/analyzer/src/navigation.rs @@ -611,15 +611,16 @@ 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, .. }`. CompletionSite::Pattern => match candidate.kind { - Kind::Enum | Kind::Variant | Kind::Module => true, + Kind::Enum | Kind::Variant | Kind::Module | Kind::Field => true, Kind::Keyword => candidate.label == "_", Kind::Function | Kind::Cell | Kind::Variable | Kind::Parameter | Kind::Struct - | Kind::Field | Kind::Type => false, }, CompletionSite::ImportPath => candidate.kind == Kind::Module, diff --git a/crates/arc/src/doc.rs b/crates/arc/src/doc.rs index 01f770e..df725bd 100644 --- a/crates/arc/src/doc.rs +++ b/crates/arc/src/doc.rs @@ -10,7 +10,7 @@ use anyhow::{Context, Result, bail}; use arcstr::Substr; use argonc::{ WorkspaceConfig, - ast::{ArgDecl, Decl, ModPath, TyParam, TySpec, TySpecKind}, + ast::{ArgDecl, Decl, ModPath, TyParam, TySpec, TySpecKind, VariantPayload}, parse::{self, AnnotatedParseAst, ParseMetadata, WorkspaceParseAst}, }; @@ -339,19 +339,35 @@ fn render_module( .variants .iter() .map(|variant| { - let payload = (!variant.payload.is_empty()).then(|| { - let types = variant - .payload - .iter() - .map(|ty| render_type(ty, module.path, targets)) - .collect::>() - .join(", "); - format!("({types})") - }); + let payload = match &variant.payload { + VariantPayload::Tuple(payload) if payload.is_empty() => String::new(), + VariantPayload::Tuple(payload) => { + let types = payload + .iter() + .map(|ty| render_type(ty, module.path, targets)) + .collect::>() + .join(", "); + format!("({types})") + } + VariantPayload::Struct(fields) => { + let fields = fields + .iter() + .map(|field| { + format!( + "{}: {}", + escape(&field.name.name), + render_type(&field.ty, module.path, targets) + ) + }) + .collect::>() + .join(", "); + format!(" {{ {fields} }}") + } + }; format!( "
  • {}{}
  • ", escape(&variant.name.name), - payload.unwrap_or_default() + payload ) }) .collect::>() @@ -644,7 +660,7 @@ mod tests { fs::write(directory.path().join("Argon.toml"), "name = \"demo\"\n").unwrap(); fs::write( directory.path().join("lib.ar"), - "//! Demo cells.\n/// Routing modes.\nenum Mode { Fast, Quiet, }\n/// Builds a route.\n/// # Arguments\n/// - `mode`: routing mode.\ncell route(mode: Mode) {}\n/// A mode or nothing.\nenum Maybe { Just(T, Mode), Nothing, }\n/// Picks a mode.\nfn pick(m: Maybe, n: Option) -> Mode { Mode::Fast }\n", + "//! Demo cells.\n/// Routing modes.\nenum Mode { Fast, Quiet, }\n/// Builds a route.\n/// # Arguments\n/// - `mode`: routing mode.\ncell route(mode: Mode) {}\n/// A mode or nothing.\nenum Maybe { Just(T, Mode), Sized { width: Int, mode: Mode }, Nothing, }\n/// Picks a mode.\nfn pick(m: Maybe, n: Option) -> Mode { Mode::Fast }\n", ) .unwrap(); let library = Library::load(directory.path().join("Argon.toml")).unwrap(); @@ -664,6 +680,10 @@ mod tests { "
  • Just(T, Mode)
  • " )); assert!(page.contains("
  • Nothing
  • ")); + // A variant with named fields renders its fields, not a tuple. + assert!(page.contains( + "
  • Sized { width: Int, mode: Mode }
  • " + )); assert!(page.contains("pick<T>(")); assert!(page.contains( "Maybe<T>" diff --git a/crates/arc/src/lib.rs b/crates/arc/src/lib.rs index 8a85936..200394d 100644 --- a/crates/arc/src/lib.rs +++ b/crates/arc/src/lib.rs @@ -566,6 +566,46 @@ cell top() { ); } + /// A variant's braces nest like any other, so a declaration and a pattern + /// that spans lines indent from the enclosing block. + #[test] + fn formats_struct_variant_braces() { + let source = r#" +enum Shape { + Circle { r: Float }, + Box { + w: Float, + h: Float, + }, +} + +fn width(s: Shape) -> Float { + match s { + Shape::Circle { r } => r, + Shape::Box { w, .. } => w, + } +} +"#; + assert_eq!( + format_source(source), + r#"enum Shape { + Circle { r: Float }, + Box { + w: Float, + h: Float, + }, +} + +fn width(s: Shape) -> Float { + match s { + Shape::Circle { r } => r, + Shape::Box { w, .. } => w, + } +} +"# + ); + } + #[test] fn formatting_check_does_not_write_files() { let directory = tempfile::tempdir().unwrap(); diff --git a/crates/compiler/src/ast/annotated.rs b/crates/compiler/src/ast/annotated.rs index 0c68e0b..253a6f3 100644 --- a/crates/compiler/src/ast/annotated.rs +++ b/crates/compiler/src/ast/annotated.rs @@ -140,7 +140,7 @@ impl AstTransformer for AstAnnotationPass { &mut self, input: &super::EnumVariant, _name: &super::Ident, - _payload: &[super::TySpec], + _payload: &super::VariantPayload, ) -> ::EnumVariant { input.metadata.clone() } diff --git a/crates/compiler/src/ast/mod.rs b/crates/compiler/src/ast/mod.rs index abcd488..158a73f 100644 --- a/crates/compiler/src/ast/mod.rs +++ b/crates/compiler/src/ast/mod.rs @@ -131,15 +131,44 @@ pub struct EnumDecl { pub metadata: T::EnumDecl, } -/// One variant of an enum, with the types of its payload: `Some(T)` or `None`. +/// One variant of an enum, with the types of its payload: `Some(T)`, +/// `Circle { r: Float }`, or `None`. #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub struct EnumVariant { pub name: Ident, - pub payload: Vec>, + pub payload: VariantPayload, pub span: cfgrammar::Span, pub metadata: T::EnumVariant, } +/// The declared payload of an [`EnumVariant`]. +#[derive_where(Debug, Clone, Serialize, Deserialize; S)] +pub enum VariantPayload { + /// Positional types, `V(A, B)`; empty for a unit variant. + Tuple(Vec>), + /// Named fields, `V { a: A, b: B }`. + Struct(Vec>), +} + +impl VariantPayload { + /// Whether this variant carries nothing. + pub fn is_unit(&self) -> bool { + matches!(self, Self::Tuple(payload) if payload.is_empty()) + } + + /// The payload types in declaration order. + pub fn tys(&self) -> impl Iterator> { + let (tuple, fields) = match self { + Self::Tuple(payload) => (Some(payload), None), + Self::Struct(fields) => (None, Some(fields)), + }; + tuple + .into_iter() + .flatten() + .chain(fields.into_iter().flatten().map(|field| &field.ty)) + } +} + #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub struct StructDecl { pub name: Ident, @@ -351,6 +380,24 @@ pub enum Pattern { fields: Vec>, span: cfgrammar::Span, }, + /// An enum variant with named fields, `E::V { f, g: name, .. }`. + StructVariant { + path: IdentPath, + fields: Vec>, + /// Whether the pattern ended in `..`, ignoring the fields not listed. + rest: bool, + span: cfgrammar::Span, + }, +} + +/// One `field: pattern` entry of a [`Pattern::StructVariant`]. +#[derive_where(Debug, Clone, Serialize, Deserialize; S)] +pub struct FieldPattern { + pub name: Ident, + pub pattern: Pattern, + /// Whether the field was written as a bare `f`, standing for `f: f`. + pub shorthand: bool, + pub span: cfgrammar::Span, } impl Pattern { @@ -358,7 +405,7 @@ impl Pattern { match self { Self::Wildcard { span } => *span, Self::Binding { name, .. } => name.span, - Self::Variant { span, .. } => *span, + Self::Variant { span, .. } | Self::StructVariant { span, .. } => *span, } } } @@ -572,7 +619,7 @@ pub trait AstTransformer { &mut self, input: &EnumVariant, name: &Ident, - payload: &[TySpec], + payload: &VariantPayload, ) -> ::EnumVariant; fn dispatch_pattern_binding( &mut self, @@ -861,11 +908,20 @@ pub trait AstTransformer { input: &EnumVariant, ) -> EnumVariant { let name = self.transform_ident(&input.name); - let payload = input - .payload - .iter() - .map(|ty| self.transform_ty_spec(ty)) - .collect_vec(); + let payload = match &input.payload { + VariantPayload::Tuple(payload) => VariantPayload::Tuple( + payload + .iter() + .map(|ty| self.transform_ty_spec(ty)) + .collect_vec(), + ), + VariantPayload::Struct(fields) => VariantPayload::Struct( + fields + .iter() + .map(|field| self.transform_struct_field(field)) + .collect_vec(), + ), + }; let metadata = self.dispatch_enum_variant(input, &name, &payload); EnumVariant { name, @@ -1106,6 +1162,31 @@ pub trait AstTransformer { .collect(), span: *span, }, + Pattern::StructVariant { + path, + fields, + rest, + span, + } => Pattern::StructVariant { + path: self.transform_ident_path(path), + fields: fields + .iter() + .map(|field| self.transform_field_pattern(field)) + .collect(), + rest: *rest, + span: *span, + }, + } + } + fn transform_field_pattern( + &mut self, + input: &FieldPattern, + ) -> FieldPattern { + FieldPattern { + name: self.transform_ident(&input.name), + pattern: self.transform_pattern(&input.pattern), + shorthand: input.shorthand, + span: input.span, } } fn transform_bin_op_expr( diff --git a/crates/compiler/src/cli.rs b/crates/compiler/src/cli.rs index ed69d5f..355d588 100644 --- a/crates/compiler/src/cli.rs +++ b/crates/compiler/src/cli.rs @@ -596,6 +596,24 @@ mod tests { assert_eq!(rect.x1.0, 20.); } + #[test] + fn execution_evaluates_a_struct_variant_cell_argument() { + let source = temp_source( + "struct-variant-args", + "enum Mode { Sized { w: Float, h: Float }, Fast, }\n\ + cell top(m: Mode) {\n\ + let w = match m { Mode::Sized { w, .. } => w, Mode::Fast => 10., };\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=w, y1=10.);\n\ + }\n", + ); + let rect = compiled_rect( + "struct-variant", + source, + "top(Mode::Sized { h: 1., w: 40. })", + ); + assert_eq!(rect.x1.0, 40.); + } + #[test] fn execution_evaluates_an_option_cell_argument() { let source = temp_source( diff --git a/crates/compiler/src/compile.rs b/crates/compiler/src/compile.rs index 27a4cf7..2c5eb86 100644 --- a/crates/compiler/src/compile.rs +++ b/crates/compiler/src/compile.rs @@ -25,10 +25,11 @@ pub use result::{ use crate::ast::annotated::AnnotatedAst; use crate::ast::{ - ArithOp, CastExpr, ComparisonOp, ConstantDecl, EnumDecl, EnumVariant, FieldAccessExpr, FnDecl, - ForLoop, IdentPath, IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, MatchArm, - MatchExpr, ModPath, Pattern, Scope, Span, StructDecl, StructField, StructLitExpr, - StructLitField, TyParam, TySpec, TySpecKind, UnaryOp, UnaryOpExpr, UseDecl, WorkspaceAst, + ArithOp, CastExpr, ComparisonOp, ConstantDecl, EnumDecl, EnumVariant, FieldAccessExpr, + FieldPattern, FnDecl, ForLoop, IdentPath, IndexExpr, IndexFieldAccessExpr, IntLiteral, + KwArgValue, MatchArm, MatchExpr, ModPath, Pattern, Scope, Span, StructDecl, StructField, + StructLitExpr, StructLitField, TyParam, TySpec, TySpecKind, UnaryOp, UnaryOpExpr, UseDecl, + VariantPayload, WorkspaceAst, }; use crate::gds::{ImportedGdsElement, import_gds}; use crate::parse::{CellInvocation, ParseOutput, WorkspaceParseAst}; @@ -658,7 +659,7 @@ impl<'a> AstTransformer for ImportPass<'a> { &mut self, _input: &EnumVariant, _name: &Ident, - _payload: &[TySpec], + _payload: &VariantPayload, ) -> ::EnumVariant { } @@ -1544,7 +1545,70 @@ pub struct VariantDef { /// The [`VarId`] the variant is bound to by `use`, the prelude, and the /// evaluator's global frame. pub id: VarId, - pub payload: Vec, + pub payload: VariantTys, +} + +/// The payload types of a [`VariantDef`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VariantTys { + /// Positional types; empty for a unit variant. + Tuple(Vec), + /// Named fields in declaration order. + Struct(IndexMap), +} + +impl VariantTys { + /// Whether the variant carries nothing. + pub fn is_unit(&self) -> bool { + matches!(self, Self::Tuple(payload) if payload.is_empty()) + } + + /// How many values the variant carries. + pub fn len(&self) -> usize { + match self { + Self::Tuple(payload) => payload.len(), + Self::Struct(fields) => fields.len(), + } + } + + /// Whether the variant carries nothing. A named payload with no fields is + /// empty without being a unit variant. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The payload types in declaration order. + pub fn tys(&self) -> impl Iterator { + let (tuple, fields) = match self { + Self::Tuple(payload) => (Some(payload), None), + Self::Struct(fields) => (None, Some(fields)), + }; + tuple + .into_iter() + .flatten() + .chain(fields.into_iter().flatten().map(|(_, ty)| ty)) + } +} + +/// A variant pattern's annotated path and the variant it resolved to, if any. +type ResolvedPatternPath = ( + IdentPath, + Option<(VarId, Arc)>, +); + +/// The checked form of a struct literal: the type it builds and, when the +/// literal names an enum variant, that variant. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct StructLitTy { + pub ty: Ty, + pub variant: Option, +} + +impl StructLitTy { + /// The checked form of a literal that builds `ty` directly. + fn of(ty: Ty) -> Self { + Self { ty, variant: None } + } } /// A struct or enum definition. @@ -2204,7 +2268,7 @@ impl AstMetadata for VarIdTyMetadata { type TupleExpr = Ty; /// The sequence type the literal was inferred to have. type SeqExpr = Ty; - type StructLitExpr = Ty; + type StructLitExpr = StructLitTy; } impl<'a> VarIdTyPass<'a> { @@ -2912,11 +2976,23 @@ impl<'a> VarIdTyPass<'a> { return Ty::Unknown; }; let explicit = explicit.or_else(|| (!ctor.args.is_empty()).then(|| ctor.args.clone())); - let is_unit = def + let payload = def .variants .get(&ctor.variant) - .is_some_and(|variant| variant.payload.is_empty()); - if is_unit { + .map(|variant| &variant.payload); + if matches!(payload, Some(VariantTys::Struct(_))) { + // A named payload has no value form as a bare name; it is built + // with braces, which `dispatch_struct_lit_expr` checks. + self.errors.push(StaticError { + span: self.span(span), + kind: StaticErrorKind::StructVariantConstruction(format!( + "{}::{}", + ctor.enum_name, ctor.variant + )), + }); + return Ty::Unknown; + } + if payload.is_some_and(VariantTys::is_unit) { let map = self.instantiate(&def.params, explicit, span, &ctor.enum_name); return Ty::Enum(Arc::new(EnumTy { def: ctor.def, @@ -3232,7 +3308,7 @@ impl<'a> VarIdTyPass<'a> { name.to_owned(), VariantDef { id: self.alloc_id(), - payload: Vec::new(), + payload: VariantTys::Tuple(Vec::new()), }, ); } @@ -3268,18 +3344,34 @@ impl<'a> VarIdTyPass<'a> { .variants .iter() .map(|variant| { - let payload = variant - .payload - .iter() - .map(|spec| self.ty_from_spec(spec)) - .collect_vec(); + let payload = match &variant.payload { + VariantPayload::Tuple(payload) => VariantTys::Tuple( + payload + .iter() + .map(|spec| self.ty_from_spec(spec)) + .collect_vec(), + ), + VariantPayload::Struct(input) => { + let mut fields = IndexMap::with_capacity(input.len()); + for field in input { + let ty = self.ty_from_spec(&field.ty); + if fields.insert(field.name.name.to_string(), ty).is_some() { + self.errors.push(StaticError { + span: self.span(field.name.span), + kind: StaticErrorKind::DuplicateNameDeclaration, + }); + } + } + VariantTys::Struct(fields) + } + }; (variant.name.name.as_str(), payload) }) .collect_vec(); self.bindings.pop(); let all = payloads .iter() - .flat_map(|(_, payload)| payload.iter().cloned()) + .flat_map(|(_, payload)| payload.tys().cloned()) .collect_vec(); let Some(AdtDef::Enum(def)) = self.local_defs.get_mut(&id) else { unreachable!("bound as an enum") @@ -3610,7 +3702,7 @@ impl<'a> VarIdTyPass<'a> { }; let map = param_map(&def.params, &enum_ty.args); for variant in def.variants.values() { - for payload in &variant.payload { + for payload in variant.payload.tys() { match self.shallow(&subst(payload, &map)) { Ty::Int | Ty::Bool | Ty::Nil | Ty::Unknown | Ty::Any | Ty::Infer(_) => {} Ty::Float => return Err(EqualityFailure::Float), @@ -3928,7 +4020,17 @@ impl<'a> VarIdTyPass<'a> { let Some(variant) = def.variants.get(&ctor.variant) else { return (None, Ty::Unknown); }; - if variant.payload.is_empty() { + let VariantTys::Tuple(payload) = variant.payload.clone() else { + self.errors.push(StaticError { + span: self.span(call_span), + kind: StaticErrorKind::StructVariantConstruction(format!( + "{}::{}", + ctor.enum_name, ctor.variant + )), + }); + return (None, Ty::Unknown); + }; + if payload.is_empty() { self.errors.push(StaticError { span: self.span(call_span), kind: StaticErrorKind::CannotCall(format!("{}::{}", ctor.enum_name, ctor.variant)), @@ -3943,17 +4045,17 @@ impl<'a> VarIdTyPass<'a> { kind: StaticErrorKind::InvalidKwArg, }); } - if args.posargs.len() != variant.payload.len() { + if args.posargs.len() != payload.len() { self.errors.push(StaticError { span: self.span(call_span), kind: StaticErrorKind::VariantPayloadArity { variant: ctor.variant.clone(), - expected: variant.payload.len(), + expected: payload.len(), found: args.posargs.len(), }, }); } - for (arg, payload) in args.posargs.iter().zip(&variant.payload) { + for (arg, payload) in args.posargs.iter().zip(&payload) { self.assert_eq_ty(arg.span(), &arg.ty(), &subst(payload, &map)); } let ty = Ty::Enum(Arc::new(EnumTy { @@ -4099,7 +4201,7 @@ impl Expr { Expr::Cast(cast) => cast.metadata.clone(), Expr::UnaryOp(unary_op_expr) => unary_op_expr.metadata.clone(), Expr::Tuple(t) => t.metadata.clone(), - Expr::StructLit(lit) => lit.metadata.clone(), + Expr::StructLit(lit) => lit.metadata.ty.clone(), } } } @@ -4168,11 +4270,37 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { .iter() .map(|variant| EnumVariant { name: self.transform_ident(&variant.name), - payload: variant - .payload - .iter() - .map(|spec| self.transform_ty_spec(spec)) - .collect(), + // `dispatch_struct_field` is unreachable in this pass, so a + // named payload's fields are built here with the types + // `define_enum_decl` resolved. + payload: match &variant.payload { + VariantPayload::Tuple(payload) => VariantPayload::Tuple( + payload + .iter() + .map(|spec| self.transform_ty_spec(spec)) + .collect(), + ), + VariantPayload::Struct(fields) => VariantPayload::Struct( + fields + .iter() + .map(|field| StructField { + name: self.transform_ident(&field.name), + ty: self.transform_ty_spec(&field.ty), + span: field.span, + metadata: def + .as_ref() + .and_then(|def| def.variants.get(variant.name.name.as_str())) + .and_then(|variant| match &variant.payload { + VariantTys::Struct(tys) => { + tys.get(field.name.name.as_str()).cloned() + } + VariantTys::Tuple(_) => None, + }) + .unwrap_or_default(), + }) + .collect(), + ), + }, span: variant.span, metadata: def .as_ref() @@ -4211,7 +4339,7 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { &mut self, _input: &EnumVariant, _name: &Ident, - _payload: &[TySpec], + _payload: &VariantPayload, ) -> ::EnumVariant { // `transform_enum_decl` builds the variants itself. unreachable!() @@ -4313,22 +4441,30 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { let lookup = if path.path.len() == 1 { self.lookup(name) } else { - let module = module_prefix( - self.current_path, - path.path.iter().map(|ident| ident.name.as_str()), - 1, - ); - self.module_item(&module, name) + // A qualified path is `Enum::Variant` or `module::item`, so the + // same resolution a variant pattern uses applies here. + match self.resolve_qualified(&path.path) { + Ok(binding) => Some(binding), + Err(error) => { + self.report_qualified_error(error, path.span); + None + } + } }; let Some((_, ty)) = lookup else { - self.errors.push(StaticError { - span: self.span(path.span), - kind: StaticErrorKind::UndeclaredVar { - name: name.to_string(), - }, - }); - return Ty::Unknown; + if path.path.len() == 1 { + self.errors.push(StaticError { + span: self.span(path.span), + kind: StaticErrorKind::UndeclaredVar { + name: name.to_string(), + }, + }); + } + return StructLitTy::default(); }; + if let Ty::Ctor(ctor) = ty { + return self.variant_lit(input, path, fields, base, &ctor); + } let Ty::Struct(struct_ty) = ty else { // An `Unknown` binding was already diagnosed where it was bound. if !matches!(ty, Ty::Unknown) { @@ -4337,14 +4473,14 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { kind: StaticErrorKind::NotAStruct, }); } - return Ty::Unknown; + return StructLitTy::default(); }; let Some(def) = self .adt_def(struct_ty.def) .and_then(AdtDef::as_struct) .cloned() else { - return Ty::Unknown; + return StructLitTy::default(); }; let explicit = self.explicit_args(path); let map = self.instantiate(&def.params, explicit, path.span, &struct_ty.name); @@ -4403,7 +4539,7 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { } } } - ty + StructLitTy::of(ty) } fn dispatch_cell_decl( @@ -4619,7 +4755,9 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { // keep it from reaching an unmatched value. fn variant_ty(arm: &MatchArm) -> Option<&Ty> { match &arm.pattern { - Pattern::Variant { path, .. } => Some(&path.metadata.1), + Pattern::Variant { path, .. } | Pattern::StructVariant { path, .. } => { + Some(&path.metadata.1) + } _ => None, } } @@ -4671,7 +4809,9 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { }); } else { match &arm.pattern { - Pattern::Variant { path, .. } => { + // 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. self.assert_eq_ty(arm.pattern.span(), &path.metadata.1, &expected_ty); @@ -5253,63 +5393,179 @@ impl<'a> VarIdTyPass<'a> { } } Pattern::Variant { path, fields, span } => { - 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 - } - } - } 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 - } - Err(error) => { - self.report_qualified_error(error, path.span); - None - } - } - }; - let explicit = self.explicit_args(path); - let output = IdentPath { - path: path - .path - .iter() - .map(|ident| self.transform_ident(ident)) - .collect(), - generic_args: path - .generic_args - .as_ref() - .map(|args| self.transform_generic_args(args)), - 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, + let (output, resolved) = self.resolve_pattern_path(path); + self.type_variant_pattern(output, resolved, fields, *span, scrutinee_ty) + } + Pattern::StructVariant { + path, + fields, + rest, + span, + } => { + let (output, resolved) = self.resolve_pattern_path(path); + self.type_struct_variant_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. + 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 + } + } + } 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 + } + Err(error) => { + self.report_qualified_error(error, path.span); + None + } + } + }; + let explicit = self.explicit_args(path); + let output = IdentPath { + path: path + .path + .iter() + .map(|ident| self.transform_ident(ident)) + .collect(), + generic_args: path + .generic_args + .as_ref() + .map(|args| self.transform_generic_args(args)), + 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) + } + + /// Types a `E::V { f: v, g }` literal, which builds a value of `ctor`'s + /// enum from the variant's named fields. + fn variant_lit( + &mut self, + input: &StructLitExpr, + path: &IdentPath, + fields: &[StructLitField], + base: &Option>, + ctor: &CtorTy, + ) -> StructLitTy { + let Some(def) = self.adt_def(ctor.def).and_then(AdtDef::as_enum).cloned() else { + return StructLitTy::default(); + }; + let ctor_ty = Ty::Ctor(Arc::new(ctor.clone())); + let payload = def.variants.get(&ctor.variant).map(|v| v.payload.clone()); + let Some(VariantTys::Struct(payload)) = payload else { + // A unit variant is a value and a tuple variant is called, so + // neither is built with braces. + self.errors.push(StaticError { + span: self.span(path.span), + kind: StaticErrorKind::NotAStructVariant(format!( + "{}::{}", + ctor.enum_name, ctor.variant + )), + }); + return StructLitTy::default(); + }; + let explicit = self.explicit_args(path); + let explicit = explicit.or_else(|| (!ctor.args.is_empty()).then(|| ctor.args.clone())); + let map = self.instantiate(&def.params, explicit, path.span, &ctor.enum_name); + let ty = Ty::Enum(Arc::new(EnumTy { + def: ctor.def, + name: ctor.enum_name.clone(), + args: def + .params + .iter() + .map(|param| map[¶m.id].clone()) + .collect(), + })); + + let mut seen = IndexSet::new(); + for field in fields { + let field_name = field.name.name.as_str(); + let Some(expected) = payload.get(field_name) else { + self.no_field_on_ty(&field.name, ctor_ty.clone()); + continue; + }; + if !seen.insert(field_name) { + self.errors.push(StaticError { + span: self.span(field.name.span), + kind: StaticErrorKind::DuplicateStructField { + field: field_name.to_string(), + }, }); - self.type_variant_pattern(output, resolved, fields, *span, scrutinee_ty) + continue; } + self.assert_eq_ty( + field.value.span(), + &field.value.ty(), + &subst(expected, &map), + ); + } + // Unlike a struct literal, a variant has no `..base`: the base would + // have to be this very variant, which only a `match` can establish. + if let Some(base) = base { + self.errors.push(StaticError { + span: self.span(base.span()), + kind: StaticErrorKind::VariantLiteralBase, + }); + } + let missing = payload + .keys() + .filter(|name| !seen.contains(name.as_str())) + .map(|name| format!("`{name}`")) + .collect_vec(); + if !missing.is_empty() { + self.errors.push(StaticError { + span: self.span(input.span), + kind: StaticErrorKind::MissingStructFields { + ty: self.display(&ctor_ty), + fields: missing.join(", "), + }, + }); + } + StructLitTy { + ty, + variant: Some(ctor.variant.clone()), } } @@ -5337,8 +5593,61 @@ impl<'a> VarIdTyPass<'a> { span, }; }; - // The scrutinee's arguments when it is this enum; fresh variables - // otherwise, which the arm's body or the scrutinee may then solve. + let (map, payload) = + self.variant_pattern_payload(&mut path, id, &ctor, &def, span, scrutinee_ty); + // Named fields are taken apart by a braced pattern; a `(..)` one here + // would bind by position. + let VariantTys::Tuple(payload) = payload else { + self.errors.push(StaticError { + span: self.span(span), + kind: StaticErrorKind::StructVariantConstruction(format!( + "{}::{}", + ctor.enum_name, ctor.variant + )), + }); + let fields = fields + .iter() + .map(|field| self.type_pattern(field, &Ty::Unknown)) + .collect(); + return Pattern::Variant { path, fields, span }; + }; + if fields.len() != payload.len() { + self.errors.push(StaticError { + span: self.span(span), + kind: StaticErrorKind::VariantPayloadArity { + variant: ctor.variant.clone(), + expected: payload.len(), + found: fields.len(), + }, + }); + } + let fields = fields + .iter() + .enumerate() + .map(|(index, field)| { + let ty = payload + .get(index) + .map_or(Ty::Unknown, |payload| subst(payload, &map)); + self.type_pattern(field, &ty) + }) + .collect(); + Pattern::Variant { path, fields, span } + } + + /// Instantiates the enum of a variant pattern, annotates its path with the + /// enum type, and returns the substitution and the variant's payload. + /// + /// The type arguments are the scrutinee's when it is this enum, and fresh + /// variables otherwise, which the arm's body or the scrutinee may solve. + fn variant_pattern_payload( + &mut self, + path: &mut IdentPath, + id: VarId, + ctor: &CtorTy, + def: &EnumDef, + span: cfgrammar::Span, + scrutinee_ty: &Ty, + ) -> (HashMap, VariantTys) { let args = match scrutinee_ty { Ty::Enum(enum_ty) if enum_ty.def == ctor.def && ctor.args.is_empty() => { enum_ty.args.clone() @@ -5365,28 +5674,105 @@ impl<'a> VarIdTyPass<'a> { .variants .get(&ctor.variant) .map(|variant| variant.payload.clone()) - .unwrap_or_default(); - if fields.len() != payload.len() { + .unwrap_or_else(|| VariantTys::Tuple(Vec::new())); + (map, payload) + } + + /// Types a `E::V { f, g: name, .. }` pattern whose path resolved to + /// `resolved`, if it did. + fn type_struct_variant_pattern( + &mut self, + mut path: IdentPath, + resolved: Option<(VarId, Arc)>, + fields: &[FieldPattern], + rest: bool, + span: cfgrammar::Span, + scrutinee_ty: &Ty, + ) -> Pattern { + let unchecked = |this: &mut Self, path| { + let fields = fields + .iter() + .map(|field| FieldPattern { + name: this.transform_ident(&field.name), + pattern: this.type_pattern(&field.pattern, &Ty::Unknown), + shorthand: field.shorthand, + span: field.span, + }) + .collect(); + Pattern::StructVariant { + 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::VariantPayloadArity { - variant: ctor.variant.clone(), - expected: payload.len(), - found: fields.len(), - }, + kind: StaticErrorKind::NotAStructVariant(format!( + "{}::{}", + ctor.enum_name, ctor.variant + )), }); - } + return unchecked(self, path); + }; + let mut seen = IndexSet::new(); let fields = fields .iter() - .enumerate() - .map(|(index, field)| { - let ty = payload - .get(index) - .map_or(Ty::Unknown, |payload| subst(payload, &map)); - self.type_pattern(field, &ty) + .map(|field| { + let name = field.name.name.as_str(); + let ty = match payload.get(name) { + Some(ty) if seen.insert(name.to_string()) => subst(ty, &map), + Some(_) => { + self.errors.push(StaticError { + span: self.span(field.name.span), + kind: StaticErrorKind::DuplicateStructField { + field: name.to_string(), + }, + }); + Ty::Unknown + } + None => self.no_field_on_ty(&field.name, ctor_ty.clone()), + }; + FieldPattern { + name: self.transform_ident(&field.name), + pattern: self.type_pattern(&field.pattern, &ty), + shorthand: field.shorthand, + span: field.span, + } }) .collect(); - Pattern::Variant { path, fields, span } + if !rest { + let missing = payload + .keys() + .filter(|name| !seen.contains(name.as_str())) + .map(|name| format!("`{name}`")) + .collect_vec(); + if !missing.is_empty() { + self.errors.push(StaticError { + span: self.span(span), + kind: StaticErrorKind::MissingPatternFields { + ty: self.display(&ctor_ty), + fields: missing.join(", "), + }, + }); + } + } + Pattern::StructVariant { + path, + fields, + rest, + span, + } } } @@ -5532,7 +5918,7 @@ impl AstTransformer for Zonker<'_> { &mut self, input: &EnumVariant, _name: &Ident, - _payload: &[TySpec], + _payload: &VariantPayload, ) -> Option { input.metadata } @@ -5702,8 +6088,11 @@ impl AstTransformer for Zonker<'_> { _path: &IdentPath, _fields: &[StructLitField], _base: &Option>, - ) -> Ty { - self.ty(&input.metadata, input.span) + ) -> StructLitTy { + StructLitTy { + ty: self.ty(&input.metadata.ty, input.span), + variant: input.metadata.variant.clone(), + } } fn dispatch_struct_lit_path( @@ -5825,11 +6214,17 @@ pub enum CellArg { Int(i64), Bool(bool), String(String), - /// An enum value, like [`Value::Enum`]: its variant and payload. + /// A unit or tuple variant, like [`Value::Enum`]: its variant and payload. Enum { variant: String, payload: Vec, }, + /// A variant with named fields, like [`Value::Enum`]: its variant and its + /// fields in declaration order. + StructVariant { + variant: String, + fields: Vec<(String, CellArg)>, + }, Seq(Vec), /// A struct value: the qualified name of its type and its fields in /// declaration order, like [`Value::Struct`]. @@ -5868,6 +6263,18 @@ pub enum CellArg { Tuple(Vec), } +/// The declared payload of `variant` of the enum `ty`, and the substitution +/// of `ty`'s arguments for its parameters. +fn variant_payload<'a>( + variant: &str, + ty: &EnumTy, + defs: &'a TypeDefs, +) -> Option<(&'a VariantTys, HashMap)> { + let def = defs.get(&ty.def).and_then(AdtDef::as_enum)?; + let variant = def.variants.get(variant)?; + Some((&variant.payload, param_map(&def.params, &ty.args))) +} + impl CellArg { /// Whether this argument inhabits `ty`. Struct and enum types are looked up /// in `defs`; a type parameter, like `Any`, admits anything. @@ -5883,19 +6290,29 @@ impl CellArg { | (Self::Path { .. }, Ty::Path) | (Self::Point(..), Ty::Point) => true, (Self::Enum { variant, payload }, Ty::Enum(ty)) => { - let Some(def) = defs.get(&ty.def).and_then(AdtDef::as_enum) else { - return false; - }; - let Some(variant) = def.variants.get(variant) else { + let Some((VariantTys::Tuple(declared), map)) = variant_payload(variant, ty, defs) + else { return false; }; - let map = param_map(&def.params, &ty.args); - payload.len() == variant.payload.len() + payload.len() == declared.len() && payload .iter() - .zip(&variant.payload) + .zip(declared) .all(|(value, ty)| value.matches_ty(&subst(ty, &map), defs)) } + (Self::StructVariant { variant, fields }, Ty::Enum(ty)) => { + let Some((VariantTys::Struct(declared), map)) = variant_payload(variant, ty, defs) + else { + return false; + }; + fields.len() == declared.len() + && fields + .iter() + .zip(declared) + .all(|((name, value), (field, ty))| { + name == field && value.matches_ty(&subst(ty, &map), defs) + }) + } (Self::Seq(values), Ty::Seq(inner)) => { values.iter().all(|value| value.matches_ty(inner, defs)) } @@ -5930,7 +6347,7 @@ impl CellArg { Self::Int(_) => "Int", Self::Bool(_) => "Bool", Self::String(_) => "String", - Self::Enum { .. } => "enum value", + Self::Enum { .. } | Self::StructVariant { .. } => "enum value", Self::Seq(_) => "sequence", Self::Struct { .. } => "struct", Self::Rect { .. } => "Rect", @@ -5959,6 +6376,8 @@ pub(crate) enum CellArgKey { String(String), /// The variant and its payload. Enum(String, Vec), + /// The variant and its named fields in declaration order. + StructVariant(String, Vec<(String, CellArgKey)>), Seq(Vec), Struct(String, Vec<(String, CellArgKey)>), /// Layer, drawability, and `x0, y0, x1, y1`. @@ -6005,6 +6424,13 @@ impl From<&CellArg> for CellArgKey { CellArg::Enum { variant, payload } => { Self::Enum(variant.clone(), payload.iter().map(Self::from).collect()) } + CellArg::StructVariant { variant, fields } => Self::StructVariant( + variant.clone(), + fields + .iter() + .map(|(field, value)| (field.clone(), Self::from(value))) + .collect(), + ), CellArg::Seq(v) => Self::Seq(v.iter().map(Self::from).collect()), CellArg::Struct { name, fields } => Self::Struct( name.clone(), @@ -8137,23 +8563,29 @@ impl<'a> ExecPass<'a> { ); } // A unit variant is a value; a tuple variant is a - // constructor awaiting its payload. + // constructor awaiting its payload. A variant with named + // fields is built by a literal, so its name has no value + // of its own and the type checker rejects every read. Decl::Enum(e) => { for variant in &e.variants { let Some(id) = variant.metadata else { continue; }; let name = variant.name.name.to_string(); - let value = if variant.payload.is_empty() { - Value::Enum(Arc::new(EnumValue { - variant: name, - payload: Vec::new(), - })) - } else { - Value::Ctor(Arc::new(CtorValue { - variant: name, - arity: variant.payload.len(), - })) + let value = match &variant.payload { + VariantPayload::Struct(_) => Value::Poison, + VariantPayload::Tuple(payload) if payload.is_empty() => { + Value::Enum(Arc::new(EnumValue { + variant: name, + payload: VariantValues::Tuple(Vec::new()), + })) + } + VariantPayload::Tuple(payload) => { + Value::Ctor(Arc::new(CtorValue { + variant: name, + arity: payload.len(), + })) + } }; let vid = self.new_ready_value(value); assert!( @@ -8649,11 +9081,16 @@ impl<'a> ExecPass<'a> { }), Expr::StructLit(lit) => { // A static error aborts compilation before anything is - // executed, so the literal is known to name a struct. - let Ty::Struct(ty) = &lit.metadata else { - unreachable!("struct literal was not resolved to a struct type") + // executed, so the literal is known to name a struct or a + // variant with named fields. + let target = match (&lit.metadata.ty, &lit.metadata.variant) { + (Ty::Struct(ty), _) => StructLitTarget::Struct(ty.clone()), + (Ty::Enum(ty), Some(variant)) => StructLitTarget::Variant { + ty: ty.clone(), + variant: variant.clone(), + }, + _ => unreachable!("struct literal was not resolved to a struct or variant"), }; - let ty = ty.clone(); self.new_deferred_value(loc, |this| { let fields = lit .fields @@ -8663,7 +9100,7 @@ impl<'a> ExecPass<'a> { let base = lit.base.as_ref().map(|base| this.visit_expr(loc, base)); PartialEvalState::StructLit(Box::new(PartialStructLit { expr: (**lit).clone(), - ty, + target, fields, base, })) @@ -8706,10 +9143,21 @@ impl<'a> ExecPass<'a> { CellArg::String(s) => Value::String(s.clone()), CellArg::Enum { variant, payload } => Value::Enum(Arc::new(EnumValue { variant: variant.clone(), - payload: payload - .iter() - .map(|arg| self.bind_cell_arg(cell_id, span, arg)) - .collect(), + payload: VariantValues::Tuple( + payload + .iter() + .map(|arg| self.bind_cell_arg(cell_id, span, arg)) + .collect(), + ), + })), + CellArg::StructVariant { variant, fields } => Value::Enum(Arc::new(EnumValue { + variant: variant.clone(), + payload: VariantValues::Struct( + fields + .iter() + .map(|(field, arg)| (field.clone(), self.bind_cell_arg(cell_id, span, arg))) + .collect(), + ), })), CellArg::Seq(v) => Value::Seq( v.iter() @@ -8868,19 +9316,34 @@ impl<'a> ExecPass<'a> { Value::Int(i) => Some(CellArg::Int(*i)), Value::Bool(b) => Some(CellArg::Bool(*b)), Value::String(s) => Some(CellArg::String(s.clone())), - Value::Enum(value) => { - let mut payload = Vec::with_capacity(value.payload.len()); - for v in &value.payload { - match self.cell_arg_from_value(cell_id, dependent_vid, v)? { - Some(arg) => payload.push(arg), - None => return Ok(None), + Value::Enum(value) => match &value.payload { + VariantValues::Tuple(values) => { + let mut payload = Vec::with_capacity(values.len()); + for v in values { + match self.cell_arg_from_value(cell_id, dependent_vid, v)? { + Some(arg) => payload.push(arg), + None => return Ok(None), + } } + Some(CellArg::Enum { + variant: value.variant.clone(), + payload, + }) } - Some(CellArg::Enum { - variant: value.variant.clone(), - payload, - }) - } + VariantValues::Struct(values) => { + let mut fields = Vec::with_capacity(values.len()); + for (name, v) in values { + match self.cell_arg_from_value(cell_id, dependent_vid, v)? { + Some(arg) => fields.push((name.clone(), arg)), + None => return Ok(None), + } + } + Some(CellArg::StructVariant { + variant: value.variant.clone(), + fields, + }) + } + }, Value::Seq(s) => { let mut args = Vec::with_capacity(s.len()); for v in s.iter() { @@ -11179,13 +11642,19 @@ impl<'a> ExecPass<'a> { self.add_value_dependent(pending, vid); false } else { + let struct_ty = match &lit.target { + StructLitTarget::Struct(ty) => Some(ty.clone()), + StructLitTarget::Variant { .. } => None, + }; // The fields not listed come from the base, which the // static check proved to be this struct unless it was - // typed `Any`. + // typed `Any`. A variant literal has no base. let base = match lit.base { None => None, Some(base) => match self.values[&base].get_ready() { - Some(Value::Struct(value)) if value.name == lit.ty.name => { + Some(Value::Struct(value)) + if struct_ty.as_ref().is_some_and(|ty| value.name == ty.name) => + { Some(value.fields.clone()) } _ => { @@ -11199,14 +11668,31 @@ impl<'a> ExecPass<'a> { // Declaration order, whatever order the literal used: // `CellArg::Struct` fields are matched pairwise against // the type's. - let Some(def) = self.defs.get(&lit.ty.def).and_then(AdtDef::as_struct) else { + let declared = match &lit.target { + StructLitTarget::Struct(ty) => self + .defs + .get(&ty.def) + .and_then(AdtDef::as_struct) + .map(|def| def.fields.keys().cloned().collect_vec()), + StructLitTarget::Variant { ty, variant } => self + .defs + .get(&ty.def) + .and_then(AdtDef::as_enum) + .and_then(|def| def.variants.get(variant)) + .and_then(|variant| match &variant.payload { + VariantTys::Struct(fields) => { + Some(fields.keys().cloned().collect_vec()) + } + VariantTys::Tuple(_) => None, + }), + }; + let Some(declared) = declared else { let span = self.span(&vref.loc, lit.expr.span); self.invalid_type(cell_id, &span); return self.poison(cell_id, vid); }; - let fields = def - .fields - .keys() + let fields = declared + .iter() .map(|name| { let explicit = lit .expr @@ -11227,13 +11713,19 @@ impl<'a> ExecPass<'a> { self.invalid_type(cell_id, &span); return self.poison(cell_id, vid); }; - self.values.insert( - vid, - DeferValue::Ready(Value::Struct(Box::new(StructValue { - name: lit.ty.name.clone(), + let value = match &lit.target { + StructLitTarget::Struct(ty) => Value::Struct(Box::new(StructValue { + name: ty.name.clone(), fields, - }))), - ); + })), + StructLitTarget::Variant { variant, .. } => { + Value::Enum(Arc::new(EnumValue { + variant: variant.clone(), + payload: VariantValues::Struct(fields), + })) + } + }; + self.values.insert(vid, DeferValue::Ready(value)); true } } @@ -11257,7 +11749,7 @@ impl<'a> ExecPass<'a> { vid, DeferValue::Ready(Value::Enum(Arc::new(EnumValue { variant: ctor.variant.clone(), - payload, + payload: VariantValues::Tuple(payload), }))), ); true @@ -11339,16 +11831,37 @@ impl<'a> ExecPass<'a> { let Value::Enum(value) = value else { return; }; - for (field, element) in fields.iter().zip(&value.payload) { - match field { - Pattern::Binding { metadata, .. } => { - let vid = self.new_ready_value(element.clone()); - frame.bindings.insert(metadata.0, vid); - } - Pattern::Wildcard { .. } | Pattern::Variant { .. } => {} - } + for (field, element) in fields.iter().zip(value.payload.values()) { + 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; + }; + for field in fields { + let Some(element) = values.get(field.name.name.as_str()) else { + continue; + }; + self.bind_payload_element(&field.pattern, element, frame); + } + } + } + } + + /// Binds one payload element of a variant pattern, which is a name or `_`. + fn bind_payload_element( + &mut self, + pattern: &Pattern, + element: &Value, + frame: &mut Frame, + ) { + if let Pattern::Binding { metadata, .. } = pattern { + let vid = self.new_ready_value(element.clone()); + frame.bindings.insert(metadata.0, vid); } } @@ -11536,8 +12049,43 @@ impl Value { #[derive(Debug, Clone)] pub struct EnumValue { pub variant: String, - /// Empty for a unit variant. - pub payload: Vec, + pub payload: VariantValues, +} + +/// The payload of an [`EnumValue`]. +#[derive(Debug, Clone)] +pub enum VariantValues { + /// Positional elements; empty for a unit variant. + Tuple(Vec), + /// Named fields in declaration order. + Struct(IndexMap), +} + +impl VariantValues { + /// How many values the variant carries. + pub fn len(&self) -> usize { + match self { + Self::Tuple(payload) => payload.len(), + Self::Struct(fields) => fields.len(), + } + } + + /// Whether the variant carries nothing. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The payload values in declaration order. + pub fn values(&self) -> impl Iterator { + let (tuple, fields) = match self { + Self::Tuple(payload) => (Some(payload), None), + Self::Struct(fields) => (None, Some(fields)), + }; + tuple + .into_iter() + .flatten() + .chain(fields.into_iter().flatten().map(|(_, value)| value)) + } } /// A tuple variant as a callable. See [`Value::Ctor`]. @@ -11552,21 +12100,43 @@ fn pattern_matches(pattern: &Pattern, value: &Value) -> match pattern { Pattern::Wildcard { .. } | Pattern::Binding { .. } => true, Pattern::Variant { path, fields, .. } => { - let Value::Enum(value) = value else { + let Some(value) = enum_value(path, value) else { return false; }; - path.path - .last() - .is_some_and(|name| *name.name == *value.variant) - && fields.len() == value.payload.len() + fields.len() == value.payload.len() && fields .iter() - .zip(&value.payload) + .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; + }; + fields.iter().all(|field| { + values + .get(field.name.name.as_str()) + .is_some_and(|element| pattern_matches(&field.pattern, element)) + }) + } } } +/// `value` as the enum value the last segment of `path` names, if it is one. +fn enum_value<'a>( + path: &IdentPath, + value: &'a Value, +) -> Option<&'a EnumValue> { + let Value::Enum(value) = value else { + return None; + }; + let variant = path.path.last()?; + (*variant.name == *value.variant).then_some(&**value) +} + /// Structural equality of two values, or `None` for a pair the evaluator /// cannot compare. fn values_equal(left: &Value, right: &Value) -> Option { @@ -11582,7 +12152,7 @@ fn values_equal(left: &Value, right: &Value) -> Option { return None; } let mut equal = true; - for (l, r) in l.payload.iter().zip(&r.payload) { + for (l, r) in l.payload.values().zip(r.payload.values()) { equal &= values_equal(l, r)?; } Some(equal) @@ -12296,14 +12866,25 @@ struct PartialSeqExpr { #[derive(Debug, Clone)] struct PartialStructLit { expr: StructLitExpr, - /// The struct being built, from the literal's checked type; its - /// definition's field order is the order the value's fields take. - ty: Arc, + /// What the literal builds, from its checked type; the declaration's field + /// order is the order the value's fields take. + target: StructLitTarget, /// One value per entry of `expr.fields`. fields: Vec, base: Option, } +/// What a [`PartialStructLit`] builds. +#[derive(Debug, Clone)] +enum StructLitTarget { + Struct(Arc), + /// A variant with named fields, and the enum it belongs to. + Variant { + ty: Arc, + variant: String, + }, +} + #[derive(Debug, Clone)] struct PartialForLoop { for_loop: ForLoop, diff --git a/crates/compiler/src/compile/result.rs b/crates/compiler/src/compile/result.rs index 7a47521..69a12d2 100644 --- a/crates/compiler/src/compile/result.rs +++ b/crates/compiler/src/compile/result.rs @@ -81,6 +81,20 @@ pub enum StaticErrorKind { expected: usize, found: usize, }, + /// A variant with named fields was used as a value or called like a tuple + /// variant; it is built with `Enum::Variant { field: value }`. + #[error("variant `{0}` has named fields; construct it with braces")] + StructVariantConstruction(String), + /// A tuple or unit variant was written with braces, which only a variant + /// with named fields accepts. + #[error("variant `{0}` has no named fields")] + NotAStructVariant(String), + /// A variant pattern without `..` omits declared fields. + #[error("missing fields {fields} in pattern of {ty}; add `..` to ignore them")] + MissingPatternFields { ty: String, fields: String }, + /// A variant literal used `..base`, which only a struct literal accepts. + #[error("`..` is not supported in a variant; every field must be given")] + VariantLiteralBase, /// A match arm after every variant is covered or after a catch-all. #[error("unreachable match arm")] UnreachableMatchArm, diff --git a/crates/compiler/src/fingerprint.rs b/crates/compiler/src/fingerprint.rs index b37d4db..9b0f2c6 100644 --- a/crates/compiler/src/fingerprint.rs +++ b/crates/compiler/src/fingerprint.rs @@ -294,7 +294,7 @@ impl Builder<'_> { let mut deps = IndexSet::new(); if let Some(def) = self.defs.get(&var).and_then(|def| def.as_enum()) { for variant in def.variants.values() { - for payload in &variant.payload { + for payload in variant.payload.tys() { self.ty(payload, &mut deps); } } @@ -416,6 +416,12 @@ impl Builder<'_> { self.pattern(field, out); } } + Pattern::StructVariant { path, fields, .. } => { + self.ident_path(path, out); + for field in fields { + self.pattern(&field.pattern, out); + } + } } } @@ -506,10 +512,10 @@ impl Builder<'_> { self.expr(item, out); } } - // The struct being built reaches us through the literal's checked - // type; its path carries no `VarId`. + // The struct or variant being built reaches us through the + // literal's checked type; its path carries no `VarId`. Expr::StructLit(e) => { - self.ty(&e.metadata, out); + self.ty(&e.metadata.ty, out); for field in &e.fields { self.expr(&field.value, out); } @@ -925,6 +931,19 @@ fn untouched() -> Float { 5. } assert_eq!(changed(base, after), ["Mode", "pick"]); } + /// A variant's named payload reaches the fingerprint like a tuple one, so + /// adding a field moves the enum and everything that matches on it. + #[test] + fn changing_a_variant_payload_invalidates_its_users() { + let base = "\ +enum Mode { Sized { w: Float }, Fast, } +fn pick(m: Mode) -> Float { match m { Mode::Sized { w, .. } => w, Mode::Fast => 1., } } +fn untouched() -> Float { 5. } +"; + let after = base.replace("{ w: Float }", "{ w: Float, h: Int }"); + assert_eq!(changed(base, &after), ["Mode", "pick"]); + } + /// Reordering a struct's fields changes its text but keeps every user /// valid, so exactly the declarations that name the struct -- as a /// parameter type, a return type, or a literal -- must change with it. diff --git a/crates/compiler/src/gdscache.rs b/crates/compiler/src/gdscache.rs index 29d6677..bee1337 100644 --- a/crates/compiler/src/gdscache.rs +++ b/crates/compiler/src/gdscache.rs @@ -181,6 +181,17 @@ fn hash_cell_arg_key(hasher: &mut fnv::FnvHasher, arg: &CellArgKey) { hash_cell_arg_key(hasher, value); } } + CellArgKey::StructVariant(variant, fields) => { + hasher.write_u8(12); + hasher.write_usize(variant.len()); + hasher.write(variant.as_bytes()); + hasher.write_usize(fields.len()); + for (field, value) in fields { + hasher.write_usize(field.len()); + hasher.write(field.as_bytes()); + hash_cell_arg_key(hasher, value); + } + } CellArgKey::Seq(values) => { hasher.write_u8(5); hasher.write_usize(values.len()); diff --git a/crates/compiler/src/incremental.rs b/crates/compiler/src/incremental.rs index da3aef9..d1a7180 100644 --- a/crates/compiler/src/incremental.rs +++ b/crates/compiler/src/incremental.rs @@ -758,6 +758,15 @@ fn hash_cell_args(args: &[CellArg], hasher: &mut impl Hasher) { variant.hash(hasher); hash_cell_args(payload, hasher); } + CellArg::StructVariant { variant, fields } => { + 12_u8.hash(hasher); + variant.hash(hasher); + fields.len().hash(hasher); + for (field, value) in fields { + field.hash(hasher); + hash_cell_args(std::slice::from_ref(value), hasher); + } + } CellArg::Seq(values) => { 5_u8.hash(hasher); hash_cell_args(values, hasher); diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 1047742..9e0699f 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -233,6 +233,7 @@ mod tests { const ARGON_GENERICS_STRUCT: &str = concatcp!(EXAMPLES_DIR, "/generics_struct/lib.ar"); 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_RECURSIVE_STRUCT: &str = concatcp!(EXAMPLES_DIR, "/recursive_struct/lib.ar"); const ARGON_GENERICS_CELL: &str = concatcp!(EXAMPLES_DIR, "/generics_cell/lib.ar"); @@ -5858,6 +5859,132 @@ cell top() { )); } + #[test] + fn struct_variants_are_checked() { + let source = |body: &str| { + format!( + "enum Shape {{ Circle {{ r: Float }}, Box(Float, Float), Empty, }}\n\ + cell c() {{ {body} }}" + ) + }; + assert!( + generic_errors(&source( + "let r = 1.; let a = Shape::Circle { r }; let b = Shape::Circle { r: 2. };" + )) + .is_empty() + ); + // Every field must be given exactly once, and only declared ones. + assert!(matches!( + generic_errors(&source("let a = Shape::Circle {};")).as_slice(), + [StaticErrorKind::MissingStructFields { ty, fields }] + if ty == "Shape::Circle" && fields == "`r`" + )); + assert!(matches!( + generic_errors(&source("let a = Shape::Circle { r: 1., r: 2. };")).as_slice(), + [StaticErrorKind::DuplicateStructField { field }] if field == "r" + )); + assert!(matches!( + generic_errors(&source("let a = Shape::Circle { r: 1., w: 2. };")).as_slice(), + [StaticErrorKind::NoFieldOnTy { field, ty }] + if field == "w" && ty == "Shape::Circle" + )); + assert!(matches!( + generic_errors(&source("let a = Shape::Circle { r: 1 };")).as_slice(), + [StaticErrorKind::IncorrectTy { expected, found }] + if expected == "Float" && found == "Int" + )); + // A variant has no `..base`, unlike a struct literal. + assert!(matches!( + generic_errors(&source( + "let a = Shape::Circle { r: 1. }; let b = Shape::Circle { ..a };" + )) + .as_slice(), + [ + StaticErrorKind::VariantLiteralBase, + StaticErrorKind::MissingStructFields { .. } + ] + )); + // The two payload forms are not interchangeable. + assert!(matches!( + generic_errors(&source("let a = Shape::Circle(1.);")).as_slice(), + [StaticErrorKind::StructVariantConstruction(ty)] if ty == "Shape::Circle" + )); + assert!(matches!( + generic_errors(&source("let a: Shape = Shape::Circle;")).as_slice(), + [StaticErrorKind::StructVariantConstruction(ty)] if ty == "Shape::Circle" + )); + assert!(matches!( + generic_errors(&source("let a = Shape::Box { w: 1., h: 2. };")).as_slice(), + [StaticErrorKind::NotAStructVariant(ty)] if ty == "Shape::Box" + )); + assert!(matches!( + generic_errors(&source("let a = Shape::Empty {};")).as_slice(), + [StaticErrorKind::NotAStructVariant(ty)] if ty == "Shape::Empty" + )); + } + + #[test] + fn struct_variant_patterns_are_checked() { + let source = |arm: &str| { + format!( + "enum Shape {{ Circle {{ r: Float }}, Box(Float, Float), Empty, }}\n\ + fn f(s: Shape) -> Float {{ match s {{ {arm} _ => 0., }} }}" + ) + }; + for arm in [ + "Shape::Circle { r } => r,", + "Shape::Circle { r: radius } => radius,", + "Shape::Circle { r: _ } => 1.,", + "Shape::Circle { .. } => 1.,", + "Shape::Circle { r, .. } => r,", + ] { + assert!(generic_errors(&source(arm)).is_empty(), "{arm}"); + } + // Without `..` every field must be named, and only declared ones. + assert!(matches!( + generic_errors(&source("Shape::Circle {} => 1.,")).as_slice(), + [StaticErrorKind::MissingPatternFields { ty, fields }] + if ty == "Shape::Circle" && fields == "`r`" + )); + assert!(matches!( + generic_errors(&source("Shape::Circle { w } => 1.,")).as_slice(), + [ + StaticErrorKind::NoFieldOnTy { .. }, + StaticErrorKind::MissingPatternFields { .. } + ] + )); + assert!(matches!( + generic_errors(&source("Shape::Circle { r, r } => r,")).as_slice(), + [StaticErrorKind::DuplicateStructField { field }] if field == "r" + )); + // The two payload forms are not interchangeable. + assert!(matches!( + generic_errors(&source("Shape::Circle(r) => r,")).as_slice(), + [StaticErrorKind::StructVariantConstruction(ty)] if ty == "Shape::Circle" + )); + assert!(matches!( + generic_errors(&source("Shape::Box { w, h } => w,")).as_slice(), + [StaticErrorKind::NotAStructVariant(ty)] if ty == "Shape::Box" + )); + // Naming a variant still covers it, so `..` does not weaken + // exhaustiveness. + assert!( + generic_errors( + "enum Shape { Circle { r: Float }, Empty, }\n\ + fn f(s: Shape) -> Float { match s { Shape::Circle { .. } => 1., Shape::Empty => 0., } }" + ) + .is_empty() + ); + assert!(matches!( + generic_errors( + "enum Shape { Circle { r: Float }, Empty, }\n\ + fn f(s: Shape) -> Float { match s { Shape::Circle { .. } => 1., } }" + ) + .as_slice(), + [StaticErrorKind::MatchArmsNotComprehensive] + )); + } + #[test] fn match_arms_after_a_catch_all_are_unreachable() { let source = |arms: &str| { @@ -6206,5 +6333,61 @@ cell top() { ); assert_eq!(top_rect_sizes(&data), [(40., 150.)]); } + + /// A named payload is built in any field order, bound by name whatever + /// order the pattern uses, and compared field by field. + #[test] + fn struct_variant_payloads_are_matched_and_compared_at_run_time() { + let data = compile_top( + "enum Shape { Circle { r: Float }, Box { w: Float, h: Float }, Empty, } + enum Tag { Named { n: Int }, Bare, } + fn width(s: Shape) -> Float { + match s { + Shape::Circle { r } => 2. * r, + Shape::Box { h: _, w } => w, + Shape::Empty => 0., + } + } + fn height(s: Shape) -> Float { + match s { Shape::Box { h: tall, .. } => tall, _ => 1., } + } + cell top() { + let w = 30.; + let boxed = Shape::Box { h: 7., w }; + let width = width(boxed) + width(Shape::Circle { r: 5. }) + width(Shape::Empty); + let same = Tag::Named { n: 2 } == Tag::Named { n: 2 }; + let differs = Tag::Named { n: 2 } == Tag::Bare; + let h = height(boxed) + if same { 100. } else { 0. } + if differs { 1000. } else { 0. }; + let r = rect(\"met1\", x0=0., y0=0., w=width, h=h); + }", + ); + assert_eq!(top_rect_sizes(&data), [(40., 107.)]); + } + + /// A variant with named fields crosses the cell boundary, where its + /// fields take declaration order whatever order the literal used. + #[test] + fn struct_variant_cell_arguments_keep_declaration_order() { + let data = compile_top( + "enum Shape { Box { w: Float, h: Float }, } + cell shape(s: Shape) { + let r = rect(\"met1\", x0=0., y0=0., w=match s { Shape::Box { w, .. } => w, }, h=10.); + } + cell top() { + let a = inst(shape(Shape::Box { w: 100., h: 1. }), x=0., y=0.); + let b = inst(shape(Shape::Box { h: 1., w: 300. }), x=0., y=100.); + }", + ); + assert_eq!(rect_widths_of(&data, "shape"), [100., 300.]); + } + + #[test] + fn argon_enum_struct_payload() { + let data = compile_example(ARGON_ENUM_STRUCT_PAYLOAD); + // The circle contributes its diameter and the box its width; the + // outline's height is the box's. + assert_eq!(top_rect_sizes(&data), [(400., 20.)]); + assert_eq!(rect_widths_of(&data, "shape_rect"), [100., 300.]); + } } pub mod cli; diff --git a/crates/compiler/src/nav.rs b/crates/compiler/src/nav.rs index 811352d..7beba05 100644 --- a/crates/compiler/src/nav.rs +++ b/crates/compiler/src/nav.rs @@ -24,11 +24,12 @@ use arcstr::ArcStr; use crate::{ ast::{ ArgDecl, CellDecl, Decl, EnumDecl, Expr, FnDecl, GenericArgs, Ident, IdentPath, ModPath, - Pattern, Scope, Statement, StructDecl, TyParam, TySpec, TySpecKind, UseDecl, WorkspaceAst, + Pattern, Scope, Statement, StructDecl, TyParam, TySpec, TySpecKind, UseDecl, + VariantPayload, WorkspaceAst, }, compile::{ AdtDef, BUILTINS, RESERVED_CELL_FIELDS, Ty, TyParamTy, TypeDefs, TypedWorkspace, VarId, - VarIdTyMetadata, module_prefix, param_map, subst, + VarIdTyMetadata, VariantTys, module_prefix, param_map, subst, }, }; @@ -1424,21 +1425,30 @@ impl<'a> Builder<'a> { }; let def = self.defs.get(&name_id).and_then(AdtDef::as_enum); let params = def.map(|def| def.params.clone()).unwrap_or_default(); - // A variant's rendering, `Some(T)` or `None`, from its checked payload. + // A variant's rendering -- `Some(T)`, `Circle { r: Float }`, or + // `None` -- from its checked payload. let variant_label = |name: &str| { let payload = def .and_then(|def| def.variants.get(name)) - .map(|variant| &variant.payload[..]) - .unwrap_or_default(); - if payload.is_empty() { - name.to_owned() - } else { - let payload = payload - .iter() - .map(ToString::to_string) - .collect::>() - .join(", "); - format!("{name}({payload})") + .map(|variant| &variant.payload); + match payload { + Some(VariantTys::Tuple(payload)) if !payload.is_empty() => { + let payload = payload + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "); + format!("{name}({payload})") + } + Some(VariantTys::Struct(fields)) => { + let fields = fields + .iter() + .map(|(field, ty)| format!("{field}: {ty}")) + .collect::>() + .join(", "); + format!("{name} {{ {fields} }}") + } + Some(VariantTys::Tuple(_)) | None => name.to_owned(), } }; let variants = decl @@ -1468,17 +1478,22 @@ impl<'a> Builder<'a> { let payload = def .and_then(|def| def.variants.get(name.as_str())) .map(|variant| variant.payload.clone()) - .unwrap_or_default(); + .unwrap_or_else(|| VariantTys::Tuple(Vec::new())); let label = format!("{}::{}", decl.name.name, variant_label(&name)); // A tuple variant is callable, so it has a signature: one - // positional parameter per payload element. - let signature = (!payload.is_empty()).then(|| SignatureInfo { - label: label.clone(), - parameters: payload - .iter() - .map(|ty| parameter(&ty.to_string(), false)) - .collect(), - }); + // positional parameter per payload element. A variant with named + // fields is built by a literal, so it has none. + let signature = match &payload { + VariantTys::Tuple(payload) if !payload.is_empty() => Some(SignatureInfo { + label: label.clone(), + parameters: payload + .iter() + .map(|ty| parameter(&ty.to_string(), false)) + .collect(), + }), + VariantTys::Tuple(_) | VariantTys::Struct(_) => None, + }; + let variant_id = def.and_then(|def| def.variants.get(name.as_str()).map(|v| v.id)); self.define( DefKey::Variant(name_id, name), SymbolKind::Variant, @@ -1490,8 +1505,36 @@ impl<'a> Builder<'a> { full_span: Some(variant.span), }, ); - for (spec, ty) in variant.payload.iter().zip(&payload) { - self.ty_spec(spec, ty); + match (&variant.payload, &payload) { + (VariantPayload::Tuple(specs), VariantTys::Tuple(tys)) => { + for (spec, ty) in specs.iter().zip(tys) { + self.ty_spec(spec, ty); + } + } + (VariantPayload::Struct(fields), VariantTys::Struct(tys)) => { + for field in fields { + let ty = tys + .get(field.name.name.as_str()) + .cloned() + .unwrap_or_default(); + self.ty_spec(&field.ty, &ty); + let Some(variant_id) = variant_id else { + continue; + }; + self.define( + DefKey::Field(variant_id, field.name.name.to_string()), + SymbolKind::Field, + &field.name, + DefinitionInfo { + detail: format!("{}: {ty}", field.name.name), + ty: Some(ty), + signature: None, + full_span: Some(field.span), + }, + ); + } + } + _ => {} } } } @@ -1980,17 +2023,27 @@ impl<'a> Builder<'a> { return; }; self.module_path(prefix); - // The struct reaches us through the literal's checked type; - // like a call's callee, the path itself carries no `VarId`. - let struct_id = match &lit.metadata { - Ty::Struct(struct_ty) => Some(struct_ty.def), - _ => None, + // The struct or variant reaches us through the literal's + // checked type; like a call's callee, the path itself carries + // no `VarId`. A variant's fields are keyed by the variant, so + // it is looked up by name in its enum. + let (target, field_id) = match (&lit.metadata.ty, &lit.metadata.variant) { + (Ty::Struct(struct_ty), _) => { + (Target::Def(DefKey::Var(struct_ty.def)), Some(struct_ty.def)) + } + (Ty::Enum(enum_ty), Some(variant)) => ( + Target::Def(DefKey::Variant(enum_ty.def, variant.clone())), + self.defs + .get(&enum_ty.def) + .and_then(AdtDef::as_enum) + .and_then(|def| def.variants.get(variant)) + .map(|variant| variant.id), + ), + _ => (Target::Unresolved, None), }; - let target = - struct_id.map_or(Target::Unresolved, |id| Target::Def(DefKey::Var(id))); self.record(name.span, target); if let Some(args) = &lit.path.generic_args { - self.generic_args(args, &lit.metadata); + self.generic_args(args, &lit.metadata.ty); } for field in &lit.fields { // A shorthand field is one token naming both the field and @@ -1998,7 +2051,7 @@ impl<'a> Builder<'a> { // local -- recorded when the value is walked -- is the more // useful place to jump. if !field.shorthand { - let target = struct_id.map_or(Target::Unresolved, |id| { + let target = field_id.map_or(Target::Unresolved, |id| { Target::Def(DefKey::Field(id, field.name.name.to_string())) }); self.record(field.name.span, target); @@ -2108,6 +2161,21 @@ impl<'a> Builder<'a> { self.pattern(field, body); } } + Pattern::StructVariant { path, fields, .. } => { + self.ident_path(path); + let variant_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| { + Target::Def(DefKey::Field(id, field.name.name.to_string())) + }); + self.record(field.name.span, target); + } + self.pattern(&field.pattern, body); + } + } } } @@ -2481,6 +2549,51 @@ fn width(s: Shape) -> Float { assert_eq!(definition.detail, "Option::Some(T)"); } + /// A variant's named fields are definitions of their own, so the field + /// names in a literal and in a pattern jump to the declaration, and the + /// variant name jumps to the variant. + #[test] + fn struct_variant_fields_resolve_and_hover() { + check( + r#" +enum Shape { + Circle { radius: Float }, + Empty, +} + +fn width(s: Shape) -> Float { + match s { + Shape::Cir$0cle { rad$0ius: r } => $0r, + Shape::Empty => 0., + } +} + +cell top() { + let c = Shape::Cir$0cle { rad$0ius: 1. }; +} +"#, + // `r` is a one-letter name, so its occurrence index counts every + // `r` in the source. + &["Circle#0", "radius#0", "r#4", "Circle#0", "radius#0"], + ); + + let (source, index, _) = index( + "enum Shape { Circle { radius: Float }, Empty, }\n\ + fn width(s: Shape) -> Float { match s { Shape::Circle { radius } => radius, Shape::Empty => 0., } }\n", + ); + let hover = |needle: &str| { + index + .hover_at(Path::new(ROOT), source.find(needle).unwrap()) + .unwrap_or_else(|| panic!("hover on {needle}")) + .contents + }; + assert_eq!( + hover("Shape {"), + "enum Shape { Circle { radius: Float }, Empty }" + ); + assert_eq!(hover("Circle {"), "Shape::Circle { radius: Float }"); + } + #[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 67854e9..7bd901a 100644 --- a/crates/compiler/src/parser/grammar.rs +++ b/crates/compiler/src/parser/grammar.rs @@ -18,11 +18,11 @@ use cfgrammar::Span; use crate::ast::{ ArgDecl, Args, ArithOp, Ast, BinOp, BinOpExpr, BoolLiteral, BoolOp, CallExpr, CastExpr, CellDecl, ComparisonOp, ConstantDecl, Decl, EmitExpr, EnumDecl, EnumVariant, Expr, - FieldAccessExpr, FloatLiteral, FnDecl, ForLoop, GenericArgs, Ident, IdentPath, IfExpr, - IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, LetBinding, MatchArm, MatchExpr, - ModDecl, NilLiteral, Pattern, Scope, SeqLiteral, Statement, StringLiteral, StructDecl, - StructField, StructLitExpr, StructLitField, TupleExpr, TyParam, TySpec, TySpecKind, UnaryOp, - UnaryOpExpr, UseDecl, + FieldAccessExpr, FieldPattern, FloatLiteral, FnDecl, ForLoop, GenericArgs, Ident, IdentPath, + IfExpr, IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, LetBinding, MatchArm, + MatchExpr, ModDecl, NilLiteral, Pattern, Scope, SeqLiteral, Statement, StringLiteral, + StructDecl, StructField, StructLitExpr, StructLitField, TupleExpr, TyParam, TySpec, TySpecKind, + UnaryOp, UnaryOpExpr, UseDecl, VariantPayload, }; use crate::compile::BUILTINS; use crate::parse::ParseMetadata; @@ -606,7 +606,7 @@ impl<'a> Parser<'a> { } } - /// `enumVariant : ident (LPAREN tySpecList RPAREN)?` + /// `enumVariant : ident (LPAREN tySpecList RPAREN | LBRACE structFields RBRACE)?` fn parse_enum_variant(&mut self) -> EnumVariant<&'a str, Md> { let lo = self.cur.start; let name = self.ident(CompletionSite::NewIdentifier); @@ -615,9 +615,16 @@ impl<'a> Parser<'a> { p.parse_ty_spec() }); self.expect(TokenKind::RParen); - payload + VariantPayload::Tuple(payload) + } else if self.eat(TokenKind::LBrace) { + let fields = + self.separated_list(TokenKind::RBrace, CompletionSite::NewIdentifier, |p| { + p.parse_struct_field() + }); + self.expect(TokenKind::RBrace); + VariantPayload::Struct(fields) } else { - Vec::new() + VariantPayload::Tuple(Vec::new()) }; EnumVariant { name, @@ -1140,7 +1147,7 @@ impl<'a> Parser<'a> { } } - /// `pattern : UNDERSCORE | identPath (LPAREN patternList RPAREN)?` + /// `pattern : UNDERSCORE | identPath (LPAREN patternList RPAREN | LBRACE fieldPatterns RBRACE)?` /// /// A bare name is a [`Pattern::Binding`]; whether it names a unit variant /// instead is decided by the type checker, as in Rust. @@ -1152,6 +1159,9 @@ 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); + } if self.eat(TokenKind::LParen) { let fields = self.separated_list(TokenKind::RParen, CompletionSite::Pattern, |p| { p.parse_sub_pattern() @@ -1174,6 +1184,66 @@ impl<'a> Parser<'a> { } } + /// `fieldPatterns : (fieldPattern (COMMA fieldPattern)* (COMMA DOTDOT | COMMA)?)? | DOTDOT` + /// + /// The `..` comes last and may not be followed by a comma, the shape + /// [`Self::parse_struct_lit`] accepts for `..base`. Because the body has + /// 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( + &mut self, + lo: u32, + path: IdentPath<&'a str, Md>, + ) -> Pattern<&'a str, Md> { + self.expect(TokenKind::LBrace); + let mut fields = Vec::new(); + let mut rest = false; + self.record_completion_site(CompletionSite::Pattern); + while !self.at(TokenKind::RBrace) && !self.at(TokenKind::Eof) { + if self.eat(TokenKind::DotDot) { + rest = true; + break; + } + fields.push(self.parse_field_pattern()); + if !self.eat(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RBrace); + Pattern::StructVariant { + path, + fields, + rest, + span: self.finish_span(lo), + } + } + + /// `fieldPattern : ident (COLON subPattern)?` + fn parse_field_pattern(&mut self) -> FieldPattern<&'a str, Md> { + let lo = self.cur.start; + self.record_completion_site(CompletionSite::Pattern); + let name = self.ident(CompletionSite::Pattern); + let (pattern, shorthand) = if self.eat(TokenKind::Colon) { + (self.parse_sub_pattern(), false) + } else { + // Shorthand: `f` stands for `f: f`. The binding is at the name's + // own span, so diagnostics and navigation on it point at the one + // token the user wrote. + let pattern = Pattern::Binding { + name: name.clone(), + metadata: (), + }; + (pattern, true) + }; + FieldPattern { + name, + pattern, + shorthand, + span: self.finish_span(lo), + } + } + /// A payload element pattern: `_` or a name. Nested variant patterns are /// not supported. fn parse_sub_pattern(&mut self) -> Pattern<&'a str, Md> { diff --git a/crates/compiler/src/parser/mod.rs b/crates/compiler/src/parser/mod.rs index 44f47b7..b49c308 100644 --- a/crates/compiler/src/parser/mod.rs +++ b/crates/compiler/src/parser/mod.rs @@ -251,6 +251,12 @@ mod tests { ("cell top() { let x = if true {} | ; }", Keyword("else")), ("cell top() { match m { | } }", Pattern), ("cell top() { match m { Some(|) => 1, } }", Pattern), + ("cell top() { match m { Shape::Box { | } => 1, } }", Pattern), + ( + "cell top() { match m { Shape::Box { w: | } => 1, } }", + Pattern, + ), + ("enum E { V { a: | } }", Type), ("fn f<|>() {}", NewIdentifier), ("fn f(o: Option<|>) {}", Type), ("cell top() { let n: | = 1; }", Type), @@ -466,9 +472,9 @@ mod tests { assert_eq!(text(option.span), "enum Option { Some(T), None, }"); assert_eq!(text(option.params[0].span), "T"); assert_eq!(text(option.variants[0].span), "Some(T)"); - assert_eq!(option.variants[0].payload.len(), 1); + assert_eq!(option.variants[0].payload.tys().count(), 1); assert_eq!(text(option.variants[1].span), "None"); - assert!(option.variants[1].payload.is_empty()); + assert!(option.variants[1].payload.is_unit()); let Decl::Cell(cell) = &ast.decls[1] else { panic!("expected a cell"); @@ -517,6 +523,107 @@ mod tests { /// A shorthand field desugars to `name: name` with the value at the name's /// own span, so later passes see an ordinary field. + #[test] + fn struct_variants_parse() { + for src in [ + "enum Shape { Circle { r: Float }, Box { w: Float, h: Float, }, Empty, }", + "enum E { V { item: T, rest: [T] }, }", + "enum E { V {}, }", + ] { + assert!( + parse(src).is_ok(), + "should parse: `{src}`: {:?}", + parse(src).err() + ); + } + for src in [ + "enum E { V { a }, }", + "enum E { V { a: }, }", + "enum E { V { a: Int b: Int }, }", + "enum E { V { a: Int, }", + ] { + assert!(parse(src).is_err(), "should be rejected: `{src}`"); + } + + let valid = [ + "match s { Shape::Circle { r } => r, _ => 0., }", + "match s { Shape::Box { w, h: tall, } => w, _ => 0., }", + "match s { Shape::Box { w, .. } => w, _ => 0., }", + "match s { Shape::Box { .. } => 1., _ => 0., }", + "match s { Shape::Box { h: _ } => 1., _ => 0., }", + "match s { lib::shapes::Shape::Box { w } => w, _ => 0., }", + "match s { Shape::Empty {} => 1., _ => 0., }", + "let x = Shape::Circle { r: 1. };", + ]; + for body in valid { + assert!(snippet_ok(body), "should parse: `{body}`"); + } + let invalid = [ + // `..` comes last and takes no trailing comma. + "match s { Shape::Box { .., w } => w, _ => 0., }", + "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 } => 1., _ => 0., }", + "match s { Shape::Box { w => 1., _ => 0., }", + ]; + for body in invalid { + assert!(!snippet_ok(body), "should be rejected: `{body}`"); + } + } + + /// A struct variant's fields keep their declared order and spans, and a + /// shorthand field pattern binds the field's own name. + #[test] + fn struct_variant_nodes_span_their_source_text() { + use crate::ast::{Decl, Expr, Pattern, Statement, VariantPayload}; + + let src = "enum Shape { Box { w: Float, h: Float }, }\n\ + cell c() {\n let v = match s { Shape::Box { w, h: tall, } => w, _ => 0., };\n}\n"; + let mut parser = super::grammar::Parser::new(src, 0); + let ast = parser.parse_root(); + assert!(parser.errors.is_empty(), "{:?}", parser.errors); + let text = |span: cfgrammar::Span| &src[span.start()..span.end()]; + + let Decl::Enum(shape) = &ast.decls[0] else { + panic!("expected an enum"); + }; + assert_eq!(text(shape.variants[0].span), "Box { w: Float, h: Float }"); + let VariantPayload::Struct(fields) = &shape.variants[0].payload else { + panic!("expected a named payload"); + }; + assert_eq!(fields.len(), 2); + assert_eq!(fields[0].name.name, "w"); + assert_eq!(text(fields[1].span), "h: Float"); + + let Decl::Cell(cell) = &ast.decls[1] else { + panic!("expected a cell"); + }; + let Statement::LetBinding(v) = &cell.scope.stmts[0] else { + panic!("expected a let"); + }; + let Expr::Match(m) = &v.value else { + panic!("expected a match"); + }; + let Pattern::StructVariant { + path, + fields, + rest, + span, + } = &m.arms[0].pattern + else { + panic!("expected a struct variant pattern"); + }; + assert_eq!(text(*span), "Shape::Box { w, h: tall, }"); + assert_eq!(path.path[1].name, "Box"); + assert!(!rest); + assert!(fields[0].shorthand); + assert!(matches!(&fields[0].pattern, Pattern::Binding { name, .. } if name.name == "w")); + assert!(!fields[1].shorthand); + assert_eq!(fields[1].name.name, "h"); + assert!(matches!(&fields[1].pattern, Pattern::Binding { name, .. } if name.name == "tall")); + } + #[test] fn shorthand_struct_fields_desugar_to_a_path_at_the_name() { use crate::ast::{Decl, Expr, Statement}; diff --git a/crates/gui/src/cli.rs b/crates/gui/src/cli.rs index be0146c..32f9739 100644 --- a/crates/gui/src/cli.rs +++ b/crates/gui/src/cli.rs @@ -1040,15 +1040,22 @@ mod tests { None, "/tmp/relay socket.sock", )); - let output = Command::new("/bin/sh") - .arg("-c") - .arg(&command) - .env("SHELL", "/bin/sh") - // An interactive shell sources `$ENV`, so clear it to keep the - // host's startup files from deciding this test's outcome. - .env("ENV", "") - .output() - .unwrap(); + let deadline = Instant::now() + Duration::from_secs(10); + let output = loop { + let output = Command::new("/bin/sh") + .arg("-c") + .arg(&command) + .env("SHELL", "/bin/sh") + .env("ENV", "") + .output() + .unwrap(); + let busy = !output.status.success() + && String::from_utf8_lossy(&output.stderr).contains("Text file busy"); + if !busy || Instant::now() >= deadline { + break output; + } + thread::sleep(Duration::from_millis(10)); + }; assert!( output.status.success(), diff --git a/docs/docs/language/control-flow.md b/docs/docs/language/control-flow.md index 9f4b4d9..c8c7642 100644 --- a/docs/docs/language/control-flow.md +++ b/docs/docs/language/control-flow.md @@ -87,7 +87,60 @@ fn width(layer: Metal) -> Float { } ``` -Match arms use `=>` and end with commas. +Match arms use `=>` and end with commas. The arms must cover every variant, +unless a bare name or `_` arm matches the rest. + +### Variants with payloads + +A variant may carry values, written either as a tuple or as named fields. A +tuple variant is constructed like a call and matched by position, where `_` +skips an element: + +```argon +enum Shape { + Circle(Float), + Box(Float, Float), +} + +fn width(s: Shape) -> Float { + match s { + Shape::Circle(r) => 2. * r, + Shape::Box(w, _) => w, + } +} +``` + +A variant with named fields is constructed with braces, like a +[struct](/language/types-values), and matched by naming the fields to bind. The +literal accepts the same `field` shorthand for `field: field`, but no `..base`: +every field must be given. A pattern must name every field too, unless it ends +in `..`; `field: name` renames a binding and `field: _` drops one. + +```argon +enum Shape { + Circle { r: Float }, + Box { w: Float, h: Float }, +} + +fn width(s: Shape) -> Float { + match s { + Shape::Circle { r } => 2. * r, + Shape::Box { w, .. } => w, + } +} + +cell top() { + let w = 300.; + let h = 20.; + // Shorthand: `w` and `h` stand for `w: w` and `h: h`. + let r = rect("met1", x0=0., y0=0., w=width(Shape::Box { w, h }), h=h); +} +``` + +A field pattern binds a name or `_`, never a nested pattern. As with a struct +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. ## `for` loops diff --git a/docs/docs/language/types-values.md b/docs/docs/language/types-values.md index 1be3c7d..aecd7a4 100644 --- a/docs/docs/language/types-values.md +++ b/docs/docs/language/types-values.md @@ -9,6 +9,8 @@ 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). + | Type | Example | Used for | | --- | --- | --- | | [`Float`](/language/types/scalars#float) | `12.`, `-0.5` | Coordinates, distances, and linear expressions | diff --git a/docs/parser.md b/docs/parser.md index aceaacd..d4be7ab 100644 --- a/docs/parser.md +++ b/docs/parser.md @@ -349,9 +349,10 @@ while !self.at(Eof) { Argument declarations (`argDecl : ident COLON tySpec (EQ expr)?`) and enum variants / struct fields are comma-separated lists parsed by the shared `separated_list` helper (§8). An enum variant is -`enumVariant : ident (LPAREN tySpecList RPAREN)?`, so `Some(T)` carries a -payload and `None` does not. A parameter with a default value is a keyword -parameter; `parse_arg_decls` gives the parameter list its own scope-ordinal +`enumVariant : ident (LPAREN tySpecList RPAREN | LBRACE structFields RBRACE)?`, +so `Some(T)` carries a positional payload, `Circle { r: Float }` a named one +reusing `parse_struct_field`, and `None` none at all. A parameter with a +default value is a keyword parameter; `parse_arg_decls` gives the parameter list its own scope-ordinal counter (§10) so scopes opened inside default values are numbered from zero. `genericParams : LT ident (COMMA ident)* COMMA? GT` follows the name of a @@ -436,10 +437,17 @@ non-consuming `parse_item` cannot spin. > *comma-terminated*, not comma-separated: a comma after **every** arm is > mandatory. The comma is part of each arm, and `matchArms : matchArm+` > requires at least one arm, so `match k {}` is a syntax error. A `pattern` is -> `_`, an `identPath`, or an `identPath` followed by a parenthesised list of -> sub-patterns, each a name or `_`; `parse_pattern` records -> `CompletionSite::Pattern`. A bare name parses as a `Pattern::Binding`, and -> the type checker decides whether it names a unit variant instead. +> `_`, an `identPath`, an `identPath` followed by a parenthesised list of +> sub-patterns, or an `identPath` followed by a braced list of field +> patterns; a sub-pattern is a name or `_` either way. `parse_pattern` +> 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 +> 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. > - **Struct literal bodies** ([§9.5](#95-struct-literals)) are comma-separated > with an optional trailing comma, but have two terminators — `}` and the > `..base` — so they keep their own loop as well. @@ -618,6 +626,10 @@ path, the explicit fields, and the optional base: own loop rather than going through `separated_list`; it terminates for the same reason, since every iteration that does not `break` consumes the separator comma. +- The same node also builds an **enum variant with named fields**, since + `E::V { r: 1. }` is `identPath LBRACE …` like any other literal. Which one a + literal builds is settled by the type checker, not the parser, and a variant + takes no `..base`. **The head restriction.** `if c {`, `match k {`, and `for v in seq {` already read `name {` as an identifier followed by the construct's own scope, so a diff --git a/examples/enum_struct_payload/Argon.toml b/examples/enum_struct_payload/Argon.toml new file mode 100644 index 0000000..334b6ad --- /dev/null +++ b/examples/enum_struct_payload/Argon.toml @@ -0,0 +1,2 @@ +name = "enum-struct-payload" +tech = "../tech/basic.tech.toml" diff --git a/examples/enum_struct_payload/lib.ar b/examples/enum_struct_payload/lib.ar new file mode 100644 index 0000000..474e37d --- /dev/null +++ b/examples/enum_struct_payload/lib.ar @@ -0,0 +1,37 @@ +enum Shape { + Circle { r: Float }, + Box { w: Float, h: Float }, + Empty, +} + +// A variant with named fields is built with braces, like a struct literal, and +// taken apart by a pattern that names the fields it binds. `f: name` renames a +// binding, `f: _` drops one, and a trailing `..` ignores the rest. +fn width(s: Shape) -> Float { + match s { + Shape::Circle { r } => 2. * r, + Shape::Box { w, .. } => w, + Shape::Empty => 0., + } +} + +fn height(s: Shape) -> Float { + match s { + Shape::Box { h: tall, w: _ } => tall, + _ => 100., + } +} + +cell shape_rect(layer: String, s: Shape) { + let r = rect(layer, x0=0., y0=0., w=width(s), h=height(s)); +} + +cell top() { + // Shorthand: `w` and `h` stand for `w: w` and `h: h`. + let w = 300.; + let h = 20.; + let shapes = cons(Shape::Circle { r: 50. }, cons(Shape::Box { w, h }, [])); + let first = inst(shape_rect("met1", head(shapes)), x=0., y=0.); + let second = inst(shape_rect("met2", std::last(shapes)), x=0., y=200.); + let outline = rect("met1", x0=0., y0=0., w=width(head(shapes)) + width(std::last(shapes)), h=height(std::last(shapes))); +}