manifest.rs raw

//! The `.gdw` widget manifest compiler: `(widget …)` forms declaring the
//! interface of a widget implemented in Rust outside the framework.
//!
//! ```lisp
//! (widget markdown
//!   (type "::guiduck_markdown::MarkdownView")
//!   (prop source String)
//!   (prop wrap bool :setter set_wrapping)
//!   (event on-link :payload String)
//!   (token link-color Brush))
//! ```
//!
//! A `.gdc` may then write `(markdown :source page-text :on-link (navigate
//! payload))` and a theme may write `(rule (markdown .doc) :link-color …)`,
//! with the same checking the builtins get.
//!
//! Manifests are s-expressions read by [`crate::sexpr`], the reader that also
//! serves `.gdc` ([`crate::parse`]) and `.gdt` ([`crate::theme`]).
//!
//! # What a manifest is, and is not
//!
//! `(type "…")` is the Rust path codegen emits. **The compiler never resolves
//! it** — a proc-macro cannot see another crate's items — so it is carried
//! verbatim and rustc reports a bad path against the generated construction.
//! That is the whole point of the split: the interface is data the compiler
//! can check a `.gdc` against, and the implementation stays Rust.
//!
//! The contract a registered widget owes is `Default + Widget` plus a setter
//! per declared prop. There are no constructor arguments: a prop is a setter
//! call, which is what lets a `.gdc` set it once at build or re-set it from a
//! binding through the same method.
//!
//! # A manifest declares nothing a builtin has not declared
//!
//! This is not a lesser way to define a widget. `container` and `button` are
//! registered under exactly this contract — see
//! [`WidgetDescriptor`](crate::registry::WidgetDescriptor) — and a `(prop …)`
//! draws from the same [`PropTy`] vocabulary their descriptors do, so an application's
//! widget can take a `Graphic` or `RichText` for the same reason an `image`
//! and a `text` can. The one thing a `.gdw` cannot declare is a
//! [`WidgetKind`](crate::registry::WidgetKind): where a widget may appear,
//! whether its content is deferred, what a menu is. Those are rules the
//! framework itself defines, and a closed set is what defining them means.
//!
//! Note that `(type …)` takes a **string**, not a symbol: the reader
//! terminates symbols at `:`, so `::guiduck_markdown::MarkdownView` cannot be
//! one. The quotes are the grammar being honest about its own lexer rather
//! than special-casing it.

use std::borrow::Cow;

use crate::ast::TypeKind;
use crate::diagnostics::Diagnostic;
use crate::registry::{
    self, EventRef, PropDecl, PropDeclKind, PropTy, ThemeValueType, WidgetDescriptor, setter_name,
};
use crate::sexpr::{self, Sexpr, Span};

/// The token types a `(token …)` may declare, spelled as the
/// [`ThemeValueType`] variants they name.
const TOKEN_TYPES: &[(&str, ThemeValueType)] = &[
    ("Brush", ThemeValueType::Brush),
    ("Number", ThemeValueType::Number),
    ("Str", ThemeValueType::Str),
    ("Graphic", ThemeValueType::Graphic),
];

/// The types a `(prop …)` may declare: the `.gdc` scalars, plus the
/// framework's own value types.
///
/// This is [`PropTy`]'s whole writable vocabulary, and it is the builtins'
/// too — an `(prop icon Graphic)` on an application's widget is checked, set,
/// and bound by exactly the code that serves an `image`'s `:source`. Records
/// are file-local to a `.gdc` and lists have no setter convention, so neither
/// crosses this boundary.
const PROP_TYPES: &[(&str, PropTy)] = &[
    ("i32", PropTy::Scalar(TypeKind::I32)),
    ("i64", PropTy::Scalar(TypeKind::I64)),
    ("f32", PropTy::Scalar(TypeKind::F32)),
    ("f64", PropTy::Scalar(TypeKind::F64)),
    ("bool", PropTy::Scalar(TypeKind::Bool)),
    ("String", PropTy::Scalar(TypeKind::String)),
    ("Brush", PropTy::Brush),
    ("Graphic", PropTy::Graphic),
    ("RichText", PropTy::RichText),
    ("ScrollAxes", PropTy::ScrollAxes),
];

