//! Component-file compiler core: s-expression reader, AST, expression //! language, validation, and IR. //! //! This crate has no dependencies and no knowledge of taffy or the widget //! crates, so it builds fast inside the proc-macro chain and is equally //! usable by the runtime interpreter. pub mod accel; pub mod asset; pub mod ast; pub mod diagnostics; pub mod expr; pub mod ir; pub mod manifest; pub mod parse; pub mod path; pub mod registry; pub mod resolve; pub mod rich; pub mod sexpr; pub mod theme; pub mod validate; use std::collections::BTreeMap; use diagnostics::Diagnostic; use ir::{Compiled, Ir}; use registry::Registry; /// Resolves a component reference (`CounterButton`) to the source of its /// file. The filesystem stays in the callers — the proc-macro and the dev /// runtime each bring their own — so the compiler core remains pure. pub trait ImportResolver { /// Return the display name (for diagnostics; typically the path) and /// source text of the named component's file. fn resolve(&mut self, name: &str) -> Result<(String, String), String>; } /// The default component search path: one directory of this name under the /// crate root, unless Cargo.toml's `[package.metadata.guiduck]` /// `component-path` says otherwise. pub const DEFAULT_COMPONENT_DIR: &str = "components"; /// The default asset search path: one directory of this name under the crate /// root, unless Cargo.toml's `[package.metadata.guiduck]` `asset-path` says /// otherwise. `(asset "icons/logo.png")` names a file within it. pub const DEFAULT_ASSET_DIR: &str = "assets"; /// The default widget-manifest search path: one directory of this name under /// the crate root, unless Cargo.toml's `[package.metadata.guiduck]` /// `widget-path` says otherwise. /// /// Unlike a component, a widget manifest is not resolved by reference: the /// vocabulary must be known *before* anything is parsed, since it decides what /// counts as a widget name at all. So every `.gdw` on this path is loaded up /// front, and a crate's widget set may live in one file or many. pub const DEFAULT_WIDGET_DIR: &str = "widgets"; /// The extension of a widget manifest. pub const WIDGET_MANIFEST_EXT: &str = "gdw"; /// The conventional file holding a component: the declared name verbatim — /// `CounterButton` lives in `CounterButton.gdc` in one of the search-path /// directories. pub fn component_file_name(name: &str) -> String { format!("{name}.gdc") } /// The generated logic method the framework calls once the component has /// mounted. It is the framework's, so a handler or a child component whose /// generated method would take this name is a diagnostic rather than a /// silently shadowed hook. pub const MOUNT_HOOK_METHOD: &str = "mounted"; /// The generated logic-factory method for a child component, by Rust /// method convention: `CounterButton` → `counter_button` (runs of capitals /// stay together: `HTTPStatus` → `http_status`). pub fn component_method_name(name: &str) -> String { let chars: Vec = name.chars().collect(); let mut out = String::new(); for (i, c) in chars.iter().enumerate() { if c.is_ascii_uppercase() && i > 0 && (!chars[i - 1].is_ascii_uppercase() || chars .get(i + 1) .is_some_and(|next| next.is_ascii_lowercase())) { out.push('_'); } out.push(c.to_ascii_lowercase()); } out } /// Whether a name in widget position is a component reference (capitalized) /// rather than a builtin widget. pub fn is_component_name(name: &str) -> bool { name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) } /// Compile `.gdc` source to IR: read, parse, validate. Component references /// are errors here — use [`compile_with`] to resolve them. pub fn compile(source: &str) -> Result> { struct NoImports; impl ImportResolver for NoImports { fn resolve(&mut self, _name: &str) -> Result<(String, String), String> { Err("component instantiation is not available in this context".into()) } } compile_with(source, &mut NoImports).map(|compiled| compiled.root) } /// Compile `.gdc` source and, transitively, every component it /// instantiates, resolving references through `resolver`, against the builtin /// widget vocabulary alone. pub fn compile_with( source: &str, resolver: &mut dyn ImportResolver, ) -> Result> { compile_with_widgets(source, resolver, &Registry::default()) } /// Compile `.gdc` source against a vocabulary that includes the widgets an /// application declared in its `.gdw` manifests. /// /// The registry reaches every file the compilation touches, imported /// components included: the widget vocabulary is a property of the *crate*, so /// a component may use its own crate's widgets wherever it is instantiated /// from. pub fn compile_with_widgets( source: &str, resolver: &mut dyn ImportResolver, registry: &Registry, ) -> Result> { let component = parse::parse(source).map_err(|d| vec![d])?; let mut components = BTreeMap::new(); let mut stack = vec![component.name.name.clone()]; let mut errors = Vec::new(); resolve_imports( &component.root, resolver, &mut components, &mut stack, registry, &mut errors, ); if !errors.is_empty() { return Err(errors); } let root = validate::validate(&component, &components, registry)?; Ok(Compiled { root, components }) } /// Walk a widget tree for component references; compile each one (and its /// own references, post-order) into `components`. fn resolve_imports( node: &ast::Node, resolver: &mut dyn ImportResolver, components: &mut BTreeMap, stack: &mut Vec, registry: &Registry, errors: &mut Vec, ) { let name = &node.widget.name; if is_component_name(name) && !components.contains_key(name) { if stack.iter().any(|entry| entry == name) { errors.push(Diagnostic::new( format!( "component instantiation cycle: {} → {name}", stack.join(" → ") ), node.widget.span, )); } else { match resolver.resolve(name) { Err(why) => errors.push(Diagnostic::new( format!("cannot resolve component `{name}`: {why}"), node.widget.span, )), Ok((display, child_source)) => { match compile_import( name, &display, &child_source, resolver, components, stack, registry, ) { Ok(child) => { components.insert(name.clone(), child); } Err(diag) => errors.push(Diagnostic::new(diag, node.widget.span)), } } } } } for child in &node.children { resolve_imports(child, resolver, components, stack, registry, errors); } } /// Compile one imported component file. Its own diagnostics render against /// its own source and come back as one message for the referencing span. fn compile_import( name: &str, display: &str, source: &str, resolver: &mut dyn ImportResolver, components: &mut BTreeMap, stack: &mut Vec, registry: &Registry, ) -> Result { let broken = |diags: &[Diagnostic]| { let rendered = diagnostics::render(diags, source, display); let indented: Vec = rendered.lines().map(|l| format!(" {l}")).collect(); format!( "component `{name}` ({display}) does not compile:\n{}", indented.join("\n") ) }; let component = parse::parse(source).map_err(|d| broken(&[d]))?; if component.name.name != name { return Err(format!( "{display} declares component `{}`, expected `{name}`", component.name.name )); } stack.push(name.to_owned()); let mut child_errors = Vec::new(); resolve_imports( &component.root, resolver, components, stack, registry, &mut child_errors, ); stack.pop(); if !child_errors.is_empty() { return Err(broken(&child_errors)); } validate::validate(&component, components, registry).map_err(|diags| broken(&diags)) }