diff --git a/README.md b/README.md index eb48306..d89b0d0 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,30 @@ Shapes built on the command line work the same way: arc run --cell 'via_array(crect(x0=0., y0=0., w=90., h=40.), 10., 20.)' ``` +Top-level declarations may appear in any order: a cell, function, struct, or +enum can be used above the line that declares it. Cells may also instantiate +themselves or one another, which is how a recursive layout is written. The +recursion has to end on a parameter, since the compiler stops at a fixed +nesting depth: + +```rust +cell tree(n: Int) { + let leaf = rect("met1", x0=0., y0=0., w=100., h=100.); + if n > 0 { + let child = inst(tree(n - 1)); + eq(child.leaf.x0, leaf.x1 + 50.); + eq(child.y, 0.); + } else { + }; +} +``` + +Cell types are nominal: two cells with the same fields are different types, +and a function that accepts either takes `Any`. The type of an instance field +is worked out when it is first read, so the one thing that cannot be resolved +is a field whose type depends on itself through the fields of other cells, +which is reported as an error at the read that closes the cycle. + 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/bench/README.md b/bench/README.md index c0876fb..d234d34 100644 --- a/bench/README.md +++ b/bench/README.md @@ -212,15 +212,14 @@ parameter; "peak" is peak heap allocated during compilation. the top-level README now bites only for genuinely dense coupled blocks, not for the common sparse-but-coupled case. -- **Hierarchy depth scales linearly.** A cell's static type (`CellTy`) records - the structural type of every field, including instantiated sub-cells. That - type is now **shared** (`Arc`-interned via `CellFnTy::cell`) instead of being - copied into each reference, so the type representation is a DAG rather than a - tree: whether a cell references its child **once** (`let i = inst(child());`) - or **twice** (the `let c = child(); let i = inst(c);` idiom from the tutorial), - `h{k}` holds shared pointers to the single type of `h{k-1}`, and both variants - cost the same — linear in depth (≈160 MiB / 0.15 s at depth 2048, the two - series within ~2% of each other). Before this fix the type was deep-copied per +- **Hierarchy depth scales linearly.** A cell's static type (`CellTy`) is + nominal: it names the declaring cell and carries no field types, so a + reference to a cell costs the same however deep the hierarchy below it is, + and whether a cell references its child **once** (`let i = inst(child());`) + or **twice** (the `let c = child(); let i = inst(c);` idiom from the + tutorial) — linear in depth (≈160 MiB / 0.15 s at depth 2048, the two series + within ~2% of each other). The type used to record the structural type of + every field, including instantiated sub-cells, and was deep-copied per reference, so the single-ref chain was quadratic (`~depth^1.4`) and the double-ref chain **doubled with every level** (`×1.9` measured), exhausting memory beyond ~depth 20 (depth 18 alone took ~3.6 GiB / 11.5 s). The remaining diff --git a/crates/compiler/src/compile.rs b/crates/compiler/src/compile.rs index dd41a0a..237a00a 100644 --- a/crates/compiler/src/compile.rs +++ b/crates/compiler/src/compile.rs @@ -79,6 +79,15 @@ const MAX_SOLVE_ITERS: u64 = 1 << 24; /// [`crate::run_with_stack`] reserves. const MAX_EVAL_DEPTH: u32 = 4096; +/// The most cell fields whose types may be waiting on one another at once. +/// +/// A field's type is computed on demand, and a demand for a field of a cell +/// that is not yet typed suspends the current statement until that field is. +/// The suspended statements form an explicit stack rather than native +/// recursion, so this only bounds pathological workspaces; a genuine cycle is +/// reported as [`StaticErrorKind::CyclicCellField`] long before it is reached. +const MAX_CELL_TYPING_DEPTH: usize = 4096; + /// The longest text label a GDSII `STRING` record may carry, in bytes. /// /// The format itself only bounds a record at `u16::MAX`, but the GDSII @@ -1100,28 +1109,169 @@ fn check_text(text: &Text, cell: CellId, errs: &mut Vec) { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub(crate) struct VarIdTyFrame { var_bindings: IndexMap, } +/// The fields of a fully typed cell, by name. +type CellFields = IndexMap; + pub(crate) struct VarIdTyPass<'a> { ast: &'a AnnotatedAst, mod_bindings: &'a IndexMap<&'a ModPath, VarIdTyFrame>, + /// Fields of the cells of every module typed before this one. + cell_fields: &'a IndexMap, current_path: &'a ModPath, next_id: VarId, bindings: Vec, errors: Vec, + /// Cells of this module that are still being typed, by their id. + cells: IndexMap>, + /// The cell declared at each index of `ast.ast.decls`. + decl_cells: IndexMap, + /// Fields of the cells of this module that are fully typed. + finished_cell_fields: IndexMap, + /// Statements suspended on the type of a field, innermost last. + goals: Vec, + /// Fields whose type could not be determined, as `(cell, statement)`. A + /// read of one is `Unknown` and raises no further demand. + poisoned: IndexSet<(VarId, usize)>, + /// The unit of source being typed, while one is. + attempt: Option, +} + +/// A cell of the current module while its body is typed. +/// +/// A cell's type is bound before any body is walked, so a cell may refer to +/// itself, or to a cell declared below it. Only the *types of its fields*, +/// the top-level `let` bindings of its body, have to wait for the body: they +/// are computed one statement at a time, in any order, as they are demanded. +struct CellTyping<'a> { + decl: &'a CellDecl, + /// Top-level `let` name to the indices of the statements declaring it, + /// ascending. A name declared more than once is re-bound in sequence: a + /// statement sees the nearest `let` above it, and the field an instance + /// answers is the last. + lets: IndexMap>, + /// The typed parameters and the frame binding them, once typed. + params: Option<(Vec>, VarIdTyFrame)>, + /// One slot per top-level statement of the body, filled as each is typed. + stmts: Vec>>, + /// The typed tail, once typed; the inner `None` is a body without a tail. + tail: Option>>, + /// The view `statement_view` last built, extended rather than rebuilt + /// when the next statement wanted is at or beyond its limit. + view: Option, +} + +/// The frame a statement of a cell is typed in: the parameters, then the +/// nearest top-level `let` of each name declared below `limit`, if it has +/// been typed. +/// +/// Kept between statements, so that typing a body in order extends it by one +/// statement at a time instead of walking every `let` above each statement. +#[derive(Default)] +struct StatementView { + /// The statements below this index are reflected. + limit: usize, + frame: VarIdTyFrame, + /// Names whose nearest `let` below `limit` is not yet typed, to the + /// statement declaring it. They are absent from `frame`, so that reading + /// one raises a demand instead of resolving to a parameter or module + /// declaration of the same name. + untyped: IndexMap, +} + +impl StatementView { + /// Reflects the statement at the limit and moves the limit past it. + fn extend(&mut self, typing: &CellTyping<'_>) { + let stmt = self.limit; + self.limit += 1; + let Statement::LetBinding(binding) = &typing.decl.scope.stmts[stmt] else { + return; + }; + let name = &binding.name.name; + match &typing.stmts[stmt] { + Some(Statement::LetBinding(typed)) => { + self.frame + .var_bindings + .insert(name.clone(), (typed.metadata, typed.value.ty())); + self.untyped.swap_remove(name.as_str()); + } + Some(_) => unreachable!("a `let` statement is typed as a `let`"), + None => { + self.frame.var_bindings.swap_remove(name.as_str()); + self.untyped.insert(name.clone(), stmt); + } + } + } +} + +/// One independently typed piece of a cell. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Unit { + /// The parameter list, including default values. + Params, + /// The top-level statement at this index of the body. + Stmt(usize), + /// The tail expression. + Tail, +} + +/// Work on the cell typing stack: type one statement of `cell`, or all of it. +#[derive(Debug, Clone, Copy)] +struct Goal { + cell: VarId, + /// The statement wanted, or `None` to type the whole cell. + target: Option, + /// The unit whose attempt this goal is suspended in, once attempted. A + /// demand for that very unit closes a cycle. + attempting: Option, +} + +/// A field type that was read before it was computed. +#[derive(Debug, Clone)] +struct Demand { + cell: VarId, + /// Index of the `let` statement declaring the field. + stmt: usize, + /// Where it was read, for the diagnostic if the demand cannot be met. + span: cfgrammar::Span, +} + +/// One attempt at typing a unit. If it raises demands, everything it did is +/// rolled back and it is attempted again once they are met. +struct Attempt { + /// The cell whose unit this is; `None` for a `fn` declaration. + cell: Option, + /// Top-level `let`s of `cell` that are in scope for this unit but not yet + /// typed, to the statement declaring them. A read of one is a demand. + visible_untyped: IndexMap, + demands: Vec, +} + +/// State shared by every module of a workspace while it is typed. +struct WorkspaceTyping<'a> { + mod_bindings: IndexMap<&'a ModPath, VarIdTyFrame>, + /// Fields of every fully typed cell, by the cell's id. + cell_fields: IndexMap, + ast: WorkspaceAst, + errors: Vec, + next_id: VarId, } pub(crate) fn execute_var_id_ty_pass<'a>( ast: &'a WorkspaceParseAst, dag: &'a ModDag<'a>, ) -> (WorkspaceAst, Vec) { - let mut mod_bindings = IndexMap::new(); - let mut workspace_ast = IndexMap::new(); - let mut errors = Vec::new(); - let mut next_id = 1; + let mut typing = WorkspaceTyping { + mod_bindings: IndexMap::new(), + cell_fields: IndexMap::new(), + ast: IndexMap::new(), + errors: Vec::new(), + next_id: 1, + }; let std_mod_path = vec!["std".to_string()]; let std_mod_path = ast.get_key_value(&std_mod_path).map(|(k, _)| k); if let Some((root, _)) = ast.get_key_value(&vec![]) { @@ -1130,59 +1280,53 @@ pub(crate) fn execute_var_id_ty_pass<'a>( .flatten() .chain(ast.keys()) { - execute_var_id_ty_pass_inner( - ast, - dag, - path, - &mut mod_bindings, - &mut workspace_ast, - &mut errors, - &mut next_id, - ); + execute_var_id_ty_pass_inner(ast, dag, path, &mut typing); } } - (workspace_ast, errors) + (typing.ast, typing.errors) } -pub(crate) fn execute_var_id_ty_pass_inner<'a>( + +fn execute_var_id_ty_pass_inner<'a>( ast: &'a WorkspaceParseAst, dag: &'a ModDag<'a>, current_path: &'a ModPath, - mod_bindings: &mut IndexMap<&'a ModPath, VarIdTyFrame>, - workspace_ast: &mut WorkspaceAst, - errors: &mut Vec, - next_id: &mut VarId, + typing: &mut WorkspaceTyping<'a>, ) { // TODO: fix hacky way to track visited modules. - if mod_bindings.contains_key(¤t_path) { + if typing.mod_bindings.contains_key(¤t_path) { return; } - mod_bindings.insert(current_path, VarIdTyFrame::default()); + typing + .mod_bindings + .insert(current_path, VarIdTyFrame::default()); for children in dag[¤t_path].keys() { - execute_var_id_ty_pass_inner( - ast, - dag, - children, - mod_bindings, - workspace_ast, - errors, - next_id, - ); + execute_var_id_ty_pass_inner(ast, dag, children, typing); } let mut pass = VarIdTyPass { ast: &ast[current_path], - mod_bindings, + mod_bindings: &typing.mod_bindings, + cell_fields: &typing.cell_fields, current_path, - next_id: *next_id, + next_id: typing.next_id, bindings: vec![VarIdTyFrame::default()], errors: vec![], + cells: IndexMap::new(), + decl_cells: IndexMap::new(), + finished_cell_fields: IndexMap::new(), + goals: Vec::new(), + poisoned: IndexSet::new(), + attempt: None, }; - let ast = pass.execute(); - workspace_ast.insert(current_path.clone(), ast); - errors.extend(pass.errors); - *next_id = pass.next_id; - mod_bindings.insert(current_path, pass.bindings.into_iter().next().unwrap()); + let module_ast = pass.execute(); + typing.ast.insert(current_path.clone(), module_ast); + typing.errors.extend(pass.errors); + typing.next_id = pass.next_id; + let module_frame = pass.bindings.into_iter().next().unwrap(); + let cell_fields = pass.finished_cell_fields; + typing.cell_fields.extend(cell_fields); + typing.mod_bindings.insert(current_path, module_frame); } #[derive(Debug, Clone)] @@ -1294,15 +1438,6 @@ impl std::fmt::Display for Ty { } } -/// Whether an instance of `cell` would answer `field`. -/// -/// A cell's public fields are its top-level `let` bindings plus -/// [`RESERVED_CELL_FIELDS`]; a GDS-backed cell publishes its geometry at -/// execution time, so it answers anything. -fn cell_has_field(cell: &CellTy, field: &str) -> bool { - cell.dynamic_fields || cell.data.contains_key(field) || RESERVED_CELL_FIELDS.contains(&field) -} - impl Ty { pub fn from_name(name: &str) -> Option { match name { @@ -1497,33 +1632,32 @@ pub struct FnTy { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CellFnTy { sig: Signature, - /// The structural type produced when this cell function is called, shared - /// with every caller and every `inst` of the result. + /// The type produced when this cell function is called, shared with every + /// caller and every `inst` of the result. pub(crate) cell: Arc, } +/// The type of a cell, and of an instance of it. +/// +/// Cell types are *nominal*: the type is the declaration, identified by +/// `def`, and carries no field information of its own. The fields an +/// instance answers are the cell's top-level `let` bindings, which the type +/// pass looks up through the declaring cell as they are needed. That is what +/// lets a cell's type exist before its body is typed, so that cells may refer +/// to one another, and to themselves, in any order. #[derive(Debug, Clone, Eq, Serialize, Deserialize)] pub struct CellTy { - /// The name the cell was declared with, carried solely so that a - /// diagnostic can say `Inst(inverter)` instead of dumping the field map. - /// - /// Deliberately excluded from `PartialEq`: cell types are *structural*, so - /// two identically shaped cells are the same type regardless of what they - /// are called, and `is_eq_ty` falls through to `==`. Including the name - /// here would silently make that check nominal. + /// The name the cell was declared with, module-qualified, carried solely + /// so that a diagnostic can say `Inst(inverter)`. name: String, - pub(crate) data: IndexMap, /// GDS-backed cells publish geometry fields at execution time. Their /// generated source declaration is intentionally only a signature, so - /// field names cannot be enumerated by the source type pass. + /// field names cannot be enumerated by the source type pass, and every + /// field reads as `Any`. dynamic_fields: bool, - /// The declaring cell's [`VarId`], for navigation from a field access back - /// to the `let` that declared the field. - /// - /// Identity, not structure: cell typing is structural, so two cells with - /// the same fields must stay interchangeable. [`PartialEq`] therefore - /// ignores this field, and it is `None` for the generated signature of a - /// GDS-backed cell. + /// The declaring cell's [`VarId`]: the type's identity, and how navigation + /// reaches the `let` declaring a field. `None` for the generated signature + /// of a GDS-backed cell. pub(crate) def: Option, } @@ -1531,20 +1665,18 @@ impl PartialEq for CellTy { fn eq(&self, other: &Self) -> bool { // Destructured rather than field-by-field so that a field added later // has to be named here, instead of being silently excluded from - // structural equality. + // equality. let Self { name: _, - data, dynamic_fields, - def: _, + def, } = self; let Self { name: _, - data: other_data, dynamic_fields: other_dynamic_fields, - def: _, + def: other_def, } = other; - data == other_data && dynamic_fields == other_dynamic_fields + def == other_def && dynamic_fields == other_dynamic_fields } } @@ -1625,10 +1757,10 @@ impl<'a> VarIdTyPass<'a> { /// The name a diagnostic should use for a declaration in this module. /// - /// Cell and enum types are structural, so two same-named declarations in - /// different modules are different types that a bare name renders - /// identically: `expected type Inst(child), found Inst(child)`. Matches - /// the qualification `ExecPass` records for the GDS exporter. + /// Two same-named declarations in different modules are different types + /// that a bare name renders identically: `expected type Inst(child), found + /// Inst(child)`. Matches the qualification `ExecPass` records for the GDS + /// exporter. fn qualified_name(&self, name: &str) -> String { self.current_path .iter() @@ -1638,7 +1770,17 @@ impl<'a> VarIdTyPass<'a> { } fn lookup(&self, name: &str) -> Option<(VarId, Ty)> { - for frame in self.bindings.iter().rev() { + // A top-level `let` of the cell being typed that is in scope but not + // yet typed hides the module's declarations, as a typed one in the + // view would. Reading it is a demand, so it must not resolve to them. + let hides_module = self + .attempt + .as_ref() + .is_some_and(|attempt| attempt.visible_untyped.contains_key(name)); + for (index, frame) in self.bindings.iter().enumerate().rev() { + if index == 0 && hides_module { + break; + } if let Some(info) = frame.var_bindings.get(name) { return Some(info.clone()); } @@ -1646,28 +1788,6 @@ impl<'a> VarIdTyPass<'a> { None } - fn unresolved_local_name_error( - &self, - name: &str, - use_span: cfgrammar::Span, - ) -> StaticErrorKind { - let is_cell_declared_later = self.ast.ast.decls.iter().any(|decl| { - matches!(decl, Decl::Cell(cell) - if cell.name.name.as_str() == name - && cell.name.span.start() > use_span.start()) - }); - - if is_cell_declared_later { - StaticErrorKind::UseBeforeDeclaration { - name: name.to_owned(), - } - } else { - StaticErrorKind::UndeclaredVar { - name: name.to_owned(), - } - } - } - fn alloc_id(&mut self) -> u64 { let id = self.next_id; self.next_id += 1; @@ -1700,7 +1820,10 @@ impl<'a> VarIdTyPass<'a> { // 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. + // signature may name a struct declared further down. Cells come last: + // their types are nominal, so binding them here, before any body is + // walked, lets a body refer to any cell of the module, including the + // one being declared. for decl in &self.ast.ast.decls { if let Decl::Enum(e) = decl { self.declare_enum_decl(e); @@ -1717,14 +1840,26 @@ impl<'a> VarIdTyPass<'a> { self.declare_fn_decl(f); } } + for (index, decl) in self.ast.ast.decls.iter().enumerate() { + if let Decl::Cell(c) = decl { + self.declare_cell_decl(index, c); + } + } - for decl in &self.ast.ast.decls { + for (index, decl) in self.ast.ast.decls.iter().enumerate() { match decl { Decl::Fn(f) => { - decls.push(Decl::Fn(self.transform_fn_decl(f))); + decls.push(Decl::Fn(self.type_fn_decl(f))); } - Decl::Cell(c) => { - decls.push(Decl::Cell(self.transform_cell_decl(c))); + Decl::Cell(_) => { + let cell = self.decl_cells[&index]; + self.goals.push(Goal { + cell, + target: None, + attempting: None, + }); + self.run_goals(); + decls.push(Decl::Cell(self.finish_cell(cell))); } Decl::Mod(m) => { decls.push(Decl::Mod(self.transform_mod_decl(m))); @@ -1768,6 +1903,450 @@ impl<'a> VarIdTyPass<'a> { ) } + /// Binds a cell's type before its body is typed, and sets up the lazy + /// typing of its fields. + /// + /// The type is built from the signature alone, like a function's: cell + /// types are nominal, so nothing about the body is needed to name the + /// type. `index` is the declaration's position in `ast.ast.decls`. + fn declare_cell_decl(&mut self, index: usize, input: &'a CellDecl) { + if BUILTINS.contains(&input.name.name.as_str()) { + self.errors.push(StaticError { + span: self.span(input.name.span), + kind: StaticErrorKind::RedeclarationOfBuiltin, + }); + } + self.check_params(&input.args); + let sig = input + .args + .iter() + .map(|arg| { + let ty_spec = self.transform_ty_spec(&arg.ty); + (arg, self.ty_from_spec(&ty_spec)) + }) + .collect(); + // Generated declarations -- the signatures standing in for GDS-backed + // cells -- sit at the front of the list. They have no declared fields + // to trace back to, so field accesses on them fall through to the + // builtin geometry fields. + let dynamic_fields = index < self.ast.generated_declarations; + let cell_id = self.alloc_id(); + let ty = Ty::CellFn(Box::new(CellFnTy { + sig, + cell: Arc::new(CellTy { + name: self.qualified_name(&input.name.name), + dynamic_fields, + def: (!dynamic_fields).then_some(cell_id), + }), + })); + self.bind(&input.name.name, cell_id, ty); + + let mut lets: IndexMap> = IndexMap::new(); + for (stmt, statement) in input.scope.stmts.iter().enumerate() { + let Statement::LetBinding(binding) = statement else { + continue; + }; + let name = &binding.name; + if RESERVED_CELL_FIELDS.contains(&name.name.as_str()) { + self.errors.push(StaticError { + span: self.span(name.span), + kind: StaticErrorKind::ReservedCellField { + name: name.name.to_string(), + }, + }); + } + lets.entry(name.name.clone()).or_default().push(stmt); + } + self.cells.insert( + cell_id, + CellTyping { + decl: input, + lets, + params: None, + stmts: input.scope.stmts.iter().map(|_| None).collect(), + tail: None, + view: None, + }, + ); + self.decl_cells.insert(index, cell_id); + } + + /// Types a function declaration, retrying whenever its body reads a cell + /// field that has not been typed yet. + /// + /// Functions are never suspended on the stack: a call needs only the + /// signature, which is bound up front, so no field can depend on a + /// function body and every retry makes progress. + fn type_fn_decl( + &mut self, + input: &'a FnDecl, + ) -> FnDecl { + loop { + let (result, _) = self.attempt(None, StatementView::default(), |pass| { + pass.transform_fn_decl(input) + }); + match result { + Ok(decl) => return decl, + Err(demands) => { + self.schedule(demands); + self.run_goals(); + } + } + } + } + + /// Types one unit of source with the frame of `view` as its innermost + /// frame. + /// + /// Returns the result, or the demands the unit raised, in which case + /// everything the attempt did is undone: what it produced depends on + /// types it did not have, so its diagnostics are dropped and the ids it + /// allocated are released for the retry. The view comes back either way, + /// holding whatever the unit bound in its frame. + fn attempt( + &mut self, + cell: Option, + view: StatementView, + unit: impl FnOnce(&mut Self) -> T, + ) -> (Result>, StatementView) { + debug_assert!(self.attempt.is_none(), "attempts do not nest"); + let errors = self.errors.len(); + let next_id = self.next_id; + self.bindings.push(view.frame); + self.attempt = Some(Attempt { + cell, + visible_untyped: view.untyped, + demands: Vec::new(), + }); + let result = unit(self); + let attempt = self.attempt.take().expect("set above"); + let view = StatementView { + limit: view.limit, + frame: self.bindings.pop().expect("pushed above"), + untyped: attempt.visible_untyped, + }; + if attempt.demands.is_empty() { + (Ok(result), view) + } else { + self.errors.truncate(errors); + self.next_id = next_id; + (Err(attempt.demands), view) + } + } + + /// The view for typing the statement at `limit` of `cell`, or its tail + /// when `limit` is the number of statements. + /// + /// The view last built is extended when the statements it reflects are a + /// prefix of those wanted; otherwise one is rebuilt from the parameters. + fn statement_view(&mut self, cell: VarId, limit: usize) -> StatementView { + let typing = &mut self.cells[&cell]; + let mut view = match typing.view.take() { + Some(view) if view.limit <= limit => view, + _ => { + let (_, params) = typing + .params + .as_ref() + .expect("parameters are typed before the body"); + StatementView { + limit: 0, + frame: params.clone(), + untyped: IndexMap::new(), + } + } + }; + while view.limit < limit { + view.extend(typing); + } + view + } + + /// Attempts one unit of `cell`, recording the result on success. + fn attempt_unit(&mut self, cell: VarId, unit: Unit) -> Result<(), Vec> { + let decl = self.cells[&cell].decl; + match unit { + Unit::Params => { + let (args, view) = self.attempt(Some(cell), StatementView::default(), |pass| { + decl.args + .iter() + .map(|arg| pass.transform_arg_decl(arg)) + .collect::>() + }); + self.cells[&cell].params = Some((args?, view.frame)); + } + Unit::Stmt(stmt) => { + let view = self.statement_view(cell, stmt); + let statement = &decl.scope.stmts[stmt]; + let (typed, mut view) = + self.attempt(Some(cell), view, |pass| pass.transform_statement(statement)); + let typing = &mut self.cells[&cell]; + let result = typed.map(|typed| typing.stmts[stmt] = Some(typed)); + // The frame holds what the statement bound, typed or not; + // reflect the statement from its slot instead. + view.extend(typing); + typing.view = Some(view); + result?; + } + Unit::Tail => { + let view = self.statement_view(cell, decl.scope.stmts.len()); + let (typed, view) = self.attempt(Some(cell), view, |pass| { + decl.scope + .tail + .as_ref() + .map(|tail| pass.transform_expr(tail)) + }); + let typing = &mut self.cells[&cell]; + typing.view = Some(view); + typing.tail = Some(typed?); + } + } + Ok(()) + } + + /// The next unit `goal` needs, or `None` once it is met. + fn next_unit(&self, goal: Goal) -> Option { + let typing = &self.cells[&goal.cell]; + if typing.params.is_none() { + return Some(Unit::Params); + } + match goal.target { + Some(stmt) => typing.stmts[stmt].is_none().then_some(Unit::Stmt(stmt)), + None => typing + .stmts + .iter() + .position(Option::is_none) + .map(Unit::Stmt) + .or_else(|| typing.tail.is_none().then_some(Unit::Tail)), + } + } + + /// Works the goal stack until it is empty. + /// + /// The top goal is always the one worked on; a goal below it is suspended + /// on a demand that some goal above it will meet. An attempt that raises + /// demands pushes a goal for each and is retried when it is on top again. + fn run_goals(&mut self) { + while let Some(goal) = self.goals.last().copied() { + let Some(unit) = self.next_unit(goal) else { + self.goals.pop(); + continue; + }; + self.goals.last_mut().expect("just peeked").attempting = Some(unit); + if let Err(demands) = self.attempt_unit(goal.cell, unit) { + self.schedule(demands); + } + } + } + + /// Pushes a goal for every demand that can be met, and poisons those that + /// cannot so that the retry does not raise them again. + fn schedule(&mut self, demands: Vec) { + for demand in demands { + let typing = &self.cells[&demand.cell]; + if typing.stmts[demand.stmt].is_some() + || self.poisoned.contains(&(demand.cell, demand.stmt)) + { + continue; + } + // The field's statement is suspended somewhere below, so its type + // depends on the statement that just read it. A cell suspended in + // its parameter list cannot type any statement either. + let cyclic = self.goals.iter().any(|goal| { + goal.cell == demand.cell + && (goal.attempting == Some(Unit::Params) + || goal.attempting == Some(Unit::Stmt(demand.stmt))) + }); + if cyclic { + let Statement::LetBinding(binding) = &typing.decl.scope.stmts[demand.stmt] else { + unreachable!("demands name `let` statements") + }; + self.errors.push(StaticError { + span: self.span(demand.span), + kind: StaticErrorKind::CyclicCellField { + cell: typing.decl.name.name.to_string(), + field: binding.name.name.to_string(), + }, + }); + self.poisoned.insert((demand.cell, demand.stmt)); + continue; + } + // Already wanted by a goal that has not been attempted yet, queued + // by an earlier demand. (One that had been attempted would be + // `attempting` this very statement: the cycle above.) The goal + // that raised this demand is retried as soon as it is on top + // again, so the wanted goal has to be worked first: move it up. + if let Some(index) = self + .goals + .iter() + .position(|goal| goal.cell == demand.cell && goal.target == Some(demand.stmt)) + { + let goal = self.goals.remove(index); + self.goals.push(goal); + continue; + } + if self.goals.len() >= MAX_CELL_TYPING_DEPTH { + self.errors.push(StaticError { + span: self.span(demand.span), + kind: StaticErrorKind::CellTypingLimitExceeded { + limit: MAX_CELL_TYPING_DEPTH, + }, + }); + self.poisoned.insert((demand.cell, demand.stmt)); + continue; + } + self.goals.push(Goal { + cell: demand.cell, + target: Some(demand.stmt), + attempting: None, + }); + } + } + + /// Assembles a cell whose every unit has been typed, and publishes its + /// fields for the cells typed after it. + fn finish_cell(&mut self, cell: VarId) -> CellDecl { + let mut typing = self.cells.swap_remove(&cell).expect("declared"); + let decl = typing.decl; + let (args, _) = typing.params.take().expect("typed by the goal stack"); + let stmts = typing + .stmts + .into_iter() + .map(|stmt| stmt.expect("typed by the goal stack")) + .collect::>(); + let tail = typing.tail.expect("typed by the goal stack"); + let name = self.transform_ident(&decl.name); + if let Some(tail) = tail.as_ref() { + self.errors.push(StaticError { + span: self.span(tail.span()), + kind: StaticErrorKind::CellWithTailExpr, + }); + } + let fields = stmts + .iter() + .filter_map(|stmt| match stmt { + Statement::LetBinding(binding) => { + Some((binding.name.name.to_string(), binding.value.ty())) + } + _ => None, + }) + .collect(); + self.finished_cell_fields.insert(cell, fields); + let scope = Scope { + scope_order: decl.scope.scope_order, + span: decl.scope.span, + metadata: tail.as_ref().map_or(Ty::Nil, Expr::ty), + stmts, + tail, + }; + CellDecl { + name, + args, + scope, + span: decl.span, + metadata: (self.ast.path.clone(), cell), + } + } + + /// Records that the current attempt read the field declared by statement + /// `stmt` of `cell` before it was typed. + fn demand(&mut self, cell: VarId, stmt: usize, span: cfgrammar::Span) { + if self.poisoned.contains(&(cell, stmt)) { + return; + } + if let Some(attempt) = self.attempt.as_mut() { + attempt.demands.push(Demand { cell, stmt, span }); + } + } + + /// Whether an unresolved `name` is a top-level `let` of the cell being + /// typed that is in scope but not yet typed. If so, the read is a demand + /// and the caller types it `Unknown` without reporting anything. + fn demand_local(&mut self, name: &str, span: cfgrammar::Span) -> bool { + let Some(attempt) = self.attempt.as_ref() else { + return false; + }; + let (Some(cell), Some(&stmt)) = (attempt.cell, attempt.visible_untyped.get(name)) else { + return false; + }; + self.demand(cell, stmt, span); + true + } + + /// Whether an instance of `cell` would answer `field`. + /// + /// A cell's public fields are its top-level `let` bindings plus + /// [`RESERVED_CELL_FIELDS`]; a GDS-backed cell publishes its geometry at + /// execution time, so it answers anything. + fn cell_declares_field(&self, cell: &CellTy, field: &str) -> bool { + if cell.dynamic_fields || RESERVED_CELL_FIELDS.contains(&field) { + return true; + } + let Some(def) = cell.def else { + return false; + }; + if let Some(fields) = self.finished_cell_fields.get(&def) { + fields.contains_key(field) + } else if let Some(typing) = self.cells.get(&def) { + typing.lets.contains_key(field) + } else { + self.cell_fields + .get(&def) + .is_some_and(|fields| fields.contains_key(field)) + } + } + + /// The type of `field` read from an instance of `cell`, whose type is + /// `base_ty`. + /// + /// A field of a cell of this module whose statement has not been typed yet + /// is a demand: the read types `Unknown` for now and the statement making + /// it is retried once the field is typed. + fn inst_field_ty( + &mut self, + cell: &CellTy, + field: &Ident, + base_ty: &Ty, + ) -> Ty { + let name = field.name.as_str(); + if RESERVED_CELL_FIELDS.contains(&name) { + return Ty::Float; + } + if cell.dynamic_fields { + return Ty::Any; + } + let Some(def) = cell.def else { + return Ty::Any; + }; + if let Some(fields) = self.finished_cell_fields.get(&def) { + return match fields.get(name) { + Some(ty) => ty.clone(), + None => self.no_field_on_ty(field, base_ty.clone()), + }; + } + if let Some(typing) = self.cells.get(&def) { + // The field is the last `let` of that name. + let Some(&stmt) = typing.lets.get(name).and_then(|stmts| stmts.last()) else { + return self.no_field_on_ty(field, base_ty.clone()); + }; + return match &typing.stmts[stmt] { + Some(Statement::LetBinding(binding)) => binding.value.ty(), + Some(_) => unreachable!("`lets` indexes `let` statements"), + None => { + self.demand(def, stmt, field.span); + Ty::Unknown + } + }; + } + match self + .cell_fields + .get(&def) + .and_then(|fields| fields.get(name)) + { + Some(ty) => ty.clone(), + None => self.no_field_on_ty(field, base_ty.clone()), + } + } + /// Reports every top-level declaration whose name an earlier declaration /// of this module already took. /// @@ -2355,14 +2934,13 @@ impl<'a> VarIdTyPass<'a> { } } } else { + if is_local && self.demand_local(name, call_span) { + return (None, Ty::Unknown); + } self.errors.push(StaticError { span: self.span(call_span), - kind: if is_local { - self.unresolved_local_name_error(name, call_span) - } else { - StaticErrorKind::UndeclaredVar { - name: name.to_owned(), - } + kind: StaticErrorKind::UndeclaredVar { + name: name.to_owned(), }, }); (None, Ty::Unknown) @@ -2429,12 +3007,17 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { // Parser grammar ensures paths cannot be empty. assert!(!input.path.is_empty()); if input.path.len() == 1 { - if let Some((varid, ty)) = self.lookup(&input.path[0].name) { + let name = &input.path[0].name; + if let Some((varid, ty)) = self.lookup(name) { (Some(varid), ty) + } else if self.demand_local(name, input.span) { + (None, Ty::Unknown) } else { self.errors.push(StaticError { span: self.span(input.span), - kind: self.unresolved_local_name_error(&input.path[0].name, input.span), + kind: StaticErrorKind::UndeclaredVar { + name: name.to_string(), + }, }); (None, Ty::Unknown) } @@ -2598,12 +3181,8 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { 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(), - } + kind: StaticErrorKind::UndeclaredVar { + name: name.to_string(), }, }); return Ty::Unknown; @@ -2667,12 +3246,13 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { fn dispatch_cell_decl( &mut self, _input: &CellDecl, - name: &Ident, + _name: &Ident, _args: &[ArgDecl], _scope: &Scope, ) -> ::CellDecl { - // TODO: Argument checks - (self.ast.path.clone(), self.lookup(&name.name).unwrap().0) + // Cells are typed one unit at a time by the goal stack and assembled + // by `finish_cell`, never by `transform_cell_decl`. + unreachable!() } fn dispatch_fn_decl( @@ -2725,88 +3305,6 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { } } - fn transform_cell_decl( - &mut self, - input: &CellDecl, - ) -> CellDecl { - if BUILTINS.contains(&input.name.name.as_str()) { - self.errors.push(StaticError { - span: self.span(input.name.span), - kind: StaticErrorKind::RedeclarationOfBuiltin, - }); - } - self.check_params(&input.args); - self.enter_scope(&input.scope); - let args: Vec<_> = input - .args - .iter() - .map(|arg| self.transform_arg_decl(arg)) - .collect(); - let scope = self.transform_scope_contents(&input.scope); - self.exit_scope(&input.scope, &scope); - if let Some(tail) = scope.tail.as_ref() { - self.errors.push(StaticError { - span: self.span(tail.span()), - kind: StaticErrorKind::CellWithTailExpr, - }); - } - let data = scope - .stmts - .iter() - .filter_map(|stmt| { - if let Statement::LetBinding(lt) = stmt { - if RESERVED_CELL_FIELDS.contains(<.name.name.as_str()) { - self.errors.push(StaticError { - span: self.span(lt.name.span), - kind: StaticErrorKind::ReservedCellField { - name: lt.name.name.to_string(), - }, - }); - } - Some((lt.name.name.to_string(), lt.value.ty())) - } else { - None - } - }) - .collect(); - let dynamic_fields = self - .ast - .ast - .decls - .iter() - .take(self.ast.generated_declarations) - .any(|decl| { - matches!(decl, Decl::Cell(cell) if cell.span == input.span && cell.name.name == input.name.name) - }); - // The cell's type embeds its own id so that a field access on an - // instance can be traced back to the declaring cell. A GDS-backed cell - // has no declared fields to trace back to, so it records none and - // field accesses on it fall through to the builtin geometry fields. - let cell_id = self.alloc_id(); - let ty = Ty::CellFn(Box::new(CellFnTy { - sig: args - .iter() - .map(|arg| (arg, arg.metadata.1.clone())) - .collect(), - cell: Arc::new(CellTy { - name: self.qualified_name(&input.name.name), - data, - dynamic_fields, - def: (!dynamic_fields).then_some(cell_id), - }), - })); - self.bind(&input.name.name, cell_id, ty); - let name = self.transform_ident(&input.name); - let metadata = self.dispatch_cell_decl(input, &name, &args, &scope); - CellDecl { - name, - scope, - args, - span: input.span, - metadata, - } - } - fn transform_call_expr( &mut self, input: &CallExpr, @@ -3037,16 +3535,7 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { "x" | "y" => Ty::Float, _ => self.no_field_on_ty(field, Ty::Point), }, - Ty::Inst(ref c) => match field.name.as_str() { - name if RESERVED_CELL_FIELDS.contains(&name) => Ty::Float, - name => c.data.get(name).cloned().unwrap_or_else(|| { - if c.dynamic_fields { - Ty::Any - } else { - self.no_field_on_ty(field, base_ty.clone()) - } - }), - }, + Ty::Inst(ref c) => self.inst_field_ty(c, field, &base_ty), // A cell's coordinates are only determined relative to a // placement, so reading its geometry before `inst(...)` is // meaningless -- and the evaluator already refuses it. Saying so @@ -3056,7 +3545,7 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { // Only when the field exists, though: telling someone who // misspelled a field to place the cell first is advice that // cannot help, so an unknown name still gets the no-field error. - Ty::Cell(ref c) if cell_has_field(c, field.name.as_str()) => { + Ty::Cell(ref c) if self.cell_declares_field(c, field.name.as_str()) => { self.errors.push(StaticError { span: self.span(field.span), kind: StaticErrorKind::CellFieldBeforePlacement { @@ -3066,7 +3555,7 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { }); Ty::Unknown } - Ty::CellFn(ref c) if cell_has_field(&c.cell, &field.name) => { + Ty::CellFn(ref c) if self.cell_declares_field(&c.cell, &field.name) => { self.errors.push(StaticError { span: self.span(field.span), kind: StaticErrorKind::CellFnFieldAccess { @@ -4796,6 +5285,18 @@ impl<'a> ExecPass<'a> { if self.entry_cell_var == Some(cell) { self.entry_cell = Some(cell_id); } + // A state for this id means the same cell, with the same arguments, is + // still executing further up the stack: a cell that instantiates + // itself with its own arguments. Waiting for the depth limit would + // only report it later, with a less specific message. + if self.cell_states.contains_key(&cell_id) { + self.errors.push(ExecError { + span: None, + cell: cell_id, + kind: ExecErrorKind::RecursiveInstantiation { cell: cell_name }, + }); + return Err(()); + } self.partial_cells.push_back(cell_id); assert!( self.cell_states diff --git a/crates/compiler/src/compile/result.rs b/crates/compiler/src/compile/result.rs index 4b4b902..fd9f622 100644 --- a/crates/compiler/src/compile/result.rs +++ b/crates/compiler/src/compile/result.rs @@ -156,11 +156,20 @@ pub enum StaticErrorKind { /// An identifier was used without being declared in the current scope. #[error("`{name}` is not declared in this scope")] UndeclaredVar { name: String }, - /// A cell was referenced before its declaration was processed. + /// The type of a cell field depends on itself. + /// + /// Cells may refer to one another in any order, and a field's type is + /// computed on demand, so the only reference that cannot be resolved is + /// one that leads back to the field being typed: `a.f` reads `b.g`, which + /// reads `a.f`. Reported at the reference that closes the cycle. #[error( - "cannot use `{name}` before its declaration; move the `cell {name} ...` declaration above this use" + "the type of `{cell}.{field}` depends on itself, directly or through the fields of other cells" )] - UseBeforeDeclaration { name: String }, + CyclicCellField { cell: String, field: String }, + /// Typing a cell field required more nested field types than the compiler + /// allows. + #[error("cell field types depend on each other more than {limit} levels deep")] + CellTypingLimitExceeded { limit: usize }, /// A value of the given type cannot be called. #[error("cannot call type {0}")] CannotCall(String), @@ -310,6 +319,10 @@ pub enum ExecErrorKind { /// Function inlining or cell instantiation nested too deeply. #[error("recursion limit of {limit} exceeded")] RecursionLimitExceeded { limit: u32 }, + /// A cell instantiated itself with the arguments it was itself called + /// with, which no depth limit would end. + #[error("cell `{cell}` instantiates itself with its own arguments")] + RecursiveInstantiation { cell: String }, /// A shape, sequence, or iteration count exceeded a compiler limit. #[error("{what} exceeds the maximum of {limit}")] LimitExceeded { what: String, limit: usize }, diff --git a/crates/compiler/src/fingerprint.rs b/crates/compiler/src/fingerprint.rs index bf78f5c..7c810b8 100644 --- a/crates/compiler/src/fingerprint.rs +++ b/crates/compiler/src/fingerprint.rs @@ -328,12 +328,8 @@ impl Builder { out.insert(*var); } } - // Stop at the declaring cell. `CellTy::data` is an `Arc`-shared DAG - // of every field's type, including instantiated sub-cells, so - // descending it is exponential in hierarchy depth -- the exact - // blow-up `CellFnTy::cell`'s sharing exists to prevent. The - // declaring cell is itself an item whose own fingerprint covers its - // fields. + // A cell type is nominal: it names the declaring cell, which is + // itself an item whose own fingerprint covers its fields. Ty::Cell(cell) | Ty::Inst(cell) => { if let Some(def) = cell.def { out.insert(def); diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 5a6c495..c9ae128 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -228,6 +228,7 @@ mod tests { 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"); + const ARGON_RECURSIVE_CELL: &str = concatcp!(EXAMPLES_DIR, "/recursive_cell/lib.ar"); const ARGON_SHAPE_CELL_ARGS: &str = concatcp!(EXAMPLES_DIR, "/shape_cell_args/lib.ar"); // --------------------------------------------------------------------- @@ -748,9 +749,9 @@ mod tests { /// Axis 4: depth of cell hierarchy. Two series are produced: `single_ref` /// references each child once and `double_ref` references it twice. Both - /// are linear in depth because the structural cell type is shared across - /// references rather than copied (see `CellFnTy::cell`); `double_ref` is - /// kept as a regression guard against the old exponential expansion. + /// are linear in depth because a cell type is nominal and names its + /// declaration rather than carrying its fields; `double_ref` is kept as a + /// regression guard against the old exponential expansion. #[test] #[ignore = "scaling benchmark; run in release, serially: cargo test -p argonc --release -- --ignored --test-threads=1 bench_"] fn bench_hierarchy() { @@ -808,11 +809,11 @@ mod tests { } write_bench_csv("hierarchy_single_ref", &rows); - // `double_ref` binds the child cell twice. With the shared (`Arc`) - // structural cell type this scales the same as `single_ref`, so it is - // swept over the same depths. (Before that fix it expanded - // exponentially and had to be capped near depth 18.) Override - // `ARGON_BENCH_HIER_DOUBLE` to push deeper. + // `double_ref` binds the child cell twice. With nominal cell types + // this scales the same as `single_ref`, so it is swept over the same + // depths. (Before that fix it expanded exponentially and had to be + // capped near depth 18.) Override `ARGON_BENCH_HIER_DOUBLE` to push + // deeper. let mut rows = Vec::new(); for depth in bench_sizes( "ARGON_BENCH_HIER_DOUBLE", @@ -1316,32 +1317,315 @@ mod tests { assert!(scope_names.contains(&"2 else")); } + /// `examples/cell_out_of_order` is `examples/hierarchy` with `top` moved + /// above the `bot` it instantiates. Declaration order does not matter, so + /// the two produce the same layout. #[test] - fn argon_cell_out_of_order_reports_use_before_declaration() { - let o = parse_workspace_with_std(ARGON_CELL_OUT_OF_ORDER); + fn argon_cell_out_of_order() { + let digest = |path: &str| { + let o = parse_workspace_with_std(path); + assert!(o.static_errors().is_empty()); + let ast = o.ast(); + compile( + &ast, + CompileInput { + cell: &["top"], + args: Vec::new(), + }, + ) + .unwrap_valid() + .geometry_digest() + }; + assert_eq!( + digest(ARGON_CELL_OUT_OF_ORDER), + digest(ARGON_HIERARCHY), + "reordering declarations changed the layout" + ); + } + + /// The drawn rectangles across every compiled cell. + fn rect_count(data: &CompiledData) -> usize { + data.cells + .values() + .flat_map(|cell| cell.objects.values()) + .filter(|object| matches!(object, SolvedValue::Rect(rect) if !rect.construction)) + .count() + } + + #[test] + fn a_cell_may_read_fields_of_a_cell_declared_below_it() { + let errors = static_errors_of( + "cell top() {\n\ + let l = inst(bot());\n\ + eq(l.met1.x0, 0.);\n\ + let w = l.met1.w;\n\ + }\n\ + cell bot() {\n\ + let met1 = rect(\"met1\", x0=0., y0=0., x1=100., y1=100.);\n\ + }\n", + ); + assert!(errors.is_empty(), "{errors:?}"); + // A misspelled field of a later cell is still a plain field error. + assert!(matches!( + static_errors_of( + "cell top() {\n\ + let l = inst(bot());\n\ + eq(l.met2.x0, 0.);\n\ + }\n\ + cell bot() {\n\ + let met1 = rect(\"met1\", x0=0., y0=0., x1=100., y1=100.);\n\ + }\n" + ) + .as_slice(), + [StaticErrorKind::NoFieldOnTy { .. }] + )); + } + + /// `examples/recursive_cell`: `tree(n)` instantiates `tree(n - 1)`, so + /// `tree(3)` draws four squares across four compiled cells. + #[test] + fn a_cell_may_instantiate_itself() { + let o = parse_workspace_with_std(ARGON_RECURSIVE_CELL); assert!(o.static_errors().is_empty()); let ast = o.ast(); - let cells = compile( + let data = compile( &ast, CompileInput { - cell: &["top"], - args: Vec::new(), + cell: &["tree"], + args: vec![CellArg::Int(3)], }, + ) + .unwrap_valid(); + assert_eq!(data.cells.len(), 4); + assert_eq!(rect_count(&data), 4); + } + + /// Recursion that never ends is a run-time error, not a hang. + #[test] + fn unbounded_cell_recursion_is_reported() { + // The same arguments name the same cell, so the cycle is caught as + // soon as it closes. + let errors = compile_source( + "cell forever() {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + let again = inst(forever());\n\ + }\n", + "forever", + Vec::new(), + ) + .unwrap_exec_errors() + .errors; + assert!( + errors.iter().any(|error| matches!( + &error.kind, + ExecErrorKind::RecursiveInstantiation { cell } if cell == "forever" + )), + "{errors:?}" ); - let errors = cells.unwrap_static_errors(); - let error = errors + // Ever-changing arguments run into the depth limit instead. + let errors = crate::run_with_stack("argon-compile", || { + compile_source( + "cell forever(n: Int) {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + let again = inst(forever(n + 1));\n\ + }\n", + "forever", + vec![CellArg::Int(0)], + ) + .unwrap_exec_errors() .errors - .iter() - .find(|error| { - matches!( - &error.kind, - StaticErrorKind::UseBeforeDeclaration { name } if name == "bot" - ) - }) - .expect("expected an explicit use-before-declaration error for `bot`"); - assert_eq!( - error.kind.to_string(), - "cannot use `bot` before its declaration; move the `cell bot ...` declaration above this use" + }); + assert!( + errors + .iter() + .any(|error| matches!(error.kind, ExecErrorKind::RecursionLimitExceeded { .. })), + "{errors:?}" + ); + } + + #[test] + fn cells_may_instantiate_each_other() { + let data = compile_source( + "cell a(n: Int) {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=10., y1=10.);\n\ + if n > 0 {\n\ + let b = inst(b(n - 1));\n\ + eq(b.r.x0, r.x1);\n\ + eq(b.y, 0.);\n\ + } else {\n\ + };\n\ + }\n\ + cell b(n: Int) {\n\ + let r = rect(\"met2\", x0=0., y0=0., x1=10., y1=10.);\n\ + if n > 0 {\n\ + let a = inst(a(n - 1));\n\ + eq(a.r.x0, r.x1);\n\ + eq(a.y, 0.);\n\ + } else {\n\ + };\n\ + }\n", + "a", + vec![CellArg::Int(3)], + ) + .unwrap_valid(); + assert_eq!(rect_count(&data), 4); + } + + /// A field is typed when it is first read, so a statement may read a + /// field declared below it through an instance of the enclosing cell. + #[test] + fn a_field_declared_later_in_the_same_cell_is_typed_on_demand() { + let errors = static_errors_of( + "cell a(n: Int) {\n\ + let k = if n > 0 { inst(a(n - 1)).z.w } else { 0. };\n\ + let z = rect(\"met1\", x0=0., y0=0., x1=10., y1=10.);\n\ + }\n", + ); + assert!(errors.is_empty(), "{errors:?}"); + } + + /// A statement that reads two untyped fields queues a goal for each. When + /// the second field's statement reads the first, the first's queued goal + /// is worked before the second statement is retried. + #[test] + fn a_queued_field_read_again_is_typed_before_its_reader_is_retried() { + let errors = static_errors_of( + "cell x() {\n\ + eq(inst(a()).f.x0, inst(b()).g.x0);\n\ + }\n\ + cell a() {\n\ + let f = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + }\n\ + cell b() {\n\ + let g = inst(a()).f;\n\ + }\n", + ); + assert!(errors.is_empty(), "{errors:?}"); + } + + /// A top-level `let` that is in scope but not yet typed hides a module + /// declaration of the same name, so a statement typed on demand reads the + /// local rather than the cell. + #[test] + fn an_untyped_local_hides_a_module_declaration_of_the_same_name() { + let errors = static_errors_of( + "cell user() {\n\ + let v = inst(top()).a;\n\ + }\n\ + cell top() {\n\ + let bot = 5.;\n\ + let a = bot + 1.;\n\ + }\n\ + cell bot() {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + }\n", + ); + assert!(errors.is_empty(), "{errors:?}"); + } + + /// A statement typed on demand, ahead of the statements above it, sees the + /// same bindings it would when typed in order: the nearest `let` of each + /// name above it, typed as it is needed. + #[test] + fn a_statement_typed_out_of_order_sees_the_nearest_lets_above_it() { + let errors = static_errors_of( + "cell user() {\n\ + eq(inst(top()).c, 1.);\n\ + }\n\ + cell top() {\n\ + let r = 1.;\n\ + let s = r + 1.;\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + let c = r.w + s;\n\ + }\n", + ); + assert!(errors.is_empty(), "{errors:?}"); + } + + /// A field whose type depends on itself is reported once, at the read that + /// closes the cycle, and nothing else cascades from it. + #[test] + fn a_field_whose_type_depends_on_itself_is_an_error() { + let errors = static_errors_of("cell a(n: Int) {\n let f = inst(a(n - 1)).f;\n}\n"); + assert!( + matches!( + errors.as_slice(), + [StaticErrorKind::CyclicCellField { cell, field }] if cell == "a" && field == "f" + ), + "{errors:?}" + ); + + let source = + "cell a() {\n let f = inst(b()).g;\n}\ncell b() {\n let g = inst(a()).f;\n}\n"; + let errors = static_errors(source); + assert!( + matches!( + errors.as_slice(), + [crate::compile::StaticError { kind: StaticErrorKind::CyclicCellField { cell, field }, span }] + if cell == "a" && field == "f" && span.span.start() == source.rfind(".f").unwrap() + 1 + ), + "{errors:?}" + ); + + // Through a local: `z` reads `k`, whose type needs `z`. + let errors = static_errors_of( + "cell a(n: Int) {\n let k = inst(a(n - 1)).z;\n let z = k;\n}\n", + ); + assert!( + matches!( + errors.as_slice(), + [StaticErrorKind::CyclicCellField { cell, field }] if cell == "a" && field == "k" + ), + "{errors:?}" + ); + } + + /// Cell types are nominal: two cells with identical fields are different + /// types, and `Any` is how code accepts either. + #[test] + fn cell_types_are_nominal() { + let cells = "cell a() {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + }\n\ + cell b() {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + }\n"; + let errors = static_errors_of(&format!( + "{cells}cell top(flag: Bool) {{\n let c = if flag {{ a() }} else {{ b() }};\n}}\n" + )); + assert!( + matches!(errors.as_slice(), [StaticErrorKind::BranchesDifferentTypes]), + "{errors:?}" + ); + let errors = static_errors_of(&format!( + "{cells}fn erase(c: Any) -> Any {{ c }}\n\ + cell top(flag: Bool) {{\n let c = inst(if flag {{ erase(a()) }} else {{ erase(b()) }});\n}}\n" + )); + assert!(errors.is_empty(), "{errors:?}"); + } + + /// A top-level `let` may re-bind a name: each statement sees the nearest + /// `let` above it, and the field an instance answers is the last one. + #[test] + fn a_repeated_top_level_let_rebinds_sequentially() { + let cells = "cell top() {\n\ + let r = rect(\"met1\", x0=0., y0=0., x1=1., y1=1.);\n\ + let s = r.w;\n\ + let r = 5.;\n\ + let t = r + 1.;\n\ + }\n"; + let errors = static_errors_of(cells); + assert!(errors.is_empty(), "{errors:?}"); + let errors = static_errors_of(&format!( + "{cells}cell user() {{\n let i = inst(top());\n let v = i.r + 1.;\n}}\n" + )); + assert!(errors.is_empty(), "{errors:?}"); + let errors = static_errors_of(&format!( + "{cells}cell user() {{\n let i = inst(top());\n let v = i.r.w;\n}}\n" + )); + assert!( + matches!(errors.as_slice(), [StaticErrorKind::NoFieldOnTy { .. }]), + "{errors:?}" ); } @@ -3419,50 +3703,6 @@ cell top() { assert!(static_errors(&call("1., b=2.")).is_empty()); } - /// Cell typing is structural. `CellTy` carries the declaring cell's `VarId` - /// so that a field access can be navigated back to its `let`, and that id - /// is deliberately excluded from `PartialEq`: if it were not, two cells - /// with identical fields would stop being interchangeable and a branch - /// over them would silently widen to `Ty::Any`. - #[test] - fn structurally_identical_cells_remain_interchangeable() { - let source = |field: &str| { - format!( - r#" - cell left() {{ - let met = rect("met1", x0=0., y0=0., x1=10., y1=10.); - }} - - cell right() {{ - let met = rect("met1", x0=0., y0=0., x1=20., y1=20.); - }} - - cell top(pick: Bool) {{ - let chosen = if pick {{ left() }} else {{ right() }}; - let placed = inst(chosen); - eq(placed.{field}.x0, 0.); - }} - "# - ) - }; - - // The branches share a cell type, so the common field type-checks. - let errors = static_errors(&source("met")); - assert!(errors.is_empty(), "{errors:#?}"); - - // And the type is still a cell rather than `Ty::Any`: an unknown field - // is caught. Were the declaring cell's id part of `CellTy` equality, - // the two branches would no longer unify, `Ty::lub` would widen the - // branch to `Ty::Any`, and this error would silently disappear. - let errors = static_errors(&source("missing")); - assert!( - errors - .iter() - .any(|error| matches!(error.kind, StaticErrorKind::NoFieldOnTy { .. })), - "{errors:#?}" - ); - } - fn comparison_source(comparison: &str) -> String { format!( r#" diff --git a/crates/compiler/src/nav.rs b/crates/compiler/src/nav.rs index db871e0..1ce5d60 100644 --- a/crates/compiler/src/nav.rs +++ b/crates/compiler/src/nav.rs @@ -209,6 +209,10 @@ pub struct NavIndex { scope_bindings: HashMap>, expression_types: HashMap>, hover_details: HashMap>, + /// Cell `VarId` to the name and type of each of its fields, in declaration + /// order. Cell types are nominal and carry no fields of their own, so an + /// instance's completions come from the declaring cell's `let` bindings. + cell_field_types: HashMap>, } impl NavIndex { @@ -434,7 +438,71 @@ impl NavIndex { }) .map(|(_, ty)| ty) }); - ty.map(field_completions).unwrap_or_default() + ty.map(|ty| self.field_completions(ty)).unwrap_or_default() + } + + /// Completions for the fields of a value of type `ty`. + fn field_completions(&self, ty: &Ty) -> Vec { + let fields: Vec<(String, Ty)> = match ty { + Ty::Rect => [ + ("x0", Ty::Float), + ("x1", Ty::Float), + ("y0", Ty::Float), + ("y1", Ty::Float), + ("w", Ty::Float), + ("h", Ty::Float), + ("layer", Ty::String), + ] + .into_iter() + .map(|(name, ty)| (name.to_owned(), ty)) + .collect(), + Ty::Polygon => [ + ("points", Ty::Seq(Box::new(Ty::Point))), + ("layer", Ty::String), + ] + .into_iter() + .map(|(name, ty)| (name.to_owned(), ty)) + .collect(), + Ty::Path => [ + ("points", Ty::Seq(Box::new(Ty::Point))), + ("layer", Ty::String), + ("width", Ty::Float), + ("begin_extension", Ty::Float), + ("end_extension", Ty::Float), + ] + .into_iter() + .map(|(name, ty)| (name.to_owned(), ty)) + .collect(), + Ty::Point => [("x", Ty::Float), ("y", Ty::Float)] + .into_iter() + .map(|(name, ty)| (name.to_owned(), ty)) + .collect(), + Ty::Inst(cell) => RESERVED_CELL_FIELDS + .into_iter() + .map(|name| (name.to_owned(), Ty::Float)) + .chain( + cell.def + .and_then(|cell| self.cell_field_types.get(&cell)) + .into_iter() + .flatten() + .cloned(), + ) + .collect(), + Ty::Tuple(items) => items + .iter() + .cloned() + .enumerate() + .map(|(index, ty)| (index.to_string(), ty)) + .collect(), + _ => Vec::new(), + }; + let mut fields = fields + .into_iter() + .map(|(name, ty)| field_candidate(name, &ty)) + .collect::>(); + fields.sort_by(|left, right| left.label.cmp(&right.label)); + fields.dedup_by(|left, right| left.label == right.label); + fields } /// Items exported by a module, or variants of an enum, named by a path @@ -793,67 +861,6 @@ fn field_candidate(name: impl Into, ty: &Ty) -> CompletionCandidate { } } -fn field_completions(ty: &Ty) -> Vec { - let fields: Vec<(String, Ty)> = match ty { - Ty::Rect => [ - ("x0", Ty::Float), - ("x1", Ty::Float), - ("y0", Ty::Float), - ("y1", Ty::Float), - ("w", Ty::Float), - ("h", Ty::Float), - ("layer", Ty::String), - ] - .into_iter() - .map(|(name, ty)| (name.to_owned(), ty)) - .collect(), - Ty::Polygon => [ - ("points", Ty::Seq(Box::new(Ty::Point))), - ("layer", Ty::String), - ] - .into_iter() - .map(|(name, ty)| (name.to_owned(), ty)) - .collect(), - Ty::Path => [ - ("points", Ty::Seq(Box::new(Ty::Point))), - ("layer", Ty::String), - ("width", Ty::Float), - ("begin_extension", Ty::Float), - ("end_extension", Ty::Float), - ] - .into_iter() - .map(|(name, ty)| (name.to_owned(), ty)) - .collect(), - Ty::Point => [("x", Ty::Float), ("y", Ty::Float)] - .into_iter() - .map(|(name, ty)| (name.to_owned(), ty)) - .collect(), - Ty::Inst(cell) => RESERVED_CELL_FIELDS - .into_iter() - .map(|name| (name.to_owned(), Ty::Float)) - .chain( - cell.data - .iter() - .map(|(name, ty)| (name.clone(), ty.clone())), - ) - .collect(), - Ty::Tuple(items) => items - .iter() - .cloned() - .enumerate() - .map(|(index, ty)| (index.to_string(), ty)) - .collect(), - _ => Vec::new(), - }; - let mut fields = fields - .into_iter() - .map(|(name, ty)| field_candidate(name, &ty)) - .collect::>(); - fields.sort_by(|left, right| left.label.cmp(&right.label)); - fields.dedup_by(|left, right| left.label == right.label); - fields -} - fn declared_signature( keyword: &str, name: &str, @@ -995,18 +1002,27 @@ impl<'a> Builder<'a> { } } Decl::Cell(decl) => { - let fields = decl + let bindings = decl .scope .stmts .iter() .filter_map(|stmt| match stmt { - Statement::LetBinding(binding) => { - Some((binding.name.name.to_string(), binding.metadata)) - } + Statement::LetBinding(binding) => Some(binding), _ => None, }) + .collect::>(); + let fields = bindings + .iter() + .map(|binding| (binding.name.name.to_string(), binding.metadata)) .collect(); self.cell_fields.insert(decl.metadata.1, fields); + let field_types = bindings + .iter() + .map(|binding| (binding.name.name.to_string(), binding.value.ty())) + .collect(); + self.index + .cell_field_types + .insert(decl.metadata.1, field_types); self.params.insert(decl.metadata.1, params(&decl.args)); self.index .module_items @@ -1947,24 +1963,43 @@ cell top() { ); } - /// Cells are not hoisted: `VarIdTyPass` binds a cell only after walking its - /// body, so using one before its declaration is a `UseBeforeDeclaration` - /// error (see `examples/cell_out_of_order`). Navigation reports what the - /// compiler resolved, so there is deliberately nothing to jump to. + /// Cell types are bound before any body is walked, so a cell used before + /// its declaration resolves like a function does (see + /// `examples/cell_out_of_order`), and so do the fields of its instances. #[test] - fn a_cell_used_before_its_declaration_is_unresolved() { + fn a_cell_used_before_its_declaration_resolves() { check( r#" cell top() { let c = lat$0er(); let i = inst(c); + eq(i.r$0.x0, 0.); } cell later() { let r = rect("met1"); } "#, - &["Unresolved"], + &["later#1", "r#3"], + ); + } + + /// A recursive cell reads the fields of an instance of itself; they + /// resolve to the `let`s of the enclosing declaration. + #[test] + fn a_recursive_cell_resolves_its_own_fields() { + check( + r#" +cell tree(n: Int) { + let leaf = rect("met1", x0=0., y0=0., w=10., h=10.); + if n > 0 { + let child = inst(tr$0ee(n - 1)); + eq(child.le$0af.x0, leaf.x1); + } else { + }; +} +"#, + &["tree#0", "leaf#0"], ); } diff --git a/examples/recursive_cell/Argon.toml b/examples/recursive_cell/Argon.toml new file mode 100644 index 0000000..ac74931 --- /dev/null +++ b/examples/recursive_cell/Argon.toml @@ -0,0 +1,2 @@ +name = "recursive-cell" +tech = "../tech/basic.tech.toml" diff --git a/examples/recursive_cell/lib.ar b/examples/recursive_cell/lib.ar new file mode 100644 index 0000000..a9e06bd --- /dev/null +++ b/examples/recursive_cell/lib.ar @@ -0,0 +1,12 @@ +// A cell may instantiate itself. `tree(n)` places a copy of `tree(n - 1)` to +// the right of its own square, so `tree(n)` is a row of `n + 1` squares. The +// recursion ends at `tree(0)`, whose `if` body is never evaluated. +cell tree(n: Int) { + let leaf = rect("met1", x0=0., y0=0., w=100., h=100.); + if n > 0 { + let child = inst(tree(n - 1)); + eq(child.leaf.x0, leaf.x1 + 50.); + eq(child.y, 0.); + } else { + }; +} diff --git a/examples/stress_hierarchy/lib.ar b/examples/stress_hierarchy/lib.ar index 6784905..8f8b7c9 100644 --- a/examples/stress_hierarchy/lib.ar +++ b/examples/stress_hierarchy/lib.ar @@ -5,8 +5,8 @@ cell h0() { // Each level `h{k}` instantiates the level below it (`h{k-1}`) and adds one // rectangle, building a layout that is `k` cells deep. Compiling `h{k}` therefore // exercises `k` levels of hierarchy. The instance is bound to a single variable -// (`i`); binding the child cell to an additional variable would cause the -// structural cell type to expand exponentially with depth (see bench/README.md). +// (`i`); the `let c = child(); let i = inst(c);` idiom is benchmarked separately +// (see bench/README.md). cell h1() { rect("met1", x0=0., y0=0., x1=10., y1=10.); let i = inst(h0());