From 5698ce7f8829c47c241e55d1f234561e6b0e738d Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Fri, 4 Sep 2026 12:01:26 -0700 Subject: [PATCH 1/2] feat(lang): support rects as cell arguments --- README.md | 34 +++ crates/compiler/src/compile.rs | 351 ++++++++++++++++++++++++-- crates/compiler/src/compile/result.rs | 2 +- crates/compiler/src/gdscache.rs | 47 ++++ crates/compiler/src/incremental.rs | 54 ++++ crates/compiler/src/lib.rs | 327 ++++++++++++++++++++++++ examples/shape_cell_args/Argon.toml | 2 + examples/shape_cell_args/lib.ar | 32 +++ 8 files changed, 827 insertions(+), 22 deletions(-) create mode 100644 examples/shape_cell_args/Argon.toml create mode 100644 examples/shape_cell_args/lib.ar diff --git a/README.md b/README.md index 48e3b8d..a106b27 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,40 @@ command line: arc run --cell 'via(ViaParams { layer: "met1", size: Size { w: 100., h: 50. }, n: 1 })' ``` +Shapes are valid cell arguments too. A `Rect`, `Polygon`, `Path`, or `Point` +parameter receives the shape by value: the caller's solver resolves its +coordinates first, and the cell sees them as constants in its own coordinate +frame, exactly as a `Float` argument arrives as a number. Placing an instance +elsewhere does not move them, and constraints inside the cell cannot move the +caller's geometry; share live geometry with a `fn` instead. Inside the cell the +shape is construction geometry: it is not drawn and does not count toward the +cell's extent, but its edges can constrain what the cell draws. `!` draws it on +its layer when it was drawn in the caller, so layout geometry and shapes read +out of an instance qualify while a `crect` or a `bbox` does not. Shapes at +different positions are different arguments, so they compile to different +cells: + +```rust +cell via_array(region: Rect, size: Float, pitch: Float) { + let via = crect(layer="via1", x0=0., y0=0., w=size, h=size); + let vias = std::max_array(via, region.w, region.h, pitch, pitch); + eq(vias.x0 - region.x0, region.x1 - vias.x1); + eq(vias.y0 - region.y0, region.y1 - vias.y1); +} + +cell top() { + let met1 = rect("met1", x0=0., y0=0., w=100., h=50.); + let met2 = rect("met2", x0=10., y0=5., w=80., h=40.); + let vias = inst(via_array(std::intersection(met1, met2), 10., 20.)); +} +``` + +Shapes built on the command line work the same way: + +```bash +arc run --cell 'via_array(crect(x0=0., y0=0., w=90., h=40.), 10., 20.)' +``` + 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/compile.rs b/crates/compiler/src/compile.rs index fffb7b7..41dbcf3 100644 --- a/crates/compiler/src/compile.rs +++ b/crates/compiler/src/compile.rs @@ -3528,6 +3528,24 @@ impl<'a> AstTransformer for VarIdTyPass<'a> { } } +/// A value passed to a cell. +/// +/// A cell is compiled on its own and named by its arguments, so an argument +/// has to be plain data: whatever the caller wrote, resolved to constants +/// before the cell runs. +/// +/// Shapes are passed *by value*. The caller's solver resolves their +/// coordinates, and the cell receives those numbers unchanged, in its own +/// coordinate frame: a cell can be instantiated anywhere, and any number of +/// times, so nothing about a placement can reach them. Constraints the cell +/// writes against a shape argument cannot move the caller's geometry either. +/// Sharing live solver variables across a call is what `fn` is for. +/// +/// Inside the cell a shape argument is construction geometry, so it draws +/// nothing on its own. `!` draws it on its layer when it is *drawable*: when +/// it stood for geometry drawn in the caller's layout, meaning it has a layer +/// and is either layout geometry or a proxy of an instance's geometry. A +/// `crect` or a `bbox` is not drawable, so `!` leaves it alone. #[derive(Debug, Clone)] pub enum CellArg { Float(f64), @@ -3543,6 +3561,33 @@ pub enum CellArg { name: String, fields: Vec<(String, CellArg)>, }, + /// A rectangle, like [`Value::Rect`]: its layer and its solved corners. + Rect { + layer: Option, + drawable: bool, + x0: f64, + y0: f64, + x1: f64, + y1: f64, + }, + /// A polygon, like [`Value::Polygon`]: its layer and its solved vertices. + Polygon { + layer: String, + drawable: bool, + points: Vec<(f64, f64)>, + }, + /// A path, like [`Value::Path`]: its layer and its solved width, + /// centerline, and end extensions. + Path { + layer: String, + drawable: bool, + width: f64, + points: Vec<(f64, f64)>, + begin_extension: f64, + end_extension: f64, + }, + /// A point, like [`Value::Point`]: its solved `x` and `y`. + Point(f64, f64), } impl CellArg { @@ -3552,7 +3597,11 @@ impl CellArg { (Self::Float(_), Ty::Float) | (Self::Int(_), Ty::Int) | (Self::Bool(_), Ty::Bool) - | (Self::String(_), Ty::String) => true, + | (Self::String(_), Ty::String) + | (Self::Rect { .. }, Ty::Rect) + | (Self::Polygon { .. }, Ty::Polygon) + | (Self::Path { .. }, Ty::Path) + | (Self::Point(..), Ty::Point) => true, (Self::Enum(variant), Ty::Enum(ty)) => ty.variants.contains(variant), (Self::Seq(values), Ty::Seq(inner)) => { values.iter().all(|value| value.matches_ty(inner)) @@ -3579,6 +3628,10 @@ impl CellArg { Self::Enum(_) => "enum variant", Self::Seq(_) => "sequence", Self::Struct { .. } => "struct", + Self::Rect { .. } => "Rect", + Self::Polygon { .. } => "Polygon", + Self::Path { .. } => "Path", + Self::Point(..) => "Point", } } } @@ -3590,6 +3643,8 @@ struct CellExecKey { scope_name: Option, } +/// A [`CellArg`] as something that can be hashed and compared: floats become +/// their bits. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) enum CellArgKey { Float(u64), @@ -3599,6 +3654,37 @@ pub(crate) enum CellArgKey { Enum(String), Seq(Vec), Struct(String, Vec<(String, CellArgKey)>), + /// Layer, drawability, and `x0, y0, x1, y1`. + Rect(Option, bool, [u64; 4]), + /// Layer, drawability, and the vertices. + Polygon(String, bool, Vec<(u64, u64)>), + /// Layer, drawability, the width and the begin and end extensions, and the + /// centerline points. + Path(String, bool, [u64; 3], Vec<(u64, u64)>), + Point(u64, u64), +} + +/// The bits of each coordinate pair. +fn point_bits(points: &[(f64, f64)]) -> Vec<(u64, u64)> { + points + .iter() + .map(|(x, y)| (x.to_bits(), y.to_bits())) + .collect() +} + +/// Consecutive coordinates paired back up into points. +fn pair_up(coords: &[f64]) -> Vec<(f64, f64)> { + coords + .as_chunks::<2>() + .0 + .iter() + .map(|&[x, y]| (x, y)) + .collect() +} + +/// Constant coordinate pairs as solver expressions. +fn constant_points(points: &[(f64, f64)]) -> Vec<(LinearExpr, LinearExpr)> { + points.iter().map(|&(x, y)| (x.into(), y.into())).collect() } impl From<&CellArg> for CellArgKey { @@ -3617,6 +3703,41 @@ impl From<&CellArg> for CellArgKey { .map(|(field, value)| (field.clone(), Self::from(value))) .collect(), ), + CellArg::Rect { + layer, + drawable, + x0, + y0, + x1, + y1, + } => Self::Rect( + layer.clone(), + *drawable, + [x0.to_bits(), y0.to_bits(), x1.to_bits(), y1.to_bits()], + ), + CellArg::Polygon { + layer, + drawable, + points, + } => Self::Polygon(layer.clone(), *drawable, point_bits(points)), + CellArg::Path { + layer, + drawable, + width, + points, + begin_extension, + end_extension, + } => Self::Path( + layer.clone(), + *drawable, + [ + width.to_bits(), + begin_extension.to_bits(), + end_extension.to_bits(), + ], + point_bits(points), + ), + CellArg::Point(x, y) => Self::Point(x.to_bits(), y.to_bits()), } } } @@ -4692,9 +4813,15 @@ impl<'a> ExecPass<'a> { ) .is_none() ); - for (val, decl) in args.into_iter().zip(cell_decl.args.iter()) { + for (arg, decl) in args.into_iter().zip(cell_decl.args.iter()) { let vid = self.value_id(); - let val = Value::from_arg(&val); + // Built directly: `Self::span` resolves through a location, and + // the cell has no frame yet. + let span = Span { + path: cell_decl.metadata.0.clone(), + span: decl.name.span, + }; + let val = self.bind_cell_arg(cell_id, &span, &arg); self.values.insert(vid, DeferValue::Ready(val)); frame.bindings.insert(decl.metadata.0, vid); } @@ -6186,10 +6313,156 @@ impl<'a> ExecPass<'a> { .insert(dependent); } + /// Binds an argument of the cell `cell_id` as a value in it. + /// + /// Scalars, sequences, and structs are copied. A shape becomes + /// construction geometry of the cell, rebuilt from the constants the + /// caller resolved. It is registered as an object, so that emitting it or + /// binding it to a field never leaves a dangling id, but not emitted + /// itself, so it draws nothing and adds nothing to the cell's extent. A + /// drawable shape is also recorded as a proxy, which is how `!` opts in + /// to drawing it here on its own layer -- see + /// [`mark_emitted_proxies_as_layout`]. `span` is the parameter's + /// declaration, which is where the shape is attributed. + fn bind_cell_arg(&mut self, cell_id: CellId, span: &Span, arg: &CellArg) -> Value { + match arg { + CellArg::Int(i) => Value::Int(*i), + CellArg::Bool(b) => Value::Bool(*b), + CellArg::Float(f) => Value::Linear(LinearExpr::from(*f)), + CellArg::String(s) => Value::String(s.clone()), + CellArg::Enum(v) => Value::EnumValue(v.clone()), + CellArg::Seq(v) => Value::Seq( + v.iter() + .map(|arg| self.bind_cell_arg(cell_id, span, arg)) + .collect(), + ), + CellArg::Struct { name, fields } => Value::Struct(Box::new(StructValue { + name: name.clone(), + fields: fields + .iter() + .map(|(field, arg)| (field.clone(), self.bind_cell_arg(cell_id, span, arg))) + .collect(), + })), + CellArg::Rect { + layer, + drawable, + x0, + y0, + x1, + y1, + } => { + let rect = Rect { + id: self.object_id(), + layer: layer.clone(), + x0: (*x0).into(), + y0: (*y0).into(), + x1: (*x1).into(), + y1: (*y1).into(), + construction: true, + span: Some(span.clone()), + }; + self.register_shape_arg(cell_id, rect.id, rect.clone().into(), *drawable); + Value::Rect(rect) + } + CellArg::Polygon { + layer, + drawable, + points, + } => { + let polygon = Polygon { + id: self.object_id(), + layer: layer.clone(), + points: constant_points(points), + construction: true, + span: Some(span.clone()), + }; + self.register_shape_arg(cell_id, polygon.id, polygon.clone().into(), *drawable); + Value::Polygon(polygon) + } + CellArg::Path { + layer, + drawable, + width, + points, + begin_extension, + end_extension, + } => { + let path = Path { + id: self.object_id(), + layer: layer.clone(), + width: (*width).into(), + points: constant_points(points), + begin_extension: (*begin_extension).into(), + end_extension: (*end_extension).into(), + construction: true, + span: Some(span.clone()), + }; + self.register_shape_arg(cell_id, path.id, path.clone().into(), *drawable); + Value::Path(path) + } + CellArg::Point(x, y) => Value::Point(((*x).into(), (*y).into())), + } + } + + /// Registers a shape argument as an object of `cell_id`. See + /// [`Self::bind_cell_arg`]. + fn register_shape_arg( + &mut self, + cell_id: CellId, + id: ObjectId, + object: Object, + drawable: bool, + ) { + let state = self.cell_state_mut(cell_id); + state.objects.insert(id, object); + if drawable { + state.proxy_objects.insert(id); + } + } + + /// Resolves `exprs` in the solver of `cell_id`, snapped to the grid like + /// every other coordinate. When any of them is still unsolved, records + /// `dependent_vid` as waiting on every variable they mention and returns + /// `None`, so that the conversion is retried once the solver progresses. + fn resolve_constants<'e>( + &mut self, + cell_id: CellId, + dependent_vid: ValueId, + exprs: impl IntoIterator, + ) -> Option> { + let exprs: Vec<&LinearExpr> = exprs.into_iter().collect(); + let solver = &self.cell_state(cell_id).solver; + let values: Option> = exprs.iter().map(|expr| solver.eval_expr(expr)).collect(); + if values.is_none() { + for expr in exprs { + for (_, var) in expr.coeffs.clone() { + self.add_var_dependent(cell_id, var, dependent_vid); + } + } + } + values + } + + /// Whether a shape of `cell_id` stands for geometry drawn in its layout: + /// it has a layer, and it is either layout geometry or a proxy of an + /// instance's geometry. See [`CellArg`]. + fn shape_drawable( + &self, + cell_id: CellId, + id: ObjectId, + construction: bool, + has_layer: bool, + ) -> bool { + has_layer && (!construction || self.cell_state(cell_id).proxy_objects.contains(&id)) + } + /// Converts an evaluated value into a cell argument. `Ok(None)` means the /// value depends on solver variables that are not resolved yet, so the /// conversion should be retried; `Err` means the value can never be passed /// to a cell and an error has been recorded. + /// + /// A shape is passed by value: its coordinates are resolved in this cell's + /// solver, and the callee receives the constants. See [`CellArg`]. pub fn cell_arg_from_value( &mut self, cell_id: CellId, @@ -6235,6 +6508,60 @@ impl<'a> ExecPass<'a> { fields, }) } + Value::Rect(r) => { + let Some(coords) = + self.resolve_constants(cell_id, dependent_vid, [&r.x0, &r.y0, &r.x1, &r.y1]) + else { + return Ok(None); + }; + Some(CellArg::Rect { + layer: r.layer.clone(), + drawable: self.shape_drawable(cell_id, r.id, r.construction, r.layer.is_some()), + x0: coords[0], + y0: coords[1], + x1: coords[2], + y1: coords[3], + }) + } + Value::Polygon(p) => { + let Some(coords) = self.resolve_constants( + cell_id, + dependent_vid, + p.points.iter().flat_map(|(x, y)| [x, y]), + ) else { + return Ok(None); + }; + Some(CellArg::Polygon { + layer: p.layer.clone(), + drawable: self.shape_drawable(cell_id, p.id, p.construction, true), + points: pair_up(&coords), + }) + } + Value::Path(p) => { + let Some(coords) = self.resolve_constants( + cell_id, + dependent_vid, + [&p.width, &p.begin_extension, &p.end_extension] + .into_iter() + .chain(p.points.iter().flat_map(|(x, y)| [x, y])), + ) else { + return Ok(None); + }; + Some(CellArg::Path { + layer: p.layer.clone(), + drawable: self.shape_drawable(cell_id, p.id, p.construction, true), + width: coords[0], + begin_extension: coords[1], + end_extension: coords[2], + points: pair_up(&coords[3..]), + }) + } + Value::Point((x, y)) => { + let Some(coords) = self.resolve_constants(cell_id, dependent_vid, [x, y]) else { + return Ok(None); + }; + Some(CellArg::Point(coords[0], coords[1])) + } // 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. @@ -8685,24 +9012,6 @@ impl Value { } } - pub fn from_arg(arg: &CellArg) -> Self { - match arg { - CellArg::Int(i) => Value::Int(*i), - CellArg::Bool(b) => Value::Bool(*b), - CellArg::Float(f) => Value::Linear(LinearExpr::from(*f)), - 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(), - })), - } - } - /// Name of this value's kind, for diagnostics. fn kind_name(&self) -> &'static str { match self { diff --git a/crates/compiler/src/compile/result.rs b/crates/compiler/src/compile/result.rs index 5e69722..4b4b902 100644 --- a/crates/compiler/src/compile/result.rs +++ b/crates/compiler/src/compile/result.rs @@ -225,7 +225,7 @@ pub enum ExecErrorKind { found: String, }, /// A cell invocation supplied an argument whose value cannot be passed to a - /// cell, such as a rectangle or an instance. + /// cell, such as an instance or a cell. #[error("invalid cell argument: a {0} value cannot be passed to a cell")] UnsupportedCellArgument(String), /// An argument in a cell invocation does not reduce to a constant, so it diff --git a/crates/compiler/src/gdscache.rs b/crates/compiler/src/gdscache.rs index 996a828..360e7d3 100644 --- a/crates/compiler/src/gdscache.rs +++ b/crates/compiler/src/gdscache.rs @@ -195,6 +195,53 @@ fn hash_cell_arg_key(hasher: &mut fnv::FnvHasher, arg: &CellArgKey) { hash_cell_arg_key(hasher, value); } } + CellArgKey::Rect(layer, drawable, corners) => { + hasher.write_u8(7); + // Tagged by presence, so that no layer and an empty layer name + // differ. + match layer { + Some(layer) => { + hasher.write_u8(1); + hasher.write_usize(layer.len()); + hasher.write(layer.as_bytes()); + } + None => hasher.write_u8(0), + } + hasher.write_u8(u8::from(*drawable)); + for bits in corners { + hasher.write_u64(*bits); + } + } + CellArgKey::Polygon(layer, drawable, points) => { + hasher.write_u8(8); + hasher.write_usize(layer.len()); + hasher.write(layer.as_bytes()); + hasher.write_u8(u8::from(*drawable)); + hash_points(hasher, points); + } + CellArgKey::Path(layer, drawable, lengths, points) => { + hasher.write_u8(9); + hasher.write_usize(layer.len()); + hasher.write(layer.as_bytes()); + hasher.write_u8(u8::from(*drawable)); + for bits in lengths { + hasher.write_u64(*bits); + } + hash_points(hasher, points); + } + CellArgKey::Point(x, y) => { + hasher.write_u8(10); + hasher.write_u64(*x); + hasher.write_u64(*y); + } + } +} + +fn hash_points(hasher: &mut fnv::FnvHasher, points: &[(u64, u64)]) { + hasher.write_usize(points.len()); + for (x, y) in points { + hasher.write_u64(*x); + hasher.write_u64(*y); } } diff --git a/crates/compiler/src/incremental.rs b/crates/compiler/src/incremental.rs index 5ce46da..d934a1f 100644 --- a/crates/compiler/src/incremental.rs +++ b/crates/compiler/src/incremental.rs @@ -770,10 +770,64 @@ fn hash_cell_args(args: &[CellArg], hasher: &mut impl Hasher) { hash_cell_args(std::slice::from_ref(value), hasher); } } + CellArg::Rect { + layer, + drawable, + x0, + y0, + x1, + y1, + } => { + 7_u8.hash(hasher); + layer.hash(hasher); + drawable.hash(hasher); + for value in [x0, y0, x1, y1] { + value.to_bits().hash(hasher); + } + } + CellArg::Polygon { + layer, + drawable, + points, + } => { + 8_u8.hash(hasher); + layer.hash(hasher); + drawable.hash(hasher); + hash_points(points, hasher); + } + CellArg::Path { + layer, + drawable, + width, + points, + begin_extension, + end_extension, + } => { + 9_u8.hash(hasher); + layer.hash(hasher); + drawable.hash(hasher); + for value in [width, begin_extension, end_extension] { + value.to_bits().hash(hasher); + } + hash_points(points, hasher); + } + CellArg::Point(x, y) => { + 10_u8.hash(hasher); + x.to_bits().hash(hasher); + y.to_bits().hash(hasher); + } } } } +fn hash_points(points: &[(f64, f64)], hasher: &mut impl Hasher) { + points.len().hash(hasher); + for (x, y) in points { + x.to_bits().hash(hasher); + y.to_bits().hash(hasher); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 4132b31..508c69a 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -166,6 +166,19 @@ mod tests { &WorkspaceConfig::default().with_tech(Some(PathBuf::from(SKY130_TECH))), ) } + + /// Compiles `cell` from a one-file workspace without the standard library. + fn compile_source(source: &str, cell: &str, args: Vec) -> CompileOutput { + let root = parse_source_text(source, PathBuf::from("/virtual/lib.ar")).unwrap(); + let ast = IndexMap::from([(Vec::new(), root)]); + compile( + &ast, + CompileInput { + cell: &[cell], + args, + }, + ) + } const ARGON_IMMEDIATE: &str = concatcp!(EXAMPLES_DIR, "/immediate/lib.ar"); const ARGON_IF: &str = concatcp!(EXAMPLES_DIR, "/if/lib.ar"); const ARGON_IF_INCONSISTENT: &str = concatcp!(EXAMPLES_DIR, "/if_inconsistent/lib.ar"); @@ -215,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_SHAPE_CELL_ARGS: &str = concatcp!(EXAMPLES_DIR, "/shape_cell_args/lib.ar"); // --------------------------------------------------------------------- // Scaling / stress benchmarks. @@ -1839,6 +1853,319 @@ mod tests { ); } + /// Shapes are passed to cells by value: the caller's solved coordinates + /// arrive as constants, and `!` draws the ones that were drawn in the + /// caller. + #[test] + fn argon_shape_cell_args() { + let o = parse_workspace_with_std(ARGON_SHAPE_CELL_ARGS); + 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]; + 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 [vias, outline] = insts.as_slice() else { + panic!("expected two instances, got {}", insts.len()); + }; + + let via_array = &cells.cells[&vias.cell]; + let rects = |cell: &crate::compile::CompiledCell| { + cell.objects + .values() + .filter_map(|object| object.get_rect()) + .cloned() + .collect::>() + }; + // The 80 x 40 overlap arrives as construction geometry, untranslated. + let region = rects(via_array) + .into_iter() + .find(|rect| rect.construction && relative_eq!(rect.x0.0, 10.)) + .expect("the region is construction geometry"); + assert_eq!(region.layer, None); + assert_relative_eq!(region.y0.0, 5., epsilon = EPSILON); + assert_relative_eq!(region.x1.0, 90., epsilon = EPSILON); + assert_relative_eq!(region.y1.0, 45., epsilon = EPSILON); + // Four columns and two rows of vias, centered in the region. + let drawn: Vec<_> = rects(via_array) + .into_iter() + .filter(|rect| !rect.construction) + .collect(); + assert_eq!(drawn.len(), 8); + assert!( + drawn + .iter() + .all(|rect| rect.layer.as_deref() == Some("via1")) + ); + let x0 = drawn.iter().map(|r| r.x0.0).fold(f64::INFINITY, f64::min); + let x1 = drawn + .iter() + .map(|r| r.x1.0) + .fold(f64::NEG_INFINITY, f64::max); + let y0 = drawn.iter().map(|r| r.y0.0).fold(f64::INFINITY, f64::min); + let y1 = drawn + .iter() + .map(|r| r.y1.0) + .fold(f64::NEG_INFINITY, f64::max); + assert_relative_eq!(x0, 15., epsilon = EPSILON); + assert_relative_eq!(x1, 85., epsilon = EPSILON); + assert_relative_eq!(y0, 10., epsilon = EPSILON); + assert_relative_eq!(y1, 40., epsilon = EPSILON); + + let outline = &cells.cells[&outline.cell]; + // `shape!` draws the polygon on its layer. + let polygon = outline + .objects + .values() + .find_map(|object| object.get_polygon()) + .expect("outline holds the polygon"); + assert!(!polygon.construction); + assert_eq!(polygon.layer, "met1"); + assert_eq!(polygon.points.len(), 3); + assert_relative_eq!(polygon.points[2].0.0, 20., epsilon = EPSILON); + assert_relative_eq!(polygon.points[2].1.0, 30., epsilon = EPSILON); + // `guide!` leaves the crect undrawn. + let rects = rects(outline); + let guide = rects + .iter() + .find(|rect| rect.construction) + .expect("the guide is construction geometry"); + assert_eq!(guide.layer, None); + assert_relative_eq!(guide.x0.0, 10., epsilon = EPSILON); + // The marker sits on the point read out of the polygon. + let marker = rects + .iter() + .find(|rect| !rect.construction) + .expect("the marker is drawn"); + assert_eq!(marker.layer.as_deref(), Some("met2")); + assert_relative_eq!(marker.x0.0, 20., epsilon = EPSILON); + assert_relative_eq!(marker.y0.0, 30., epsilon = EPSILON); + assert_relative_eq!(marker.x1.0, 25., epsilon = EPSILON); + assert_relative_eq!(marker.y1.0, 35., epsilon = EPSILON); + } + + /// Shapes nested in sequences and structs, and proxies read out of an + /// instance, all arrive as construction geometry; `!` redraws exactly the + /// ones that were drawn in the caller. + #[test] + fn nested_and_proxied_shape_cell_arguments() { + let source = r#" +struct Pair { + a: Rect, + b: Point, +} + +cell leaf() { + let m = rect("met1", x0=0., y0=0., w=10., h=10.); +} + +cell child(rs: [Rect], p: Pair, proxied: Rect) { + let first = head(rs)!; + let second = head(tail(rs))!; + let redrawn = proxied!; + let m = rect("met2", x0=p.b.x, y0=p.b.y, w=p.a.w, h=p.a.h); +} + +cell top() { + let r1 = rect("met1", x0=0., y0=0., w=10., h=10.); + let r2 = crect(layer="via1", x0=50., y0=50., w=5., h=5.); + let poly = polygon("met1", 3, x0=0., y0=0., x1=4., y1=0., x2=2., y2=3.); + let l = inst(leaf(), x=100., y=0.); + let c = inst(child(cons(r1, cons(r2, [])), Pair { a: r2, b: poly.points[2] }, l.m), x=0., y=0.); +} +"#; + let cells = compile_source(source, "top", Vec::new()).unwrap_valid(); + let top = &cells.cells[&cells.top]; + let child = top + .objects + .values() + .filter_map(|object| object.get_instance()) + .find(|inst| relative_eq!(inst.x, 0.)) + .expect("child instance"); + let child = &cells.cells[&child.cell]; + let rects: Vec<_> = child + .objects + .values() + .filter_map(|object| object.get_rect()) + .collect(); + let at = |x0: f64, y0: f64| { + rects + .iter() + .filter(|rect| relative_eq!(rect.x0.0, x0) && relative_eq!(rect.y0.0, y0)) + .collect::>() + }; + // `r1` was layout in `top`, so `first!` draws it here. + let [first] = at(0., 0.)[..] else { + panic!("expected one rect at the origin"); + }; + assert!(!first.construction); + assert_eq!(first.layer.as_deref(), Some("met1")); + // `r2` is a crect. It arrives twice, once in the sequence and once in + // the struct, and `second!` draws neither. + let second = at(50., 50.); + assert_eq!(second.len(), 2); + assert!( + second + .iter() + .all(|rect| rect.construction && rect.layer.as_deref() == Some("via1")) + ); + // `l.m` is a proxy of drawn geometry, in `top`'s frame, so `!` redraws + // it where `top` sees it. + let [redrawn] = at(100., 0.)[..] else { + panic!("expected one rect at the proxy's position"); + }; + assert!(!redrawn.construction); + assert_eq!(redrawn.layer.as_deref(), Some("met1")); + assert_relative_eq!(redrawn.x1.0, 110., epsilon = EPSILON); + // The struct's point and rect were read as constants. + let [m] = at(2., 3.)[..] else { + panic!("expected one rect at the polygon's vertex"); + }; + assert_eq!(m.layer.as_deref(), Some("met2")); + assert_relative_eq!(m.x1.0, 7., epsilon = EPSILON); + assert_relative_eq!(m.y1.0, 8., epsilon = EPSILON); + } + + /// Shape values passed through the positional cell API are checked against + /// the declared parameter type, and name the cell like every other + /// argument. + #[test] + fn shape_cell_arguments_are_checked_and_name_the_cell() { + let ast = parse_workspace_with_std(ARGON_SHAPE_CELL_ARGS).ast(); + let region = |x0: f64| CellArg::Rect { + layer: None, + drawable: false, + x0, + y0: 0., + x1: x0 + 90., + y1: 40., + }; + let via_array = |args: Vec| { + compile( + &ast, + CompileInput { + cell: &["via_array"], + args, + }, + ) + }; + let cells = + via_array(vec![region(0.), CellArg::Float(10.), CellArg::Float(20.)]).unwrap_valid(); + // Five columns and two rows fit a 90 x 40 region. + let drawn = cells.cells[&cells.top] + .objects + .values() + .filter_map(|object| object.get_rect()) + .filter(|rect| !rect.construction) + .count(); + assert_eq!(drawn, 10); + // The same shape names the same cell; a shape elsewhere is a different + // argument, so it is a different cell. + let same = + via_array(vec![region(0.), CellArg::Float(10.), CellArg::Float(20.)]).unwrap_valid(); + assert_eq!(cells.top, same.top); + let moved = + via_array(vec![region(100.), CellArg::Float(10.), CellArg::Float(20.)]).unwrap_valid(); + assert_ne!(cells.top, moved.top); + + let mismatch = |args: Vec, index: usize, expected: &str, found: &str| { + let errors = via_array(args).unwrap_exec_errors().errors; + assert!( + errors.iter().any(|error| matches!( + &error.kind, + ExecErrorKind::InvalidCellArgumentType { index: i, expected: e, found: f } + if *i == index && e == expected && f == found + )), + "{errors:?}" + ); + }; + mismatch( + vec![CellArg::Float(1.), CellArg::Float(10.), CellArg::Float(20.)], + 1, + "Rect", + "Float", + ); + mismatch( + vec![region(0.), region(0.), CellArg::Float(20.)], + 2, + "Float", + "Rect", + ); + } + + /// A shape built in a cell invocation, as `arc run --cell` and the GUI + /// open-cell command supply it. The invocation's own geometry is not part + /// of the output. + #[test] + fn argon_shape_cell_invocation() { + let mut ast = parse_workspace_with_std(ARGON_SHAPE_CELL_ARGS).ast(); + let invocation = crate::parse::splice_cell_invocation( + &mut ast, + "via_array(crect(x0=0., y0=0., w=90., h=40.), 10., 20.)", + ) + .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(); + assert_eq!(cells.cells.len(), 1); + let top = &cells.cells[&cells.top]; + assert!(top.name.ends_with("via_array"), "{}", top.name); + let region = top + .objects + .values() + .filter_map(|object| object.get_rect()) + .find(|rect| rect.construction && rect.layer.is_none() && relative_eq!(rect.x1.0, 90.)) + .expect("the region arrives as construction geometry"); + assert_relative_eq!(region.y1.0, 40., epsilon = EPSILON); + let drawn = top + .objects + .values() + .filter_map(|object| object.get_rect()) + .filter(|rect| !rect.construction) + .count(); + assert_eq!(drawn, 10); + } + + /// A shape whose coordinates the caller never pins down is passed once the + /// caller's solver has settled them, and the caller is the cell reported. + #[test] + fn an_unsolved_shape_argument_waits_for_the_caller() { + let source = r#" +cell child(r: Rect) { + let v = rect("met1", x0=r.x0, y0=r.y0, w=10., h=10.); +} + +cell top() { + let m = rect("met1", w=50., h=50.); + let c = inst(child(m), x=0., y=0.); +} +"#; + let output = compile_source(source, "top", Vec::new()).unwrap_exec_errors(); + let cells = output.output.expect("the layout is still produced"); + assert_eq!(cells.cells.len(), 2); + assert!(!output.errors.is_empty()); + assert!( + output.errors.iter().all(|error| { + matches!(error.kind, ExecErrorKind::Underconstrained) && error.cell == cells.top + }), + "{:?}", + output.errors + ); + } + /// Type-checks a one-file workspace and returns the kinds of its static /// errors. fn static_errors_of(source: &str) -> Vec { diff --git a/examples/shape_cell_args/Argon.toml b/examples/shape_cell_args/Argon.toml new file mode 100644 index 0000000..f147982 --- /dev/null +++ b/examples/shape_cell_args/Argon.toml @@ -0,0 +1,2 @@ +name = "shape_cell_args" +tech = "../tech/basic.tech.toml" diff --git a/examples/shape_cell_args/lib.ar b/examples/shape_cell_args/lib.ar new file mode 100644 index 0000000..0de0521 --- /dev/null +++ b/examples/shape_cell_args/lib.ar @@ -0,0 +1,32 @@ +// Shapes are passed to cells by value. The caller solves the shape, and the +// cell receives its layer and coordinates as constants in its own frame, the +// way a `Float` argument arrives as a number. Constraints written here cannot +// move the caller's geometry; share live geometry with a `fn` instead. + +// Fills `region` with square vias on a pitch, centered in the region. The +// region is construction geometry inside this cell: it is not drawn, but its +// edges constrain the array. +cell via_array(region: Rect, size: Float, pitch: Float) { + let via = crect(layer="via1", x0=0., y0=0., w=size, h=size); + let vias = std::max_array(via, region.w, region.h, pitch, pitch); + eq(vias.x0 - region.x0, region.x1 - vias.x1); + eq(vias.y0 - region.y0, region.y1 - vias.y1); +} + +// `!` draws a shape argument on its layer when it was layout geometry in the +// caller. `guide` comes from a `crect`, so `guide!` leaves it undrawn. +cell outline(shape: Polygon, corner: Point, guide: Rect) { + let drawn = shape!; + let ghost = guide!; + let marker = rect("met2", x0=corner.x, y0=corner.y, w=5., h=5.); +} + +cell top() { + let met1 = rect("met1", x0=0., y0=0., w=100., h=50.); + let met2 = rect("met2", x0=10., y0=5., w=80., h=40.); + let overlap = std::intersection(met1, met2); + let vias = inst(via_array(overlap, 10., 20.), x=0., y=0.); + + let tri = polygon("met1", 3, x0=0., y0=0., x1=40., y1=0., x2=20., y2=30.); + let o = inst(outline(tri, tri.points[2], overlap), x=200., y=0.); +} From 089f565d6f8fc63cea6c699685b56bc16e20e718 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Fri, 4 Sep 2026 12:11:25 -0700 Subject: [PATCH 2/2] also support tuples --- README.md | 4 +- crates/compiler/src/compile.rs | 27 +++++++ crates/compiler/src/gdscache.rs | 7 ++ crates/compiler/src/incremental.rs | 4 + crates/compiler/src/lib.rs | 115 +++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fdb42b6..eb48306 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ 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: +the construct's body. Struct and tuple 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 })' diff --git a/crates/compiler/src/compile.rs b/crates/compiler/src/compile.rs index 41dbcf3..dd41a0a 100644 --- a/crates/compiler/src/compile.rs +++ b/crates/compiler/src/compile.rs @@ -3588,6 +3588,8 @@ pub enum CellArg { }, /// A point, like [`Value::Point`]: its solved `x` and `y`. Point(f64, f64), + /// A tuple, like [`Value::Tuple`]: its elements in order. + Tuple(Vec), } impl CellArg { @@ -3607,6 +3609,13 @@ impl CellArg { values.iter().all(|value| value.matches_ty(inner)) } (Self::Seq(values), Ty::SeqNil) => values.is_empty(), + (Self::Tuple(values), Ty::Tuple(tys)) => { + values.len() == tys.len() + && values + .iter() + .zip(tys) + .all(|(value, ty)| value.matches_ty(ty)) + } (Self::Struct { name, fields }, Ty::Struct(ty)) => { *name == ty.name && fields.len() == ty.fields.len() @@ -3632,6 +3641,7 @@ impl CellArg { Self::Polygon { .. } => "Polygon", Self::Path { .. } => "Path", Self::Point(..) => "Point", + Self::Tuple(_) => "tuple", } } } @@ -3662,6 +3672,7 @@ pub(crate) enum CellArgKey { /// centerline points. Path(String, bool, [u64; 3], Vec<(u64, u64)>), Point(u64, u64), + Tuple(Vec), } /// The bits of each coordinate pair. @@ -3738,6 +3749,7 @@ impl From<&CellArg> for CellArgKey { point_bits(points), ), CellArg::Point(x, y) => Self::Point(x.to_bits(), y.to_bits()), + CellArg::Tuple(v) => Self::Tuple(v.iter().map(Self::from).collect()), } } } @@ -6401,6 +6413,11 @@ impl<'a> ExecPass<'a> { Value::Path(path) } CellArg::Point(x, y) => Value::Point(((*x).into(), (*y).into())), + CellArg::Tuple(v) => Value::Tuple( + v.iter() + .map(|arg| self.bind_cell_arg(cell_id, span, arg)) + .collect(), + ), } } @@ -6562,6 +6579,16 @@ impl<'a> ExecPass<'a> { }; Some(CellArg::Point(coords[0], coords[1])) } + Value::Tuple(items) => { + let mut args = Vec::with_capacity(items.len()); + for v in items { + match self.cell_arg_from_value(cell_id, dependent_vid, v)? { + Some(arg) => args.push(arg), + None => return Ok(None), + } + } + Some(CellArg::Tuple(args)) + } // 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. diff --git a/crates/compiler/src/gdscache.rs b/crates/compiler/src/gdscache.rs index 360e7d3..870d803 100644 --- a/crates/compiler/src/gdscache.rs +++ b/crates/compiler/src/gdscache.rs @@ -234,6 +234,13 @@ fn hash_cell_arg_key(hasher: &mut fnv::FnvHasher, arg: &CellArgKey) { hasher.write_u64(*x); hasher.write_u64(*y); } + CellArgKey::Tuple(values) => { + hasher.write_u8(11); + hasher.write_usize(values.len()); + for value in values { + hash_cell_arg_key(hasher, value); + } + } } } diff --git a/crates/compiler/src/incremental.rs b/crates/compiler/src/incremental.rs index d934a1f..34aaaec 100644 --- a/crates/compiler/src/incremental.rs +++ b/crates/compiler/src/incremental.rs @@ -816,6 +816,10 @@ fn hash_cell_args(args: &[CellArg], hasher: &mut impl Hasher) { x.to_bits().hash(hasher); y.to_bits().hash(hasher); } + CellArg::Tuple(values) => { + 11_u8.hash(hasher); + hash_cell_args(values, hasher); + } } } } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 508c69a..5a6c495 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -2139,6 +2139,121 @@ cell top() { assert_eq!(drawn, 10); } + /// Tuples are cell arguments like sequences and structs: element by + /// element, shapes included, and checked against the declared arity and + /// element types. + #[test] + fn tuple_cell_arguments() { + let source = r#" +struct Placed { + at: (Float, Float), + size: (Int, Rect), +} + +cell child(span: (Float, Float), p: Placed) { + let w = span.1 - span.0; + let m = rect("met2", x0=p.at.0, y0=p.at.1, w=w, h=(p.size.0 as Float)); + let drawn = p.size.1!; +} + +cell top() { + let r = rect("met1", x0=0., y0=0., w=10., h=10.); + let c = inst(child((5., 25.,), Placed { at: (100., 200.,), size: (3, r,) }), x=0., y=0.); +} +"#; + let cells = compile_source(source, "top", Vec::new()).unwrap_valid(); + assert_eq!(cells.cells.len(), 2); + let (_, child) = cells + .cells + .iter() + .find(|(id, _)| **id != cells.top) + .expect("child cell"); + let rects: Vec<_> = child + .objects + .values() + .filter_map(|object| object.get_rect()) + .collect(); + assert_eq!(rects.len(), 2); + // Every element of the nested tuples was read as a constant. + let m = rects + .iter() + .find(|rect| rect.layer.as_deref() == Some("met2")) + .expect("the marker is drawn"); + assert_relative_eq!(m.x0.0, 100., epsilon = EPSILON); + assert_relative_eq!(m.y0.0, 200., epsilon = EPSILON); + assert_relative_eq!(m.x1.0, 120., epsilon = EPSILON); + assert_relative_eq!(m.y1.0, 203., epsilon = EPSILON); + // The rect inside the tuple is drawable, so `!` draws it. + let drawn = rects + .iter() + .find(|rect| rect.layer.as_deref() == Some("met1")) + .expect("the tuple's rect is drawn"); + assert!(!drawn.construction); + assert_relative_eq!(drawn.x1.0, 10., epsilon = EPSILON); + + // Through the positional API the tuple is checked element by element. + let pair = |a: f64, b: f64| CellArg::Tuple(vec![CellArg::Float(a), CellArg::Float(b)]); + let placed = |size: CellArg| CellArg::Struct { + name: "Placed".to_owned(), + fields: vec![ + ("at".to_owned(), pair(100., 200.)), + ("size".to_owned(), size), + ], + }; + let rect = CellArg::Rect { + layer: Some("met1".to_owned()), + drawable: true, + x0: 0., + y0: 0., + x1: 10., + y1: 10., + }; + let root = parse_source_text(source, PathBuf::from("/virtual/lib.ar")).unwrap(); + let ast = IndexMap::from([(Vec::new(), root)]); + let child = |args: Vec| { + compile( + &ast, + CompileInput { + cell: &["child"], + args, + }, + ) + }; + let good = child(vec![ + pair(5., 25.), + placed(CellArg::Tuple(vec![CellArg::Int(3), rect.clone()])), + ]) + .unwrap_valid(); + assert_eq!(good.cells.len(), 1); + for (args, found) in [ + // Wrong arity. + ( + vec![ + CellArg::Tuple(vec![CellArg::Float(5.)]), + placed(CellArg::Tuple(vec![CellArg::Int(3), rect.clone()])), + ], + "tuple", + ), + // Wrong element type, nested in a struct field. + ( + vec![ + pair(5., 25.), + placed(CellArg::Tuple(vec![CellArg::Float(3.), rect.clone()])), + ], + "struct", + ), + ] { + let errors = child(args).unwrap_exec_errors().errors; + assert!( + errors.iter().any(|error| matches!( + &error.kind, + ExecErrorKind::InvalidCellArgumentType { found: f, .. } if f == found + )), + "{errors:?}" + ); + } + } + /// A shape whose coordinates the caller never pins down is passed once the /// caller's solver has settled them, and the caller is the cell reported. #[test]