//! The compiled component IR: everything both consumers (the proc-macro
//! codegen and the runtime interpreter) need, as plain data with no
//! dependency on taffy or the widget crates.
use std::collections::BTreeMap;
use crate::ast::TypeKind;
use crate::expr::Expr;
use crate::registry::{EventProp, PropTy};
/// A compiled component together with every component it transitively
/// instantiates, keyed by declared name (`CounterButton`).
#[derive(Clone, Debug, PartialEq)]
pub struct Compiled {
pub root: Ir,
pub components: BTreeMap<String, Ir>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Ir {
pub name: String,
pub records: Vec<IrRecord>,
pub props: Vec<IrProp>,
pub outputs: Vec<IrOutput>,
pub states: Vec<IrState>,
pub handlers: Vec<IrHandler>,
/// Flattened widget tree; index 0 is the root, children by index.
pub nodes: Vec<IrNode>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IrHandler {
pub name: String,
/// The payloads the handler receives, in order: one for widget
/// payload-carrying events (`:on-change`), any number for invocation
/// wires (`:on-click (moved dx dy)`); empty for plain handlers.
pub payloads: Vec<TypeKind>,
/// The Rust path this handler returns, for a handler wired to a widget
/// [`query`](IrQuery); `None` for an ordinary event handler, which returns
/// nothing. Set during validation from the query the handler answers.
pub returns: Option<String>,
}
/// A resolved element type: a scalar, or a record by index into the
/// file's record table (records are file-local in v1, so an index keeps
/// this Copy).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ElemKind {
Scalar(TypeKind),
Record(u32),
}
/// A value type: an element, or a list of them.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ValueTy {
pub kind: ElemKind,
pub list: bool,
}
impl ValueTy {
pub fn scalar(kind: TypeKind) -> Self {
Self {
kind: ElemKind::Scalar(kind),
list: false,
}
}
/// The Rust spelling, for diagnostics (`i32`, `Vec<Todo>`).
pub fn rust_name(self, records: &[IrRecord]) -> String {
let element = match self.kind {
ElemKind::Scalar(kind) => kind.rust_name().to_owned(),
ElemKind::Record(index) => records
.get(index as usize)
.map(|r| r.name.clone())
.unwrap_or_else(|| "?".to_owned()),
};
if self.list {
format!("Vec<{element}>")
} else {
element
}
}
}
/// One `(record …)` declaration: flat, scalar fields (nested records and
/// list fields are planned extensions).
#[derive(Clone, Debug, PartialEq)]
pub struct IrRecord {
pub name: String,
pub fields: Vec<(String, TypeKind)>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IrProp {
pub name: String,
pub ty: ValueTy,
pub default: Option<Literal>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IrOutput {
pub name: String,
pub ty: TypeKind,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IrState {
pub name: String,
pub ty: ValueTy,
pub init: Literal,
}
/// What a node instantiates: a widget, another `.gdc` component (a
/// capitalized name in widget position), or the component's slot — the point
/// where an instance's children project in.
#[derive(Clone, Debug, PartialEq)]
pub enum IrWidget {
/// A widget: the Rust type to build and everything this node gives it.
/// One variant, because a builtin and a `.gdw`-declared widget are
/// registered on the same terms — see [`IrWidgetNode`].
Widget(IrWidgetNode),
Component(String),
Slot,
/// A structural `(if …)`: children are the branches, the condition is
/// in [`IrNode::control`].
If,
/// A keyed `(for …)`: the single child is the row template, the loop
/// data is in [`IrNode::control`].
For,
}
impl IrWidget {
/// The widget this node instantiates, if it instantiates one.
pub fn widget(&self) -> Option<&IrWidgetNode> {
match self {
Self::Widget(widget) => Some(widget),
Self::Component(_) | Self::Slot | Self::If | Self::For => None,
}
}
}
/// A widget as one node instantiates it: the Rust type to construct, and
/// everything this node gives it.
///
/// **This is the whole of how a widget is built, and there is one of it.** A
/// `container` and an application's `markdown` arrive here identically —
/// `Default + Widget` plus a setter per property — because that is the
/// contract both are registered under.
///
/// The registry's facts are resolved *in* here at lowering rather than left in
/// a side table the way [`IrWidget::Component`] leaves a component's body in
/// [`Compiled::components`]. The asymmetry is real and deliberate: a component
/// is a recursive tree that must be keyed and shared, while a widget is flat
/// leaf data. Copying it makes a node self-describing — a consumer needs no
/// registry to emit one — and there is exactly one writer (validation), so the
/// copies cannot drift.
#[derive(Clone, Debug, PartialEq)]
pub struct IrWidgetNode {
/// The name written in the `.gdc` (`container`, `markdown`) — the key a
/// theme rule matches on, and what diagnostics call it.
pub name: String,
/// The Rust path to construct, verbatim from the descriptor or the
/// manifest's `(type "…")`. The compiler never resolves it; rustc does,
/// against the generated construction.
pub type_path: String,
/// The properties this node gives the widget, in written order, followed
/// by the one the framework supplies (a menu's submenu-ness). This node's
/// children are *not* here — they live in [`IrNode::children`], and the tree
/// hands every widget its content builder over them.
pub props: Vec<IrWidgetProp>,
/// The declared event wires this node makes. Builtin event wires are
/// [`IrNode::events`]: those are keyed by the closed [`EventProp`] set,
/// whose payload contract the registry knows.
pub events: Vec<IrUserEvent>,
/// The declared query wires this node makes — `:get-image (load payload)`.
/// A query's handler returns a value the widget consumes, so codegen
/// installs it as a resolver rather than dispatching it as an event.
pub queries: Vec<IrQuery>,
}
impl IrWidgetNode {
/// The value this node gives one property, if it gives it one.
pub fn prop(&self, name: &str) -> Option<&IrWidgetProp> {
self.props.iter().find(|prop| prop.name == name)
}
}
/// One property given to a widget.
#[derive(Clone, Debug, PartialEq)]
pub struct IrWidgetProp {
/// The name the registry declares and the `.gdc` writes (`corner-radius`,
/// `source`).
pub name: String,
/// The Rust setter to call — a descriptor's method, a manifest's
/// `:setter`, or the `set_<snake(name)>` convention both default to.
pub setter: String,
/// The declared type, so a consumer can emit the value at the width the
/// setter takes. **The one place a value's width is decided**, which is
/// what stops two emitters from disagreeing about a cast.
pub ty: PropTy,
pub value: IrPropValue,
}
/// What a property is set from.
#[derive(Clone, Debug, PartialEq)]
pub enum IrPropValue {
/// A compile-time constant: set once, at construction.
Static(Literal),
/// A reactive expression: re-applied through the setter when a
/// referenced signal changes.
Binding(Expr),
}
/// One event wire on a `.gdw`-declared widget — `:on-link navigate` or
/// `:on-link (navigate payload page.id)`.
///
/// Shaped like [`IrEvent`] but not it: that one is keyed by the closed
/// [`EventProp`] set, whose payload contract the registry knows, while this
/// one carries the name and payload its manifest declared.
#[derive(Clone, Debug, PartialEq)]
pub struct IrUserEvent {
/// The event name the manifest declares and the `.gdc` writes (`on-link`).
pub name: String,
/// The value the widget delivers to the handler, if any.
pub payload: Option<TypeKind>,
pub handler: String,
/// Invocation arguments; empty for the bare-name form.
pub args: Vec<Expr>,
}
/// One query wire on a `.gdw`-declared widget — `:get-image (load payload)`.
///
/// Unlike [`IrUserEvent`], the handler *returns* a value; codegen installs it
/// as a resolver closure through [`setter`](Self::setter), so the widget calls
/// it synchronously.
#[derive(Clone, Debug, PartialEq)]
pub struct IrQuery {
/// The query name the manifest declares and the `.gdc` writes (`get-image`).
pub name: String,
/// The widget method that installs the resolver.
pub setter: String,
/// The value handed to the handler — the thing being resolved.
pub payload: TypeKind,
/// The Rust path the handler returns, verbatim from the manifest.
pub returns: String,
/// The handler to call. It receives exactly the value being resolved (the
/// query's `payload`); anything else it needs it reads from `cx`, which
/// keeps the compiled and dev-interpreter paths identical.
pub handler: String,
}
/// Whether a component declares a slot (instances may then pass children).
pub fn has_slot(ir: &Ir) -> bool {
ir.nodes.iter().any(|node| node.widget == IrWidget::Slot)
}
#[derive(Clone, Debug, PartialEq)]
pub struct IrNode {
pub widget: IrWidget,
/// Style classes for theme rule matching, from `:class "a b"`.
pub classes: Vec<String>,
pub style: StyleIr,
/// Event wires to declared handlers.
pub events: Vec<IrEvent>,
/// Whether this widget takes initial focus at mount (`:autofocus true`).
pub autofocus: bool,
/// `:enabled expr` — a boolean expression driving the widget's enabled
/// flag (disabling is inherited by the subtree). `None` means always
/// enabled. Universal: valid on every builtin and on instances (where
/// it applies to the instance root).
pub enabled: Option<Expr>,
/// `:focused expr` — a boolean expression driving whether this widget
/// holds keyboard focus. `None` leaves focus to the tree (clicks, Tab,
/// and the focus invariant). Universal.
///
/// Controlled, like `:checked` and `:open`: the application owns the
/// state, the widget never flips it, and `:on-focus-change` reports every
/// transition so the state can stay true.
pub focused: Option<Expr>,
/// `:tooltip "text"` — shown after the pointer rests on this widget.
/// Universal.
pub tooltip: Option<String>,
/// A menu item's keyboard accelerator (`:accel "Ctrl+S"`), parsed.
///
/// The *keystroke* half: an accelerator fires while its menu is closed, so
/// the tree registers it at mount, beside the menu, where the row does not
/// exist. The text half is an ordinary property of the row, and is in
/// [`IrWidgetNode::props`] like any other. Meaningless on other widgets.
pub accel: Option<crate::accel::AccelIr>,
/// Props given to a component instance, as expressions evaluated in the
/// *parent's* scope; omitted defaulted props come from the child's own
/// declarations. Meaningless on builtin widgets.
pub component_props: Vec<(String, Expr)>,
/// Child-output → parent-handler wires on a component instance —
/// `:on-changed apply`, or an invocation `(apply payload todo.id)`
/// whose arguments evaluate at dispatch. Meaningless on builtin
/// widgets.
pub component_outputs: Vec<IrOutputWire>,
/// Explicit instance identity (`:key "left"`) for state matching across
/// hot reloads; unkeyed instances match by occurrence order among their
/// unkeyed same-type siblings. Meaningless on builtin widgets.
pub component_key: Option<String>,
/// The structural data of an [`IrWidget::If`] or [`IrWidget::For`]
/// node; `None` for everything else.
pub control: Option<ControlIr>,
pub children: Vec<usize>,
}
/// The data of a structural control node.
#[derive(Clone, Debug, PartialEq)]
pub enum ControlIr {
/// `(if cond then else?)` — `children[0]` is the then-branch,
/// `children[1]` (when present) the else-branch.
If { cond: Expr },
/// `(for var list :index i :key expr body)` — `children[0]` is the row
/// template.
For {
var: String,
index: Option<String>,
/// The list to iterate: the name of a `(List T)` prop or state.
list: String,
/// The element type of that list.
element: ElemKind,
/// Row identity; `None` keys by position.
key: Option<Expr>,
},
}
/// One instance output wire.
#[derive(Clone, Debug, PartialEq)]
pub struct IrOutputWire {
pub output: String,
pub handler: String,
/// Invocation arguments; empty for the bare-name form (which passes
/// the output's payload as the single argument).
pub args: Vec<Expr>,
}
/// One event wire: `:on-click increment` (no args) or
/// `:on-click (remove todo.id)` (arguments evaluated at dispatch time, in
/// the node's scope).
#[derive(Clone, Debug, PartialEq)]
pub struct IrEvent {
pub event: EventProp,
pub handler: String,
pub args: Vec<Expr>,
}
/// Which axes a scroll area scrolls — the value of an `:axis` property.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ScrollAxesIr {
Vertical,
Horizontal,
Both,
}
/// A resolved compile-time constant.
#[derive(Clone, Debug, PartialEq)]
pub enum Literal {
Int(i64),
Float(f64),
Bool(bool),
Str(String),
/// sRGB 8-bit color, from `"#rrggbb"` / `"#rrggbbaa"`.
Color(u8, u8, u8, u8),
/// `(list …)` of literal elements, for `(List T)` defaults and inits.
List(Vec<Literal>),
/// A record literal, fields in declaration order after validation.
Record {
name: String,
fields: Vec<(String, Literal)>,
},
/// A vector path — `(path …)` or `(svg-path "…")` — as a sequence of
/// drawing commands in a normalized coordinate space (the consumer
/// scales them to the element it decorates).
Path(Vec<PathCmd>),
/// Rich text — `(rich "Hi " (bold "there"))` — flattened to runs. The
/// nesting is gone by here: a consumer gets a string and the ranges that
/// differ from it, and nothing walks a tree at runtime.
Rich(Vec<RichRun>),
/// An asset reference — `(asset "path")` — carrying the path exactly as
/// written. Nothing here says what kind of asset it is: the slot's type
/// decides what the bytes mean, and resolving them against the asset
/// search path belongs to the consumer, which keeps this compiler
/// filesystem-free.
Asset(String),
/// Which axes a scroll area scrolls, from `:axis vertical|horizontal|both`.
ScrollAxes(ScrollAxesIr),
}
/// One command of a vector [`Literal::Path`]. Coordinates are `f64` in the
/// path's own space; there is no implicit unit — a theme conventionally
/// draws marks in the unit square `[0,1]×[0,1]` and lets the widget scale.
/// One styled run of rich text: its characters, and what differs from the
/// paragraph's own style. Styles are already resolved through the nesting.
#[derive(Clone, Debug, PartialEq)]
pub struct RichRun {
pub text: String,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
/// sRGB 8-bit.
pub color: Option<(u8, u8, u8, u8)>,
pub size: Option<f64>,
/// What `(link "target" …)` this run is inside, if any — the string the
/// paragraph hands back on `:on-link`. Its meaning is the application's:
/// nothing here parses it, because nothing here knows whether it is a
/// URL, a page name, or a record id.
pub link: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathCmd {
/// Start a new subpath at `(x, y)`.
Move(f64, f64),
/// Straight line to `(x, y)`.
Line(f64, f64),
/// Quadratic Bézier through control `(cx, cy)` to `(x, y)`.
Quad(f64, f64, f64, f64),
/// Cubic Bézier through controls `(c1x, c1y)`, `(c2x, c2y)` to `(x, y)`.
Cubic(f64, f64, f64, f64, f64, f64),
/// Close the current subpath.
Close,
}
/// The static layout style of one node — a dependency-free mirror of the
/// taffy properties the surface grammar exposes.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StyleIr {
pub width: Option<DimIr>,
pub height: Option<DimIr>,
/// Uniform padding, logical px.
pub padding: Option<f32>,
/// Uniform gap, logical px.
pub gap: Option<f32>,
pub direction: Option<DirectionIr>,
pub align_items: Option<AlignIr>,
pub justify_content: Option<AlignIr>,
pub align_self: Option<AlignIr>,
pub grow: Option<f32>,
pub shrink: Option<f32>,
pub basis: Option<DimIr>,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum DimIr {
Px(f32),
Percent(f32),
Auto,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DirectionIr {
Row,
Column,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AlignIr {
Start,
End,
Center,
Stretch,
}
/// Parse `#rgb`, `#rrggbb`, or `#rrggbbaa`.
pub fn parse_color(text: &str) -> Option<Literal> {
let hex = text.strip_prefix('#')?;
let component = |s: &str| u8::from_str_radix(s, 16).ok();
match hex.len() {
3 => {
let mut it = hex.chars().map(|c| component(&format!("{c}{c}")));
Some(Literal::Color(it.next()??, it.next()??, it.next()??, 255))
}
6 | 8 => {
let r = component(&hex[0..2])?;
let g = component(&hex[2..4])?;
let b = component(&hex[4..6])?;
let a = if hex.len() == 8 {
component(&hex[6..8])?
} else {
255
};
Some(Literal::Color(r, g, b, a))
}
_ => None,
}
}