diff --git a/README.md b/README.md index ddd85d1..48e3b8d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,51 @@ Positional parameters cannot be passed by name, and keyword parameters cannot be passed positionally. Keyword arguments also work in cell invocations: `arc run --cell 'via("met1", 100., n=3)'`. +Structs group related values under named fields. Declare them at module level, +construct them with braces, and read fields with `.`. Every field must be given +exactly once unless `..base` supplies the ones not listed, and a bare `name` is +shorthand for `name: name`: + +```rust +struct Size { + w: Float, + h: Float, +} + +struct ViaParams { + layer: String, + size: Size, + n: Int, +} + +fn grow(s: Size, by: Float) -> Size { + Size { w: s.w + by, ..s } +} + +cell via(p: ViaParams) { + let r = rect(p.layer, x0=0., y0=0., w=p.size.w, h=p.size.h); +} + +cell top() { + let size = grow(Size { w: 100., h: 50. }, 10.); + let n = 2; + let v = inst(via(ViaParams { layer: "met1", size, n })); +} +``` + +Struct types are nominal: two structs with the same fields are different types, +and a struct may not contain itself. Fields may have any type, including enums, +other structs, sequences, and tuples. A struct declared in another module is +imported with `use`, and a literal may name its module, as in +`geom::Size { w: 1., h: 2. }`. Inside an `if` condition, a `match` scrutinee, or +a `for` sequence a literal must be parenthesized, since `name {` there begins +the construct's body. Struct values are valid cell arguments, including on the +command line: + +```bash +arc run --cell 'via(ViaParams { layer: "met1", size: Size { w: 100., h: 50. }, n: 1 })' +``` + GDS imports are zero-argument cells. A module-qualified entry such as `"macros::sram"` can be referenced as `lib::macros::sram()` or imported with `use lib::macros::sram;`. Paths in the manifest are relative to `Argon.toml`, diff --git a/crates/compiler/src/ast/annotated.rs b/crates/compiler/src/ast/annotated.rs index 55b190a..4e914fb 100644 --- a/crates/compiler/src/ast/annotated.rs +++ b/crates/compiler/src/ast/annotated.rs @@ -60,9 +60,12 @@ impl AnnotatedAst { Decl::Enum(e) => { decls.push(Decl::Enum(pass.transform_enum_decl(e))); } - // Unsupported declaration kinds are rejected by the parser - // before the annotation pass is entered. - Decl::Struct(_) | Decl::Constant(_) => {} + Decl::Struct(s) => { + decls.push(Decl::Struct(pass.transform_struct_decl(s))); + } + // Constant declarations are rejected by the parser before the + // annotation pass is entered. + Decl::Constant(_) => {} } } @@ -133,6 +136,24 @@ impl AstTransformer for AstAnnotationPass { input.metadata.clone() } + fn dispatch_struct_decl( + &mut self, + input: &super::StructDecl, + _name: &super::Ident, + _fields: &[super::StructField], + ) -> ::StructDecl { + input.metadata.clone() + } + + fn dispatch_struct_field( + &mut self, + input: &super::StructField, + _name: &super::Ident, + _ty: &super::TySpec, + ) -> ::StructField { + input.metadata.clone() + } + fn dispatch_cell_decl( &mut self, input: &super::CellDecl, @@ -236,6 +257,23 @@ impl AstTransformer for AstAnnotationPass { input.metadata.clone() } + fn dispatch_struct_lit_expr( + &mut self, + input: &super::StructLitExpr, + _path: &super::IdentPath, + _fields: &[super::StructLitField], + _base: &Option>, + ) -> ::StructLitExpr { + input.metadata.clone() + } + + fn dispatch_struct_lit_path( + &mut self, + input: &super::IdentPath, + ) -> ::IdentPath { + input.metadata.clone() + } + fn dispatch_field_access_expr( &mut self, input: &super::FieldAccessExpr, diff --git a/crates/compiler/src/ast/mod.rs b/crates/compiler/src/ast/mod.rs index e07e5e9..7121f43 100644 --- a/crates/compiler/src/ast/mod.rs +++ b/crates/compiler/src/ast/mod.rs @@ -119,7 +119,7 @@ pub struct StructDecl { #[derive_where(Debug, Clone, Serialize, Deserialize; S)] pub struct StructField { pub name: Ident, - pub ty: Ident, + pub ty: TySpec, pub span: cfgrammar::Span, pub metadata: T::StructField, } @@ -263,6 +263,7 @@ pub enum Expr { Scope(Box>), Cast(Box>), Tuple(TupleExpr), + StructLit(Box>), } #[derive_where(Debug, Clone, Serialize, Deserialize; S)] @@ -390,6 +391,31 @@ pub struct TupleExpr { pub metadata: T::TupleExpr, } +/// A struct literal, `Name { field: value, .. }`. +#[derive_where(Debug, Clone, Serialize, Deserialize; S)] +pub struct StructLitExpr { + /// The struct being constructed, optionally module-qualified. + pub path: IdentPath, + pub fields: Vec>, + /// The `..base` expression, from which every field not listed is taken. + pub base: Option>, + pub span: cfgrammar::Span, + pub metadata: T::StructLitExpr, +} + +/// One `field: value` entry of a [`StructLitExpr`]. +#[derive_where(Debug, Clone, Serialize, Deserialize; S)] +pub struct StructLitField { + pub name: Ident, + /// For the shorthand `field` (no value), an [`Expr::IdentPath`] naming + /// `field` at the same span as `name`. + pub value: Expr, + /// Whether the field was written as the shorthand `field` rather than + /// `field: value`. + pub shorthand: bool, + pub span: cfgrammar::Span, +} + impl Expr { pub fn span(&self) -> cfgrammar::Span { match self { @@ -412,6 +438,7 @@ impl Expr { Self::Scope(x) => x.span, Self::Cast(x) => x.span, Self::Tuple(x) => x.span, + Self::StructLit(x) => x.span, } } } @@ -443,6 +470,7 @@ pub trait AstMetadata { type Typ: Debug + Clone + Serialize + DeserializeOwned; type CastExpr: Debug + Clone + Serialize + DeserializeOwned; type TupleExpr: Debug + Clone + Serialize + DeserializeOwned; + type StructLitExpr: Debug + Clone + Serialize + DeserializeOwned; } pub trait AstTransformer { @@ -465,6 +493,18 @@ pub trait AstTransformer { name: &Ident, variants: &[Ident], ) -> ::EnumDecl; + fn dispatch_struct_decl( + &mut self, + input: &StructDecl, + name: &Ident, + fields: &[StructField], + ) -> ::StructDecl; + fn dispatch_struct_field( + &mut self, + input: &StructField, + name: &Ident, + ty: &TySpec, + ) -> ::StructField; fn dispatch_cell_decl( &mut self, input: &CellDecl, @@ -536,6 +576,23 @@ pub trait AstTransformer { input: &TupleExpr, items: &[Expr], ) -> ::TupleExpr; + fn dispatch_struct_lit_expr( + &mut self, + input: &StructLitExpr, + path: &IdentPath, + fields: &[StructLitField], + base: &Option>, + ) -> ::StructLitExpr; + /// Metadata for the path of a struct literal. + /// + /// The path names a declaration, not a variable or an enum variant, so it + /// is resolved by [`Self::dispatch_struct_lit_expr`] and deliberately + /// bypasses [`Self::dispatch_ident_path`], which would read a qualified + /// path's last two segments as `Enum::Variant`. + fn dispatch_struct_lit_path( + &mut self, + input: &IdentPath, + ) -> ::IdentPath; fn dispatch_field_access_expr( &mut self, input: &FieldAccessExpr, @@ -663,6 +720,38 @@ pub trait AstTransformer { metadata, } } + fn transform_struct_decl( + &mut self, + input: &StructDecl, + ) -> StructDecl { + let name = self.transform_ident(&input.name); + let fields = input + .fields + .iter() + .map(|field| self.transform_struct_field(field)) + .collect_vec(); + let metadata = self.dispatch_struct_decl(input, &name, &fields); + StructDecl { + name, + fields, + span: input.span, + metadata, + } + } + fn transform_struct_field( + &mut self, + input: &StructField, + ) -> StructField { + let name = self.transform_ident(&input.name); + let ty = self.transform_ty_spec(&input.ty); + let metadata = self.dispatch_struct_field(input, &name, &ty); + StructField { + name, + ty, + span: input.span, + metadata, + } + } fn transform_cell_decl( &mut self, input: &CellDecl, @@ -1058,6 +1147,41 @@ pub trait AstTransformer { } } + fn transform_struct_lit_expr( + &mut self, + input: &StructLitExpr, + ) -> StructLitExpr { + let path = IdentPath { + path: input + .path + .path + .iter() + .map(|ident| self.transform_ident(ident)) + .collect(), + metadata: self.dispatch_struct_lit_path(&input.path), + span: input.path.span, + }; + let fields = input + .fields + .iter() + .map(|field| StructLitField { + name: self.transform_ident(&field.name), + value: self.transform_expr(&field.value), + shorthand: field.shorthand, + span: field.span, + }) + .collect_vec(); + let base = input.base.as_ref().map(|base| self.transform_expr(base)); + let metadata = self.dispatch_struct_lit_expr(input, &path, &fields, &base); + StructLitExpr { + path, + fields, + base, + span: input.span, + metadata, + } + } + fn transform_string_literal( &mut self, input: &StringLiteral, @@ -1103,6 +1227,7 @@ pub trait AstTransformer { Expr::Scope(scope) => Expr::Scope(Box::new(self.transform_scope(scope))), Expr::Cast(cast) => Expr::Cast(Box::new(self.transform_cast(cast))), Expr::Tuple(tuple) => Expr::Tuple(self.transform_tuple_expr(tuple)), + Expr::StructLit(lit) => Expr::StructLit(Box::new(self.transform_struct_lit_expr(lit))), } } } diff --git a/crates/compiler/src/cli.rs b/crates/compiler/src/cli.rs index 300ce62..bd83bec 100644 --- a/crates/compiler/src/cli.rs +++ b/crates/compiler/src/cli.rs @@ -448,7 +448,7 @@ mod tests { #[test] fn unsupported_source_is_a_diagnostic_not_a_panic() { - let source = temp_source("unsupported", "struct Point {}\n"); + let source = temp_source("unsupported", "const X: Int = 1;\n"); let diagnostic = render_failed(failed(check_args(source))); assert!( diagnostic.contains("error: error during parsing"), diff --git a/crates/compiler/src/compile.rs b/crates/compiler/src/compile.rs index ab2276e..d6c3957 100644 --- a/crates/compiler/src/compile.rs +++ b/crates/compiler/src/compile.rs @@ -27,7 +27,8 @@ use crate::ast::annotated::AnnotatedAst; use crate::ast::{ ArithOp, CastExpr, ComparisonOp, ConstantDecl, EnumDecl, FieldAccessExpr, FnDecl, ForLoop, IdentPath, IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, MatchExpr, ModPath, Scope, - Span, TySpec, TySpecKind, UnaryOp, UnaryOpExpr, UseDecl, WorkspaceAst, + Span, StructDecl, StructField, StructLitExpr, StructLitField, TySpec, TySpecKind, UnaryOp, + UnaryOpExpr, UseDecl, WorkspaceAst, }; use crate::gds::{ImportedGdsElement, import_gds}; use crate::parse::{CellInvocation, ParseOutput, WorkspaceParseAst}; @@ -534,14 +535,31 @@ impl<'a> ImportPass<'a> { self.record_dependency(self.use_module_path(u), u.span); } Decl::Enum(_) => {} + Decl::Struct(s) => { + self.transform_struct_decl(s); + } // `parse_ast` rejects these before this pass. Keep direct // library callers non-panicking if they construct an AST. - Decl::Struct(_) | Decl::Constant(_) => continue, + Decl::Constant(_) => continue, } } (self.deps, self.errors) } + + /// Records the module a qualified item path names: everything before the + /// final segment, resolved like a `use`. A call and a struct literal both + /// name an item this way. + fn record_item_path_dependency(&mut self, path: &IdentPath) { + if path.path.len() > 1 && path.path[0].name != "std" { + let module = module_prefix( + self.current_path, + path.path.iter().map(|ident| ident.name.as_str()), + 1, + ); + self.record_dependency(module, path.span); + } + } } impl<'a> AstTransformer for ImportPass<'a> { @@ -597,6 +615,38 @@ impl<'a> AstTransformer for ImportPass<'a> { ) -> ::EnumDecl { } + fn dispatch_struct_decl( + &mut self, + _input: &StructDecl, + _name: &Ident, + _fields: &[StructField], + ) -> ::StructDecl { + } + + fn dispatch_struct_field( + &mut self, + _input: &StructField, + _name: &Ident, + _ty: &TySpec, + ) -> ::StructField { + } + + fn dispatch_struct_lit_expr( + &mut self, + _input: &StructLitExpr, + path: &IdentPath, + _fields: &[StructLitField], + _base: &Option>, + ) -> ::StructLitExpr { + self.record_item_path_dependency(path); + } + + fn dispatch_struct_lit_path( + &mut self, + _input: &IdentPath, + ) -> ::IdentPath { + } + fn dispatch_cell_decl( &mut self, _input: &CellDecl, @@ -719,28 +769,7 @@ impl<'a> AstTransformer for ImportPass<'a> { func: &IdentPath, _args: &crate::ast::Args, ) -> ::CallExpr { - if func.path.len() > 1 && func.path[0].name != "std" { - let path = if func.path[0].name == "lib" { - func.path - .iter() - .skip(1) - .dropping_back(1) - .map(|ident| ident.name.to_string()) - .collect_vec() - } else { - self.current_path - .iter() - .cloned() - .chain( - func.path - .iter() - .dropping_back(1) - .map(|ident| ident.name.to_string()), - ) - .collect_vec() - }; - self.record_dependency(path, func.span); - } + self.record_item_path_dependency(func); } fn dispatch_emit_expr( @@ -1189,6 +1218,9 @@ pub enum Ty { CellFn(Box), Seq(Box), Tuple(Vec), + /// A user-declared `struct`. Nominal: two declarations with identical + /// fields are distinct types. + Struct(Arc), } #[derive(Debug, Clone, Copy)] @@ -1257,6 +1289,7 @@ impl std::fmt::Display for Ty { Ty::Enum(e) => write!(f, "enum {} {{{}}}", e.name, e.variants.iter().format(", ")), Ty::Seq(inner) => write!(f, "[{inner}]"), Ty::Tuple(elements) => write!(f, "({})", elements.iter().format(", ")), + Ty::Struct(s) => write!(f, "struct {}", s.name), } } } @@ -1528,13 +1561,36 @@ pub struct EnumTy { pub(crate) variants: IndexSet, } +/// The type of a `struct` declaration. +#[derive(Debug, Clone, Eq, Serialize, Deserialize)] +pub struct StructTy { + /// The [`VarId`] the struct's name is bound to, which doubles as the type's + /// identity. Struct types are *nominal*, so two same-shaped declarations + /// are different types, and `id` is the only field equality reads. Being + /// the name's own id, it also lets navigation and fingerprinting reach the + /// declaration without an `EnumId`-style side map. + pub(crate) id: VarId, + /// The declared name, module-qualified, for diagnostics. + pub(crate) name: String, + /// The fields and their types, in declaration order. + pub(crate) fields: IndexMap, +} + +impl PartialEq for StructTy { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + impl AstMetadata for VarIdTyMetadata { type Ident = (); type IdentPath = (Option, Ty); /// `None` when the name was rejected before it could be bound. type EnumDecl = Option<(VarId, EnumId)>; - type StructDecl = (); - type StructField = (); + /// `None` when the name was rejected before it could be bound. + type StructDecl = Option; + /// The field's resolved type. + type StructField = Ty; type CellDecl = (PathBuf, VarId); type ConstantDecl = (); type LetBinding = VarId; @@ -1556,6 +1612,7 @@ impl AstMetadata for VarIdTyMetadata { type Typ = (); type CastExpr = Ty; type TupleExpr = Ty; + type StructLitExpr = Ty; } impl<'a> VarIdTyPass<'a> { @@ -1638,9 +1695,11 @@ impl<'a> VarIdTyPass<'a> { fn execute(&mut self) -> AnnotatedAst { let mut decls = Vec::new(); // Enum types must exist before imports and function signatures are - // resolved. Imports are then installed before functions are declared, - // allowing imported enum types in signatures and imported functions in - // any declaration body regardless of source order. + // resolved. Imports are then installed before structs and functions + // are declared, allowing imported enum and struct types in fields and + // signatures, and imported functions in any declaration body, + // regardless of source order. Structs come before functions so that a + // signature may name a struct declared further down. for decl in &self.ast.ast.decls { if let Decl::Enum(e) = decl { self.declare_enum_decl(e); @@ -1651,6 +1710,7 @@ impl<'a> VarIdTyPass<'a> { self.declare_use_decl(u); } } + self.declare_struct_decls(); for decl in &self.ast.ast.decls { if let Decl::Fn(f) = decl { self.declare_fn_decl(f); @@ -1674,9 +1734,12 @@ impl<'a> VarIdTyPass<'a> { Decl::Enum(e) => { decls.push(Decl::Enum(self.transform_enum_decl(e))); } + Decl::Struct(s) => { + decls.push(Decl::Struct(self.transform_struct_decl(s))); + } // `parse_ast` rejects these before this pass. Keep direct // library callers non-panicking if they construct an AST. - Decl::Struct(_) | Decl::Constant(_) => continue, + Decl::Constant(_) => continue, } } @@ -1789,6 +1852,107 @@ impl<'a> VarIdTyPass<'a> { self.alloc(&input.name.name, ty); } + /// Declares every struct in the module, each after the local structs its + /// fields name, so that a field may refer to a struct declared further + /// down the file. + /// + /// A struct whose fields lead back to itself has no finite value, since + /// there is no optional type to end the recursion. The field that closes + /// the cycle is reported and typed `Unknown`, so the declaration still + /// binds and its other uses are checked normally. + fn declare_struct_decls(&mut self) { + let structs: IndexMap<&'a str, &'a StructDecl> = self + .ast + .ast + .decls + .iter() + .filter_map(|decl| match decl { + Decl::Struct(s) => Some((s.name.name.as_str(), s)), + _ => None, + }) + .collect(); + let mut visiting = IndexSet::new(); + let mut declared = IndexSet::new(); + for name in structs.keys().copied().collect::>() { + self.declare_struct_after_deps(name, &structs, &mut visiting, &mut declared); + } + } + + fn declare_struct_after_deps( + &mut self, + name: &'a str, + structs: &IndexMap<&'a str, &'a StructDecl>, + visiting: &mut IndexSet<&'a str>, + declared: &mut IndexSet<&'a str>, + ) { + if declared.contains(name) { + return; + } + let decl = structs[name]; + visiting.insert(name); + let mut recursive = IndexSet::new(); + for (index, field) in decl.fields.iter().enumerate() { + for dep in ty_spec_names(&field.ty) { + // Anything that is not a struct of this module -- a primitive, + // an enum, an import, a typo -- is resolved by `ty_from_spec`. + if !structs.contains_key(dep) { + continue; + } + if visiting.contains(dep) { + recursive.insert(index); + } else { + self.declare_struct_after_deps(dep, structs, visiting, declared); + } + } + } + visiting.swap_remove(name); + self.declare_struct_decl(decl, &recursive); + declared.insert(name); + } + + /// Declares one struct. `recursive` holds the indices of the fields that + /// would make the type contain itself. + fn declare_struct_decl( + &mut self, + input: &'a StructDecl, + recursive: &IndexSet, + ) { + if BUILTINS.contains(&input.name.name.as_str()) { + self.errors.push(StaticError { + span: self.span(input.name.span), + kind: StaticErrorKind::RedeclarationOfBuiltin, + }); + return; + } + let id = self.alloc_id(); + let mut fields = IndexMap::with_capacity(input.fields.len()); + for (index, field) in input.fields.iter().enumerate() { + let ty = if recursive.contains(&index) { + self.errors.push(StaticError { + span: self.span(field.ty.span), + kind: StaticErrorKind::RecursiveStruct { + name: input.name.name.to_string(), + }, + }); + Ty::Unknown + } else { + 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, + }); + } + } + let ty = Ty::Struct(Arc::new(StructTy { + id, + name: self.qualified_name(&input.name.name), + fields, + })); + self.bind(&input.name.name, id, ty); + } + fn ty_from_spec(&mut self, spec: &TySpec) -> Ty { match &spec.kind { TySpecKind::Ident(ident) => Ty::from_name(ident.name.as_str()).unwrap_or_else(|| { @@ -2167,6 +2331,16 @@ impl<'a> VarIdTyPass<'a> { } } +/// The identifiers a type annotation is built from: `[(A, B)]` names `A` and +/// `B`. +fn ty_spec_names(spec: &TySpec) -> Vec<&str> { + match &spec.kind { + TySpecKind::Ident(ident) => vec![ident.name.as_str()], + TySpecKind::Seq(inner) => ty_spec_names(inner), + TySpecKind::Tuple(items) => items.iter().flat_map(ty_spec_names).collect(), + } +} + impl Expr { pub(crate) fn ty(&self) -> Ty { match self { @@ -2191,6 +2365,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(), } } } @@ -2286,6 +2461,170 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { } } + fn transform_struct_decl( + &mut self, + input: &StructDecl, + ) -> StructDecl { + // `declare_struct_decls` already resolved every field type; the + // annotated fields carry those types rather than resolving the specs a + // second time, which would report each error twice. + let struct_ty = match self.lookup(&input.name.name) { + Some((_, Ty::Struct(struct_ty))) => Some(struct_ty), + _ => None, + }; + let name = self.transform_ident(&input.name); + let fields = input + .fields + .iter() + .map(|field| { + let ty = struct_ty + .as_ref() + .and_then(|struct_ty| struct_ty.fields.get(field.name.name.as_str())) + .cloned() + .unwrap_or_default(); + StructField { + name: self.transform_ident(&field.name), + ty: self.transform_ty_spec(&field.ty), + span: field.span, + metadata: ty, + } + }) + .collect_vec(); + let metadata = self.dispatch_struct_decl(input, &name, &fields); + StructDecl { + name, + fields, + span: input.span, + metadata, + } + } + + fn dispatch_struct_decl( + &mut self, + _input: &StructDecl, + name: &Ident, + _fields: &[StructField], + ) -> ::StructDecl { + // Like `dispatch_enum_decl`: a name that collided with a builtin was + // never bound, and there is no id to report. + match self.lookup(&name.name) { + Some((var_id, Ty::Struct(_))) => Some(var_id), + _ => None, + } + } + + fn dispatch_struct_field( + &mut self, + _input: &StructField, + _name: &Ident, + _ty: &TySpec, + ) -> ::StructField { + // `transform_struct_decl` builds the fields itself. + unreachable!() + } + + fn dispatch_struct_lit_path( + &mut self, + _input: &IdentPath, + ) -> ::IdentPath { + // Resolved by `dispatch_struct_lit_expr`, whose metadata carries the + // struct type; like a call's `func` path, this one stays unresolved. + (None, Ty::Unknown) + } + + fn dispatch_struct_lit_expr( + &mut self, + input: &StructLitExpr, + path: &IdentPath, + fields: &[StructLitField], + base: &Option>, + ) -> ::StructLitExpr { + let name = &path.path.last().expect("paths are non-empty").name; + 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, + ); + if &module == self.current_path { + self.lookup(name) + } else { + self.mod_bindings + .get(&module) + .and_then(|frame| frame.var_bindings.get(name.as_str()).cloned()) + } + }; + let Some((_, ty)) = lookup else { + self.errors.push(StaticError { + span: self.span(path.span), + kind: if path.path.len() == 1 { + self.unresolved_local_name_error(name, path.span) + } else { + StaticErrorKind::UndeclaredVar { + name: name.to_string(), + } + }, + }); + return Ty::Unknown; + }; + let Ty::Struct(struct_ty) = ty else { + // An `Unknown` binding was already diagnosed where it was bound. + if !matches!(ty, Ty::Unknown) { + self.errors.push(StaticError { + span: self.span(path.span), + kind: StaticErrorKind::NotAStruct, + }); + } + return Ty::Unknown; + }; + let ty = Ty::Struct(struct_ty.clone()); + + let mut seen = IndexSet::new(); + for field in fields { + let field_name = field.name.name.as_str(); + let Some(expected) = struct_ty.fields.get(field_name) else { + self.no_field_on_ty(&field.name, 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(), + }, + }); + continue; + } + self.assert_eq_ty(field.value.span(), &field.value.ty(), expected); + } + + match base { + // Every field not listed comes from the base, which therefore has + // to be this very struct. + Some(base) => self.assert_eq_ty(base.span(), &base.ty(), &ty), + None => { + let missing = struct_ty + .fields + .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: ty.to_string(), + fields: missing.join(", "), + }, + }); + } + } + } + ty + } + fn dispatch_cell_decl( &mut self, _input: &CellDecl, @@ -2698,6 +3037,10 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { }); Ty::Unknown } + Ty::Struct(ref s) => match s.fields.get(field.name.as_str()) { + Some(ty) => ty.clone(), + None => self.no_field_on_ty(field, base_ty.clone()), + }, // Propagate any and unknown types without throwing an error. Ty::Any => Ty::Any, Ty::Unknown => Ty::Unknown, @@ -3155,6 +3498,12 @@ pub enum CellArg { /// An enum variant, identified by name like [`Value::EnumValue`]. Enum(String), Seq(Vec), + /// A struct value: the qualified name of its type and its fields in + /// declaration order, like [`Value::Struct`]. + Struct { + name: String, + fields: Vec<(String, CellArg)>, + }, } impl CellArg { @@ -3170,6 +3519,14 @@ impl CellArg { values.iter().all(|value| value.matches_ty(inner)) } (Self::Seq(values), Ty::SeqNil) => values.is_empty(), + (Self::Struct { name, fields }, Ty::Struct(ty)) => { + *name == ty.name + && fields.len() == ty.fields.len() + && fields + .iter() + .zip(&ty.fields) + .all(|((name, value), (field, ty))| name == field && value.matches_ty(ty)) + } _ => false, } } @@ -3182,6 +3539,7 @@ impl CellArg { Self::String(_) => "String", Self::Enum(_) => "enum variant", Self::Seq(_) => "sequence", + Self::Struct { .. } => "struct", } } } @@ -3201,6 +3559,7 @@ pub(crate) enum CellArgKey { String(String), Enum(String), Seq(Vec), + Struct(String, Vec<(String, CellArgKey)>), } impl From<&CellArg> for CellArgKey { @@ -3212,6 +3571,13 @@ impl From<&CellArg> for CellArgKey { CellArg::String(s) => Self::String(s.clone()), CellArg::Enum(v) => Self::Enum(v.clone()), CellArg::Seq(v) => Self::Seq(v.iter().map(Self::from).collect()), + CellArg::Struct { name, fields } => Self::Struct( + name.clone(), + fields + .iter() + .map(|(field, value)| (field.clone(), Self::from(value))) + .collect(), + ), } } } @@ -5741,6 +6107,28 @@ impl<'a> ExecPass<'a> { .collect(), }) }), + 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") + }; + let ty = ty.clone(); + self.new_deferred_value(loc, |this| { + let fields = lit + .fields + .iter() + .map(|field| this.visit_expr(loc, &field.value)) + .collect(); + let base = lit.base.as_ref().map(|base| this.visit_expr(loc, base)); + PartialEvalState::StructLit(Box::new(PartialStructLit { + expr: (**lit).clone(), + ty, + fields, + base, + })) + }) + } } } @@ -5795,6 +6183,19 @@ impl<'a> ExecPass<'a> { } Some(CellArg::Seq(args)) } + Value::Struct(value) => { + let mut fields = Vec::with_capacity(value.fields.len()); + for (name, v) in value.fields.iter() { + match self.cell_arg_from_value(cell_id, dependent_vid, v)? { + Some(arg) => fields.push((name.clone(), arg)), + None => return Ok(None), + } + } + Some(CellArg::Struct { + name: value.name.clone(), + fields, + }) + } // Already reported when it was poisoned. The caller turns this // `Err` into poison of its own rather than a second diagnostic // naming a type the value never had. @@ -7580,6 +7981,18 @@ impl<'a> ExecPass<'a> { self.values.insert(vid, DeferValue::Ready(val)); true } + ValueRef::Struct(value) => { + let field = field_access_expr.expr.field.name.as_str(); + // The base may have arrived as `Any`, so the field + // was never checked against the struct. + let Some(val) = value.fields.get(field).cloned() else { + let span = self.span(&vref.loc, field_access_expr.expr.span); + self.invalid_type(cell_id, &span); + return self.poison(cell_id, vid); + }; + self.values.insert(vid, DeferValue::Ready(val)); + true + } ValueRef::Inst(inst) => { let val = match field_access_expr.expr.field.name.as_str() { "x" => Some(Value::Linear(inst.x.clone())), @@ -7982,6 +8395,71 @@ impl<'a> ExecPass<'a> { false } } + PartialEvalState::StructLit(lit) => { + let pending = lit + .fields + .iter() + .copied() + .chain(lit.base) + .find(|input| !self.values[input].is_ready()); + if let Some(pending) = pending { + self.add_value_dependent(pending, vid); + false + } else { + // The fields not listed come from the base, which the + // static check proved to be this struct unless it was + // typed `Any`. + 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.fields.clone()) + } + _ => { + let base = lit.expr.base.as_ref().expect("base was evaluated"); + let span = self.span(&vref.loc, base.span()); + self.invalid_type(cell_id, &span); + return self.poison(cell_id, vid); + } + }, + }; + // Declaration order, whatever order the literal used: + // `CellArg::Struct` fields are matched pairwise against + // the type's. + let fields = lit + .ty + .fields + .keys() + .map(|name| { + let explicit = lit + .expr + .fields + .iter() + .zip(&lit.fields) + .find(|(field, _)| field.name.name == *name) + .map(|(_, value)| self.values[value].get_ready().cloned()); + let value = match explicit { + Some(value) => value, + None => base.as_ref().and_then(|base| base.get(name).cloned()), + }; + value.map(|value| (name.clone(), value)) + }) + .collect::>>(); + let Some(fields) = fields else { + let span = self.span(&vref.loc, lit.expr.span); + 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(), + fields, + }))), + ); + true + } + } PartialEvalState::ForLoop(f) => { if let Defer::Ready(val) = &self.values[&f.seq] { let seq = match val.as_ref() { @@ -8141,6 +8619,10 @@ pub enum Value { Inst(Instance), Seq(Seq), Tuple(Vec), + /// A struct value. Boxed like [`Value::Fn`]: a struct is rarely stored + /// per sequence element, and its field map is large relative to the + /// scalar variants. + Struct(Box), SeqNil, Nil, /// A value whose diagnostic has already been reported. @@ -8172,6 +8654,13 @@ impl Value { CellArg::String(s) => Value::String(s.clone()), CellArg::Enum(v) => Value::EnumValue(v.clone()), CellArg::Seq(v) => Value::Seq(v.iter().map(Self::from_arg).collect()), + CellArg::Struct { name, fields } => Value::Struct(Box::new(StructValue { + name: name.clone(), + fields: fields + .iter() + .map(|(field, value)| (field.clone(), Self::from_arg(value))) + .collect(), + })), } } @@ -8193,6 +8682,7 @@ impl Value { Self::Inst(_) => "instance", Self::Seq(_) | Self::SeqNil => "sequence", Self::Tuple(_) => "tuple", + Self::Struct(_) => "struct", Self::Nil => "nil", // Matches `Ty::Unknown`'s rendering. Reaching a diagnostic that // names a poisoned value means one was not suppressed upstream; @@ -8222,6 +8712,17 @@ impl Value { } } +/// A struct value. See [`Value::Struct`]. +#[derive(Debug, Clone)] +pub struct StructValue { + /// The module-qualified name of the declaring struct, matching + /// `StructTy::name`, so that `..base` and cell arguments can check that a + /// value which arrived as `Any` is the struct they expect. + pub name: String, + /// The fields in declaration order. + pub fields: IndexMap, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Instance { pub id: ObjectId, @@ -8729,6 +9230,7 @@ enum PartialEvalState { Constraint(PartialConstraint), Cast(Box>), Tuple(PartialTupleExpr), + StructLit(Box>), ForLoop(Box>), } @@ -8766,6 +9268,7 @@ impl PartialEvalState { Self::Constraint(c) => vec![c.lhs, c.rhs], Self::Cast(e) => vec![e.state.value], Self::Tuple(e) => e.items.clone(), + Self::StructLit(e) => e.fields.iter().copied().chain(e.base).collect(), Self::ForLoop(f) => vec![f.seq], } } @@ -8894,6 +9397,17 @@ struct PartialTupleExpr { items: Vec, } +#[derive(Debug, Clone)] +struct PartialStructLit { + expr: StructLitExpr, + /// The struct being built, from the literal's checked type; its field + /// order is the order the value's fields take. + ty: Arc, + /// One value per entry of `expr.fields`. + fields: Vec, + base: Option, +} + #[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 882014f..7fca422 100644 --- a/crates/compiler/src/compile/result.rs +++ b/crates/compiler/src/compile/result.rs @@ -44,6 +44,18 @@ 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. + #[error("expected a struct type")] + NotAStruct, + /// A struct literal gives the same field twice. + #[error("field `{field}` is specified more than once")] + DuplicateStructField { field: String }, + /// A struct literal without `..base` omits declared fields. + #[error("missing fields {fields} in initializer of {ty}")] + MissingStructFields { ty: String, fields: String }, + /// A struct contains itself, directly or through its fields' types. + #[error("struct `{name}` contains itself; recursive structs are not supported")] + RecursiveStruct { name: String }, /// A cell had an expression in tail position, which is not permitted. #[error("cells may not have an expression in tail position")] CellWithTailExpr, diff --git a/crates/compiler/src/fingerprint.rs b/crates/compiler/src/fingerprint.rs index f5062af..bf78f5c 100644 --- a/crates/compiler/src/fingerprint.rs +++ b/crates/compiler/src/fingerprint.rs @@ -45,14 +45,15 @@ pub type Fingerprint = u64; /// /// `mod` and `use` are absent because neither can be called or named as a type, /// and a reference through an alias resolves to the original declaration's own -/// `VarId`. `struct` and `const` are absent because `parser::check_unsupported` -/// rejects both; the matches over [`Decl`] below are exhaustive so that -/// supporting either becomes a compile error here. +/// `VarId`. `const` is absent because `parse_ast` rejects it; the matches over +/// [`Decl`] below are exhaustive so that supporting it becomes a compile error +/// here. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ItemKind { Cell, Fn, Enum, + Struct, } impl ItemKind { @@ -61,6 +62,7 @@ impl ItemKind { Self::Cell => 0, Self::Fn => 1, Self::Enum => 2, + Self::Struct => 3, } } } @@ -198,7 +200,18 @@ impl Builder { span_range(decl.name.span), ) } - Decl::Struct(_) | Decl::Constant(_) | Decl::Mod(_) | Decl::Use(_) => { + Decl::Struct(decl) => { + let Some(var) = decl.metadata else { + continue; + }; + ( + var, + ItemKind::Struct, + decl.name.name.as_str(), + span_range(decl.span), + ) + } + Decl::Constant(_) | Decl::Mod(_) | Decl::Use(_) => { continue; } }; @@ -215,10 +228,10 @@ impl Builder { write_module(&mut hasher, module); write_str(&mut hasher, name); match decl { - // A cell's or function's declaration span runs from its - // keyword to its closing brace with no surrounding trivia, - // so this is exactly the declaration's text. - Decl::Cell(_) | Decl::Fn(_) => { + // A cell's, function's, or struct's declaration span runs + // from its keyword to its closing brace with no surrounding + // trivia, so this is exactly the declaration's text. + Decl::Cell(_) | Decl::Fn(_) | Decl::Struct(_) => { write_str(&mut hasher, &annotated.text[span.clone()]); } // An `enum` has no span to slice, and its structure is its @@ -229,7 +242,7 @@ impl Builder { write_str(&mut hasher, &variant.name); } } - Decl::Struct(_) | Decl::Constant(_) | Decl::Mod(_) | Decl::Use(_) => { + Decl::Constant(_) | Decl::Mod(_) | Decl::Use(_) => { unreachable!("filtered above") } } @@ -275,12 +288,19 @@ impl Builder { self.scope(&decl.scope, &mut deps); (decl.metadata.1, deps) } + // A struct means what its fields' types mean. + Decl::Struct(decl) => { + let Some(var) = decl.metadata else { + continue; + }; + let mut deps = IndexSet::new(); + for field in &decl.fields { + self.ty(&field.metadata, &mut deps); + } + (var, deps) + } // An enum's meaning is entirely its own variant list. - Decl::Enum(_) - | Decl::Struct(_) - | Decl::Constant(_) - | Decl::Mod(_) - | Decl::Use(_) => continue, + Decl::Enum(_) | Decl::Constant(_) | Decl::Mod(_) | Decl::Use(_) => continue, }; deps.shift_remove(&var); deps.retain(|dep| self.items.contains_key(dep)); @@ -324,6 +344,11 @@ impl Builder { out.insert(def); } } + // Stop at the declaring struct for the same reason as a cell: the + // struct is itself an item whose own fingerprint covers its fields. + Ty::Struct(struct_ty) => { + out.insert(struct_ty.id); + } Ty::Seq(inner) => self.ty(inner, out), Ty::Tuple(items) => { for item in items { @@ -449,6 +474,17 @@ impl Builder { self.expr(item, out); } } + // The struct being built reaches us through the literal's checked + // type; its path carries no `VarId`. + Expr::StructLit(e) => { + self.ty(&e.metadata, out); + for field in &e.fields { + self.expr(&field.value, out); + } + if let Some(base) = &e.base { + self.expr(base, out); + } + } Expr::Nil(_) | Expr::SeqNil(_) | Expr::FloatLiteral(_) @@ -700,6 +736,10 @@ mod tests { Some((var, _)) => (var, decl.name.name.to_string()), None => continue, }, + Decl::Struct(decl) => match decl.metadata { + Some(var) => (var, decl.name.name.to_string()), + None => continue, + }, _ => continue, }; named.insert(name, index.fingerprint(var).expect("every item is indexed")); @@ -848,6 +888,29 @@ fn untouched() -> Float { 5. } 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. + #[test] + fn a_struct_change_reaches_everything_that_names_it() { + let base = "\ +struct Size { w: Float, h: Float, } +struct Outer { size: Size, } +fn area(s: Size) -> Float { s.w * s.h } +fn unit() -> Size { Size { w: 1., h: 1. } } +fn width() -> Float { Size { w: 1., h: 2. }.w } +fn untouched() -> Float { 5. } +"; + let after = base.replace( + "struct Size { w: Float, h: Float, }", + "struct Size { h: Float, w: Float, }", + ); + assert_eq!( + changed(base, &after), + ["Outer", "Size", "area", "unit", "width"] + ); + } + /// An enum used only as a parameter type, never matched on. The reference /// carries no `VarId`, so this only works through the `EnumId` map. #[test] diff --git a/crates/compiler/src/gdscache.rs b/crates/compiler/src/gdscache.rs index 52b3dfb..996a828 100644 --- a/crates/compiler/src/gdscache.rs +++ b/crates/compiler/src/gdscache.rs @@ -184,6 +184,17 @@ fn hash_cell_arg_key(hasher: &mut fnv::FnvHasher, arg: &CellArgKey) { hash_cell_arg_key(hasher, value); } } + CellArgKey::Struct(name, fields) => { + hasher.write_u8(6); + hasher.write_usize(name.len()); + hasher.write(name.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); + } + } } } diff --git a/crates/compiler/src/incremental.rs b/crates/compiler/src/incremental.rs index ac4715c..5ce46da 100644 --- a/crates/compiler/src/incremental.rs +++ b/crates/compiler/src/incremental.rs @@ -761,6 +761,15 @@ fn hash_cell_args(args: &[CellArg], hasher: &mut impl Hasher) { 5_u8.hash(hasher); hash_cell_args(values, hasher); } + CellArg::Struct { name, fields } => { + 6_u8.hash(hasher); + name.hash(hasher); + fields.len().hash(hasher); + for (field, value) in fields { + field.hash(hasher); + hash_cell_args(std::slice::from_ref(value), hasher); + } + } } } } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index f364d88..f5b7ea6 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -214,6 +214,7 @@ mod tests { const ARGON_PATH: &str = concatcp!(EXAMPLES_DIR, "/path/lib.ar"); const ARGON_KWARGS_FN: &str = concatcp!(EXAMPLES_DIR, "/kwargs_fn/lib.ar"); const ARGON_KWARGS_CELL: &str = concatcp!(EXAMPLES_DIR, "/kwargs_cell/lib.ar"); + const ARGON_STRUCTS: &str = concatcp!(EXAMPLES_DIR, "/structs/lib.ar"); // --------------------------------------------------------------------- // Scaling / stress benchmarks. @@ -1705,6 +1706,261 @@ mod tests { } } + #[test] + fn argon_structs() { + let o = parse_workspace_with_std(ARGON_STRUCTS); + assert!(o.static_errors().is_empty(), "{:?}", o.static_errors()); + let cells = compile( + &o.ast(), + CompileInput { + cell: &["top"], + args: Vec::new(), + }, + ) + .unwrap_valid(); + let top = &cells.cells[&cells.top]; + // `via(params)` and `via(wide)` are two parameterizations of `via`. + assert_eq!(cells.cells.len(), 3); + let mut insts: Vec<_> = top + .objects + .values() + .filter_map(|object| object.get_instance()) + .collect(); + insts.sort_by(|a, b| a.x.total_cmp(&b.x)); + let [narrow, wide] = insts.as_slice() else { + panic!("expected two instances, got {}", insts.len()); + }; + let child_rect = |cell| { + cells.cells[&cell] + .objects + .values() + .find_map(|object| object.get_rect()) + .cloned() + .expect("via emits a rect") + }; + // `grow(square(100.), 10.)`: `..s` keeps `h` at 100 while `w` grows. + let rect = child_rect(narrow.cell); + assert_eq!(rect.layer.as_deref(), Some("met1")); + assert_relative_eq!(rect.x1.0, 110., epsilon = EPSILON); + assert_relative_eq!(rect.y1.0, 100., epsilon = EPSILON); + // `Size { w: 300., ..size }` inside `ViaParams { .., ..params }`. + let rect = child_rect(wide.cell); + assert_eq!(rect.layer.as_deref(), Some("met1")); + assert_relative_eq!(rect.x1.0, 300., epsilon = EPSILON); + assert_relative_eq!(rect.y1.0, 100., epsilon = EPSILON); + assert_relative_eq!(wide.x, 160., epsilon = EPSILON); + // The outline is sized from a sequence of structs. + let outline = top + .objects + .values() + .find_map(|object| object.get_rect()) + .expect("top emits the outline"); + assert_eq!(outline.layer.as_deref(), Some("met2")); + assert_relative_eq!(outline.x1.0, 460., epsilon = EPSILON); + assert_relative_eq!(outline.y1.0, 100., epsilon = EPSILON); + } + + /// A struct literal in a cell invocation, as `arc run --cell` and the GUI + /// open-cell command supply it. + #[test] + fn argon_structs_cell_invocation() { + let mut ast = parse_workspace_with_std(ARGON_STRUCTS).ast(); + let invocation = crate::parse::splice_cell_invocation( + &mut ast, + "via(ViaParams { layer: \"met2\", size: geom::Size { w: 40., h: 20. }, n: 1 })", + ) + .expect("invocation should splice"); + let (typed, errors) = static_compile(&ast).unwrap(); + assert!(errors.errors.is_empty(), "{:?}", errors.errors); + let config = WorkspaceConfig::default().with_tech(Some(PathBuf::from(BASIC_TECH))); + let cells = + crate::compile::execute_cell_invocation(&typed, &invocation, &config).unwrap_valid(); + let rect = cells.cells[&cells.top] + .objects + .values() + .find_map(|object| object.get_rect()) + .expect("via emits a rect"); + assert_eq!(rect.layer.as_deref(), Some("met2")); + assert_relative_eq!(rect.x1.0, 40., epsilon = EPSILON); + assert_relative_eq!(rect.y1.0, 20., epsilon = EPSILON); + } + + /// Struct values passed through the positional cell API are checked + /// against the declared parameter type, like every other argument. + #[test] + fn struct_cell_arguments_are_checked_at_runtime() { + let ast = parse_workspace_with_std(ARGON_STRUCTS).ast(); + let size = |w: f64, h: f64| CellArg::Struct { + name: "geom::Size".to_owned(), + fields: vec![ + ("w".to_owned(), CellArg::Float(w)), + ("h".to_owned(), CellArg::Float(h)), + ], + }; + let params = CellArg::Struct { + name: "ViaParams".to_owned(), + fields: vec![ + ("layer".to_owned(), CellArg::String("met1".to_owned())), + ("size".to_owned(), size(30., 20.)), + ("n".to_owned(), CellArg::Int(1)), + ], + }; + let cells = compile( + &ast, + CompileInput { + cell: &["via"], + args: vec![params], + }, + ) + .unwrap_valid(); + let rect = cells.cells[&cells.top] + .objects + .values() + .find_map(|object| object.get_rect()) + .expect("via emits a rect"); + assert_relative_eq!(rect.x1.0, 30., epsilon = EPSILON); + assert_relative_eq!(rect.y1.0, 20., epsilon = EPSILON); + + let wrong = compile( + &ast, + CompileInput { + cell: &["via"], + args: vec![size(30., 20.)], + }, + ); + let errors = wrong.unwrap_exec_errors(); + assert!( + errors + .errors + .iter() + .any(|e| matches!(e.kind, ExecErrorKind::InvalidCellArgumentType { .. })), + "{:?}", + errors.errors + ); + } + + /// Type-checks a one-file workspace and returns the kinds of its static + /// errors. + fn static_errors_of(source: &str) -> Vec { + let root = parse_source_text(source, PathBuf::from("/virtual/lib.ar")).unwrap(); + let ast = IndexMap::from([(Vec::new(), root)]); + let (_, output) = static_compile(&ast).unwrap(); + output.errors.into_iter().map(|error| error.kind).collect() + } + + /// The static errors of `body` as the body of a function that sees a + /// `Size`, an identically shaped `Other`, and an `Int`. + fn struct_errors(body: &str) -> Vec { + static_errors_of(&format!( + "struct Size {{ w: Float, h: Float, }}\n\ + struct Other {{ w: Float, h: Float, }}\n\ + fn f(s: Size, o: Other, n: Int) -> Float {{ {body} }}\n" + )) + } + + #[test] + fn struct_literals_are_checked_against_the_declaration() { + assert!(struct_errors("Size { w: 1., h: 2. }.w").is_empty()); + // Fields may come in any order; `..base` supplies the ones not listed. + assert!(struct_errors("Size { h: 2., w: 1. }.w").is_empty()); + assert!(struct_errors("Size { w: 1., ..s }.h").is_empty()); + assert!(struct_errors("Size { ..s }.h").is_empty()); + assert!(struct_errors("Size { w: s.w, ..s }.h").is_empty()); + + assert!(matches!( + struct_errors("Size { w: 1. }.w").as_slice(), + [StaticErrorKind::MissingStructFields { ty, fields }] + if ty == "struct Size" && fields == "`h`" + )); + assert!(matches!( + struct_errors("Size { w: 1., h: 2., d: 3. }.w").as_slice(), + [StaticErrorKind::NoFieldOnTy { field, ty }] if field == "d" && ty == "struct Size" + )); + assert!(matches!( + struct_errors("Size { w: 1., w: 2., h: 3. }.w").as_slice(), + [StaticErrorKind::DuplicateStructField { field }] if field == "w" + )); + assert!(matches!( + struct_errors("Size { w: 1, h: 2. }.w").as_slice(), + [StaticErrorKind::IncorrectTy { expected, found }] + if expected == "Float" && found == "Int" + )); + // Structs are nominal: a same-shaped struct is not a valid base. + assert!(matches!( + struct_errors("Size { w: 1., ..o }.w").as_slice(), + [StaticErrorKind::IncorrectTy { expected, found }] + if expected == "struct Size" && found == "struct Other" + )); + assert!(matches!( + struct_errors("Size { w: 1., ..n }.w").as_slice(), + [StaticErrorKind::IncorrectTy { found, .. }] if found == "Int" + )); + assert!(matches!( + struct_errors("n { w: 1., h: 2. }.w").as_slice(), + [StaticErrorKind::NotAStruct] + )); + assert!(matches!( + struct_errors("Nope { w: 1., h: 2. }.w").as_slice(), + [StaticErrorKind::UndeclaredVar { name }] if name == "Nope" + )); + assert!(matches!( + struct_errors("s.d").as_slice(), + [StaticErrorKind::NoFieldOnTy { field, ty }] if field == "d" && ty == "struct Size" + )); + // A shorthand field reads a local of the same name. + assert!(matches!( + struct_errors("Size { w, h: 1. }.w").as_slice(), + [StaticErrorKind::UndeclaredVar { name }] if name == "w" + )); + assert!(matches!( + struct_errors("Size { w: s.w, ..s }.w + Size { ..o }.w").as_slice(), + [StaticErrorKind::IncorrectTy { .. }] + )); + // Structs cannot be compared. + let errors = struct_errors("if (s == Size { w: 1., h: 1. }) { 1. } else { 2. }"); + assert!(!errors.is_empty()); + assert!( + errors + .iter() + .all(|e| matches!(e, StaticErrorKind::ComparisonInvalidType)), + "{errors:?}" + ); + } + + #[test] + fn struct_declarations_are_checked() { + assert!(matches!( + static_errors_of("struct Node { next: Node, }\n").as_slice(), + [StaticErrorKind::RecursiveStruct { name }] if name == "Node" + )); + let errors = static_errors_of("struct A { b: [B], }\nstruct B { a: (Int, A), }\n"); + assert!( + matches!(errors.as_slice(), [StaticErrorKind::RecursiveStruct { .. }]), + "{errors:?}" + ); + assert!(matches!( + static_errors_of("struct S { a: Nope, }\n").as_slice(), + [StaticErrorKind::UnknownType] + )); + assert!(matches!( + static_errors_of("struct S { a: Int, a: Float, }\n").as_slice(), + [StaticErrorKind::DuplicateNameDeclaration] + )); + assert!(matches!( + static_errors_of("struct rect { a: Int, }\n").as_slice(), + [StaticErrorKind::RedeclarationOfBuiltin] + )); + // Forward references that do not close a cycle are fine, in any order. + assert!( + static_errors_of( + "struct Outer { inner: Inner, list: [Inner], pair: (Inner, Int), }\n\ + struct Inner { x: Float, }\n\ + fn f(o: Outer) -> Float { o.inner.x + o.list[0].x + o.pair.0.x }\n" + ) + .is_empty() + ); + } + #[test] fn argon_library() { let o = parse_workspace_with_std(ARGON_LIBRARY); diff --git a/crates/compiler/src/nav.rs b/crates/compiler/src/nav.rs index 14f63e3..f9ec65b 100644 --- a/crates/compiler/src/nav.rs +++ b/crates/compiler/src/nav.rs @@ -23,7 +23,7 @@ use arcstr::ArcStr; use crate::{ ast::{ ArgDecl, CellDecl, Decl, EnumDecl, Expr, FnDecl, Ident, IdentPath, ModPath, Scope, - Statement, TySpec, TySpecKind, UseDecl, WorkspaceAst, + Statement, StructDecl, TySpec, TySpecKind, UseDecl, WorkspaceAst, }, compile::{BUILTINS, EnumId, Ty, VarId, VarIdTyMetadata, module_prefix}, }; @@ -31,13 +31,15 @@ use crate::{ /// Identity of something that can be navigated to. /// /// A [`VarId`] already distinguishes every `fn`, `cell`, `let`, parameter, -/// loop variable, and enum *name*, so it does most of the work. Enum variants -/// and modules are the two things it does not cover. +/// loop variable, enum *name*, and struct *name*, so it does most of the work. +/// Enum variants, struct fields, and modules are the things it does not cover. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum DefKey { Var(VarId), /// A variant, keyed by the `VarId` of its enum's name and its own name. Variant(VarId, String), + /// A field, keyed by the `VarId` of its struct's name and its own name. + Field(VarId, String), Module(ModPath), } @@ -50,6 +52,8 @@ pub enum SymbolKind { LoopVar, Enum, Variant, + Struct, + Field, Module, } @@ -457,8 +461,25 @@ impl<'a> Builder<'a> { self.record(decl.ident.span, target); } Decl::Use(decl) => self.use_decl(decl), + Decl::Struct(decl) => self.struct_decl(decl), // Rejected by `parse_ast` before this pass ever runs. - Decl::Struct(_) | Decl::Constant(_) => {} + Decl::Constant(_) => {} + } + } + + fn struct_decl(&mut self, decl: &'a StructDecl) { + // As for an enum, a name the type pass rejected has no id. + let Some(name_id) = decl.metadata else { + return; + }; + self.define(DefKey::Var(name_id), SymbolKind::Struct, &decl.name); + for field in &decl.fields { + self.define( + DefKey::Field(name_id, field.name.name.to_string()), + SymbolKind::Field, + &field.name, + ); + self.ty_spec(&field.ty, &field.metadata); } } @@ -550,6 +571,7 @@ impl<'a> Builder<'a> { Decl::Fn(decl) if decl.name.name == name => Some(decl.metadata.1), Decl::Cell(decl) if decl.name.name == name => Some(decl.metadata.1), Decl::Enum(decl) if decl.name.name == name => decl.metadata.map(|(id, _)| id), + Decl::Struct(decl) if decl.name.name == name => decl.metadata, _ => None, }); if declared.is_some() || depth == MAX_REEXPORT_DEPTH { @@ -598,6 +620,9 @@ impl<'a> Builder<'a> { .map_or(Target::Unresolved, |id| Target::Def(DefKey::Var(*id))); self.record(name.span, target); } + (TySpecKind::Ident(name), Ty::Struct(struct_ty)) => { + self.record(name.span, Target::Def(DefKey::Var(struct_ty.id))); + } (TySpecKind::Ident(name), ty) => { // `ty_from_spec` resolves a name that is not a primitive by // looking it up, so an annotation can also name a declaration. @@ -724,6 +749,37 @@ impl<'a> Builder<'a> { self.expr(item); } } + Expr::StructLit(lit) => { + let Some((name, prefix)) = lit.path.path.split_last() else { + 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.id), + _ => None, + }; + let target = + struct_id.map_or(Target::Unresolved, |id| Target::Def(DefKey::Var(id))); + self.record(name.span, target); + for field in &lit.fields { + // A shorthand field is one token naming both the field and + // a local. `target_at` keeps one target per span, and the + // 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| { + Target::Def(DefKey::Field(id, field.name.name.to_string())) + }); + self.record(field.name.span, target); + } + self.expr(&field.value); + } + if let Some(base) = &lit.base { + self.expr(base); + } + } Expr::Nil(_) | Expr::SeqNil(_) | Expr::FloatLiteral(_) @@ -811,6 +867,7 @@ impl<'a> Builder<'a> { Ty::Inst(_) | Ty::Rect | Ty::Polygon | Ty::Path | Ty::Point => { Target::Builtin(Builtin::Field(name.to_string())) } + Ty::Struct(struct_ty) => Target::Def(DefKey::Field(struct_ty.id, name.to_string())), _ => Target::Unresolved, } } @@ -924,6 +981,45 @@ cell top(wid$0th: Float) { ); } + #[test] + fn struct_names_fields_and_literals() { + check( + r#" +struct Size { + wd: Float, + ht: Float, +} + +fn expand(sz: Si$0ze, by: Float) -> Size { + Si$0ze { wd$0: sz.wd$0 + by, ..sz$0 } +} + +fn same(wd: Float) -> Size { + let ht = wd; + Size { wd$0, ht$0: ht } +} +"#, + // A shorthand field navigates to the local it reads, not the field. + &["Size#0", "Size#0", "wd#0", "wd#0", "sz#0", "wd#3", "ht#0"], + ); + } + + #[test] + fn struct_typed_cell_parameters() { + check( + r#" +struct Params { + layer: String, +} + +cell via(p: Par$0ams) { + let r = rect(p.lay$0er, x0=0., y0=0., x1=1., y1=1.); +} +"#, + &["Params#0", "layer#0"], + ); + } + #[test] fn an_inner_binding_shadows_an_outer_one() { check( diff --git a/crates/compiler/src/parse.rs b/crates/compiler/src/parse.rs index 2387c3a..fcdbf95 100644 --- a/crates/compiler/src/parse.rs +++ b/crates/compiler/src/parse.rs @@ -59,6 +59,7 @@ impl AstMetadata for ParseMetadata { type FnDecl = (); type CastExpr = (); type TupleExpr = (); + type StructLitExpr = (); } /// The two files a `mod ;` declaration can name. diff --git a/crates/compiler/src/parser/grammar.rs b/crates/compiler/src/parser/grammar.rs index 04b15e9..53bc3f8 100644 --- a/crates/compiler/src/parser/grammar.rs +++ b/crates/compiler/src/parser/grammar.rs @@ -23,8 +23,8 @@ use crate::ast::{ CellDecl, ComparisonOp, ConstantDecl, Decl, EmitExpr, EnumDecl, Expr, FieldAccessExpr, FloatLiteral, FnDecl, ForLoop, Ident, IdentPath, IfExpr, IndexExpr, IndexFieldAccessExpr, IntLiteral, KwArgValue, LetBinding, MatchArm, MatchExpr, ModDecl, NilLiteral, Scope, - SeqNilLiteral, Statement, StringLiteral, StructDecl, StructField, TupleExpr, TySpec, - TySpecKind, UnaryOp, UnaryOpExpr, UseDecl, + SeqNilLiteral, Statement, StringLiteral, StructDecl, StructField, StructLitExpr, + StructLitField, TupleExpr, TySpec, TySpecKind, UnaryOp, UnaryOpExpr, UseDecl, }; use crate::compile::BUILTINS; use crate::parse::ParseMetadata; @@ -111,6 +111,9 @@ pub struct Parser<'a> { /// Next semantic scope ordinal in each enclosing lexical scope. scope_orders: Vec, depth: u32, + /// Whether `name {` must be read as an identifier followed by a scope + /// rather than as a struct literal. See [`Parser::with_struct_literals`]. + no_struct_literal: bool, pub errors: Vec, } @@ -129,10 +132,27 @@ impl<'a> Parser<'a> { ntok: 0, scope_orders: vec![0], depth: 0, + no_struct_literal: false, errors: Vec::new(), } } + /// Runs `f` with struct literals allowed or forbidden, restoring the + /// previous setting afterwards. + /// + /// A struct literal is forbidden at the top level of an `if` condition, a + /// `match` scrutinee, and a `for` sequence, where `name {` already opens + /// the construct's own scope; Rust has the same rule, and the same escape + /// hatch of wrapping the literal in parentheses. Parentheses, brackets, + /// call arguments, struct literal bodies, match arm bodies, and brace + /// scopes lift the restriction again. + fn with_struct_literals(&mut self, allowed: bool, f: impl FnOnce(&mut Self) -> T) -> T { + let saved = std::mem::replace(&mut self.no_struct_literal, !allowed); + let result = f(self); + self.no_struct_literal = saved; + result + } + // ------------------------------------------------------------------ // Token plumbing // ------------------------------------------------------------------ @@ -414,12 +434,12 @@ impl<'a> Parser<'a> { } } - /// `structField : ident COLON ident` + /// `structField : ident COLON tySpec` fn parse_struct_field(&mut self) -> StructField<&'a str, Md> { let lo = self.cur.start; let name = self.ident(); self.expect(TokenKind::Colon); - let ty = self.ident(); + let ty = self.parse_ty_spec(); StructField { name, ty, @@ -608,6 +628,10 @@ impl<'a> Parser<'a> { } fn parse_unannotated_scope(&mut self, scope_order: u64) -> Scope<&'a str, Md> { + self.with_struct_literals(true, |p| p.parse_unannotated_scope_inner(scope_order)) + } + + fn parse_unannotated_scope_inner(&mut self, scope_order: u64) -> Scope<&'a str, Md> { if !self.enter_depth() { self.error_at(self.span(self.cur), "nesting too deep".to_string()); let lo = self.cur.start; @@ -718,7 +742,7 @@ impl<'a> Parser<'a> { self.expect(TokenKind::KwFor); let var = self.ident(); self.expect(TokenKind::KwIn); - let seq = self.parse_expr(0); + let seq = self.with_struct_literals(false, |p| p.parse_expr(0)); let body = self.parse_scope(); ForLoop { var, @@ -732,7 +756,7 @@ impl<'a> Parser<'a> { fn parse_if(&mut self, scope_order: u64, lo: u32) -> IfExpr<&'a str, Md> { self.expect(TokenKind::KwIf); - let cond = self.parse_expr(0); + let cond = self.with_struct_literals(false, |p| p.parse_expr(0)); let then = self.parse_scope(); self.expect(TokenKind::KwElse); let else_ = self.parse_scope(); @@ -750,7 +774,7 @@ impl<'a> Parser<'a> { fn parse_match(&mut self) -> MatchExpr<&'a str, Md> { let lo = self.cur.start; self.expect(TokenKind::KwMatch); - let scrutinee = self.parse_expr(0); + let scrutinee = self.with_struct_literals(false, |p| p.parse_expr(0)); let lbrace = self.expect(TokenKind::LBrace); let mut arms = Vec::new(); while !self.at(TokenKind::RBrace) && !self.at(TokenKind::Eof) { @@ -783,7 +807,10 @@ impl<'a> Parser<'a> { let lo = self.cur.start; let pattern = self.parse_ident_path(); self.expect(TokenKind::FatArrow); - let expr = self.parse_expr(0); + // An arm body is bounded by its comma, not by `{`, so a struct + // literal is unambiguous here even when the whole `match` sits in + // an `if`/`match`/`for` head. + let expr = self.with_struct_literals(true, |p| p.parse_expr(0)); self.expect(TokenKind::Comma); MatchArm { pattern, @@ -949,7 +976,7 @@ impl<'a> Parser<'a> { } TokenKind::LBrack => { self.bump(); - let index = self.parse_expr(0); + let index = self.with_struct_literals(true, |p| p.parse_expr(0)); self.expect(TokenKind::RBrack); Expr::Index(Box::new(IndexExpr { base: lhs, @@ -1011,6 +1038,8 @@ impl<'a> Parser<'a> { self.next_scope_order() }; Expr::Call(self.finish_call(scope_order, lo, path)) + } else if self.at(TokenKind::LBrace) && !self.no_struct_literal { + Expr::StructLit(Box::new(self.parse_struct_lit(path))) } else { Expr::IdentPath(path) } @@ -1037,9 +1066,74 @@ impl<'a> Parser<'a> { } } + /// `structLit : identPath LBRACE structLitBody RBRACE`, where + /// `structLitBody : (structLitField (COMMA structLitField)* (COMMA structBase | COMMA)?)? | structBase` + /// and `structBase : DOTDOT expr`. + /// + /// The `..base` comes last, after a comma, and may not be followed by one, + /// which is the shape Rust accepts. Because the body has two terminators + /// (`}` and `..`) it does not go through `separated_list`; termination + /// holds for the same reason, since every iteration that does not `break` + /// consumes the separator. + fn parse_struct_lit(&mut self, path: IdentPath<&'a str, Md>) -> StructLitExpr<&'a str, Md> { + let lo = path.span.start() as u32; + self.expect(TokenKind::LBrace); + let mut fields = Vec::new(); + let mut base = None; + self.with_struct_literals(true, |p| { + while !p.at(TokenKind::RBrace) && !p.at(TokenKind::Eof) { + if p.eat(TokenKind::DotDot) { + base = Some(p.parse_expr(0)); + break; + } + fields.push(p.parse_struct_lit_field()); + if !p.eat(TokenKind::Comma) { + break; + } + } + }); + self.expect(TokenKind::RBrace); + StructLitExpr { + path, + fields, + base, + span: self.finish_span(lo), + metadata: (), + } + } + + /// `structLitField : ident (COLON expr)?` + fn parse_struct_lit_field(&mut self) -> StructLitField<&'a str, Md> { + let lo = self.cur.start; + let name = self.ident(); + let (value, shorthand) = if self.eat(TokenKind::Colon) { + (self.parse_expr(0), false) + } else { + // Shorthand: `x` stands for `x: x`. The value is a path at the + // name's own span, so diagnostics and navigation on it point at the + // one token the user wrote. + let value = Expr::IdentPath(IdentPath { + path: vec![name.clone()], + metadata: (), + span: name.span, + }); + (value, true) + }; + StructLitField { + name, + value, + shorthand, + span: self.finish_span(lo), + } + } + /// `( )` nil, `( expr )` parenthesized group (unwrapped), or /// `( expr , (expr ,)* )` tuple (a comma after every element is required). fn parse_paren(&mut self) -> Expr<&'a str, Md> { + self.with_struct_literals(true, |p| p.parse_paren_inner()) + } + + fn parse_paren_inner(&mut self) -> Expr<&'a str, Md> { let lp = self.bump(); if self.at(TokenKind::RParen) { let rp = self.bump(); @@ -1117,6 +1211,10 @@ impl<'a> Parser<'a> { /// `args : posArgList (COMMA kwArgList)? COMMA? | kwArgList COMMA? | ε` fn parse_args(&mut self) -> Args<&'a str, Md> { + self.with_struct_literals(true, |p| p.parse_args_inner()) + } + + fn parse_args_inner(&mut self) -> Args<&'a str, Md> { let lparen_end = self.prev_end; let lo = self.cur.start; let mut posargs = Vec::new(); diff --git a/crates/compiler/src/parser/lexer.rs b/crates/compiler/src/parser/lexer.rs index cc5d350..2fc1f89 100644 --- a/crates/compiler/src/parser/lexer.rs +++ b/crates/compiler/src/parser/lexer.rs @@ -138,6 +138,11 @@ impl<'a> Lexer<'a> { b'-' if peek2(1) == b'>' => (TokenKind::Arrow, 2), b'&' if peek2(1) == b'&' => (TokenKind::AmpAmp, 2), b'|' if peek2(1) == b'|' => (TokenKind::PipePipe, 2), + // `..` (struct update syntax) wins over `.`, so `1..` is `1` `..` + // rather than the float `1.` followed by `.`; the only construct + // that changes is a tuple index on a float literal, which never + // type-checked. + b'.' if peek2(1) == b'.' => (TokenKind::DotDot, 2), b':' => (TokenKind::Colon, 1), b'=' => (TokenKind::Eq, 1), b'!' => (TokenKind::Bang, 1), diff --git a/crates/compiler/src/parser/mod.rs b/crates/compiler/src/parser/mod.rs index fbae345..8a5a275 100644 --- a/crates/compiler/src/parser/mod.rs +++ b/crates/compiler/src/parser/mod.rs @@ -46,10 +46,6 @@ pub fn parse_ast(input: ArcStr, path: PathBuf) -> Result Some(ParseError { - span: decl.name.span, - message: "struct declarations are not implemented".to_string(), - }), Decl::Constant(decl) => Some(ParseError { span: decl.name.span, message: "constant declarations are not implemented".to_string(), @@ -135,6 +131,31 @@ mod tests { "let x = a && b!;", "let x = a < b && c >= d || !e;", "if a && b {} else {}", + // Struct literals: named fields in any order, shorthand fields, + // and a `..base` that must come last after a comma. + "let p = Point { x: 1., y: 2. };", + "let p = Point { x: 1., y: 2., };", + "let p = geom::Point { x: 1., y: 2. };", + "let p = lib::geom::Point { x: 1., y: 2. };", + "let p = Point { x, y };", + "let p = Point { x, y: 2. };", + "let p = Point { ..base };", + "let p = Point { x: 1., ..base };", + "let p = Point { x, ..base };", + "let p = Unit {};", + "let x = Point { x: 1., y: 2. }.x;", + "let p = Outer { inner: Inner { a: 1 }, list: cons(Inner { a: 2 }, []) };", + "let p = Point { x: if c { 1. } else { 2. }, y: 2. };", + "foo(Point { x: 1., y: 2. }, p=Point { ..base });", + "let x = arr[Point { i: 0 }.i];", + // A literal in an `if`/`match`/`for` head needs parentheses, but + // is fine inside the construct's scopes and arms. + "if (p == Point { x: 1., y: 2. }) {} else {}", + "if c { Point { x: 1., y: 2. } } else { q }", + "match k { A => Point { x: 1., y: 2. }, }", + "for i in seq { let p = Point { x: i, y: i }; }", + // A `match` in an `if` head still allows literals in its arms. + "if match k { A => Point { x: 1. }, }.x == 1. {} else {}", ]; for body in valid { assert!(snippet_ok(body), "should parse: `{body}`"); @@ -153,12 +174,64 @@ mod tests { "let x = 99999999999999999999;", // out of range for Int "let x = 1 . 5;", // a float may not be split by trivia "let x = t.99999999999999999999;", // out of range tuple index + // `name {` in an `if`/`match`/`for` head is the construct's scope. + "if p == Point { x: 1. } {} else {}", + "match Point { x: 1. } { A => 1, }", + "for p in Point { xs: [] }.xs {}", + "let p = Point { x: };", // missing value + "let p = Point { x: 1. ..b };", // a comma is required before `..` + "let p = Point { ..b, };", // no comma after the base + "let p = Point { ..b, x: 1. };", // the base must come last + "let p = Point { x.y };", // a field is a bare identifier + "let p = Point { x: 1. y: 2. };", ]; for body in invalid { assert!(!snippet_ok(body), "should be rejected: `{body}`"); } } + #[test] + fn struct_declarations_take_type_specs() { + assert!(parse("struct S { a: [Float], b: (Int, Int), c: Other, }").is_ok()); + assert!(parse("struct Unit {}").is_ok()); + assert!(parse("struct S { a }").is_err()); + assert!(parse("struct S { a: }").is_err()); + assert!(parse("struct S { a: Float b: Float }").is_err()); + } + + /// 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 shorthand_struct_fields_desugar_to_a_path_at_the_name() { + use crate::ast::{Decl, Expr, Statement}; + + let ast = parse("cell __t__() { let p = Point { x, y: 1., ..q }; }").unwrap(); + let Decl::Cell(cell) = &ast.ast.decls[0] else { + panic!("expected a cell"); + }; + let Statement::LetBinding(binding) = &cell.scope.stmts[0] else { + panic!("expected a let binding"); + }; + let Expr::StructLit(lit) = &binding.value else { + panic!("expected a struct literal"); + }; + assert_eq!(lit.path.path.len(), 1); + assert_eq!(lit.fields.len(), 2); + assert!(lit.fields[0].shorthand); + assert_eq!(lit.fields[0].value.span(), lit.fields[0].name.span); + assert!(matches!( + &lit.fields[0].value, + Expr::IdentPath(path) if path.path.len() == 1 && path.path[0].name == "x" + )); + assert!(!lit.fields[1].shorthand); + assert!(matches!(lit.base, Some(Expr::IdentPath(_)))); + // The literal spans from its path to the closing brace. + assert_eq!( + &ast.text[lit.span.start()..lit.span.end()], + "Point { x, y: 1., ..q }" + ); + } + /// Renders an expression fully parenthesized. fn shape( expr: &crate::ast::Expr, diff --git a/crates/compiler/src/parser/token.rs b/crates/compiler/src/parser/token.rs index 03e0dd7..93d7557 100644 --- a/crates/compiler/src/parser/token.rs +++ b/crates/compiler/src/parser/token.rs @@ -43,6 +43,7 @@ pub enum TokenKind { Arrow, // -> AmpAmp, // && PipePipe, // || + DotDot, // .. // Single-character operators / punctuation. Lt, // < @@ -106,6 +107,7 @@ impl TokenKind { Arrow => "'->'", AmpAmp => "'&&'", PipePipe => "'||'", + DotDot => "'..'", Lt => "'<'", Gt => "'>'", Eq => "'='", diff --git a/docs/parser.md b/docs/parser.md index 6c29bd8..4546e07 100644 --- a/docs/parser.md +++ b/docs/parser.md @@ -139,7 +139,7 @@ is irrelevant for Argon.) - **Keywords** — `enum struct match const cell mod if fn else let for in as true false` (15 of them). - **Names & literals** — `Ident`, `IntLit`, `StrLit`. -- **Multi-character operators** — `:: => == != >= <= -> && ||`. +- **Multi-character operators** — `:: => == != >= <= -> && || ..`. - **Single-character operators / punctuation** — `< > = ! + - * / % ( ) { } [ ] . : ; ,`. - **`Eof`** — the empty range `[len, len)`. @@ -211,9 +211,9 @@ Notes on individual rules: closing quote produces an `Error` token spanning what was consumed. - **`lex_operator`** uses **maximal munch**: two-character operators are matched before their one-character prefixes (`::` before `:`, `==`/`=>` before `=`, - `->` before `-`, etc.). `&&` and `||` are the exception with no one-character - form: Argon has no bitwise operators, so a lone `&` or `|` is an `Error` - token like any other unrecognized byte. + `->` before `-`, `..` before `.`, etc.). `&&` and `||` are the exception with + no one-character form: Argon has no bitwise operators, so a lone `&` or `|` + is an `Error` token like any other unrecognized byte. ### Error tokens and UTF-8 @@ -344,7 +344,7 @@ while !self.at(Eof) { | Keyword | Rule | AST node | Shape | |----------|---------------------|---------------|-------| | `enum` | `parse_enum_decl` | `EnumDecl` | `enum Name { ident, … }` | -| `struct` | `parse_struct_decl` | `StructDecl` | `struct Name { field: Ty, … }` | +| `struct` | `parse_struct_decl` | `StructDecl` | `struct Name { field: tySpec, … }` | | `cell` | `parse_cell_decl` | `CellDecl` | `cell Name(argDecls) scope` | | `fn` | `parse_fn_decl` | `FnDecl` | `fn Name(argDecls) (-> Ty)? scope` | | `const` | `parse_const_decl` | `ConstantDecl`| `const Name: Ty = expr;` | @@ -419,6 +419,9 @@ non-consuming `parse_item` cannot spin. > - **Match arms** (`matchArm : identPath FAT_ARROW expr COMMA`). The comma is > part of each arm, and `matchArms : matchArm+` requires at least one arm, so > `match k {}` is a syntax error. +> - **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. --- @@ -503,7 +506,8 @@ level). `Scope`. These are full expressions, so the Pratt loop can still attach trailing operators to them. - identifier → an `identPath` (`a::b::c`); becomes a `Call` if followed by - `(`, otherwise an `IdentPath`. + `(`, a `StructLit` if followed by `{` where a struct literal is allowed + ([§9.5](#95-struct-literals)), otherwise an `IdentPath`. - integer / string / `true` / `false` → the corresponding literal. - **`parse_suffix`** applies one postfix operator to the accumulated `lhs`: @@ -533,6 +537,10 @@ floats from `IntLit DOT IntLit?`: checking — but it parses uniformly, like every other primary's `.field`.) - `1.0.2` → `IndexFieldAccess(FloatLiteral(1.0), 2)`: the first `.0` completes the float, the second `.2` is a tuple-index suffix. +- `1..` → `IntLit DotDot`: the lexer's maximal munch gives the struct-update + `..` priority over `.`, so a float with no fractional digits cannot be + followed directly by another `.`. Nothing meaningful is lost — `1..2` used to + parse as a tuple index on a float, which never type-checked. A literal's value is parsed from the raw source slice. A slice that does not parse — an integer outside `i64`, or a float split by trivia (`1 . 5`) — is a @@ -554,6 +562,33 @@ as `(-a).b` (`FieldAccess(Neg(a), b)`), and `-a!` as `(-a)!`. The suffix cluster binds tighter than the binary operators, but a prefix operator grabs only its immediate primary and the suffix then applies to the whole unary expression. +### 9.5 Struct literals + +`Name { field: expr, other, ..base }` parses to a `StructLitExpr` holding the +path, the explicit fields, and the optional base: + +- A field written without a value is **shorthand** for `field: field`. The + parser desugars it to an `IdentPath` at the name's own span and sets + `shorthand`, so the type checker and evaluator see an ordinary field while + navigation can still tell the two spellings apart. +- The **`..base`** must come last, after a comma, and takes no comma after it — + Rust's rule. `Point { x: 1. ..b }`, `Point { ..b, }`, and `Point { ..b, x: 1. }` + are all syntax errors. +- The body has two terminators (`}` and `..`), so `parse_struct_lit` has its + 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 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 +struct literal is not allowed at the top level of those three expressions: +write `if (p == Point { x: 1. }) {`. This is Rust's rule as well. The +`no_struct_literal` flag on `Parser` implements it: `with_struct_literals` sets +the flag around the head expression and clears it again inside parentheses, +brackets, call arguments, struct literal bodies, match arm bodies, and brace +scopes, so the literal is fine in an `if` branch, a `match` arm, a `for` +body, or a parenthesized condition. + --- ## 10. Scopes, statements, and tails diff --git a/examples/structs/Argon.toml b/examples/structs/Argon.toml new file mode 100644 index 0000000..cf696bc --- /dev/null +++ b/examples/structs/Argon.toml @@ -0,0 +1,2 @@ +name = "structs" +tech = "../tech/basic.tech.toml" diff --git a/examples/structs/geom.ar b/examples/structs/geom.ar new file mode 100644 index 0000000..c5ffbd7 --- /dev/null +++ b/examples/structs/geom.ar @@ -0,0 +1,4 @@ +struct Size { + w: Float, + h: Float, +} diff --git a/examples/structs/lib.ar b/examples/structs/lib.ar new file mode 100644 index 0000000..f4d4915 --- /dev/null +++ b/examples/structs/lib.ar @@ -0,0 +1,42 @@ +mod geom; + +use lib::geom::Size; + +struct ViaParams { + layer: String, + size: Size, + n: Int, +} + +// `..s` copies every field that is not listed from `s`. +fn grow(s: Size, by: Float) -> Size { + Size { w: s.w + by, ..s } +} + +// Shorthand: `w` and `h` stand for `w: w` and `h: h`. +fn square(w: Float) -> Size { + let h = w; + Size { w, h } +} + +fn total_width(sizes: [Size], pitch: Float) -> Float { + if sizes == [] { + 0. + } else { + head(sizes).w + pitch + total_width(tail(sizes), pitch) + } +} + +cell via(p: ViaParams) { + let r = rect(p.layer, x0=0., y0=0., w=p.size.w, h=p.size.h); +} + +cell top() { + let size = grow(square(100.), 10.); + let params = ViaParams { layer: "met1", size, n: 2 }; + let wide = ViaParams { size: geom::Size { w: 300., ..size }, ..params }; + let v1 = inst(via(params), x=0., y=0.); + let v2 = inst(via(wide), x=size.w + 50., y=0.); + let sizes = cons(params.size, cons(wide.size, [])); + let outline = rect("met2", x0=0., y0=0., w=total_width(sizes, 50.) - 50., h=size.h); +}