fn prop_type_names() -> String {
    PROP_TYPES
        .iter()
        .map(|(name, _)| *name)
        .collect::<Vec<_>>()
        .join(", ")
}

/// Compile one `.gdw` source text to the widgets it declares.
///
/// A manifest may hold any number of `(widget …)` forms — a crate's widget set
/// is one file — so duplicate names *within* it are caught here; a name that
/// collides across manifests, or with a builtin, is
/// [`Registry::insert`](registry::Registry::insert)'s to reject, since only
/// the registry sees the whole vocabulary.
pub fn compile_manifest(source: &str) -> Result<Vec<WidgetDescriptor>, Vec<Diagnostic>> {
    let doc = sexpr::read(source).map_err(|e| vec![Diagnostic::new(e.message, e.span)])?;
    let mut widgets: Vec<WidgetDescriptor> = Vec::new();
    let mut errors = Vec::new();
    for form in &doc.values {
        match widget(form) {
            Ok(declared) => {
                if widgets.iter().any(|w| w.name == declared.name) {
                    errors.push(Diagnostic::new(
                        format!("`{}` is already declared in this manifest", declared.name),
                        declared
                            .name_span
                            .expect("a manifest-declared widget carries its name span"),
                    ));
                    continue;
                }
                widgets.push(declared);
            }
            Err(diagnostics) => errors.extend(diagnostics),
        }
    }
    if errors.is_empty() {
        Ok(widgets)
    } else {
        Err(errors)
    }
}

