ir.rs raw

//! 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,
    }
}