From 038c390e1174e5d9f9922434a77716d00c04efa8 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Fri, 4 Sep 2026 13:30:54 -0700 Subject: [PATCH] feat(docs): create docs website --- .gitignore | 2 + Cargo.lock | 1 + README.md | 17 + crates/analyzer/src/cell_edit.rs | 479 + crates/analyzer/src/lib.rs | 74 + crates/analyzer/src/rpc.rs | 4 +- crates/arc/Cargo.toml | 1 + crates/arc/src/cli.rs | 54 +- crates/arc/src/doc.rs | 616 + crates/arc/src/lib.rs | 1 + crates/gui/assets/icons/file-circle-plus.svg | 7 + crates/gui/assets/icons/file-pen.svg | 6 + crates/gui/src/actions.rs | 2 + crates/gui/src/editor/mod.rs | 24 +- crates/gui/src/editor/toolbars.rs | 35 +- crates/gui/src/lib.rs | 26 +- docs/README.md | 49 + docs/content/gui/cell-management.md | 32 + docs/content/gui/drawing.md | 43 + docs/content/gui/hierarchy-layers.md | 29 + docs/content/gui/shortcuts-config.md | 66 + docs/content/gui/workspace.md | 35 + .../guides/getting-started/constraints.md | 56 + .../guides/getting-started/first-cell.md | 67 + .../getting-started/hierarchy-export.md | 51 + .../guides/getting-started/installation.md | 53 + docs/content/guides/index.md | 12 + .../content/language/builtins/collections.mdx | 89 + .../content/language/builtins/constraints.mdx | 68 + docs/content/language/builtins/geometry.mdx | 135 + docs/content/language/builtins/hierarchy.mdx | 53 + docs/content/language/builtins/index.md | 34 + docs/content/language/cells-functions.md | 63 + docs/content/language/constraints.md | 49 + docs/content/language/control-flow.md | 54 + docs/content/language/geometry.md | 58 + docs/content/language/modules-manifests.md | 55 + docs/content/language/overview.md | 48 + docs/content/language/std.mdx | 179 + docs/content/language/technology.md | 44 + docs/content/language/types-values.md | 57 + docs/content/language/types/collections.mdx | 29 + docs/content/language/types/instance.mdx | 35 + docs/content/language/types/path.mdx | 24 + docs/content/language/types/point.mdx | 23 + docs/content/language/types/polygon.mdx | 32 + docs/content/language/types/rect.mdx | 40 + docs/content/language/types/scalars.mdx | 50 + docs/content/tools/arc.md | 110 + docs/content/tools/argonc.md | 30 + docs/content/tools/argone.md | 51 + docs/content/tools/neovim.md | 36 + docs/content/tools/overview.md | 17 + docs/docusaurus.config.ts | 134 + docs/package-lock.json | 18985 ++++++++++++++++ docs/package.json | 34 + docs/parser.md | 4 +- docs/sidebars.ts | 80 + docs/src/components/ApiReference.tsx | 101 + docs/src/css/custom.css | 156 + docs/src/pages/index.module.css | 314 + docs/src/pages/index.tsx | 245 + docs/src/theme/prism-include-languages.js | 44 + docs/static/img/argon-mark.svg | 6 + docs/static/img/argon-social-card.svg | 8 + docs/static/img/gui.png | Bin 0 -> 602457 bytes docs/tsconfig.json | 13 + lua/argon/commands/gui.lua | 20 + lua/argon/commands/init.lua | 18 + 69 files changed, 23454 insertions(+), 13 deletions(-) create mode 100644 crates/analyzer/src/cell_edit.rs create mode 100644 crates/arc/src/doc.rs create mode 100644 crates/gui/assets/icons/file-circle-plus.svg create mode 100644 crates/gui/assets/icons/file-pen.svg create mode 100644 docs/README.md create mode 100644 docs/content/gui/cell-management.md create mode 100644 docs/content/gui/drawing.md create mode 100644 docs/content/gui/hierarchy-layers.md create mode 100644 docs/content/gui/shortcuts-config.md create mode 100644 docs/content/gui/workspace.md create mode 100644 docs/content/guides/getting-started/constraints.md create mode 100644 docs/content/guides/getting-started/first-cell.md create mode 100644 docs/content/guides/getting-started/hierarchy-export.md create mode 100644 docs/content/guides/getting-started/installation.md create mode 100644 docs/content/guides/index.md create mode 100644 docs/content/language/builtins/collections.mdx create mode 100644 docs/content/language/builtins/constraints.mdx create mode 100644 docs/content/language/builtins/geometry.mdx create mode 100644 docs/content/language/builtins/hierarchy.mdx create mode 100644 docs/content/language/builtins/index.md create mode 100644 docs/content/language/cells-functions.md create mode 100644 docs/content/language/constraints.md create mode 100644 docs/content/language/control-flow.md create mode 100644 docs/content/language/geometry.md create mode 100644 docs/content/language/modules-manifests.md create mode 100644 docs/content/language/overview.md create mode 100644 docs/content/language/std.mdx create mode 100644 docs/content/language/technology.md create mode 100644 docs/content/language/types-values.md create mode 100644 docs/content/language/types/collections.mdx create mode 100644 docs/content/language/types/instance.mdx create mode 100644 docs/content/language/types/path.mdx create mode 100644 docs/content/language/types/point.mdx create mode 100644 docs/content/language/types/polygon.mdx create mode 100644 docs/content/language/types/rect.mdx create mode 100644 docs/content/language/types/scalars.mdx create mode 100644 docs/content/tools/arc.md create mode 100644 docs/content/tools/argonc.md create mode 100644 docs/content/tools/argone.md create mode 100644 docs/content/tools/neovim.md create mode 100644 docs/content/tools/overview.md create mode 100644 docs/docusaurus.config.ts create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/sidebars.ts create mode 100644 docs/src/components/ApiReference.tsx create mode 100644 docs/src/css/custom.css create mode 100644 docs/src/pages/index.module.css create mode 100644 docs/src/pages/index.tsx create mode 100644 docs/src/theme/prism-include-languages.js create mode 100644 docs/static/img/argon-mark.svg create mode 100644 docs/static/img/argon-social-card.svg create mode 100644 docs/static/img/gui.png create mode 100644 docs/tsconfig.json diff --git a/.gitignore b/.gitignore index 3d738b37..46a691fe 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,8 @@ tags out node_modules +.docusaurus +build *.tsbuildinfo Argon_ICCAD_2026.pdf diff --git a/Cargo.lock b/Cargo.lock index ab1913a0..5e16597d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ name = "arc" version = "0.1.0" dependencies = [ "anyhow", + "arcstr", "argonc", "clap", "indexmap", diff --git a/README.md b/README.md index 031140c8..32bc8f2d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,23 @@ general-purpose programming language. The main goal of Argon is to allow interop enable the creation of most practical parametric cells, and allow for performance optimizations such as caching and incremental compilation. +## Documentation + +The documentation site is built with Docusaurus and lives in [`docs/`](docs). Its pages are +under [`docs/content/`](docs/content), split into [guides](docs/content/guides/index.md), a +[language reference](docs/content/language/overview.md), a +[GUI manual](docs/content/gui/workspace.md), and a +[tools reference](docs/content/tools/overview.md), each with its own sidebar. + +```bash +cd docs +npm install +npm start +``` + +Run `npm run build` for a production build with strict internal-link and anchor checks. +See [`docs/README.md`](docs/README.md) for the layout of the site. + ## Installation To use Argon, you will need: diff --git a/crates/analyzer/src/cell_edit.rs b/crates/analyzer/src/cell_edit.rs new file mode 100644 index 00000000..3e6c439e --- /dev/null +++ b/crates/analyzer/src/cell_edit.rs @@ -0,0 +1,479 @@ +//! Source edits for creating and semantically renaming cells. + +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, +}; + +use arcstr::Substr; +use argonc::{ + ast::{AstMetadata, Decl, Ident, ModPath, UseDecl}, + compile::{self, BUILTINS}, + parse::{self, ParseMetadata, WorkspaceParseAst}, +}; +use tower_lsp_server::ls_types::{Range, TextEdit, Uri}; + +use crate::document::Document; + +const KEYWORDS: &[&str] = &[ + "as", "cell", "const", "else", "enum", "false", "fn", "for", "if", "in", "let", "match", "mod", + "struct", "true", "use", +]; + +pub(crate) struct RenameCellEdit { + pub(crate) changes: HashMap>, + pub(crate) invocation: String, +} + +pub(crate) fn validate_cell_name(name: &str) -> Result<(), String> { + let mut bytes = name.bytes(); + if !bytes + .next() + .is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic()) + || !bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric()) + { + return Err(format!("`{name}` is not a valid Argon identifier")); + } + if KEYWORDS.contains(&name) { + return Err(format!("`{name}` is a reserved Argon keyword")); + } + if BUILTINS.contains(&name) { + return Err(format!( + "`{name}` is an Argon built-in and cannot name a cell" + )); + } + Ok(()) +} + +fn declaration_name(decl: &Decl) -> Option<&str> { + match decl { + Decl::Enum(decl) => Some(decl.name.name.as_str()), + Decl::Struct(decl) => Some(decl.name.name.as_str()), + Decl::Constant(decl) => Some(decl.name.name.as_str()), + Decl::Cell(decl) => Some(decl.name.name.as_str()), + Decl::Mod(decl) => Some(decl.ident.name.as_str()), + Decl::Use(decl) => decl + .alias + .as_ref() + .or_else(|| decl.path.last()) + .map(|ident| ident.name.as_str()), + Decl::Fn(decl) => Some(decl.name.name.as_str()), + } +} + +fn ensure_name_is_available( + workspace: &WorkspaceParseAst, + module_path: &ModPath, + name: &str, +) -> Result<(), String> { + let module = workspace + .get(module_path) + .ok_or_else(|| "The cell's source module is no longer available".to_owned())?; + if module + .ast + .decls + .iter() + .filter_map(declaration_name) + .any(|existing| existing == name) + { + return Err(format!("`{name}` is already declared in this module")); + } + Ok(()) +} + +pub(crate) fn new_cell_edit( + workspace: &WorkspaceParseAst, + source_path: &Path, + name: &str, +) -> Result { + validate_cell_name(name)?; + let (module_path, module) = workspace + .iter() + .find(|(_, module)| module.path == source_path) + .ok_or_else(|| "The active buffer is not part of this Argon workspace".to_owned())?; + ensure_name_is_available(workspace, module_path, name)?; + + let source = module.source_text.as_str(); + let separator = if source.is_empty() || source.ends_with("\n\n") { + "" + } else if source.ends_with('\n') { + "\n" + } else { + "\n\n" + }; + let document = Document::new(&module.source_text, 0); + let end = document.offset_to_pos(source.len()); + Ok(TextEdit { + range: Range::new(end, end), + new_text: format!("{separator}cell {name}() {{\n}}\n"), + }) +} + +fn use_module_path(current_path: &ModPath, use_decl: &UseDecl) -> ModPath { + let module_parts = &use_decl.path[..use_decl.path.len().saturating_sub(1)]; + match use_decl.path.first().map(|ident| ident.name.as_str()) { + Some("std") => vec!["std".to_owned()], + Some("lib") => module_parts + .iter() + .skip(1) + .map(|ident| ident.name.to_string()) + .collect(), + Some(_) => current_path + .iter() + .cloned() + .chain(module_parts.iter().map(|ident| ident.name.to_string())) + .collect(), + None => current_path.clone(), + } +} + +fn reference_module_path(current_path: &ModPath, path: &[Ident]) -> ModPath +where + S: AsRef, + M: AstMetadata, +{ + if path.len() <= 1 { + return current_path.clone(); + } + match path[0].name.as_ref() { + "std" => vec!["std".to_owned()], + "lib" => path + .iter() + .skip(1) + .take(path.len() - 2) + .map(|ident| ident.name.as_ref().to_owned()) + .collect(), + _ => current_path + .iter() + .cloned() + .chain( + path.iter() + .take(path.len() - 1) + .map(|ident| ident.name.as_ref().to_owned()), + ) + .collect(), + } +} + +/// Names in each module that resolve to the renamed cell, mapped to the name +/// they will expose after the edit. Explicit aliases therefore map to +/// themselves, while ordinary imports propagate the new declaration name. +fn target_names( + workspace: &WorkspaceParseAst, + target_module_path: &ModPath, + old_name: &str, + new_name: &str, +) -> HashMap> { + let mut names = HashMap::from([( + target_module_path.clone(), + HashMap::from([(old_name.to_owned(), new_name.to_owned())]), + )]); + loop { + let mut additions = Vec::new(); + for (module_path, module) in workspace { + for use_decl in module.ast.decls.iter().filter_map(|decl| match decl { + Decl::Use(use_decl) => Some(use_decl), + _ => None, + }) { + let Some(imported_name) = use_decl.path.last() else { + continue; + }; + let imported_module = use_module_path(module_path, use_decl); + let Some(post_rename_name) = names + .get(&imported_module) + .and_then(|module_names| module_names.get(imported_name.name.as_str())) + else { + continue; + }; + let local_name = use_decl.alias.as_ref().unwrap_or(imported_name); + let post_rename_local_name = use_decl + .alias + .as_ref() + .map_or_else(|| post_rename_name.clone(), |alias| alias.name.to_string()); + let already_known = names.get(module_path).is_some_and(|module_names| { + module_names.contains_key(local_name.name.as_str()) + }); + if !already_known { + additions.push(( + module_path.clone(), + local_name.name.to_string(), + post_rename_local_name, + )); + } + } + } + if additions.is_empty() { + return names; + } + for (module_path, old, new) in additions { + names.entry(module_path).or_default().insert(old, new); + } + } +} + +fn add_edit( + workspace: &WorkspaceParseAst, + seen: &mut HashSet<(PathBuf, usize, usize)>, + changes: &mut HashMap>, + path: &Path, + span: cfgrammar::Span, + new_name: &str, +) -> Result<(), String> { + if !seen.insert((path.to_owned(), span.start(), span.end())) { + return Ok(()); + } + let module = workspace + .values() + .find(|module| module.path == path) + .ok_or_else(|| "A cell reference is outside the current workspace".to_owned())?; + if span.end() > module.source_text.len() { + return Err("Imported GDS cells and generated declarations are read-only".to_owned()); + } + let uri = Uri::from_file_path(path) + .ok_or_else(|| format!("Could not convert `{}` to an editor URI", path.display()))?; + let document = Document::new(&module.source_text, 0); + changes.entry(uri).or_default().push(TextEdit { + range: Range::new( + document.offset_to_pos(span.start()), + document.offset_to_pos(span.end()), + ), + new_text: new_name.to_owned(), + }); + Ok(()) +} + +pub(crate) fn rename_cell_edits( + workspace: &WorkspaceParseAst, + current_invocation: &str, + new_name: &str, +) -> Result { + validate_cell_name(new_name)?; + + let parsed_invocation = parse::parse_cell(current_invocation) + .map_err(|error| format!("The open cell invocation is invalid: {error}"))?; + let invocation_name = parsed_invocation + .func + .path + .last() + .ok_or_else(|| "The open cell invocation has no cell name".to_owned())?; + + let mut analysis_ast = workspace.clone(); + let invocation = parse::splice_cell_invocation(&mut analysis_ast, current_invocation) + .map_err(|error| format!("The open cell invocation is invalid: {error}"))?; + let (typed, _) = compile::static_compile(&analysis_ast) + .ok_or_else(|| "The workspace has no root module".to_owned())?; + let invocation_span = invocation.span(); + let target_id = typed + .values() + .find(|module| module.path == invocation_span.path) + .and_then(|module| module.span2call.get(&invocation_span)) + .and_then(|call| call.metadata.0) + .ok_or_else(|| format!("Could not resolve the open cell `{}`", invocation_name.name))?; + + let (target_module_path, target_source_path, old_name, declaration_span) = typed + .iter() + .find_map(|(module_path, module)| { + module.ast.decls.iter().find_map(|decl| match decl { + Decl::Cell(cell) if cell.metadata.1 == target_id => Some(( + module_path.clone(), + cell.metadata.0.clone(), + cell.name.name.to_string(), + cell.name.span, + )), + _ => None, + }) + }) + .ok_or_else(|| "The open cell is not a source-defined Argon cell".to_owned())?; + + if old_name == new_name { + return Err(format!("The open cell is already named `{new_name}`")); + } + ensure_name_is_available(workspace, &target_module_path, new_name)?; + let target_names = target_names(workspace, &target_module_path, &old_name, new_name); + + let mut changes = HashMap::new(); + let mut seen = HashSet::new(); + add_edit( + workspace, + &mut seen, + &mut changes, + &target_source_path, + declaration_span, + new_name, + )?; + + // Calls carry the resolved declaration ID. Module name propagation tells + // us whether their final segment changes or is a stable explicit alias. + for (module_path, module) in &typed { + let Some(source_module) = workspace.get(module_path) else { + continue; + }; + for call in module.span2call.values() { + let Some(name) = call.func.path.last() else { + continue; + }; + let referenced_module = reference_module_path(module_path, &call.func.path); + let renamed = target_names + .get(&referenced_module) + .and_then(|module_names| module_names.get(name.name.as_str())); + if call.metadata.0 == Some(target_id) + && name.span.end() <= source_module.source_text.len() + && let Some(renamed) = renamed + && renamed != name.name.as_str() + { + add_edit( + workspace, + &mut seen, + &mut changes, + &source_module.path, + name.span, + renamed, + )?; + } + } + } + + // Import declarations do not carry resolved IDs. The propagated name map + // follows re-exports transitively using the type checker's path rules. + for (module_path, module) in workspace { + for use_decl in module.ast.decls.iter().filter_map(|decl| match decl { + Decl::Use(use_decl) => Some(use_decl), + _ => None, + }) { + let Some(name) = use_decl.path.last() else { + continue; + }; + let imported_module = use_module_path(module_path, use_decl); + let renamed = target_names + .get(&imported_module) + .and_then(|module_names| module_names.get(name.name.as_str())); + if let Some(renamed) = renamed + && renamed != name.name.as_str() + { + add_edit( + workspace, + &mut seen, + &mut changes, + &module.path, + name.span, + renamed, + )?; + } + } + } + + let mut renamed_invocation = current_invocation.to_owned(); + let invocation_module = reference_module_path(&ModPath::new(), &parsed_invocation.func.path); + let renamed_invocation_name = target_names + .get(&invocation_module) + .and_then(|module_names| module_names.get(invocation_name.name)); + if let Some(renamed_name) = renamed_invocation_name + && renamed_name != invocation_name.name + { + renamed_invocation.replace_range( + invocation_name.span.start()..invocation_name.span.end(), + renamed_name, + ); + } + Ok(RenameCellEdit { + changes, + invocation: renamed_invocation, + }) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use argonc::parse; + + use super::{new_cell_edit, rename_cell_edits, validate_cell_name}; + + #[test] + fn cell_names_must_be_plain_non_reserved_identifiers() { + assert!(validate_cell_name("guard_ring_2").is_ok()); + assert!(validate_cell_name("2guard").is_err()); + assert!(validate_cell_name("guard-ring").is_err()); + assert!(validate_cell_name("cell").is_err()); + assert!(validate_cell_name("rect").is_err()); + } + + #[test] + fn new_cells_are_separated_from_existing_source() { + let directory = tempfile::tempdir().unwrap(); + let source_path = directory.path().join("lib.ar"); + fs::write(&source_path, "cell top() {}\n").unwrap(); + let workspace = parse::parse_workspace_with_std(&source_path).ast(); + let edit = new_cell_edit(&workspace, &source_path, "child").unwrap(); + assert_eq!(edit.new_text, "\ncell child() {\n}\n"); + assert!(new_cell_edit(&workspace, &source_path, "top").is_err()); + } + + #[test] + fn rename_tracks_declarations_imports_and_resolved_calls() { + let directory = tempfile::tempdir().unwrap(); + let source_path = directory.path().join("lib.ar"); + let module_path = directory.path().join("blocks.ar"); + fs::write( + &source_path, + "mod blocks;\nuse blocks::child;\ncell top() { child(); }\n", + ) + .unwrap(); + fs::write( + &module_path, + "// child in a comment\ncell child() { let label = \"child\"; }\n", + ) + .unwrap(); + let workspace = parse::parse_workspace_with_std(&source_path).ast(); + let rename = rename_cell_edits(&workspace, "lib::blocks::child()", "unit").unwrap(); + + assert_eq!(rename.invocation, "lib::blocks::unit()"); + assert_eq!(rename.changes.values().map(Vec::len).sum::(), 3); + assert_eq!( + rename + .changes + .values() + .flat_map(|edits| edits.iter()) + .filter(|edit| edit.new_text == "unit") + .count(), + 3 + ); + } + + #[test] + fn rename_preserves_explicit_local_aliases() { + let directory = tempfile::tempdir().unwrap(); + let source_path = directory.path().join("lib.ar"); + let module_path = directory.path().join("blocks.ar"); + fs::write( + &source_path, + "mod blocks;\nuse blocks::child as placed;\ncell top() { placed(); }\n", + ) + .unwrap(); + fs::write(&module_path, "cell child() {}\n").unwrap(); + let workspace = parse::parse_workspace_with_std(&source_path).ast(); + let rename = rename_cell_edits(&workspace, "placed()", "unit").unwrap(); + + assert_eq!(rename.invocation, "placed()"); + assert_eq!(rename.changes.values().map(Vec::len).sum::(), 2); + } + + #[test] + fn rename_propagates_through_unaliased_reexports() { + let directory = tempfile::tempdir().unwrap(); + let source_path = directory.path().join("lib.ar"); + let public_path = directory.path().join("public.ar"); + let blocks_path = directory.path().join("blocks.ar"); + fs::write( + &source_path, + "mod public;\nmod blocks;\nuse public::child;\ncell top() { child(); }\n", + ) + .unwrap(); + fs::write(&public_path, "use lib::blocks::child;\n").unwrap(); + fs::write(&blocks_path, "cell child() {}\n").unwrap(); + let workspace = parse::parse_workspace_with_std(&source_path).ast(); + let rename = rename_cell_edits(&workspace, "lib::blocks::child()", "unit").unwrap(); + + assert_eq!(rename.changes.values().map(Vec::len).sum::(), 4); + } +} diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index b7bedf02..fad51842 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -1,3 +1,4 @@ +mod cell_edit; mod compiler_worker; pub mod document; pub mod rpc; @@ -1103,6 +1104,12 @@ struct InstantiateParams { cell: String, } +#[derive(Serialize, Deserialize)] +struct CellNameParams { + name: String, + uri: Uri, +} + const PREVIEW_BINDING_PREFIX: &str = "__argon_preview_instance"; fn preview_instance_cell( @@ -1252,6 +1259,71 @@ impl Backend { Ok(()) } + async fn new_cell(&self, params: CellNameParams) -> Result<()> { + let Some(source_path) = params.uri.to_file_path().map(|path| path.into_owned()) else { + self.state + .report_message( + MessageType::ERROR, + "The active buffer does not have a file path", + ) + .await; + return Ok(()); + }; + let Some(ast) = self.state.current_editor_ast().await else { + self.state + .report_message(MessageType::ERROR, rpc::OUT_OF_SYNC_MESSAGE) + .await; + return Ok(()); + }; + let edit = match cell_edit::new_cell_edit(&ast, &source_path, ¶ms.name) { + Ok(edit) => edit, + Err(error) => { + self.state.report_message(MessageType::ERROR, error).await; + return Ok(()); + } + }; + let invocation = format!("{}()", params.name); + let previous = { + let mut source = self.state.source_state.lock().await; + source.cell.replace(invocation) + }; + if !self.state.apply_source_edit(params.uri, edit).await { + self.state.source_state.lock().await.cell = previous; + } + Ok(()) + } + + async fn rename_cell(&self, params: CellNameParams) -> Result<()> { + let Some(ast) = self.state.current_editor_ast().await else { + self.state + .report_message(MessageType::ERROR, rpc::OUT_OF_SYNC_MESSAGE) + .await; + return Ok(()); + }; + let current_invocation = self.state.source_state.lock().await.cell.clone(); + let Some(current_invocation) = current_invocation else { + self.state + .report_message(MessageType::ERROR, "Open a cell before renaming it") + .await; + return Ok(()); + }; + let rename = match cell_edit::rename_cell_edits(&ast, ¤t_invocation, ¶ms.name) { + Ok(rename) => rename, + Err(error) => { + self.state.report_message(MessageType::ERROR, error).await; + return Ok(()); + } + }; + let previous = { + let mut source = self.state.source_state.lock().await; + source.cell.replace(rename.invocation) + }; + if !self.state.apply_source_changes(rename.changes, None).await { + self.state.source_state.lock().await.cell = previous; + } + Ok(()) + } + async fn instantiate(&self, params: InstantiateParams) -> Result<()> { let Some(connection) = self.state.gui_connection().await else { self.state @@ -1668,6 +1740,8 @@ pub async fn main_with_io_on_listener( }) .custom_method("custom/startGui", Backend::start_gui) .custom_method("custom/openCell", Backend::open_cell) + .custom_method("custom/newCell", Backend::new_cell) + .custom_method("custom/renameCell", Backend::rename_cell) .custom_method("custom/inst", Backend::instantiate) .custom_method("custom/reloadConfig", Backend::reload_config) .custom_method("custom/setConfig", Backend::set_config) diff --git a/crates/analyzer/src/rpc.rs b/crates/analyzer/src/rpc.rs index 785c21d9..e6c2136f 100644 --- a/crates/analyzer/src/rpc.rs +++ b/crates/analyzer/src/rpc.rs @@ -340,7 +340,7 @@ impl State { } } - async fn apply_source_changes( + pub(crate) async fn apply_source_changes( &self, changes: HashMap>, focus: Option, @@ -405,7 +405,7 @@ impl State { } } - async fn apply_source_edit(&self, uri: Uri, edit: TextEdit) -> bool { + pub(crate) async fn apply_source_edit(&self, uri: Uri, edit: TextEdit) -> bool { self.apply_source_changes(HashMap::from([(uri.clone(), vec![edit])]), Some(uri)) .await } diff --git a/crates/arc/Cargo.toml b/crates/arc/Cargo.toml index 39c37f2c..68c75674 100644 --- a/crates/arc/Cargo.toml +++ b/crates/arc/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] anyhow = { workspace = true } argonc = { version = "0.1.0", path = "../compiler" } +arcstr = { workspace = true } clap = { workspace = true } indexmap = { workspace = true } serde = { workspace = true } diff --git a/crates/arc/src/cli.rs b/crates/arc/src/cli.rs index a5709d26..638993c7 100644 --- a/crates/arc/src/cli.rs +++ b/crates/arc/src/cli.rs @@ -5,7 +5,7 @@ use std::{ process::{Command, ExitCode, Stdio}, }; -use crate::{Library, create_workspace, find_manifest_path, format_workspace}; +use crate::{Library, create_workspace, doc, find_manifest_path, format_workspace}; use anyhow::{Context, Result, anyhow, bail}; use argonc::diagnostics::{self, Diagnostic}; use clap::{Args, Parser, Subcommand}; @@ -27,6 +27,8 @@ enum CommandKind { Check(LibraryArgs), /// Execute an Argon cell and write the compiler output. Run(RunArgs), + /// Generate static HTML API documentation for an Argon library. + Doc(DocArgs), } #[derive(Debug, Args)] @@ -73,12 +75,23 @@ struct RunArgs { gds: bool, } +#[derive(Debug, Args)] +struct DocArgs { + /// Path to Argon.toml. Defaults to the nearest manifest in this directory or a parent. + #[arg(long, value_name = "PATH")] + manifest_path: Option, + /// Documentation output directory. Defaults to target/doc. + #[arg(short, long, value_name = "DIR")] + output: Option, +} + pub fn run() -> ExitCode { let result = match Cli::parse().command { CommandKind::New(args) => new(args), CommandKind::Fmt(args) => fmt(args), CommandKind::Check(args) => check(args), CommandKind::Run(args) => run_cell(args), + CommandKind::Doc(args) => generate_docs(args), }; match result { Ok(()) => ExitCode::SUCCESS, @@ -89,6 +102,27 @@ pub fn run() -> ExitCode { } } +fn generate_docs(args: DocArgs) -> Result<()> { + let manifest_path = match args.manifest_path { + Some(path) => path, + None => find_manifest_path(".")?, + }; + let library = Library::load(&manifest_path)?; + let output = args.output.unwrap_or_else(|| library.target_path("doc")); + status("Documenting", &library.name); + let report = doc::generate(&library, &output)?; + status( + "Generated", + &format!( + "{} module{} at {}", + report.modules, + if report.modules == 1 { "" } else { "s" }, + report.output.join("index.html").display() + ), + ); + Ok(()) +} + fn fmt(args: FmtArgs) -> Result<()> { let manifest_path = match args.manifest_path { Some(path) => path, @@ -243,3 +277,21 @@ fn print_error(message: &str) { eprintln!("error: {message}"); } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use clap::Parser; + + use super::{Cli, CommandKind}; + + #[test] + fn parses_documentation_output_directory() { + let cli = Cli::try_parse_from(["arc", "doc", "--output", "site"]).unwrap(); + let CommandKind::Doc(args) = cli.command else { + panic!("doc subcommand should be selected"); + }; + assert_eq!(args.output, Some(PathBuf::from("site"))); + } +} diff --git a/crates/arc/src/doc.rs b/crates/arc/src/doc.rs new file mode 100644 index 00000000..546eec98 --- /dev/null +++ b/crates/arc/src/doc.rs @@ -0,0 +1,616 @@ +//! Static, rustdoc-style documentation generation for Argon libraries. + +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result, bail}; +use arcstr::Substr; +use argonc::{ + WorkspaceConfig, + ast::{ArgDecl, Decl, ModPath, TySpec, TySpecKind}, + parse::{self, AnnotatedParseAst, ParseMetadata, WorkspaceParseAst}, +}; + +use crate::Library; + +const STYLE: &str = r#":root { + color-scheme: light dark; + --bg: #fbfaff; + --panel: #ffffff; + --text: #262231; + --muted: #6e687b; + --line: #e4dff0; + --accent: #7147b8; + --accent-soft: #f0e9fb; + --code: #f5f1fa; +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #17141d; + --panel: #1e1a27; + --text: #eee9f5; + --muted: #aaa1b8; + --line: #393143; + --accent: #c09aef; + --accent-soft: #302441; + --code: #282131; + } +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; + color: var(--text); + background: var(--bg); + font: 15px/1.6 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } +code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } +.layout { display: grid; grid-template-columns: 250px minmax(0, 1fr); min-height: 100vh; } +.sidebar { + position: sticky; top: 0; height: 100vh; overflow: auto; + padding: 28px 22px; border-right: 1px solid var(--line); background: var(--panel); +} +.brand { display: block; color: var(--text); font-size: 18px; font-weight: 720; margin-bottom: 22px; } +.brand-mark { color: var(--accent); margin-right: 7px; } +.nav-label { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; } +.module-nav { list-style: none; padding: 0; margin: 8px 0 24px; } +.module-nav a { display: block; padding: 5px 8px; border-radius: 6px; color: var(--muted); } +.module-nav a:hover, .module-nav a.current { color: var(--text); background: var(--accent-soft); text-decoration: none; } +main { width: min(980px, 100%); padding: 52px 56px 90px; } +.eyebrow, .source { color: var(--muted); font-size: 13px; } +h1 { margin: 5px 0 12px; font-size: clamp(30px, 5vw, 44px); line-height: 1.15; letter-spacing: -.025em; } +h2 { margin: 42px 0 14px; padding-bottom: 8px; border-bottom: 1px solid var(--line); font-size: 22px; } +h3 { margin: 0; font-size: 17px; } +.lead { max-width: 720px; color: var(--muted); font-size: 17px; } +.item { margin: 16px 0; padding: 18px 20px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); } +.item-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } +.signature { margin: 12px 0; padding: 13px 15px; overflow-x: auto; border-radius: 7px; background: var(--code); font-size: 14px; } +.kw { color: var(--accent); font-weight: 700; } +.name { color: var(--text); font-weight: 650; } +.type { color: var(--text); } +.doc { max-width: 760px; } +.doc code { padding: 1px 5px; border-radius: 4px; background: var(--code); } +.doc h4 { margin: 18px 0 5px; } +.doc p { margin: 8px 0; } +.doc ul { margin: 8px 0; padding-left: 22px; } +.empty { color: var(--muted); font-style: italic; } +.module-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; } +.module-card { display: block; padding: 16px 18px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); color: var(--text); } +.module-card:hover { border-color: var(--accent); text-decoration: none; } +.module-card span { display: block; color: var(--muted); font-size: 13px; } +table { width: 100%; border-collapse: collapse; margin: 14px 0 6px; } +th, td { padding: 7px 10px; border-bottom: 1px solid var(--line); text-align: left; } +th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .05em; } +@media (max-width: 760px) { + .layout { display: block; } + .sidebar { position: static; width: 100%; height: auto; border-right: 0; border-bottom: 1px solid var(--line); } + main { padding: 34px 22px 70px; } + .module-nav { display: flex; flex-wrap: wrap; gap: 3px; } +} +"#; + +pub struct DocReport { + pub output: PathBuf, + pub modules: usize, +} + +struct Module<'a> { + path: &'a ModPath, + ast: &'a AnnotatedParseAst, +} + +type TypeTargets = HashMap>; + +pub fn generate(library: &Library, output: impl AsRef) -> Result { + let output = output.as_ref(); + let config = WorkspaceConfig::new(&library.root) + .with_dependencies(library.dependencies.clone()) + .with_gds_imports(library.gds.clone()); + let parsed = parse::parse_workspace_with_config(&config); + let errors = parsed.static_errors(); + if !errors.is_empty() { + let messages = errors + .iter() + .map(|error| format!("{}: {}", error.span.path.display(), error.kind)) + .collect::>() + .join("\n"); + bail!("cannot document a workspace with parse errors:\n{messages}"); + } + let workspace = parsed.ast(); + let modules = documented_modules(library, &workspace); + fs::create_dir_all(output).with_context(|| { + format!( + "could not create documentation directory '{}'", + output.display() + ) + })?; + fs::write(output.join("style.css"), STYLE) + .with_context(|| format!("could not write '{}'", output.join("style.css").display()))?; + + let type_targets = type_targets(&modules); + let navigation = module_navigation(&modules); + let index = render_index(library, &modules, &navigation); + fs::write(output.join("index.html"), index) + .with_context(|| format!("could not write '{}'", output.join("index.html").display()))?; + for module in &modules { + let file_name = module_file_name(module.path); + let page = render_module(library, module, &modules, &navigation, &type_targets); + fs::write(output.join(&file_name), page) + .with_context(|| format!("could not write '{}'", output.join(file_name).display()))?; + } + + Ok(DocReport { + output: output.to_path_buf(), + modules: modules.len(), + }) +} + +fn documented_modules<'a>(library: &Library, workspace: &'a WorkspaceParseAst) -> Vec> { + workspace + .iter() + .filter(|(path, _)| { + path.first() + .is_none_or(|first| first != "std" && !library.dependencies.contains_key(first)) + }) + .map(|(path, ast)| Module { path, ast }) + .collect() +} + +fn type_targets(modules: &[Module<'_>]) -> TypeTargets { + let mut targets = HashMap::new(); + for module in modules { + for declaration in &module.ast.ast.decls { + if let Decl::Enum(enum_) = declaration + && enum_.name.span.end() <= module.ast.source_text.len() + { + targets + .entry(enum_.name.name.to_string()) + .or_insert_with(Vec::new) + .push(( + module.path.clone(), + format!("{}#enum.{}", module_file_name(module.path), enum_.name.name), + )); + } + } + } + targets +} + +fn module_name(path: &ModPath) -> String { + if path.is_empty() { + "crate".to_owned() + } else { + format!("crate::{}", path.join("::")) + } +} + +fn module_file_name(path: &ModPath) -> String { + let slug = if path.is_empty() { + "root".to_owned() + } else { + path.join("-") + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '-' + } + }) + .collect() + }; + format!("module-{slug}.html") +} + +fn module_navigation(modules: &[Module<'_>]) -> String { + modules + .iter() + .map(|module| { + format!( + "
  • {}
  • ", + escape(&module_name(module.path)), + module_file_name(module.path), + escape(&module_name(module.path)) + ) + }) + .collect::>() + .join("") +} + +fn page_shell( + library: &Library, + title: &str, + current_module: Option<&str>, + navigation: &str, + body: &str, +) -> String { + let navigation = current_module.map_or_else( + || navigation.to_owned(), + |current| { + navigation.replace( + &format!("data-module=\"{}\"", escape(current)), + &format!("class=\"current\" data-module=\"{}\"", escape(current)), + ) + }, + ); + format!( + "{title} · {library}
    {body}
    ", + title = escape(title), + library = escape(&library.name), + ) +} + +fn render_index(library: &Library, modules: &[Module<'_>], navigation: &str) -> String { + let cards = modules + .iter() + .map(|module| { + let documentation = module_doc(&module.ast.source_text); + let summary = documentation + .lines() + .next() + .unwrap_or("Module documentation"); + format!( + "{}{}", + module_file_name(module.path), + escape(&module_name(module.path)), + escape(summary) + ) + }) + .collect::>() + .join(""); + let body = format!( + "
    Argon library

    {}

    Generated API documentation for this library's source modules, cells, functions, and enum types.

    Modules

    {cards}
    ", + escape(&library.name) + ); + page_shell(library, &library.name, None, navigation, &body) +} + +fn render_module( + library: &Library, + module: &Module<'_>, + modules: &[Module<'_>], + navigation: &str, + targets: &TypeTargets, +) -> String { + let name = module_name(module.path); + let relative_path = module + .ast + .path + .strip_prefix(library.directory()) + .unwrap_or(&module.ast.path); + let docs = module_doc(&module.ast.source_text); + let child_modules = module.ast.ast.decls.iter().filter_map(|declaration| { + let Decl::Mod(declaration) = declaration else { + return None; + }; + let mut child = module.path.clone(); + child.push(declaration.ident.name.to_string()); + modules + .iter() + .any(|candidate| candidate.path == &child) + .then(|| { + format!( + "{}Module", + module_file_name(&child), + escape(&module_name(&child)) + ) + }) + }); + let child_modules = child_modules.collect::>().join(""); + + let mut cells = Vec::new(); + let mut functions = Vec::new(); + let mut enums = Vec::new(); + for declaration in &module.ast.ast.decls { + match declaration { + Decl::Cell(cell) if cell.name.span.end() <= module.ast.source_text.len() => { + cells.push(render_callable( + "cell", + cell.name.name.as_str(), + &cell.args, + None, + cell.span.start(), + cell.name.span.start(), + module, + targets, + )); + } + Decl::Fn(function) if function.name.span.end() <= module.ast.source_text.len() => { + functions.push(render_callable( + "fn", + function.name.name.as_str(), + &function.args, + function.return_ty.as_ref(), + function.span.start(), + function.name.span.start(), + module, + targets, + )); + } + Decl::Enum(enum_) if enum_.name.span.end() <= module.ast.source_text.len() => { + let variants = enum_ + .variants + .iter() + .map(|variant| format!("
  • {}
  • ", escape(&variant.name))) + .collect::>() + .join(""); + enums.push(render_item( + "enum", + enum_.name.name.as_str(), + &format!( + "enum {}", + escape(&enum_.name.name) + ), + enum_.name.span.start(), + enum_.name.span.start(), + module, + &format!("
      {variants}
    "), + )); + } + _ => {} + } + } + + let mut body = format!( + "
    Module

    {}

    {}
    {}", + escape(&name), + escape(&relative_path.display().to_string()), + render_doc(&docs) + ); + if !child_modules.is_empty() { + body.push_str(&format!( + "

    Modules

    {child_modules}
    " + )); + } + push_section(&mut body, "Cells", cells); + push_section(&mut body, "Functions", functions); + push_section(&mut body, "Enums", enums); + if child_modules.is_empty() && !body.contains("class=\"item\"") && docs.trim().is_empty() { + body.push_str("

    This module has no documented declarations.

    "); + } + page_shell(library, &name, Some(&name), navigation, &body) +} + +fn push_section(body: &mut String, title: &str, items: Vec) { + if !items.is_empty() { + body.push_str(&format!("

    {}

    {}", escape(title), items.join(""))); + } +} + +#[expect( + clippy::too_many_arguments, + reason = "all arguments describe one declaration" +)] +fn render_callable( + kind: &str, + name: &str, + args: &[ArgDecl], + return_ty: Option<&TySpec>, + declaration_start: usize, + name_start: usize, + module: &Module<'_>, + targets: &TypeTargets, +) -> String { + let arguments = args + .iter() + .map(|argument| { + format!( + "{}: {}", + escape(&argument.name.name), + render_type(&argument.ty, module.path, targets) + ) + }) + .collect::>() + .join(", "); + let returns = return_ty.map_or_else(String::new, |ty| { + format!(" -> {}", render_type(ty, module.path, targets)) + }); + let signature = format!( + "{} {}({arguments}){returns}", + escape(kind), + escape(name) + ); + let argument_table = (!args.is_empty()).then(|| { + let rows = args + .iter() + .map(|argument| { + format!( + "{}{}", + escape(&argument.name.name), + render_type(&argument.ty, module.path, targets) + ) + }) + .collect::>() + .join(""); + format!("{rows}
    ArgumentType
    ") + }); + render_item( + kind, + name, + &signature, + declaration_start, + name_start, + module, + argument_table.as_deref().unwrap_or(""), + ) +} + +fn render_item( + kind: &str, + name: &str, + signature: &str, + declaration_start: usize, + name_start: usize, + module: &Module<'_>, + details: &str, +) -> String { + let docs = declaration_doc(&module.ast.source_text, declaration_start); + let line = module.ast.source_text[..name_start] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1; + format!( + "

    {name}

    line {line}
    {signature}
    {docs}{details}
    ", + kind = escape(kind), + anchor = escape(name), + name = escape(name), + docs = render_doc(&docs), + ) +} + +fn render_type( + ty: &TySpec, + current_module: &ModPath, + targets: &TypeTargets, +) -> String { + match &ty.kind { + TySpecKind::Ident(ident) => { + let name = ident.name.as_str(); + let target = targets.get(name).and_then(|candidates| { + candidates + .iter() + .find(|(path, _)| path == current_module) + .or_else(|| (candidates.len() == 1).then(|| &candidates[0])) + }); + target.map_or_else( + || format!("{}", escape(name)), + |(_, href)| { + format!( + "{}", + escape(href), + escape(name) + ) + }, + ) + } + TySpecKind::Seq(inner) => format!("[{}]", render_type(inner, current_module, targets)), + TySpecKind::Tuple(items) if items.is_empty() => "()".to_owned(), + TySpecKind::Tuple(items) => format!( + "({},)", + items + .iter() + .map(|item| render_type(item, current_module, targets)) + .collect::>() + .join(", ") + ), + } +} + +fn module_doc(source: &str) -> String { + source + .lines() + .skip_while(|line| line.trim().is_empty()) + .take_while(|line| line.trim_start().starts_with("//!")) + .map(|line| line.trim_start().trim_start_matches("//!").trim_start()) + .collect::>() + .join("\n") +} + +fn declaration_doc(source: &str, declaration_start: usize) -> String { + let line_start = source[..declaration_start] + .rfind('\n') + .map_or(0, |position| position + 1); + let mut lines = Vec::new(); + for line in source[..line_start].lines().rev() { + let trimmed = line.trim_start(); + let Some(comment) = trimmed.strip_prefix("///") else { + break; + }; + lines.push(comment.trim_start()); + } + lines.reverse(); + lines.join("\n") +} + +fn render_doc(doc: &str) -> String { + if doc.trim().is_empty() { + return String::new(); + } + let mut html = String::from("
    "); + let mut in_list = false; + for line in doc.lines() { + let line = line.trim(); + if let Some(item) = line.strip_prefix("- ") { + if !in_list { + html.push_str("
      "); + in_list = true; + } + html.push_str(&format!("
    • {}
    • ", render_inline(item))); + continue; + } + if in_list { + html.push_str("
    "); + in_list = false; + } + if let Some(heading) = line.strip_prefix("### ") { + html.push_str(&format!("

    {}

    ", render_inline(heading))); + } else if let Some(heading) = line.strip_prefix("## ") { + html.push_str(&format!("

    {}

    ", render_inline(heading))); + } else if let Some(heading) = line.strip_prefix("# ") { + html.push_str(&format!("

    {}

    ", render_inline(heading))); + } else if !line.is_empty() { + html.push_str(&format!("

    {}

    ", render_inline(line))); + } + } + if in_list { + html.push_str(""); + } + html.push_str("
    "); + html +} + +fn render_inline(text: &str) -> String { + let mut output = String::new(); + for (index, part) in text.split('`').enumerate() { + if index % 2 == 1 { + output.push_str(&format!("{}", escape(part))); + } else { + output.push_str(&escape(part)); + } + } + output +} + +fn escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +#[cfg(test)] +mod tests { + use std::fs; + + use crate::{Library, doc}; + + #[test] + fn generates_linked_static_library_documentation() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("Argon.toml"), "name = \"demo\"\n").unwrap(); + fs::write( + directory.path().join("lib.ar"), + "//! Demo cells.\n/// Routing modes.\nenum Mode { Fast, Quiet, }\n/// Builds a route.\n/// # Arguments\n/// - `mode`: routing mode.\ncell route(mode: Mode) {}\n", + ) + .unwrap(); + let library = Library::load(directory.path().join("Argon.toml")).unwrap(); + let output = directory.path().join("generated-docs"); + let report = doc::generate(&library, &output).unwrap(); + + assert_eq!(report.modules, 1); + let page = fs::read_to_string(output.join("module-root.html")).unwrap(); + assert!(page.contains("Demo cells.")); + assert!(page.contains("id=\"cell.route\"")); + assert!(page.contains("href=\"module-root.html#enum.Mode\"")); + assert!(page.contains("routing mode")); + assert!(!page.contains(" + + + + + + diff --git a/crates/gui/assets/icons/file-pen.svg b/crates/gui/assets/icons/file-pen.svg new file mode 100644 index 00000000..78469372 --- /dev/null +++ b/crates/gui/assets/icons/file-pen.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/gui/src/actions.rs b/crates/gui/src/actions.rs index 7f55f6e5..57cbd548 100644 --- a/crates/gui/src/actions.rs +++ b/crates/gui/src/actions.rs @@ -31,6 +31,8 @@ actions!( ShowMessages, InstantiateCommand, OpenCellCommand, + NewCellCommand, + RenameCellCommand, Cancel, Backspace, Delete, diff --git a/crates/gui/src/editor/mod.rs b/crates/gui/src/editor/mod.rs index 1001e199..03448888 100644 --- a/crates/gui/src/editor/mod.rs +++ b/crates/gui/src/editor/mod.rs @@ -22,8 +22,8 @@ use tower_lsp_server::ls_types::MessageType; use crate::{ actions::{ - FocusInvoker, FocusInvokerCommandBar, InstantiateCommand, OpenCellCommand, Redo, Save, - ShowDiagnostics, ShowMessages, Undo, + FocusInvoker, FocusInvokerCommandBar, InstantiateCommand, NewCellCommand, OpenCellCommand, + Redo, RenameCellCommand, Save, ShowDiagnostics, ShowMessages, Undo, }, editor::{canvas::ToolState, input::TextInput}, rpc::SyncLangServerClient, @@ -917,6 +917,24 @@ impl Editor { self.open_invoking_command(Some("Argon openCell "), true, cx); } + fn new_cell_command( + &mut self, + _: &NewCellCommand, + _window: &mut Window, + cx: &mut Context, + ) { + self.open_invoking_command(Some("Argon newCell "), true, cx); + } + + fn rename_cell_command( + &mut self, + _: &RenameCellCommand, + _window: &mut Window, + cx: &mut Context, + ) { + self.open_invoking_command(Some("Argon renameCell "), true, cx); + } + fn open_invoking_command( &mut self, command: Option<&str>, @@ -1103,6 +1121,8 @@ impl Render for Editor { .on_action(cx.listener(Self::show_messages)) .on_action(cx.listener(Self::instantiate_command)) .on_action(cx.listener(Self::open_cell_command)) + .on_action(cx.listener(Self::new_cell_command)) + .on_action(cx.listener(Self::rename_cell_command)) .font_family("Zed Plex Sans") .size_full() .flex() diff --git a/crates/gui/src/editor/toolbars.rs b/crates/gui/src/editor/toolbars.rs index 348f7740..842f67ac 100644 --- a/crates/gui/src/editor/toolbars.rs +++ b/crates/gui/src/editor/toolbars.rs @@ -10,8 +10,8 @@ use itertools::Itertools; use crate::{ actions::{ - DrawDim, DrawPath, DrawPolygon, DrawRect, InstantiateCommand, OpenCellCommand, Redo, - SelectMode, Undo, + DrawDim, DrawPath, DrawPolygon, DrawRect, InstantiateCommand, NewCellCommand, + OpenCellCommand, Redo, RenameCellCommand, SelectMode, Undo, }, editor::{ CompileOutputState, Layers, ScopeAddress, ScopePath, @@ -621,6 +621,18 @@ impl Render for ToolBar { .dispatch_action(LangServerAction::Redo); }), }, + ToolbarItem::Button { + id: "btn_new_cell", + icon: "icons/file-circle-plus.svg", + label: "New cell", + action: Box::new(NewCellCommand), + highlighted: Box::new(|_| false), + on_click: Arc::new(|_state, cx| { + cx.defer(move |cx| { + cx.dispatch_action(&NewCellCommand); + }); + }), + }, ToolbarItem::Button { id: "btn_open_cell", icon: "icons/folder-open.svg", @@ -633,6 +645,18 @@ impl Render for ToolBar { }); }), }, + ToolbarItem::Button { + id: "btn_rename_cell", + icon: "icons/file-pen.svg", + label: "Rename cell", + action: Box::new(RenameCellCommand), + highlighted: Box::new(|_| false), + on_click: Arc::new(|_state, cx| { + cx.defer(move |cx| { + cx.dispatch_action(&RenameCellCommand); + }); + }), + }, ToolbarItem::Divider("divider_history_select"), ToolbarItem::Button { id: "btn_select", @@ -792,7 +816,10 @@ mod tool_bar_tests { use super::hotkey_text; use crate::{ - actions::{DrawDim, DrawPath, DrawPolygon, DrawRect, InstantiateCommand, SelectMode, Undo}, + actions::{ + DrawDim, DrawPath, DrawPolygon, DrawRect, InstantiateCommand, NewCellCommand, + RenameCellCommand, SelectMode, Undo, + }, key_bindings, }; @@ -821,6 +848,8 @@ mod tool_bar_tests { assert_eq!(hotkey_text(&SelectMode, window), Some("S".into())); assert_eq!(hotkey_text(&DrawDim, window), Some("D".into())); assert_eq!(hotkey_text(&InstantiateCommand, window), Some("I".into())); + assert!(hotkey_text(&NewCellCommand, window).is_some()); + assert!(hotkey_text(&RenameCellCommand, window).is_some()); assert_eq!(hotkey_text(&Undo, window), Some("U".into())); // The path tool has no binding, so its tooltip shows the label alone. assert_eq!(hotkey_text(&DrawPath, window), None); diff --git a/crates/gui/src/lib.rs b/crates/gui/src/lib.rs index 91c17025..db84955a 100644 --- a/crates/gui/src/lib.rs +++ b/crates/gui/src/lib.rs @@ -100,7 +100,13 @@ fn run_inner( }, Menu { name: "File".into(), - items: vec![MenuItem::action("Save", Save)], + items: vec![ + MenuItem::action("New Cell…", NewCellCommand), + MenuItem::action("Open Cell…", OpenCellCommand), + MenuItem::action("Rename Cell…", RenameCellCommand), + MenuItem::separator(), + MenuItem::action("Save", Save), + ], }, Menu { name: "Edit".into(), @@ -161,6 +167,8 @@ fn key_bindings() -> Vec { KeyBinding::new("d", DrawDim, Some(CANVAS_CONTEXT)), KeyBinding::new("i", InstantiateCommand, Some(CANVAS_CONTEXT)), KeyBinding::new("o", OpenCellCommand, Some(CANVAS_CONTEXT)), + KeyBinding::new("cmd-n", NewCellCommand, None), + KeyBinding::new("cmd-shift-r", RenameCellCommand, None), KeyBinding::new("f", Fit, Some(CANVAS_CONTEXT)), KeyBinding::new("q", Edit, Some(CANVAS_CONTEXT)), KeyBinding::new("u", Undo, Some(CANVAS_CONTEXT)), @@ -235,6 +243,8 @@ mod tests { command_bar_count: usize, instantiate_count: usize, open_cell_count: usize, + new_cell_count: usize, + rename_cell_count: usize, focus_invoker_count: usize, show_diagnostics_count: usize, show_messages_count: usize, @@ -272,6 +282,10 @@ mod tests { cx.listener(|view, _: &InstantiateCommand, _, _| view.instantiate_count += 1), ) .on_action(cx.listener(|view, _: &OpenCellCommand, _, _| view.open_cell_count += 1)) + .on_action(cx.listener(|view, _: &NewCellCommand, _, _| view.new_cell_count += 1)) + .on_action( + cx.listener(|view, _: &RenameCellCommand, _, _| view.rename_cell_count += 1), + ) .on_action( cx.listener(|view, _: &FocusInvoker, _, _| view.focus_invoker_count += 1), ) @@ -310,6 +324,8 @@ mod tests { command_bar_count: 0, instantiate_count: 0, open_cell_count: 0, + new_cell_count: 0, + rename_cell_count: 0, focus_invoker_count: 0, show_diagnostics_count: 0, show_messages_count: 0, @@ -330,7 +346,7 @@ mod tests { .unwrap(); cx.simulate_keystrokes( *window, - "u r p i o 0 1 * left right up down cmd-= cmd-+ cmd-- ctrl-= ctrl-+ ctrl-- ctrl-shift-d ctrl-shift-m : ctrl-\\ cmd-s", + "u r p i o 0 1 * left right up down cmd-= cmd-+ cmd-- ctrl-= ctrl-+ ctrl-- ctrl-shift-d ctrl-shift-m : ctrl-\\ cmd-s cmd-n cmd-shift-r", ); window .update(cx, |view, _, _| { @@ -340,6 +356,8 @@ mod tests { assert_eq!(view.command_bar_count, 0); assert_eq!(view.instantiate_count, 0); assert_eq!(view.open_cell_count, 0); + assert_eq!(view.new_cell_count, 1); + assert_eq!(view.rename_cell_count, 1); assert_eq!(view.zero_count, 0); assert_eq!(view.one_count, 0); assert_eq!(view.all_count, 0); @@ -358,7 +376,7 @@ mod tests { .unwrap(); cx.simulate_keystrokes( *window, - "u r p i o 0 1 * left right up down cmd-= cmd-+ cmd-- ctrl-= ctrl-+ ctrl-- ctrl-shift-d ctrl-shift-m : ctrl-\\ cmd-s", + "u r p i o 0 1 * left right up down cmd-= cmd-+ cmd-- ctrl-= ctrl-+ ctrl-- ctrl-shift-d ctrl-shift-m : ctrl-\\ cmd-s cmd-n cmd-shift-r", ); window .update(cx, |view, _, _| { @@ -368,6 +386,8 @@ mod tests { assert_eq!(view.command_bar_count, 1); assert_eq!(view.instantiate_count, 1); assert_eq!(view.open_cell_count, 1); + assert_eq!(view.new_cell_count, 2); + assert_eq!(view.rename_cell_count, 2); assert_eq!(view.zero_count, 1); assert_eq!(view.one_count, 1); assert_eq!(view.all_count, 1); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..38cc8b6a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,49 @@ +# Argon documentation + +This directory holds the documentation site and the contributor notes. + +## Site + +The site is built with Docusaurus and lives entirely in this directory, so the +repository root stays a plain Cargo workspace. + +```bash +cd docs +npm install +npm start +``` + +`npm run build` writes a production build to `build/`. Internal links and +anchors are checked as part of the build and a broken one fails it. + +| Path | Purpose | +| --- | --- | +| `content/` | The published pages, one directory per book (see below) | +| `docusaurus.config.ts`, `sidebars.ts` | Site configuration and navigation | +| `src/pages/index.tsx` | The front page | +| `src/components/ApiReference.tsx` | Components used by the reference pages | +| `src/theme/prism-include-languages.js` | Prism grammar for ` ```argon ` code fences | +| `static/img/` | Favicon, social card, and the GUI screenshot on the front page | + +Each book has its own sidebar. Pages are served from the site root, so the +directory name is also the URL prefix. + +| Directory | Sidebar | Contents | +| --- | --- | --- | +| `content/guides/` | Guides | `index.md` lists the guides; each guide is a subdirectory, currently only `getting-started/` | +| `content/language/` | Language | Language chapters, then `builtins/`, `std.mdx`, and `types/` for the reference | +| `content/gui/` | GUI | The visual editor | +| `content/tools/` | Tools | `arc`, `argone`, `argonc`, and the Neovim plugin | + +Sidebar entries in `sidebars.ts` are paths relative to `content/`. Links within +pages use absolute URL paths such as `/language/types/rect`. + +`static/img/gui.png` is a screenshot of the GUI with `diff_vco_top()` from +`pdks/sky130` open in dark mode, cropped to remove the window title bar. Retake +it after visible GUI changes. + +## Contributor notes + +`developers.md`, `parser.md`, and the `gpui-*.md` files are notes for people +working on Argon itself. They are not part of the site. `figures/` holds the +source figures for the paper and README. diff --git a/docs/content/gui/cell-management.md b/docs/content/gui/cell-management.md new file mode 100644 index 00000000..717c70f5 --- /dev/null +++ b/docs/content/gui/cell-management.md @@ -0,0 +1,32 @@ +--- +title: Cell management +description: Open, create, and rename cells from the GUI. +--- + +# Cell management + +You can open, create, and rename cells from the GUI. Creating and renaming edit the source through Neovim. + +## Open a cell + +Press O and type a full invocation, arguments included. Opening only changes what the canvas shows; it doesn't touch the source. + +## Create a cell + +Choose **New Cell…** from the File menu, click **New cell** in the toolbar, or press Cmd+N. Type a name and confirm. Argon inserts an empty cell into the current module and opens it. + +The name must be a valid identifier, not a keyword, and not already declared in the module. The source buffer must also be editable through Neovim. + +The insertion is an ordinary buffer edit, so it marks the buffer modified and can be undone. + +## Rename the open cell + +Choose **Rename Cell…** from the File menu, click **Rename cell** in the toolbar, or press Cmd+Shift+R while a source-defined cell is open. Type the new name and confirm. The declaration and every reference that resolves to it are updated. + +Rename is semantic. Comments, strings, fields, functions, and unrelated cells that happen to share the name are left alone. Imported GDS cells and other read-only declarations can't be renamed. + +Once the edit is applied, Argon reopens the same invocation under the new name. If the name is invalid or already taken, nothing changes and an error is shown. + +:::info +Creating and renaming need a running Neovim and analyzer, because the GUI never writes to source files directly. +::: diff --git a/docs/content/gui/drawing.md b/docs/content/gui/drawing.md new file mode 100644 index 00000000..89ffa26b --- /dev/null +++ b/docs/content/gui/drawing.md @@ -0,0 +1,43 @@ +--- +title: Drawing and editing +description: The canvas tools for geometry, dimensions, and instances. +--- + +# Drawing and editing + +Canvas shortcuts work while the canvas has focus and no text field is active. + +## Select + +Press S. You can select shapes, instances, edges, and dimension labels; what's selected determines which edits are available. Press Q to edit a selected dimension. + +## Rectangle + +Pick a layer, press R, and click two opposite corners. The [`rect`](/language/builtins/geometry#rect) call the GUI writes uses initial values (`x0i` and so on), so you can drag the rectangle around until constraints pin it down. + +## Polygon + +Pick a layer, press P, and click the vertices in order. Enter closes the polygon and writes it to the source; Esc discards it. + +Each vertex is constrained independently. A vertex with one constrained axis can still be dragged along the other. + +## Path + +Choose Path from the Tools menu or the tool strip, pick a layer, and click the centerline points. A path's points, width, and end extensions are all editable. + +## Dimension + +Press D, click two compatible edges, then click where the label should go. Type a float such as `50.` or a cell parameter such as `width`. + +Dimensions also work on rectangles imported from GDS. If the technology file configures pin layers, imported pin labels become fields you can refer to. + +## Instance + +Select the target scope in the hierarchy sidebar, press I, and type a cell invocation. Move the preview and click to place it. Placement stays active so you can drop more copies; press Esc to stop. + +## Reading the canvas + +- Solid edges are fixed by constraints. +- Dashed edges have at least one coordinate that is still free. +- Dragging a free coordinate updates its `*i` argument in the source. +- If a hand-written constructor has no `*i` arguments, the first drag adds them. diff --git a/docs/content/gui/hierarchy-layers.md b/docs/content/gui/hierarchy-layers.md new file mode 100644 index 00000000..9d0fa5c1 --- /dev/null +++ b/docs/content/gui/hierarchy-layers.md @@ -0,0 +1,29 @@ +--- +title: Hierarchy and layers +description: The hierarchy and layer sidebars. +--- + +# Hierarchy and layers + +## Hierarchy + +The hierarchy sidebar shows the open cell, its scopes, and the instances nested inside it. Select a scope before placing an instance to choose where the new call goes in the source. + +The depth controls limit how many levels of hierarchy are drawn. When a child is collapsed to its bounding box, those edges are what [`bbox(instance)`](/language/builtins/hierarchy#bbox) refers to in the source. + +Opening a child from the hierarchy opens it with the exact arguments of that instance. + +## Layers + +The layer sidebar lists the layers in the active [technology file](/language/technology). Pick a visible, valid layer before drawing. + +Per layer, the technology file controls: + +- Fill and border colors. +- Border width and line style. +- Visibility and validity. +- Grouping, and whether a group starts expanded. +- Stipple and line patterns. +- Transparency, markings, and animation. + +An invalid layer can be shown but not drawn on. Hiding a layer hides its geometry without changing the source. diff --git a/docs/content/gui/shortcuts-config.md b/docs/content/gui/shortcuts-config.md new file mode 100644 index 00000000..66b56e14 --- /dev/null +++ b/docs/content/gui/shortcuts-config.md @@ -0,0 +1,66 @@ +--- +title: Shortcuts and configuration +description: Keyboard shortcuts, the configuration file, and troubleshooting. +--- + +# Shortcuts and configuration + +## Keyboard shortcuts + +| Shortcut | Action | +| --- | --- | +| R | Rectangle tool | +| P | Polygon tool | +| S | Select mode | +| D | Dimension tool | +| Q | Edit the selected item | +| I | Place an instance | +| O | Open a cell | +| Cmd+N | Create a cell in the current module | +| Cmd+Shift+R | Rename the open cell | +| F | Fit the layout to the canvas | +| U | Undo (in the source buffer) | +| Ctrl+R | Redo | +| Arrow keys | Pan | +| Cmd/Ctrl++ / - | Zoom in or out | +| : | Focus the Neovim command line | +| Ctrl+Backslash | Switch between the GUI and Neovim | +| Ctrl+Shift+D | Show diagnostics | +| Ctrl+Shift+M | Show messages | +| Esc | Cancel the current operation | +| Enter | Confirm or finish the current operation | + +## Configuration + +Argon reads `$XDG_CONFIG_HOME/argon/config.toml`, or `~/.config/argon/config.toml` if that variable isn't set. + +```toml +[gui] +dark_mode = true +hierarchy_depth = 3 +icon_size = 20 +font_size = 14 + +[log] +level = "info" +``` + +`font_size` and `icon_size` are in logical pixels, from 1 to 256. After editing the file, run `:Argon reload`. To change a value for this session only, use `:Argon set`; to write the current settings back to disk, use `:Argon saveConfig`. + +## Troubleshooting + +### The open cell is invalid after a signature change + +Press O and reopen it with arguments that match the new signature. Argon won't guess values for new parameters. + +### Something won't move + +A solid edge or an existing dimension is already fixing that coordinate. An initial value can't override a constraint. + +### You need more detail on an error + +Run `:Argon diagnostics` and `:Argon log`. For more, set `log.level = "debug"` and reload the configuration. + +### The GUI didn't start + +Check that `argon-analyzer` and `argone` are on your `PATH`, the buffer's file ends in `.ar`, and the library has `Argon.toml` and `lib.ar`. Then run `:Argon gui`. diff --git a/docs/content/gui/workspace.md b/docs/content/gui/workspace.md new file mode 100644 index 00000000..e5fca403 --- /dev/null +++ b/docs/content/gui/workspace.md @@ -0,0 +1,35 @@ +--- +title: GUI workspace +description: How the visual editor is laid out and how it relates to the source. +sidebar_label: Workspace +--- + +# GUI workspace + +The GUI shows the cell compiled from your current Neovim buffers. It's built for looking at layout and editing it spatially; the source stays the single source of truth. + +## Regions + +| Region | Purpose | +| --- | --- | +| Canvas | Solved geometry, selection, dimensions, and placement previews. | +| Tool strip | Select, rectangle, polygon, path, dimension, and instance tools. | +| Hierarchy sidebar | Scopes and nested instances. Selecting one sets where new geometry is inserted. | +| Layer sidebar | The drawing layer and per-layer visibility. | +| Neovim command line | Where the GUI asks for text, such as a cell invocation or a name. | + +## Open a cell + +Press O, type an invocation such as `inverter(1200., 2000., 4)`, and press Enter. The arguments are parsed and type-checked in the library's scope. + +What you open is an invocation, not just a cell name. If you change a cell's signature, reopen it with matching arguments. + +## Two-way editing + +1. Neovim sends the current source to the analyzer. +2. The analyzer parses, checks, and compiles the open cell. +3. The GUI draws the latest valid result. +4. When you draw or drag on the canvas, the GUI asks the analyzer for a source edit. +5. Neovim applies the edit, and the loop starts again. + +A compile error keeps the last good layout on screen unless the open cell itself no longer compiles. Press Ctrl+Shift+D to see diagnostics. diff --git a/docs/content/guides/getting-started/constraints.md b/docs/content/guides/getting-started/constraints.md new file mode 100644 index 00000000..cd23f44e --- /dev/null +++ b/docs/content/guides/getting-started/constraints.md @@ -0,0 +1,56 @@ +--- +title: Add constraints +description: Constrain the two rectangles and turn width and height into parameters. +--- + +# Add constraints + +A constraint fixes a relationship between geometric values. It's different from the initial values the GUI wrote when you drew the rectangles: a constraint determines a value, while an initial value only positions something that is otherwise free. + +## Inset the inner rectangle + +Press D for the Dimension tool. Click the matching edge on each rectangle, then click where the label should go. Type `50.` and press Enter. + +Do the same for the other three sides. + +:::warning Floats need a decimal point +`50` is an [`Int`](/language/types/scalars#int); `50.` is a [`Float`](/language/types/scalars#float). Coordinates and dimensions are floats. +::: + +In source, the four dimensions amount to: + +```argon +eq(inner.x0, outer.x0 + 50.); +eq(inner.y0, outer.y0 + 50.); +eq(inner.x1, outer.x1 - 50.); +eq(inner.y1, outer.y1 - 50.); +``` + +## Make width and height parameters + +Give the cell two arguments and constrain the outer rectangle to them: + +```argon +cell inset_rect(w: Float, h: Float) { + let outer = rect("met1", x0=0., y0=0.); + eq(outer.w, w); + eq(outer.h, h); + + let inner = rect("met2"); + eq(inner.x0, outer.x0 + 50.); + eq(inner.y0, outer.y0 + 50.); + eq(inner.x1, outer.x1 - 50.); + eq(inner.y1, outer.y1 - 50.); +} +``` + +The cell now takes two arguments, so `inset_rect()` no longer compiles. Press O and open `inset_rect(200., 200.)`. + +## Reading the canvas + +- A solid edge is fixed by constraints. +- A dashed edge still depends on an initial value. +- Dragging a dashed edge updates its `*i` argument in the source. +- An initial value never overrides a constraint. + +[Constraints and fallback values](/language/constraints) covers the model in more depth. Next: [Hierarchy and export](./hierarchy-export). diff --git a/docs/content/guides/getting-started/first-cell.md b/docs/content/guides/getting-started/first-cell.md new file mode 100644 index 00000000..4b13f237 --- /dev/null +++ b/docs/content/guides/getting-started/first-cell.md @@ -0,0 +1,67 @@ +--- +title: Your first cell +description: Create a library and open a cell in the GUI. +--- + +# Your first cell + +In this step you create a library, open it in Neovim and the GUI, and draw two rectangles. + +## Create a library + +```bash +arc new tutorial +cd tutorial +``` + +This creates three files: + +```text +tutorial/ +├── Argon.toml # library manifest +├── tech.toml # units, layers, and display styles +└── lib.ar # Argon source +``` + +`lib.ar` starts with an empty `top()` cell. A cell is a layout definition you can call, like a parameterized block. + +## Open the editor + +From the library directory: + +```bash +argone . +``` + +This starts Neovim, the analyzer, and the GUI window. Neovim owns the source; the analyzer compiles it and sends the result to the GUI. + +Click the canvas and press O. The command line opens with `:Argon openCell` filled in. Type `top()` and press Enter. + +## Replace the starter cell + +Replace the contents of `lib.ar` with: + +```argon title="lib.ar" +cell inset_rect() { +} +``` + +Press O again and open `inset_rect()`. You don't need to save first: the analyzer compiles the buffer as you type, and the canvas updates a moment after you stop. + +## Draw two rectangles + +1. Pick the `met2` layer in the layer sidebar. +2. Press R and click two opposite corners. +3. Pick `met1` and draw a larger rectangle around the first. +4. Press Esc to return to selection mode. + +Look at `lib.ar` in Neovim: each rectangle is now a `rect` call. The GUI writes into the buffer, not the file, so these edits can be undone like any other. The calls use initial values such as `x0i` and `y1i`, which is what lets you drag unconstrained edges later. + +## Check the project + +```bash +arc fmt +arc check +``` + +Next, [add constraints](./constraints) to replace the sketched positions with relationships. diff --git a/docs/content/guides/getting-started/hierarchy-export.md b/docs/content/guides/getting-started/hierarchy-export.md new file mode 100644 index 00000000..33328b75 --- /dev/null +++ b/docs/content/guides/getting-started/hierarchy-export.md @@ -0,0 +1,51 @@ +--- +title: Hierarchy and export +description: Place cells inside other cells, check the library, and write GDS. +--- + +# Hierarchy and export + +A cell becomes reusable once you place it inside another cell. + +## Compose a parent cell + +Add this after `inset_rect`: + +```argon +cell triple_rect() { + let first = inst(inset_rect(200., 200.), x=0., y=0.); + let second = inst(inset_rect(240., 180.), x=300., y=0.); + let third = inst(inset_rect(160., 260.), x=650., y=0.); +} +``` + +Save, then open `triple_rect()` from the canvas. The hierarchy sidebar lists the three instances under the root scope. + +You can also place instances from the GUI: press I, type a cell invocation, and click to place it in the selected scope. Placement stays active so you can drop several copies; press Esc when you're done. + +## Format and check + +```bash +arc fmt +arc check +``` + +`arc fmt --check` reports unformatted files without changing them, which is useful in CI. + +## Export + +```bash +arc run --cell 'triple_rect()' +``` + +This writes the compiled cell to `target/argon.bin`. Add `--gds` to also write `target/argon.gds`: + +```bash +arc run --cell 'triple_rect()' --gds +``` + +:::note +Quote the cell expression so the shell doesn't interpret the parentheses. +::: + +That's the end of the getting-started guide. Other guides are listed on the [Guides](/guides) page. From here, the [language reference](/language/overview) covers the rest of the language, and the [GUI](/gui/workspace) and [tools](/tools/overview) books cover the editor and the command line in detail. diff --git a/docs/content/guides/getting-started/installation.md b/docs/content/guides/getting-started/installation.md new file mode 100644 index 00000000..680199b3 --- /dev/null +++ b/docs/content/guides/getting-started/installation.md @@ -0,0 +1,53 @@ +--- +title: Install Argon +description: Install the Argon command-line tools and the Neovim plugin. +--- + +# Install Argon + +Argon is installed from source with Cargo. The `argon` package provides four executables: `arc`, `argonc`, `argone`, and `argon-analyzer`. + +## Prerequisites + +- A Rust toolchain with Cargo. +- Neovim 0.12 or newer. +- Git. + +## Install + +```bash +cargo install --git https://github.com/ucb-substrate/argon --locked argon +``` + +Or, from a local checkout: + +```bash +cargo install --locked --path crates/argon +``` + +Check that the tools are on your `PATH`: + +```bash +arc --version +argone --version +``` + +:::note +The Rust and Neovim versions the project is tested against change over time. If an install fails, check the CI configuration in the repository for the versions currently in use. +::: + +## Add the Neovim plugin + +With Neovim's built-in package manager: + +```lua +vim.pack.add({ + 'https://github.com/ucb-substrate/argon', +}) +``` + +The plugin detects `.ar` files and starts `argon-analyzer` from your `PATH`. + +## Next + +[Your first cell](./first-cell) creates a library and opens it in the editor. diff --git a/docs/content/guides/index.md b/docs/content/guides/index.md new file mode 100644 index 00000000..0dffc1c7 --- /dev/null +++ b/docs/content/guides/index.md @@ -0,0 +1,12 @@ +--- +title: Guides +description: Step-by-step guides for building layouts with Argon. +--- + +# Guides + +Each guide builds something concrete from start to finish. Work through the steps in order; later guides assume the earlier ones. + +- **[Getting started](/guides/getting-started/installation)**. Install Argon, draw and constrain a cell, build a small hierarchy, and export GDS. Start here. + +The guides don't cover every feature. For that, see the [language reference](/language/overview), the [GUI manual](/gui/workspace), and the [tools reference](/tools/overview). diff --git a/docs/content/language/builtins/collections.mdx b/docs/content/language/builtins/collections.mdx new file mode 100644 index 00000000..51313f34 --- /dev/null +++ b/docs/content/language/builtins/collections.mdx @@ -0,0 +1,89 @@ +--- +title: Collection built-ins +description: Construct and traverse homogeneous sequences. +sidebar_label: Collections +--- + +import {ApiItem, ParameterTable, Returns} from '@site/src/components/ApiReference'; + +# Collection built-ins + +These functions build and take apart [`[T]`](/language/types/collections#sequences) sequences. + +## `list` + + [T]'} summary={<>Builds a sequence from one or more values of the same type.}> + + + +The values, in order. + +```argon +let widths = list(80., 120., 160.); +``` + + + +## `cons` + + [T]'} summary={<>Prepends one value to an existing sequence.}> + + + +A new sequence starting with value. + +```argon +let values = cons(10., cons(20., [])); +``` + + + +## `head` + + T'} summary={<>Returns the first element of a sequence.}> + + + +The first element. + +Fails at run time if the sequence is empty. + + + +## `tail` + + [T]'} summary={<>Returns every element after the first.}> + + + +Everything after the first element. + +Fails at run time if the sequence is empty. + + + +## `range_full` + + [Int]'} summary={<>Builds an integer range from a start, stop, and step.}> + + + +The range as a sequence. + +For the common case of counting from zero, use [`std::range(stop)`](/language/std#range). + + diff --git a/docs/content/language/builtins/constraints.mdx b/docs/content/language/builtins/constraints.mdx new file mode 100644 index 00000000..06a207fb --- /dev/null +++ b/docs/content/language/builtins/constraints.mdx @@ -0,0 +1,68 @@ +--- +title: Constraint built-ins +description: Create solver values, equality constraints, and visual dimensions. +sidebar_label: Constraints +--- + +import {ApiItem, ParameterTable, Returns} from '@site/src/components/ApiReference'; + +# Constraint built-ins + +Constraints relate [`Float`](/language/types/scalars#float) expressions; the solver finds geometry that satisfies them. + +## `float` + + Float'} summary={<>Creates a new solver variable.}> + +Use it for a value you want to name now and constrain later. + +A variable with no constraints on it yet. + +```argon +let center = float(); +eq(center, (bounds.x0 + bounds.x1) / 2.); +``` + + + +## `eq` + + ()'} summary={<>Constrains two linear expressions to be equal.}> + + + +Nothing; the constraint is added to the current cell. + +```argon +eq(inner.x0, outer.x0 + inset); +eq(inner.w, outer.w - 2. * inset); +``` + +Conflicting constraints are reported when the cell runs. + + + +## `dimension` + + ()'} summary={<>Adds an equality constraint together with the dimension label the GUI draws for it.}> + + + +Nothing; the constraint and its label are added to the current cell. + +:::tip Prefer the GUI for dimensions +The Dimension tool fills in the orientation and label-placement arguments from the edges you click. In hand-written source, use `eq` unless you want a visible dimension. +::: + + diff --git a/docs/content/language/builtins/geometry.mdx b/docs/content/language/builtins/geometry.mdx new file mode 100644 index 00000000..9c76ef2f --- /dev/null +++ b/docs/content/language/builtins/geometry.mdx @@ -0,0 +1,135 @@ +--- +title: Geometry built-ins +description: The rect, crect, polygon, path, and text constructors. +sidebar_label: Geometry +--- + +import {ApiItem, ParameterTable, Returns} from '@site/src/components/ApiReference'; + +# Geometry built-ins + +These constructors add geometry to the current cell. Coordinate arguments are [`Float`](/language/types/scalars#float) expressions. + +## `rect` + + Rect'} summary={<>Draws an axis-aligned rectangle on a layer.}> + + + +The rectangle. + +```argon +let gate = rect("poly", x0=0., y0=0., w=80., h=400.); +``` + +Give enough values or constraints to pin the rectangle down. If you drag an under-constrained rectangle in the GUI, it adds the missing initial values. + + + +## `crect` + + Rect'} summary={<>Creates a construction rectangle: usable in constraints and as bounds, but not exported.}> + + + +The construction rectangle. It takes part in constraints but is left out of layout export. + +```argon +let bounds = crect(x0=0., y0=0., w=1000., h=800.); +``` + + + +## `polygon` + + Polygon'} summary={<>Creates a polygon with a fixed number of vertices, each constrained on its own.}> + + + +The polygon. + +```argon +let triangle = polygon("met1", 3, + x0=0., y0=0., + x1=100., y1=0., + x2=50., y2=80., +); +``` + + + +## `path` + + Path'} summary={<>Creates a constant-width path along a centerline.}> + + + +The path. + +```argon +let route = path("met2", 2, width=20., x0=0., y0=0., x1=300., y1=0.); +``` + + + +## `text` + + ()'} summary={<>Places a text label on a layer.}> + + + +Nothing; the label is added to the current cell. + +```argon +text("VDD", "text.label", 40., 80.); +``` + + diff --git a/docs/content/language/builtins/hierarchy.mdx b/docs/content/language/builtins/hierarchy.mdx new file mode 100644 index 00000000..981cba1e --- /dev/null +++ b/docs/content/language/builtins/hierarchy.mdx @@ -0,0 +1,53 @@ +--- +title: Hierarchy built-ins +description: Place cell instances and inspect cell or instance bounds. +sidebar_label: Hierarchy +--- + +import {ApiItem, ParameterTable, Returns} from '@site/src/components/ApiReference'; + +# Hierarchy built-ins + +## `inst` + + Inst'} summary={<>Places a cell in the current scope.}> + + + +The instance, with its position and the fields its cell exports. + +```argon +let placed = inst(inverter(1200., 2000., 4), x=100., y=50., angle=90); +``` + + + +## `bbox` + + Rect'} summary={<>Returns the bounding box of a cell or a placed instance, as a construction rectangle.}> + + + +A construction rectangle around the layout geometry. + +```argon +let child = inverter(1200., 2000., 4); +let local = bbox(child); +let placed = inst(child, x=100., y=50.); +let transformed = bbox(placed); +``` + +Construction-only geometry does not count toward the bounding box. + + diff --git a/docs/content/language/builtins/index.md b/docs/content/language/builtins/index.md new file mode 100644 index 00000000..7afc5e2a --- /dev/null +++ b/docs/content/language/builtins/index.md @@ -0,0 +1,34 @@ +--- +title: Built-in functions +description: Index of the functions and types available to Argon source. +--- + +# Built-in functions + +Built-in functions are provided by the compiler and need no module prefix. They fall into four groups: + +| Page | Functions | +| --- | --- | +| [Geometry](/language/builtins/geometry) | `rect`, `crect`, `polygon`, `path`, `text` | +| [Constraints](/language/builtins/constraints) | `float`, `eq`, `dimension` | +| [Hierarchy](/language/builtins/hierarchy) | `inst`, `bbox` | +| [Collections](/language/builtins/collections) | `list`, `cons`, `head`, `tail`, `range_full` | + +Functions written in Argon itself live under `std::`; see the [standard library](/language/std). + +## Types + +| Category | Types | +| --- | --- | +| Scalars | [`Float`](/language/types/scalars#float), [`Int`](/language/types/scalars#int), [`Bool`](/language/types/scalars#bool), [`String`](/language/types/scalars#string), [`Any`](/language/types/scalars#any), [`()`](/language/types/scalars#unit) | +| Geometry | [`Rect`](/language/types/rect), [`Polygon`](/language/types/polygon), [`Path`](/language/types/path), [`Point`](/language/types/point) | +| Hierarchy | [`Cell`](/language/types/instance#cell-values), [`Inst`](/language/types/instance#instance-values) | +| Collections | [`[T]`](/language/types/collections#sequences), [`(A, B)`](/language/types/collections#tuples) | + +## Signature notation + +- Arguments before the keyword list are positional and required. +- `name?` is an optional keyword argument. +- `T` is a type parameter inferred from the arguments. +- Initial values end in `i`, such as `x0i` or `widthi`. +- Unless stated otherwise, coordinates and dimensions are [`Float`](/language/types/scalars#float). diff --git a/docs/content/language/cells-functions.md b/docs/content/language/cells-functions.md new file mode 100644 index 00000000..8046ee7d --- /dev/null +++ b/docs/content/language/cells-functions.md @@ -0,0 +1,63 @@ +--- +title: Cells and functions +description: Cells describe layout; functions compute values. +--- + +# Cells and functions + +Cells and functions look alike but do different jobs: a cell describes layout, a function computes a value. + +## Cells + +A cell describes layout and can be placed inside other cells: + +```argon +cell pad(width: Float, height: Float) { + rect("met1", x0=0., y0=0., w=width, h=height); +} +``` + +Calling `pad(100., 80.)` gives you a cell value. Pass it to [`inst`](/language/builtins/hierarchy#inst) to place it. + +## Functions + +A function computes a value. Its last expression is the return value: + +```argon +fn half(value: Float) -> Float { + value / 2. +} +``` + +A function can also emit geometry or constraints into the scope it's called from: + +```argon +fn align_left(a: Rect, b: Rect) { + eq(a.x0, b.x0); +} +``` + +## Arguments and return types + +Argument types are written `name: Type`, and the return type follows `->`. Some argument types can be inferred, but writing them out gives clearer call sites and error messages. + +```argon +fn inset_bounds(rect_: Rect, amount: Float) -> Rect { + crect( + x0=rect_.x0 + amount, + y0=rect_.y0 + amount, + x1=rect_.x1 - amount, + y1=rect_.y1 - amount, + ) +} +``` + +## Bindings and order + +`let` introduces an immutable binding: + +```argon +let bounds = bbox(child); +``` + +Top-level declarations are resolved across the whole module, so a cell can call a function declared further down the file. diff --git a/docs/content/language/constraints.md b/docs/content/language/constraints.md new file mode 100644 index 00000000..2167a789 --- /dev/null +++ b/docs/content/language/constraints.md @@ -0,0 +1,49 @@ +--- +title: Constraints and fallback values +description: Equality constraints, free values, and the initial values the GUI edits. +--- + +# Constraints and fallback values + +Argon has two ways to give a coordinate a value. A constraint fixes it. An initial value only says where it starts, and the GUI can move it. + +## Equality constraints + +[`eq(left, right)`](/language/builtins/constraints#eq) makes two linear float expressions equal: + +```argon +eq(inner.x0, outer.x0 + inset); +eq(inner.y0, outer.y0 + inset); +eq(inner.x1, outer.x1 - inset); +eq(inner.y1, outer.y1 - inset); +``` + +Either side can mix cell parameters, literals, and geometry fields, as long as the result is linear. + +## Free values + +[`float()`](/language/builtins/constraints#float) creates a new solver variable with no value yet: + +```argon +let center = float(); +eq(center, (bounds.x0 + bounds.x1) / 2.); +``` + +## Initial values + +Keyword arguments ending in `i`, such as `x0i`, `y1i`, `widthi`, or `x2i`, are initial values. The GUI uses them for any coordinate that no constraint determines: + +```argon +let shape = rect("met1", x0i=20., y0i=30., x1i=120., y1i=90.); +``` + +- Dashed edges are under-constrained. +- Dragging an under-constrained shape updates its initial values. +- Adding a constraint turns the edge solid. +- An initial value never overrides a constraint. + +You don't need to write initial values by hand. The first time you drag a shape, the GUI adds any that are missing. + +## Dimensions + +The Dimension tool writes [`dimension`](/language/builtins/constraints#dimension) calls, which record both the constraint and where its label sits. Create them from the canvas; their arguments are tedious to write by hand. diff --git a/docs/content/language/control-flow.md b/docs/content/language/control-flow.md new file mode 100644 index 00000000..bdeeb3b2 --- /dev/null +++ b/docs/content/language/control-flow.md @@ -0,0 +1,54 @@ +--- +title: Control flow +description: if, match, and for. +--- + +# Control flow + +Argon has `if`, `match` on enums, and `for` over sequences. `if` and `match` are expressions. + +## `if` expressions + +An `if` is an expression, so it can be the body of a function. Both branches must have the same type. + +```argon +fn choose_pitch(dense: Bool) -> Float { + if dense { + 80. + } else { + 120. + } +} +``` + +## Enums and `match` + +An enum is a fixed set of variants: + +```argon +enum Metal { + M1, + M2, +} + +fn width(layer: Metal) -> Float { + match layer { + Metal::M1 => 80., + Metal::M2 => 120., + } +} +``` + +Match arms use `=>` and end with commas. + +## `for` loops + +A `for` loop walks a sequence, usually to emit geometry or instances: + +```argon +for i in std::range(4) { + rect("met1", x0=(i as Float) * 100., y0=0., w=60., h=60.); +} +``` + +[`std::range(stop)`](/language/std#range) yields the integers from zero up to, but not including, `stop`. diff --git a/docs/content/language/geometry.md b/docs/content/language/geometry.md new file mode 100644 index 00000000..a86bfdbc --- /dev/null +++ b/docs/content/language/geometry.md @@ -0,0 +1,58 @@ +--- +title: Geometry +description: Rectangles, polygons, paths, and text. +--- + +# Geometry + +Geometry constructors take positional arguments first, then keyword arguments. The coordinates they expose are solver variables, not plain numbers, so you can constrain them after the fact. + +## Rectangles + +```argon +let metal = rect("met1", x0=0., y0=0., w=200., h=100.); +let bounds = crect(x0=0., y0=0., x1=400., y1=300.); +``` + +[`rect`](/language/builtins/geometry#rect) draws a rectangle on a layer. [`crect`](/language/builtins/geometry#crect) makes a construction rectangle, which isn't exported and needs no layer. + +Both have `x0`, `y0`, `x1`, `y1`, `w`, and `h`; see [`Rect`](/language/types/rect) for how they relate. + +## Polygons + +[`polygon`](/language/builtins/geometry#polygon) takes a layer and a vertex count. Set or constrain each coordinate individually: + +```argon +let outline = polygon( + "met1", 3, + x0=0., y0=0., + x1=100., y1=0., + x2=50., y2=80., +); +``` + +`outline.x2` and `outline.points[2].x` are the same coordinate. + +## Paths + +A path is a centerline with a width, and optional extensions past each end: + +```argon +let route = path( + "met2", 3, + width=20., + x0=0., y0=0., + x1=100., y1=0., + x2=100., y2=100., +); +``` + +See [`Path`](/language/types/path) and the [`path` constructor](/language/builtins/geometry#path). + +## Text + +```argon +text("VDD", "text.label", 40., 80.); +``` + +[`text`](/language/builtins/geometry#text) places a label on a text layer. diff --git a/docs/content/language/modules-manifests.md b/docs/content/language/modules-manifests.md new file mode 100644 index 00000000..7c554394 --- /dev/null +++ b/docs/content/language/modules-manifests.md @@ -0,0 +1,55 @@ +--- +title: Modules and manifests +description: Split source across files and declare dependencies. +--- + +# Modules and manifests + +Modules split a library's source across files. `Argon.toml` names the library and lists what it depends on. + +## File modules + +Declare a child module with `mod`: + +```argon title="lib.ar" +mod utils; + +cell top() { + let spacing = utils::default_spacing(); +} +``` + +`mod utils;` loads `utils.ar`. A module can also be a directory containing a `mod.ar`. + +Paths start with `std::` for the standard library, `lib::` for the root of the current library, or a dependency's name for that dependency. + +## Library manifest + +`Argon.toml` names the library and points at its technology file, dependencies, and GDS imports: + +```toml +name = "my-library" +tech = "tech.toml" + +[dependencies] +devices = "../devices" + +[gds] +"macros::sram" = "gds/sram.gds" +``` + +Paths are relative to the manifest. Each GDS import becomes a zero-argument cell at the given module path. + +## Project layout + +```text +my-library/ +├── Argon.toml +├── tech.toml +├── lib.ar +├── utils.ar +└── nested/ + └── mod.ar +``` + +[`arc check`](/tools/arc#arc-check) parses, resolves, and type-checks the whole library. diff --git a/docs/content/language/overview.md b/docs/content/language/overview.md new file mode 100644 index 00000000..ef6e36c3 --- /dev/null +++ b/docs/content/language/overview.md @@ -0,0 +1,48 @@ +--- +title: Language overview +description: The main ideas and syntax of the Argon language. +sidebar_label: Overview +--- + +# Language overview + +Argon is a statically typed language for describing integrated-circuit layout. The syntax and type system are modeled on Rust. What's different is that geometric values, such as the edges of a rectangle, are variables in a linear constraint system rather than fixed numbers. + +```argon +cell via_array(cols: Int, pitch: Float) { + let cut = rect("via", w=20., h=20.); + + for i in std::range(cols) { + inst(cut, x=(i as Float) * pitch, y=0.); + } +} +``` + +## Core ideas + +- A **cell** is a layout definition you can call. +- A **function** computes a value, or emits geometry and constraints into the scope that calls it. +- Geometry constructors create rectangles, polygons, paths, and text. +- [`Float`](/language/types/scalars#float) values, including geometry, can be related with equality constraints. +- Calling a cell produces a cell value; [`inst`](/language/builtins/hierarchy#inst) places it in the hierarchy. +- Modules and manifests organize source, dependencies, technology data, and GDS imports. + +## Syntax + +Declarations and blocks use braces, and statements end with semicolons. A block's last expression, written without a semicolon, is its value. + +```argon +fn half(value: Float) -> Float { + value / 2. +} +``` + +`let` bindings are immutable. Functions can be used before they're defined; cells are resolved in source order, so declare a cell before the cells that call it. + +## Chapters + +- [Types and values](./types-values) +- [Cells and functions](./cells-functions) +- [Geometry](./geometry) +- [Constraints](./constraints) +- [Modules and manifests](./modules-manifests) diff --git a/docs/content/language/std.mdx b/docs/content/language/std.mdx new file mode 100644 index 00000000..ff7ba7bf --- /dev/null +++ b/docs/content/language/std.mdx @@ -0,0 +1,179 @@ +--- +title: Standard library +description: Rectangle, array, comparison, and sequence helpers in std. +--- + +import {ApiItem, ParameterTable, Returns} from '@site/src/components/ApiReference'; + +# Standard library + +The `std` module is written in Argon itself. Call its functions with the `std::` prefix. + +## `std::max` {#max} + + Float'} summary={<>Returns the greater of two floats.}> + + + +`x1` when `x0 < x1`; otherwise `x0`. + + + +## `std::min` {#min} + + Float'} summary={<>Returns the lesser of two floats.}> + + + +`x0` when `x0 < x1`; otherwise `x1`. + + + +## `std::intersection` {#intersection} + + Rect'} summary={<>The overlap of two rectangles, as a construction rectangle.}> + + + +A construction rectangle whose lower edges are the larger of the two inputs' and whose upper edges are the smaller. + +If the rectangles don't overlap, the result is inverted rather than empty: this computes edge expressions and doesn't test for overlap. + + + +## `std::union` {#union} + + Rect'} summary={<>The smallest construction rectangle containing both inputs.}> + + + +A construction rectangle from the smaller lower edges and the larger upper edges. + + + +## `std::array` {#array} + + Rect'} summary={<>Draws a row of rectangles and returns its bounds.}> + + + +A construction rectangle around the row. + + + +## `std::array2` {#array2} + + Rect'} summary={<>Draws a grid of rectangles and returns its bounds.}> + + + +A construction rectangle around the grid. + + + +## `std::max_array` {#max-array} + + Rect'} summary={<>Draws the largest grid of rectangles that fits in a given width and height.}> + + + +The bounds from `std::array2`, or a zero-size rectangle if nothing fits. + + + +## `std::eq_rect` {#eq-rect} + + ()'} summary={<>Constrains two rectangles to coincide.}> + + + +Nothing; four edge constraints are added. + + + +## `std::center_rects` {#center-rects} + + ()'} summary={<>Constrains two rectangles to share a center.}> + + + +Nothing; two center constraints are added. + + + +## `std::crect2rect` {#crect2rect} + + Rect'} summary={<>Draws a rectangle with the same layer and edges as a construction rectangle.}> + + + +The drawn rectangle. + + + +## `std::last` {#last} + + Any'} summary={<>Returns the last element of a sequence.}> + + + +The last element, typed as Any. + + + +## `std::range` {#range} + + [Int]'} summary={<>Returns the integers from 0 up to stop.}> + + + +Integers from zero up to, but not including, stop. + +```argon +for index in std::range(4) { + // index is 0, 1, 2, then 3 +} +``` + + diff --git a/docs/content/language/technology.md b/docs/content/language/technology.md new file mode 100644 index 00000000..7b476a3f --- /dev/null +++ b/docs/content/language/technology.md @@ -0,0 +1,44 @@ +--- +title: Technology files +description: Units, grid, GDS layer mapping, and layer styles. +--- + +# Technology files + +The technology file is TOML. It sets the units and grid, maps Argon layers to GDS layers, and says how each layer is drawn. + +```toml +dbu = 1e-10 +display_unit = 10 +grid = 1 +style_name = "Default Layer Properties" + +[[layers]] +name = "met1" +gds = [1, 0] +fill = "#0000ff" +border = "#0000ff" + +[[layers]] +name = "text.label" +gds = [10, 0] +fill = "#0080ff" +border = "#0080ff" +``` + +## Global values + +| Key | Meaning | +| --- | --- | +| `dbu` | Physical size of one GDS database unit | +| `display_unit` | Size of one source-coordinate unit, in database units | +| `grid` | Snap grid for the solver and the GUI | +| `style_name` | Name given to the layer-style collection | + +## Layers + +Each `[[layers]]` entry names a layer and gives its GDS layer and datatype. The optional style settings control fill, border, line style, visibility, validity, grouping, patterns, transparency, markings, and animation. + +The GUI's layer sidebar comes from this file. A layer that is visible but not valid can be looked at but not drawn on. + +The `tech` field in [`Argon.toml`](./modules-manifests#library-manifest) says which technology file a library uses. diff --git a/docs/content/language/types-values.md b/docs/content/language/types-values.md new file mode 100644 index 00000000..1be71aa4 --- /dev/null +++ b/docs/content/language/types-values.md @@ -0,0 +1,57 @@ +--- +title: Types and values +description: Scalars, collections, tuples, and layout types. +--- + +# Types and values + +Argon's types fall into scalars, collections, tuples, and layout types. Write types out in cell and function signatures: some can be inferred, but explicit signatures make call sites and error messages clearer. + +| Type | Example | Used for | +| --- | --- | --- | +| [`Float`](/language/types/scalars#float) | `12.`, `-0.5` | Coordinates, distances, and linear expressions | +| [`Int`](/language/types/scalars#int) | `12`, `-3` | Counts, indices, and discrete parameters | +| [`Bool`](/language/types/scalars#bool) | `true`, `false` | Conditions and flags | +| [`String`](/language/types/scalars#string) | `"met1"` | Layer names and text | +| [`Rect`](/language/types/rect) | `rect("met1")` | Rectangles, drawn or construction-only | +| [`Polygon`](/language/types/polygon) | `polygon("met1", 3)` | Polygons | +| [`Path`](/language/types/path) | `path("met1", 2)` | Paths with a width | +| [`Point`](/language/types/point) | `shape.points[0]` | A polygon or path vertex | +| [`Inst`](/language/types/instance) | `inst(child())` | A placed cell | +| [`[T]`](/language/types/collections#sequences) | `[Float]` | A sequence of one type | +| [`(A, B)`](/language/types/collections#tuples) | `(3, 5,)` | A fixed-size tuple of mixed types | +| [`Any`](/language/types/scalars#any) | — | A value of any type | +| [`()`](/language/types/scalars#unit) | `()` | The unit value and type | + +## Numeric literals + +The decimal point is what makes a literal a float: + +```argon +let count = 50; // Int +let distance = 50.; // Float +``` + +Geometry and constraints use `Float`. Counts and indices use `Int`. + +## Operators and casts + +Arithmetic: `+`, `-`, `*`, `/`, and `%`. Comparison: `==`, `!=`, `<`, `<=`, `>`, and `>=`. + +Cast with `as`: + +```argon +let offset = (index as Float) * pitch; +``` + +## Sequences and tuples + +Build a sequence with [`list`](/language/builtins/collections#list) or [`cons`](/language/builtins/collections#cons), index it with brackets, and walk it with [`head`](/language/builtins/collections#head) and [`tail`](/language/builtins/collections#tail). + +```argon +let widths = list(80., 120., 160.); +let first = widths[0]; +let pair = (first, 3,); +``` + +[`std::range`](/language/std#range) makes an integer sequence for loops. diff --git a/docs/content/language/types/collections.mdx b/docs/content/language/types/collections.mdx new file mode 100644 index 00000000..10c2fc8c --- /dev/null +++ b/docs/content/language/types/collections.mdx @@ -0,0 +1,29 @@ +--- +title: Sequences and tuples +description: Sequences of one type and fixed-size tuples of mixed types. +--- + +# Sequences and tuples + +## Sequences + +`[T]` is an ordered sequence whose elements all have type `T`. + +```argon +let values: [Float] = list(10., 20., 30.); +let first = values[0]; +``` + +The empty literal `[]` works as an empty sequence of any element type. Build and take apart sequences with [`list`, `cons`, `head`, and `tail`](/language/builtins/collections). + +## Tuples + +`(A, B)` is a fixed-size tuple whose positions can have different types. Write a trailing comma to make the syntax unambiguous. + +```argon +let placement = (100., 200., true,); +let x = placement.0; +let reflected = placement.2; +``` + +The empty tuple `()` is the [unit type](./scalars#unit). diff --git a/docs/content/language/types/instance.mdx b/docs/content/language/types/instance.mdx new file mode 100644 index 00000000..12045c8b --- /dev/null +++ b/docs/content/language/types/instance.mdx @@ -0,0 +1,35 @@ +--- +title: Cells and instances +description: Cell values and placed instances. +--- + +import {FieldTable} from '@site/src/components/ApiReference'; + +# Cells and instances + +## Cell values + +Calling a cell runs its body with the given arguments and produces a cell value: + +```argon +let child = inverter(1200., 2000., 4); +``` + +A cell value isn't placed anywhere until you pass it to [`inst`](/language/builtins/hierarchy#inst). Pass it to [`bbox`](/language/builtins/hierarchy#bbox) to get its bounds. + +## Instance values + +`Inst` is a placed cell. Its type carries the fields the cell exports. + +', type: 'Declared type', description: 'A field exported by the cell, moved into the parent coordinate system where that applies.'}, +]} /> + +```argon +let placed = inst(inverter(1200., 2000., 4), x=100., y=50.); +let bounds = bbox(placed); +``` + +Imported GDS cells expose fields found in the file, including pin names when pin layers are configured. diff --git a/docs/content/language/types/path.mdx b/docs/content/language/types/path.mdx new file mode 100644 index 00000000..861f05ec --- /dev/null +++ b/docs/content/language/types/path.mdx @@ -0,0 +1,24 @@ +--- +title: Path +description: Fields and coordinate access for path geometry. +--- + +import {FieldTable} from '@site/src/components/ApiReference'; + +# `Path` + +`Path` is a constant-width path along a centerline. Create one with [`path`](/language/builtins/geometry#path). + +## Fields + + + +Every numeric constructor argument has an initial-value form ending in `i`: `widthi`, `begin_extensioni`, `x0i`, and so on. diff --git a/docs/content/language/types/point.mdx b/docs/content/language/types/point.mdx new file mode 100644 index 00000000..00c30008 --- /dev/null +++ b/docs/content/language/types/point.mdx @@ -0,0 +1,23 @@ +--- +title: Point +description: A solver-backed polygon or path coordinate pair. +--- + +import {FieldTable} from '@site/src/components/ApiReference'; + +# `Point` + +`Point` is a vertex of a [`Polygon`](./polygon) or [`Path`](./path), taken from its `points` sequence. + + + +```argon +let route = path("met1", 2, x0=0., y0=0., x1=100., y1=40.); +let endpoint = route.points[1]; +eq(endpoint.x, 100.); +``` + +You can't construct a point on its own; it always comes from a polygon or path. diff --git a/docs/content/language/types/polygon.mdx b/docs/content/language/types/polygon.mdx new file mode 100644 index 00000000..11bcd07d --- /dev/null +++ b/docs/content/language/types/polygon.mdx @@ -0,0 +1,32 @@ +--- +title: Polygon +description: Fields and coordinate access for polygon geometry. +--- + +import {FieldTable} from '@site/src/components/ApiReference'; + +# `Polygon` + +`Polygon` is a fixed number of vertices on a layer. Create one with [`polygon`](/language/builtins/geometry#polygon). + +## Fields + + + +`shape.x2` and `shape.points[2].x` refer to the same coordinate. + +```argon +let triangle = polygon( + "met1", 3, + x0=0., y0=0., + x1=100., y1=0., + x2=50., y2=80., +); +``` + +The constructor's `xNi` and `yNi` arguments set initial coordinates. diff --git a/docs/content/language/types/rect.mdx b/docs/content/language/types/rect.mdx new file mode 100644 index 00000000..e7180fd2 --- /dev/null +++ b/docs/content/language/types/rect.mdx @@ -0,0 +1,40 @@ +--- +title: Rect +description: Fields and behavior of rectangular Argon geometry. +--- + +import {FieldTable} from '@site/src/components/ApiReference'; + +# `Rect` + +`Rect` is an axis-aligned rectangle. Drawn rectangles from [`rect`](/language/builtins/geometry#rect) and construction rectangles from [`crect`](/language/builtins/geometry#crect) both have this type. + +## Fields + + + +## How the fields relate + +Give any combination of edges and sizes that pins the rectangle down, and the solver derives the rest: + +```argon +let r = rect("met1", x0=10., y0=20., w=100., h=80.); +eq(r.x1, 110.); +eq(r.y1, 100.); +``` + +Arguments such as `x0` are constraints. Arguments such as `x0i` are initial values for whatever is left free. + +## Related + +- [`rect`](/language/builtins/geometry#rect) and [`crect`](/language/builtins/geometry#crect) +- [`bbox`](/language/builtins/hierarchy#bbox) +- [`std::intersection`](/language/std#intersection) and [`std::union`](/language/std#union) diff --git a/docs/content/language/types/scalars.mdx b/docs/content/language/types/scalars.mdx new file mode 100644 index 00000000..02458556 --- /dev/null +++ b/docs/content/language/types/scalars.mdx @@ -0,0 +1,50 @@ +--- +title: Scalar and utility types +description: Float, Int, Bool, String, Any, and unit values. +sidebar_label: Scalars +--- + +# Scalar and utility types + +## `Float` {#float} + +`Float` is the type of coordinates, distances, and other solver expressions. A float literal has a decimal point: `12.` or `-0.5`. + +Used by the [geometry constructors](/language/builtins/geometry), [constraints](/language/builtins/constraints), and instance positions. + +:::warning +`12` is an `Int`. Write `12.` for a float. +::: + +## `Int` {#int} + +`Int` is for counts, indices, point counts, and rotation angles. + +Cast to a float with `value as Float`. + +## `Bool` {#bool} + +`Bool` is `true` or `false`. Conditions must be booleans, and built-ins use them for flags such as `reflect` and `construction`. + +## `String` {#string} + +`String` is text. Layer names and labels are strings. + +```argon +rect("met1"); +text("VDD", "text.label", 0., 0.); +``` + +## `Any` {#any} + +`Any` erases a value's static type. It's occasionally useful for mixed or generic sequences, but concrete types give you field access and better error messages. + +## Unit `()` {#unit} + +`()` is the return type of functions that produce nothing useful, such as ones that only emit geometry or constraints. + +```argon +fn align_left(a: Rect, b: Rect) -> () { + eq(a.x0, b.x0); +} +``` diff --git a/docs/content/tools/arc.md b/docs/content/tools/arc.md new file mode 100644 index 00000000..b48cd918 --- /dev/null +++ b/docs/content/tools/arc.md @@ -0,0 +1,110 @@ +--- +title: arc command reference +description: Create, format, check, run, and document Argon libraries. +sidebar_label: arc +--- + +# `arc` command reference + +`arc` manages Argon libraries. It reads `Argon.toml` to find the source, technology file, dependencies, and GDS imports, so you don't pass them on the command line. + +## `arc new` + +```text +arc new [--name ] +``` + +| Argument | Required | Description | +| --- | --- | --- | +| `` | Yes | Directory to create. Must not already exist. Its last component is the default library name. | +| `--name ` | No | Library name to write to `Argon.toml` instead. | + +Creates `Argon.toml`, `lib.ar`, and `tech.toml`. + +```bash +arc new my-layout +arc new layouts/demo --name demo +``` + +## `arc fmt` + +```text +arc fmt [--manifest-path ] [--check] +``` + +| Option | Default | Description | +| --- | --- | --- | +| `--manifest-path ` | Nearest manifest | Library to format. | +| `--check` | Off | Report unformatted files and exit non-zero, without changing them. | + +```bash +arc fmt +arc fmt --check +``` + +## `arc check` + +```text +arc check [--manifest-path ] [--argonc ] +``` + +| Option | Default | Description | +| --- | --- | --- | +| `--manifest-path ` | `Argon.toml` | Library to check. | +| `--argonc ` | `ARGONC` or `argonc` | Compiler executable to use. | + +Parses, resolves, and type-checks the library without running a cell. No technology file is needed. + +## `arc run` + +```text +arc run --cell [--output ] [--gds] +``` + +| Option | Required/default | Description | +| --- | --- | --- | +| `--cell ` | Required | Cell to run, such as `top(10., 20.)`. Arguments are Argon expressions evaluated in the library's scope. | +| `-o, --output ` | `target/argon.bin` | Where to write the compiled layout. | +| `--gds` | Off | Also write `target/argon.gds`. | +| `--manifest-path ` | `Argon.toml` | Library to run. | +| `--argonc ` | `ARGONC` or `argonc` | Compiler executable to use. | + +```bash +arc run --cell 'top(10., 20.)' +arc run --cell 'top(10., 20.)' --gds +``` + +:::tip +Quote the cell expression so the shell doesn't interpret the parentheses. +::: + +## `arc doc` + +```text +arc doc [--manifest-path ] [--output ] +``` + +| Option | Default | Description | +| --- | --- | --- | +| `--manifest-path ` | Nearest manifest | Library to document. | +| `-o, --output ` | `target/doc` | Where to write the generated site. | + +Generates a standalone HTML reference for the library, one page per module, with signatures, argument tables, enum variants, source locations, and links between the library's own types. Documentation comes from `//!` module comments and `///` declaration comments. Doctests are not run. + +```argon title="lib.ar" +//! Standard cells for this library. + +/// Draws a square on the requested layer. +/// +/// # Arguments +/// - `layer`: technology layer name. +/// - `size`: square edge length. +cell square(layer: String, size: Float) { + rect(layer, x0=0., y0=0., w=size, h=size); +} +``` + +```bash +arc doc +open target/doc/index.html +``` diff --git a/docs/content/tools/argonc.md b/docs/content/tools/argonc.md new file mode 100644 index 00000000..b6f019fc --- /dev/null +++ b/docs/content/tools/argonc.md @@ -0,0 +1,30 @@ +--- +title: argonc command reference +description: Run the Argon compiler directly. +sidebar_label: argonc +--- + +# `argonc` command reference + +`argonc` is the compiler. It takes every input on the command line, so for everyday work use [`arc`](./arc), which reads them from `Argon.toml`. + +```text +argonc (--check | --cell ) [OPTIONS] +``` + +| Argument or option | Required/default | Description | +| --- | --- | --- | +| `` | Required | Library directory or its `lib.ar`. | +| `--check` | One mode required | Parse, resolve, and type-check, then stop. Can't be combined with `--cell`. | +| `--cell ` | One mode required | Cell to run. | +| `--tech ` | Required with `--cell` | Technology file. | +| `--dependency ` | Repeatable | Add a dependency. The path can be a directory or a `lib.ar`. | +| `--gds-import ` | Repeatable | Import a GDS cell. `NAME` may be a module path. | +| `-o, --output ` | Beside `lib.ar` | Where to write the compiled layout. | +| `--gds ` | None | Also write GDS to this path. | +| `--error-format` | `human` | `human` or `json`. | + +```bash +argonc . --check +argonc . --cell 'top()' --tech tech.toml -o target/top.bin --gds target/top.gds +``` diff --git a/docs/content/tools/argone.md b/docs/content/tools/argone.md new file mode 100644 index 00000000..a980b4eb --- /dev/null +++ b/docs/content/tools/argone.md @@ -0,0 +1,51 @@ +--- +title: argone command reference +description: Start Neovim and the GUI together, locally or over SSH. +sidebar_label: argone +--- + +# `argone` command reference + +`argone` starts Neovim on an Argon project and launches the GUI alongside it. + +```text +argone [--nvim ] [PATH] +``` + +| Argument | Default | Description | +| --- | --- | --- | +| `[PATH]` | `.` | Project directory or `.ar` file. A directory must contain `lib.ar`. | +| `--nvim ` | `nvim` | Neovim executable to use. | + +```bash +argone . +argone examples/hierarchy +``` + +## Remote editing with SSH + +`argone ssh` runs Neovim and the analyzer on a remote machine and the GUI on your own. + +```text +argone ssh [PATH] [OPTIONS] +``` + +| Argument or option | Default | Description | +| --- | --- | --- | +| `` | Required | Host name or OpenSSH config alias. | +| `[PATH]` | `.` | Project directory or file on the remote machine. | +| `--ssh ` | `ssh` | SSH executable to use. | +| `-o, --ssh-option