/// `(widget name (type "…") item…)`
fn widget(form: &Sexpr) -> Result<WidgetDescriptor, Vec<Diagnostic>> {
    let Some(items) = form.as_list() else {
        return Err(vec![Diagnostic::new(
            format!("expected a `(widget …)` form, found {}", form.kind_name()),
            form.span(),
        )]);
    };
    if items.first().and_then(Sexpr::as_symbol) != Some("widget") {
        return Err(vec![Diagnostic::new(
            "a `.gdw` file holds `(widget …)` forms",
            form.span(),
        )]);
    }
    let (name, name_span) = match items.get(1) {
        Some(Sexpr::Symbol(name, span)) => (name.clone(), *span),
        other => {
            return Err(vec![Diagnostic::new(
                "widget needs a name symbol, e.g. `(widget markdown …)`",
                other.map_or(form.span(), Sexpr::span),
            )]);
        }
    };
    // A capitalized name in widget position is a component reference, so a
    // widget spelled that way could never be written.
    if crate::is_component_name(&name) {
        return Err(vec![Diagnostic::new(
            format!(
                "widget names are lowercase: a capitalized `{name}` in widget \
                 position is a component reference, so nothing could write this \
                 widget"
            ),
            name_span,
        )]);
    }

    let mut errors = Vec::new();
    let mut type_path: Option<String> = None;
    // Props, events, and queries are accumulated apart so a name can be
    // reported against the right category, then concatenated in that order —
    // which is the order `lookup` searches and `known_props` lists.
    let mut props: Vec<PropDecl> = Vec::new();
    let mut events: Vec<PropDecl> = Vec::new();
    let mut queries: Vec<PropDecl> = Vec::new();
    let mut tokens: Vec<(Cow<'static, str>, ThemeValueType)> = Vec::new();
    // The framework's structural facts: where a widget may appear, whether a
    // file may write it. Both default to "no rule / writable", so an ordinary
    // application widget declares neither.
    let mut required_parents: Vec<Cow<'static, str>> = Vec::new();
    let mut is_writable = true;

    for item in &items[2..] {
        let Some(list) = item.as_list() else {
            errors.push(Diagnostic::new(
                format!(
                    "expected a `(type …)`, `(prop …)`, `(event …)`, or \
                     `(token …)` form, found {}",
                    item.kind_name()
                ),
                item.span(),
            ));
            continue;
        };
        match list.first().and_then(Sexpr::as_symbol) {
            Some("type") => match type_form(list, item.span()) {
                Ok(path) => {
                    if type_path.is_some() {
                        errors.push(Diagnostic::new(
                            "a widget has exactly one `(type …)`",
                            item.span(),
                        ));
                    } else {
                        type_path = Some(path);
                    }
                }
                Err(diagnostic) => errors.push(diagnostic),
            },
            Some("prop") => match prop_form(list, item.span()) {
                Ok(prop) => match declared_twice(&prop.name, &props, &events) {
                    Some(what) => errors.push(Diagnostic::new(
                        format!("`{}` is already {what} of `{name}`", prop.name),
                        item.span(),
                    )),
                    None => props.push(prop),
                },
                Err(diagnostic) => errors.push(diagnostic),
            },
            Some("event") => match event_form(list, item.span()) {
                Ok(event) => match declared_twice(&event.name, &props, &events) {
                    Some(what) => errors.push(Diagnostic::new(
                        format!("`{}` is already {what} of `{name}`", event.name),
                        item.span(),
                    )),
                    None => events.push(event),
                },
                Err(diagnostic) => errors.push(diagnostic),
            },
            Some("query") => match query_form(list, item.span()) {
                Ok(query) => match declared_twice(&query.name, &props, &events) {
                    Some(what) => errors.push(Diagnostic::new(
                        format!("`{}` is already {what} of `{name}`", query.name),
                        item.span(),
                    )),
                    None if queries.iter().any(|q| q.name == query.name) => {
                        errors.push(Diagnostic::new(
                            format!("`{}` is already a query of `{name}`", query.name),
                            item.span(),
                        ))
                    }
                    None => queries.push(query),
                },
                Err(diagnostic) => errors.push(diagnostic),
            },
            Some("token") => match token_form(list, item.span()) {
                Ok((token, ty)) => {
                    // Tokens live in the theme's namespace, not the `.gdc`'s,
                    // so they only collide with each other.
                    if tokens.iter().any(|(t, _)| *t == token) {
                        errors.push(Diagnostic::new(
                            format!("`{token}` is already a token of `{name}`"),
                            item.span(),
                        ));
                    } else {
                        tokens.push((Cow::Owned(token), ty));
                    }
                }
                Err(diagnostic) => errors.push(diagnostic),
            },
            // The widget names this one may appear directly inside — a menu
            // item inside a menu, and so on. The names are checked against the
            // real vocabulary when the registry is built.
            Some("parents") => match parents_form(list, item.span()) {
                Ok(parents) => required_parents = parents,
                Err(diagnostic) => errors.push(diagnostic),
            },
            // A widget a parent builds internally, that no `.gdc` writes (a
            // menu's popup panel). Writable by default.
            Some("internal") => {
                if list.len() > 1 {
                    errors.push(Diagnostic::new(
                        "`(internal)` takes no arguments",
                        item.span(),
                    ));
                } else {
                    is_writable = false;
                }
            }
            // An accelerator: a rendered string *and* a keystroke the tree
            // registers at mount, so it is its own form rather than a `(prop
            // …)`. Its setter is the string half.
            Some("accel") => match accel_form(list, item.span()) {
                Ok(accel) => match declared_twice(&accel.name, &props, &events) {
                    Some(what) => errors.push(Diagnostic::new(
                        format!("`{}` is already {what} of `{name}`", accel.name),
                        item.span(),
                    )),
                    None => props.push(accel),
                },
                Err(diagnostic) => errors.push(diagnostic),
            },
            _ => errors.push(Diagnostic::new(
                "expected `(type …)`, `(prop …)`, `(event …)`, `(query …)`, \
                 `(token …)`, `(parents …)`, `(accel …)`, or `(internal)`",
                item.span(),
            )),
        }
    }

    let Some(type_path) = type_path else {
        errors.push(Diagnostic::new(
            format!(
                "`{name}` has no `(type …)`; a widget manifest declares the Rust \
                 path to construct, e.g. `(type \"::my_crate::Markdown\")`"
            ),
            name_span,
        ));
        return Err(errors);
    };

    if !errors.is_empty() {
        return Err(errors);
    }
    // Props, then events, then queries: the order `lookup` searches and
    // `known_props` lists. The tokens are one owned group, the manifest's twin
    // of a builtin's shared borrowed groups.
    let mut decls = props;
    decls.extend(events);
    decls.extend(queries);
    Ok(WidgetDescriptor {
        name: Cow::Owned(name),
        name_span: Some(name_span),
        type_path: Cow::Owned(type_path),
        required_parents: Cow::Owned(required_parents),
        is_writable,
        props: Cow::Owned(decls),
        theme_tokens: Cow::Owned(vec![Cow::Owned(tokens)]),
    })
}

/// `(parents name…)` — the widget names this one may appear directly inside.
fn parents_form(list: &[Sexpr], span: Span) -> Result<Vec<Cow<'static, str>>, Diagnostic> {
    if list.len() < 2 {
        return Err(Diagnostic::new(
            "`(parents …)` lists the widget names this one may appear inside, \
             e.g. `(parents menu context-menu)`",
            span,
        ));
    }
    list[1..]
        .iter()
        .map(|item| match item.as_symbol() {
            Some(name) => Ok(Cow::Owned(name.to_owned())),
            None => Err(Diagnostic::new(
                "a parent is a widget name symbol, e.g. `menu`",
                item.span(),
            )),
        })
        .collect()
}

