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;