//! 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::>() .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> { let doc = sexpr::read(source).map_err(|e| vec![Diagnostic::new(e.message, e.span)])?; let mut widgets: Vec = 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> { 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 = 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 = Vec::new(); let mut events: Vec = Vec::new(); let mut queries: Vec = 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> = 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>, 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_` convention. fn accel_form(list: &[Sexpr], span: Span) -> Result { 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 { 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 { 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 { 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 { 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::>() .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 { 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 { 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;