/// `(accel :setter method)` — a menu accelerator. The property is always named
/// `accel` (the keyword a `.gdc` writes); `:setter` gives the method for its
/// string half, defaulting to the `set_<name>` convention.
fn accel_form(list: &[Sexpr], span: Span) -> Result<PropDecl, Diagnostic> {
    let mut method = None;
    let mut rest = list[1..].iter();
    while let Some(item) = rest.next() {
        match item {
            Sexpr::Keyword(kw, kw_span) if kw == "setter" => {
                let value = rest
                    .next()
                    .ok_or_else(|| Diagnostic::new("`:setter` needs a method name", *kw_span))?;
                method = Some(expect_name(Some(value), "setter method name", *kw_span)?.0);
            }
            other => {
                return Err(Diagnostic::new(
                    "`(accel …)` takes only `:setter method`",
                    other.span(),
                ));
            }
        }
    }
    let _ = span;
    Ok(PropDecl {
        name: Cow::Borrowed("accel"),
        kind: PropDeclKind::Accel {
            method: Cow::Owned(method.unwrap_or_else(|| setter_name("accel"))),
        },
    })
}

/// `(query name :payload Type :setter method :returns "Path")`
fn query_form(list: &[Sexpr], span: Span) -> Result<PropDecl, Diagnostic> {
    let (name, _name_span) = expect_name(list.get(1), "query name", span)?;
    let mut payload = None;
    let mut setter = None;
    let mut returns = None;
    let mut rest = list[2..].iter();
    while let Some(item) = rest.next() {
        match item {
            Sexpr::Keyword(kw, kw_span) if kw == "payload" => {
                payload = Some(expect_scalar_type(rest.next(), *kw_span)?);
            }
            Sexpr::Keyword(kw, kw_span) if kw == "setter" => {
                let value = rest
                    .next()
                    .ok_or_else(|| Diagnostic::new("`:setter` needs a method name", *kw_span))?;
                setter = Some(expect_name(Some(value), "setter method name", *kw_span)?.0);
            }
            Sexpr::Keyword(kw, kw_span) if kw == "returns" => {
                let value = rest
                    .next()
                    .ok_or_else(|| Diagnostic::new("`:returns` needs a type path", *kw_span))?;
                match value {
                    Sexpr::Str(path, _) => returns = Some(path.clone()),
                    other => {
                        return Err(Diagnostic::new(
                            "`:returns` takes a Rust path in quotes, e.g. \
                             `:returns \"::my_crate::Image\"`",
                            other.span(),
                        ));
                    }
                }
            }
            other => {
                return Err(Diagnostic::new(
                    format!(
                        "`query` knows `:payload`, `:setter`, and `:returns`, not {} — a query \
                         resolves a value the handler returns",
                        other.kind_name()
                    ),
                    other.span(),
                ));
            }
        }
    }
    let payload = payload.ok_or_else(|| {
        Diagnostic::new(
            "a query needs a `:payload` type — the value it hands the handler",
            span,
        )
    })?;
    let returns = returns.ok_or_else(|| {
        Diagnostic::new(
            "a query needs a `:returns` type — the Rust path the handler returns",
            span,
        )
    })?;
    let method = setter.unwrap_or_else(|| setter_name(&name));
    Ok(PropDecl {
        name: Cow::Owned(name),
        kind: PropDeclKind::Query {
            setter: Cow::Owned(method),
            payload,
            returns: Cow::Owned(returns),
        },
    })
}

/// Whether a `.gdc`-facing name is already spoken for on this widget. Props
/// and events share one keyword namespace — they are both `:name value` on a
/// node — so they are checked against each other and against the universal
/// vocabulary every widget already has.
fn declared_twice(name: &str, props: &[PropDecl], events: &[PropDecl]) -> Option<&'static str> {
    if props.iter().any(|p| p.name == name) {
        return Some("a prop");
    }
    if events.iter().any(|e| e.name == name) {
        return Some("an event");
    }
    if registry::is_universal_prop(name) {
        return Some("a property every widget already has");
    }
    None
}

/// `(type "::path::To::Widget")`
fn type_form(list: &[Sexpr], span: Span) -> Result<String, Diagnostic> {
    match (list.get(1), list.len()) {
        (Some(Sexpr::Str(path, path_span)), 2) => {
            if path.trim().is_empty() {
                return Err(Diagnostic::new("`(type …)` needs a Rust path", *path_span));
            }
            Ok(path.clone())
        }
        _ => Err(Diagnostic::new(
            "`(type …)` takes one string, the Rust path to construct — e.g. \
             `(type \"::my_crate::Markdown\")`",
            span,
        )),
    }
}

/// `(prop name Type)` or `(prop name Type :setter set_thing)`
fn prop_form(list: &[Sexpr], span: Span) -> Result<PropDecl, Diagnostic> {
    let (name, _name_span) = expect_name(list.get(1), "prop name", span)?;
    let ty = expect_prop_type(list.get(2), span)?;
    let mut setter = None;
    let mut rest = list[3..].iter();
    while let Some(item) = rest.next() {
        match item {
            Sexpr::Keyword(kw, kw_span) if kw == "setter" => {
                let value = rest
                    .next()
                    .ok_or_else(|| Diagnostic::new("`:setter` needs a method name", *kw_span))?;
                setter = Some(expect_name(Some(value), "setter method name", *kw_span)?.0);
            }
            other => {
                return Err(Diagnostic::new(
                    format!(
                        "`prop` knows `:setter`, not {} — a prop is a name, a \
                         type, and the setter to call",
                        other.kind_name()
                    ),
                    other.span(),
                ));
            }
        }
    }
    let method = setter.unwrap_or_else(|| setter_name(&name));
    Ok(PropDecl {
        name: Cow::Owned(name),
        kind: PropDeclKind::Setter {
            method: Cow::Owned(method),
            ty,
        },
    })
}

/// `(event on-name)` or `(event on-name :payload Type)`
fn event_form(list: &[Sexpr], span: Span) -> Result<PropDecl, Diagnostic> {
    let (name, name_span) = expect_name(list.get(1), "event name", span)?;
    if !name.starts_with("on-") || name.len() == 3 {
        return Err(Diagnostic::new(
            format!(
                "event names start with `on-`, e.g. `(event on-link …)`; a \
                 `.gdc` wires it as `:{name}`, and the prefix is what tells a \
                 reader it is a wire and not a value"
            ),
            name_span,
        ));
    }
    let mut payload = None;
    let mut builtin = false;
    let mut rest = list[2..].iter();
    while let Some(item) = rest.next() {
        match item {
            Sexpr::Keyword(kw, kw_span) if kw == "payload" => {
                payload = Some(expect_scalar_type(rest.next(), *kw_span)?);
            }
            // `:builtin` marks one of the framework's own semantic events (a
            // `text`'s `on-link`, a `checkbox`'s `on-toggle`): the widget emits
            // a typed `EventData`, routed by its own `EventKind` rather than the
            // erased user path. Its payload is the framework's, so it is not
            // declared here.
            Sexpr::Keyword(kw, _) if kw == "builtin" => builtin = true,
            other => {
                return Err(Diagnostic::new(
                    format!(
                        "`event` knows `:payload` and `:builtin`, not {} — an \
                         event is a name and, at most, the value it delivers",
                        other.kind_name()
                    ),
                    other.span(),
                ));
            }
        }
    }
    let kind = if builtin {
        if payload.is_some() {
            return Err(Diagnostic::new(
                "a `:builtin` event's payload is the framework's, so it is not \
                 declared here",
                name_span,
            ));
        }
        let event = registry::EventProp::from_widget_event(&name).ok_or_else(|| {
            Diagnostic::new(
                format!("`{name}` is not one of the framework's builtin events"),
                name_span,
            )
        })?;
        EventRef::Builtin(event)
    } else {
        EventRef::User { payload }
    };
    Ok(PropDecl {
        name: Cow::Owned(name),
        kind: PropDeclKind::Event(kind),
    })
}

/// `(token name Brush|Number|Str|Graphic)`
fn token_form(list: &[Sexpr], span: Span) -> Result<(String, ThemeValueType), Diagnostic> {
    let (name, _) = expect_name(list.get(1), "token name", span)?;
    let Some(ty_form) = list.get(2) else {
        return Err(Diagnostic::new(
            format!("token `{name}` needs a type ({})", token_type_names()),
            span,
        ));
    };
    if let Some(extra) = list.get(3) {
        return Err(Diagnostic::new(
            "`token` takes a name and a type",
            extra.span(),
        ));
    }
    let Sexpr::Symbol(ty_name, ty_span) = ty_form else {
        return Err(Diagnostic::new(
            format!(
                "expected a token type ({}), found {}",
                token_type_names(),
                ty_form.kind_name()
            ),
            ty_form.span(),
        ));
    };
    match TOKEN_TYPES.iter().find(|(n, _)| n == ty_name) {
        Some((_, ty)) => Ok((name, *ty)),
        None => Err(Diagnostic::new(
            format!(
                "`{ty_name}` is not a token type; expected one of: {}",
                token_type_names()
            ),
            *ty_span,
        )),
    }
}

fn token_type_names() -> String {
    TOKEN_TYPES
        .iter()
        .map(|(n, _)| *n)
        .collect::<Vec<_>>()
        .join(", ")
}

fn expect_name(
    value: Option<&Sexpr>,
    what: &str,
    parent: Span,
) -> Result<(String, Span), Diagnostic> {
    match value {
        Some(Sexpr::Symbol(name, span)) => Ok((name.clone(), *span)),
        Some(other) => Err(Diagnostic::new(
            format!("expected {what}, found {}", other.kind_name()),
            other.span(),
        )),
        None => Err(Diagnostic::new(format!("missing {what}"), parent)),
    }
}

/// A prop's type: [`PROP_TYPES`].
fn expect_prop_type(value: Option<&Sexpr>, parent: Span) -> Result<PropTy, Diagnostic> {
    let (name, span) = expect_name(value, "a type", parent)?;
    PROP_TYPES
        .iter()
        .find(|(candidate, _)| *candidate == name)
        .map(|(_, ty)| *ty)
        .ok_or_else(|| {
            Diagnostic::new(
                format!(
                    "unknown type `{name}`; expected one of {}",
                    prop_type_names()
                ),
                span,
            )
        })
}

/// A payload type: the `.gdc` scalar set, and only that. An event delivers a
/// value to a handler, and a handler's parameters are `.gdc` types — so unlike
/// a prop, whose type is whatever its Rust setter takes, this cannot widen
/// past what a `(handler …)` can declare.
fn expect_scalar_type(value: Option<&Sexpr>, parent: Span) -> Result<TypeKind, Diagnostic> {
    let (name, span) = expect_name(value, "a type", parent)?;
    TypeKind::parse(&name).ok_or_else(|| {
        Diagnostic::new(
            format!("unknown type `{name}`; expected one of i32, i64, f32, f64, bool, String"),
            span,
        )
    })
}

#[cfg(test)]
mod tests;