Skip to main content

bern_compiler/
lib.rs

1//! The Bern compiler front end.
2//!
3//! It reads the explicitly listed source files with the same parser the
4//! macros use, checks commands, bindings, localization, windows and
5//! component references, and produces the versioned [`bern_ir::Application`]
6//! that independent target modules consume. It never runs application code.
7
8mod check;
9pub mod config;
10mod features;
11mod placement;
12pub mod style;
13
14pub use check::{Signature, location};
15pub use config::{Config, SourceFile};
16
17use bern_ir::{self as ir, IR_VERSION};
18use bern_protocol::{
19    EnumDef, FieldDef, LocalizedText, ProtocolDescriptor, RecordDef, Type, TypeTable, Value,
20    VariantDef, compose,
21};
22use bern_syntax::{self as ast, ValueKind};
23use bern_target_api::Diagnostic;
24use check::{Checker, ContributedPage, Contributions, PageSlot, shared_diagnostic};
25use std::collections::{BTreeMap, BTreeSet};
26
27/// Result of a successful compilation.
28pub struct Compiled {
29    pub app: ir::Application,
30    /// Rust path of the protocol module, e.g. `showcase_protocol::showcase`.
31    pub protocol_module: String,
32    /// Rust paths of the modules' protocol modules, in application order.
33    pub module_protocols: Vec<String>,
34    /// Versioned descriptors of the component libraries in use.
35    pub libraries: Vec<ir::ComponentLibrary>,
36    pub warnings: Vec<Diagnostic>,
37}
38
39struct Located<T> {
40    file: String,
41    module: Vec<String>,
42    /// Namespace of the application or module the source belongs to.
43    home: String,
44    item: T,
45}
46
47/// Which protocol a `…::Client` parameter names: the protocol modules of
48/// the application and of its modules, by their Rust module name.
49struct Clients {
50    /// `(Rust module name, namespace)`, the application first.
51    protocols: Vec<(String, String)>,
52}
53
54impl Clients {
55    /// The namespace of a `Client` type written as `path`, in a source of
56    /// the module (or application) whose namespace is `home`. A bare
57    /// `Client` names the home protocol.
58    fn namespace(&self, path: &[String], home: &str) -> Option<String> {
59        if path.len() == 1 {
60            return Some(home.to_string());
61        }
62        let module = &path[path.len() - 2];
63        self.protocols
64            .iter()
65            .find(|(name, _)| name == module)
66            .map(|(_, namespace)| namespace.clone())
67    }
68}
69
70fn syn_error(file: &str, error: syn::Error) -> Vec<Diagnostic> {
71    error
72        .into_iter()
73        .map(|error| shared_diagnostic(file, error.span(), &error.to_string()))
74        .collect()
75}
76
77fn read(file: &SourceFile) -> Result<ast::SourceItems, Vec<Diagnostic>> {
78    read_selected(file, &bern_syntax::build::Modules::default())
79}
80
81fn read_selected(
82    file: &SourceFile,
83    modules: &bern_syntax::build::Modules,
84) -> Result<ast::SourceItems, Vec<Diagnostic>> {
85    let text = std::fs::read_to_string(&file.path).map_err(|error| {
86        vec![Diagnostic::error(format!(
87            "cannot read {}: {error}",
88            file.path.display()
89        ))]
90    })?;
91    bern_syntax::items::parse_source_file_with_modules(&text, modules)
92        .map_err(|error| syn_error(&file.display, error))
93}
94
95/// Resolve a UI-side type reference against the merged type table.
96fn resolve_type(ty: &ast::TypeRef, types: &TypeTable) -> Result<Type, String> {
97    Ok(match ty {
98        ast::TypeRef::Unit => Type::Unit,
99        ast::TypeRef::Bool => Type::Bool,
100        ast::TypeRef::Int => Type::Int,
101        ast::TypeRef::Float => Type::Float,
102        ast::TypeRef::Text => Type::Text,
103        ast::TypeRef::Optional(inner) => Type::optional(resolve_type(inner, types)?),
104        ast::TypeRef::List(inner) => Type::list(resolve_type(inner, types)?),
105        ast::TypeRef::Named(path) => {
106            let name = path.last().unwrap().clone();
107            if types.enum_def(&name).is_some() {
108                Type::Enum { name }
109            } else if types.record_def(&name).is_some() {
110                Type::Record { name }
111            } else {
112                return Err(format!(
113                    "unknown type `{name}`; declare it with #[bern::value] in a listed source"
114                ));
115            }
116        }
117    })
118}
119
120fn initial_value(expr: &ast::Expr, ty: &Type, types: &TypeTable) -> Result<Value, String> {
121    let value = match expr {
122        ast::Expr::Literal { value, .. } => Checker::literal(value).0,
123        ast::Expr::Unary {
124            op: ast::UnaryOp::Neg,
125            expr,
126            ..
127        } => match initial_value(expr, ty, types)? {
128            Value::Int(value) => Value::Int(-value),
129            Value::Float(value) => Value::Float(-value),
130            _ => return Err("only numbers can be negated".into()),
131        },
132        ast::Expr::Variant(variant) if variant.fields.is_empty() => {
133            let def = types
134                .enum_def(&variant.ty.to_string())
135                .ok_or_else(|| format!("unknown enum `{}`", variant.ty))?;
136            let index = def
137                .variant_index(&variant.variant.to_string())
138                .ok_or_else(|| format!("`{}` has no variant `{}`", variant.ty, variant.variant))?;
139            Value::Enum {
140                variant: index as u32,
141                fields: vec![],
142            }
143        }
144        _ => return Err("an initial value is a literal or a unit enum variant".into()),
145    };
146    value
147        .check(ty, types)
148        .map_err(|error| format!("initial value does not match the field: {error}"))?;
149    Ok(value)
150}
151
152/// Types written in the fields of a value declaration.
153fn value_types(value: &ast::ValueItem) -> Vec<&ast::TypeRef> {
154    match &value.kind {
155        ValueKind::Enum(variants) => variants
156            .iter()
157            .flat_map(|variant| variant.fields.iter().map(|field| &field.ty))
158            .collect(),
159        ValueKind::Record(fields) => fields.iter().map(|field| &field.ty).collect(),
160    }
161}
162
163/// Add framework value types such as `MediaSource` when declarations use
164/// them and the protocol has not brought them in already.
165fn add_framework_types(types: &mut TypeTable, mentioned: &[&ast::TypeRef]) {
166    if types.record_def(bern_media::MEDIA_SOURCE).is_none()
167        && mentioned
168            .iter()
169            .any(|ty| bern_syntax::mentions(ty, bern_media::MEDIA_SOURCE))
170    {
171        types.records.push(bern_media::media_source_def());
172    }
173    let date = bern_protocol::format::CALENDAR_DATE;
174    if types.record_def(date).is_none()
175        && mentioned.iter().any(|ty| bern_syntax::mentions(ty, date))
176    {
177        types
178            .records
179            .push(bern_protocol::format::calendar_date_def());
180    }
181}
182
183fn merge_ui_types(
184    base: &TypeTable,
185    values: &[Located<ast::ValueItem>],
186    languages: (&[String], bool),
187    diagnostics: &mut Vec<Diagnostic>,
188) -> TypeTable {
189    let mut types = base.clone();
190    let protocol_names: BTreeSet<String> = types
191        .enums
192        .iter()
193        .map(|def| def.name.clone())
194        .chain(types.records.iter().map(|def| def.name.clone()))
195        .collect();
196    let mut seen = BTreeSet::new();
197    for value in values {
198        let name = value.item.name.to_string();
199        if name == bern_media::MEDIA_SOURCE || name == bern_protocol::format::CALENDAR_DATE {
200            diagnostics.push(
201                Diagnostic::error(format!(
202                    "`{name}` is a framework type; use it from `bern::prelude` instead of declaring it",
203                ))
204                .at(location(&value.file, value.item.span)),
205            );
206            continue;
207        }
208        if protocol_names.contains(&name) || !seen.insert(name.clone()) {
209            diagnostics.push(
210                Diagnostic::error(format!(
211                    "type `{name}` is declared more than once; Bern type names are unique per application"
212                ))
213                .at(location(&value.file, value.item.span)),
214            );
215            continue;
216        }
217        match &value.item.kind {
218            ValueKind::Enum(_) => types.enums.push(EnumDef {
219                name,
220                variants: vec![],
221            }),
222            ValueKind::Record(_) => types.records.push(RecordDef {
223                name,
224                fields: vec![],
225            }),
226        }
227    }
228    let lookup = types.clone();
229    let languages = Some(languages);
230    for value in values {
231        let name = value.item.name.to_string();
232        let field = |field: &ast::FieldItem, diagnostics: &mut Vec<Diagnostic>| match resolve_type(
233            &field.ty, &lookup,
234        ) {
235            Ok(ty) => {
236                let content = bern_syntax::lower::content(field, &ty).unwrap_or_else(|error| {
237                    diagnostics.extend(syn_error(&value.file, error));
238                    false
239                });
240                Some(FieldDef {
241                    name: field.name.to_string(),
242                    ty,
243                    doc: field.doc.clone(),
244                    content,
245                })
246            }
247            Err(message) => {
248                diagnostics.push(Diagnostic::error(message).at(location(&value.file, field.span)));
249                None
250            }
251        };
252        match &value.item.kind {
253            ValueKind::Enum(variants) => {
254                let mut defs = Vec::new();
255                for variant in variants {
256                    let fields: Vec<FieldDef> = variant
257                        .fields
258                        .iter()
259                        .filter_map(|f| field(f, diagnostics))
260                        .collect();
261                    let label = variant.label.as_ref().and_then(|tr| {
262                        for (span, message) in bern_syntax::lower::check_languages(tr, languages) {
263                            diagnostics.push(shared_diagnostic(&value.file, span, &message));
264                        }
265                        bern_syntax::lower::label(&name, &variant.name.to_string(), tr, &fields)
266                            .map_err(|error| diagnostics.extend(syn_error(&value.file, error)))
267                            .ok()
268                    });
269                    defs.push(VariantDef {
270                        name: variant.name.to_string(),
271                        fields,
272                        label,
273                    });
274                }
275                if let Some(def) = types.enums.iter_mut().find(|def| def.name == name) {
276                    def.variants = defs;
277                }
278            }
279            ValueKind::Record(fields) => {
280                let defs = fields
281                    .iter()
282                    .filter_map(|f| field(f, diagnostics))
283                    .collect();
284                if let Some(def) = types.records.iter_mut().find(|def| def.name == name) {
285                    def.fields = defs;
286                }
287            }
288        }
289    }
290    if let Err(error) = types.validate() {
291        diagnostics.push(Diagnostic::error(error.message));
292    }
293    types
294}
295
296fn param_kind(
297    param: &ast::Param,
298    clients: Option<(&Clients, &str)>,
299    windows_handle: Option<&str>,
300    players_handle: Option<&str>,
301    sessions: &[ir::SessionDef],
302    types: &TypeTable,
303) -> Result<ir::ParamKind, String> {
304    let name = param.ty.name().unwrap_or_default().to_string();
305    match param.kind {
306        ast::ParamKind::Session => {
307            if sessions.iter().any(|session| session.name == name) {
308                Ok(ir::ParamKind::Session { session: name })
309            } else {
310                Err(format!(
311                    "`#[session]` needs a #[bern::session] struct; `{name}` is not one"
312                ))
313            }
314        }
315        ast::ParamKind::Prop => match &param.ty {
316            ast::TypeRef::Named(path)
317                if name == "Client"
318                    && clients
319                        .is_some_and(|(clients, home)| clients.namespace(path, home).is_some()) =>
320            {
321                let (clients, home) = clients.expect("checked");
322                Ok(ir::ParamKind::Client {
323                    namespace: clients.namespace(path, home).expect("checked"),
324                })
325            }
326            ast::TypeRef::Named(_) if Some(name.as_str()) == windows_handle => {
327                Ok(ir::ParamKind::Windows)
328            }
329            ast::TypeRef::Named(_) if Some(name.as_str()) == players_handle => {
330                Ok(ir::ParamKind::Players)
331            }
332            ast::TypeRef::Named(_) if sessions.iter().any(|session| session.name == name) => Err(
333                format!("session state `{name}` must be marked `#[session]`"),
334            ),
335            other => resolve_type(other, types).map(|ty| ir::ParamKind::Value { ty }),
336        },
337    }
338}
339
340/// `#[content]` is for text parameters.
341fn check_content_param(
342    param: &ast::Param,
343    kind: &ir::ParamKind,
344    file: &str,
345    diagnostics: &mut Vec<Diagnostic>,
346) {
347    if !param.content {
348        return;
349    }
350    match kind {
351        ir::ParamKind::Value { ty } if bern_syntax::lower::is_text(ty) => {}
352        _ => diagnostics.push(
353            Diagnostic::error(format!(
354                "`#[content]` marks text that is data, such as a title; `{}` is not text",
355                param.name
356            ))
357            .at(location(file, param.span)),
358        ),
359    }
360}
361
362/// Names of a component's `#[content]` parameters.
363fn content_params(component: &ast::ComponentItem) -> BTreeSet<String> {
364    component
365        .params
366        .iter()
367        .filter(|param| param.content)
368        .map(|param| param.name.to_string())
369        .collect()
370}
371
372/// Window handle name and window ids declared by the root component.
373fn root_windows(root: &ast::ComponentItem) -> Option<(String, Vec<String>)> {
374    let ast::Node::Element(set) = root.body.nodes.first()? else {
375        return None;
376    };
377    if set.name != "WindowSet" {
378        return None;
379    }
380    let handle = match &set.prop("handle")?.value {
381        ast::Expr::Path(path) => path.single()?.to_string(),
382        _ => return None,
383    };
384    let ids = set
385        .children
386        .iter()
387        .flatten()
388        .filter_map(|child| match child {
389            ast::Node::Element(window) => match &window.prop("id")?.value {
390                ast::Expr::Path(path) => path.single().map(ToString::to_string),
391                _ => None,
392            },
393            _ => None,
394        })
395        .collect();
396    Some((handle, ids))
397}
398
399fn component_refs(node: &ir::Node, out: &mut Vec<String>) {
400    node.walk(&mut |node| {
401        if let ir::Node::Component(reference) = node {
402            out.push(reference.component.clone());
403        }
404    });
405}
406
407/// Session state types the elements of `node` read and write themselves,
408/// not those of the components it places.
409fn read_sessions(node: &ir::Node) -> Vec<String> {
410    let mut sessions = Vec::new();
411    let mut exprs: Vec<&ir::Expr> = Vec::new();
412    node.walk(&mut |node| match node {
413        ir::Node::Element(element) => {
414            for prop in element.props.values() {
415                match prop {
416                    ir::Prop::Text { text } => exprs.extend(text.exprs()),
417                    ir::Prop::Expr { expr } => exprs.push(expr),
418                    ir::Prop::Field { field } => sessions.push(field.session.clone()),
419                    ir::Prop::Query { query } => exprs.extend(&query.args),
420                    _ => {}
421                }
422            }
423            match &element.action {
424                Some(ir::Action::Engine { args, .. } | ir::Action::Media { args, .. }) => {
425                    exprs.extend(args);
426                }
427                Some(ir::Action::Session { field, op }) => {
428                    sessions.push(field.session.clone());
429                    if let ir::SessionOp::Set { value } | ir::SessionOp::PushStr { value } = op {
430                        exprs.push(value);
431                    }
432                }
433                Some(ir::Action::Window { .. }) | None => {}
434            }
435        }
436        ir::Node::If(node) => exprs.extend(node.branches.iter().map(|(when, _)| when)),
437        ir::Node::IfLet(node) => exprs.push(&node.value),
438        ir::Node::Match(node) => exprs.push(&node.value),
439        ir::Node::Component(_) => {}
440    });
441    sessions.extend(exprs.into_iter().flat_map(ir::Expr::sessions));
442    let mut seen = BTreeSet::new();
443    sessions.retain(|session| seen.insert(session.clone()));
444    sessions
445}
446
447fn find_cycle(components: &BTreeMap<String, Vec<String>>) -> Option<Vec<String>> {
448    fn visit(
449        name: &str,
450        graph: &BTreeMap<String, Vec<String>>,
451        stack: &mut Vec<String>,
452        done: &mut BTreeSet<String>,
453    ) -> Option<Vec<String>> {
454        if let Some(position) = stack.iter().position(|entry| entry == name) {
455            let mut cycle = stack[position..].to_vec();
456            cycle.push(name.to_string());
457            return Some(cycle);
458        }
459        if done.contains(name) {
460            return None;
461        }
462        stack.push(name.to_string());
463        for callee in graph.get(name).into_iter().flatten() {
464            if let Some(cycle) = visit(callee, graph, stack, done) {
465                return Some(cycle);
466            }
467        }
468        stack.pop();
469        done.insert(name.to_string());
470        None
471    }
472    let mut done = BTreeSet::new();
473    for name in components.keys() {
474        if let Some(cycle) = visit(name, components, &mut Vec::new(), &mut done) {
475            return Some(cycle);
476        }
477    }
478    None
479}
480
481fn window_catalog(
482    root: &ir::Element,
483    handle: String,
484    graph: &BTreeMap<String, Vec<String>>,
485    signatures: &BTreeMap<String, Signature>,
486    diagnostics: &mut Vec<Diagnostic>,
487) -> Option<ir::WindowCatalog> {
488    let primary = match root.prop("primary") {
489        Some(ir::Prop::Window { id }) => id.clone(),
490        _ => return None,
491    };
492    let mut windows = Vec::new();
493    for child in &root.children {
494        let ir::Node::Element(window) = child else {
495            continue;
496        };
497        let id = match window.prop("id") {
498            Some(ir::Prop::Window { id }) => id.clone(),
499            _ => continue,
500        };
501        let title = match window.text("title") {
502            Some(ir::Text::Localized { text, .. }) => text.clone(),
503            _ => continue,
504        };
505        let content = window.children.first()?.clone();
506        let mut reachable = Vec::new();
507        component_refs(&content, &mut reachable);
508        let mut queue = reachable.clone();
509        while let Some(name) = queue.pop() {
510            for callee in graph.get(&name).into_iter().flatten() {
511                if !reachable.contains(callee) {
512                    reachable.push(callee.clone());
513                    queue.push(callee.clone());
514                }
515            }
516        }
517        let mut sessions: Vec<String> = Vec::new();
518        for name in &reachable {
519            for (_, kind) in signatures
520                .get(name)
521                .map(|s| s.params.as_slice())
522                .unwrap_or(&[])
523            {
524                if let ir::ParamKind::Session { session } = kind
525                    && !sessions.contains(session)
526                {
527                    sessions.push(session.clone());
528                }
529            }
530        }
531        // Elements written directly in the window use its sessions too.
532        for session in read_sessions(&content) {
533            if !sessions.contains(&session) {
534                sessions.push(session);
535            }
536        }
537        let keyword = |name: &str, default: &str| -> String {
538            window.keyword(name).unwrap_or(default).to_string()
539        };
540        let initial = match keyword("initial", "closed").as_str() {
541            "open" => ir::WindowInitial::Open,
542            _ => ir::WindowInitial::Closed,
543        };
544        let presentation = match keyword("presentation", "auto").as_str() {
545            "virtual" => ir::Presentation::Virtual,
546            "floating" => ir::Presentation::Floating,
547            "native_required" => ir::Presentation::NativeRequired,
548            _ => ir::Presentation::Auto,
549        };
550        let role = match keyword("role", "standard").as_str() {
551            "settings" => ir::WindowRole::Settings,
552            "dialog" => ir::WindowRole::Dialog,
553            _ => ir::WindowRole::Standard,
554        };
555        let close = match (window.expr("confirm_close"), window.text("close_message")) {
556            (Some(when), Some(message)) => Some(ir::CloseCheck {
557                when: when.clone(),
558                message: message.clone(),
559                save: window.action.clone(),
560            }),
561            _ => None,
562        };
563        if let Some(check) = &close
564            && matches!(
565                check.save,
566                Some(
567                    ir::Action::Window { .. }
568                        | ir::Action::Media { .. }
569                        | ir::Action::Session { .. }
570                )
571            )
572        {
573            diagnostics.push(
574                Diagnostic::error(format!(
575                    "window `{id}`: the save action must be an engine command"
576                ))
577                .at(window.source.clone()),
578            );
579        }
580        // The close check and the window's own expressions read that
581        // window instance's drafts.
582        let mut reads: Vec<(String, &ir::Expr)> = close
583            .iter()
584            .flat_map(ir::CloseCheck::exprs)
585            .map(|expr| ("close check".to_string(), expr))
586            .collect();
587        for name in ["immersive", "idle_enabled", "landscape"] {
588            if let Some(expr) = window.expr(name) {
589                reads.push((format!("`{name}`"), expr));
590            }
591        }
592        for (what, expr) in reads {
593            for session in expr.sessions() {
594                if !sessions.contains(&session) {
595                    diagnostics.push(
596                        Diagnostic::error(format!(
597                            "window `{id}`'s {what} reads `{session}`, which this window does not own; its components own: {}",
598                            if sessions.is_empty() { "none".to_string() } else { sessions.join(", ") }
599                        ))
600                        .at(window.source.clone()),
601                    );
602                }
603            }
604        }
605        // Session parameters of the root exist per window instance and are
606        // only meaningful in that window's close check.
607        let mut content_sessions = Vec::new();
608        content.walk(&mut |node| {
609            if let ir::Node::Component(reference) = node {
610                for (_, arg) in &reference.args {
611                    if let ir::ComponentArg::Value { expr } = arg {
612                        content_sessions.extend(expr.sessions());
613                    }
614                }
615            }
616        });
617        if let Some(session) = content_sessions.first() {
618            diagnostics.push(
619                Diagnostic::error(format!(
620                    "window `{id}` passes `{session}` state from the root; components take their own `#[session]` parameters"
621                ))
622                .at(window.source.clone()),
623            );
624        }
625        windows.push(ir::WindowDef {
626            id,
627            title,
628            initial,
629            presentation,
630            role,
631            width: window.number("width"),
632            height: window.number("height"),
633            min_width: window.number("min_width"),
634            min_height: window.number("min_height"),
635            content,
636            sessions,
637            close,
638            chrome: match window.keyword("chrome") {
639                Some("unified") => ir::WindowChrome::Unified,
640                _ => ir::WindowChrome::Standard,
641            },
642            immersive: window.expr("immersive").cloned(),
643            idle_after: window.number("idle_after"),
644            idle_enabled: window.expr("idle_enabled").cloned(),
645            landscape: window.expr("landscape").cloned(),
646        });
647    }
648    if windows
649        .iter()
650        .filter(|window| window.role == ir::WindowRole::Settings)
651        .count()
652        > 1
653    {
654        diagnostics.push(
655            Diagnostic::error("only one window may have `role: settings`").at(root.source.clone()),
656        );
657    }
658    if windows
659        .iter()
660        .find(|window| window.id == primary)
661        .is_some_and(|window| window.role == ir::WindowRole::Settings)
662    {
663        diagnostics.push(
664            Diagnostic::error("the primary window cannot be the settings window")
665                .at(root.source.clone()),
666        );
667    }
668    let bars: Vec<&ir::Element> = root
669        .children
670        .iter()
671        .filter_map(|child| match child {
672            ir::Node::Element(bar) if bar.kind == "MenuBar" => Some(bar),
673            _ => None,
674        })
675        .collect();
676    if let [_, second, ..] = bars.as_slice() {
677        diagnostics.push(
678            Diagnostic::error("a `WindowSet` has one `MenuBar`; put every menu in it")
679                .at(second.source.clone()),
680        );
681    }
682    let menu_bar = bars.first().map(|bar| (*bar).clone());
683    // The menu bar's items read and write the primary window's drafts,
684    // so that window owns them.
685    if let Some(bar) = &menu_bar
686        && let Some(window) = windows.iter_mut().find(|window| window.id == primary)
687    {
688        for session in read_sessions(&ir::Node::Element(bar.clone())) {
689            if !window.sessions.contains(&session) {
690                window.sessions.push(session);
691            }
692        }
693    }
694    let items: Vec<&ir::Element> = root
695        .children
696        .iter()
697        .filter_map(|child| match child {
698            ir::Node::Element(item) if item.kind == "StatusItem" => Some(item),
699            _ => None,
700        })
701        .collect();
702    if let [_, second, ..] = items.as_slice() {
703        diagnostics.push(
704            Diagnostic::error("a `WindowSet` has one `StatusItem`; put every item in its menu")
705                .at(second.source.clone()),
706        );
707    }
708    let status_item = items.first().map(|item| (*item).clone());
709    if let Some(item) = &status_item {
710        check_status_item(item, diagnostics);
711    }
712    Some(ir::WindowCatalog {
713        handle,
714        primary,
715        windows,
716        accent: root.expr("accent").cloned(),
717        palette: root.expr("palette").cloned(),
718        appearance: root.expr("appearance").cloned(),
719        icon: root.expr("icon").cloned(),
720        menu_bar,
721        status_item,
722    })
723}
724
725/// The `StatusItem`'s menu runs while no window is open: it reads engine
726/// state and runs commands, and takes nothing that needs a window: drafts,
727/// the window's own fields, files asked for in its open panel, shortcuts.
728fn check_status_item(item: &ir::Element, diagnostics: &mut Vec<Diagnostic>) {
729    let node = ir::Node::Element(item.clone());
730    if let Some(session) = read_sessions(&node).first() {
731        diagnostics.push(
732            Diagnostic::error(format!(
733                "the `StatusItem`'s menu runs without a window, so it has no drafts: read engine state and run commands instead of `{session}`"
734            ))
735            .at(item.source.clone()),
736        );
737    }
738    node.walk(&mut |node| {
739        let exprs: Vec<&ir::Expr> = match node {
740            ir::Node::Element(element) => {
741                for prop in ["pick", "shortcut"] {
742                    if element.prop(prop).is_some() {
743                        diagnostics.push(
744                            Diagnostic::error(format!(
745                                "a `CommandItem` of the `StatusItem` takes no `{prop}:`: its menu belongs to no window"
746                            ))
747                            .at(element.source.clone()),
748                        );
749                    }
750                }
751                element
752                    .props
753                    .values()
754                    .flat_map(|prop| match prop {
755                        ir::Prop::Text { text } => text.exprs(),
756                        ir::Prop::Expr { expr } => vec![expr],
757                        _ => Vec::new(),
758                    })
759                    .chain(match &element.action {
760                        Some(ir::Action::Engine { args, .. } | ir::Action::Media { args, .. }) => {
761                            args.iter().collect()
762                        }
763                        _ => Vec::new(),
764                    })
765                    .collect()
766            }
767            ir::Node::If(node) => node.branches.iter().map(|(when, _)| when).collect(),
768            ir::Node::IfLet(node) => vec![&node.value],
769            ir::Node::Match(node) => vec![&node.value],
770            ir::Node::Component(_) => Vec::new(),
771        };
772        for expr in exprs {
773            expr.walk(&mut |expr| {
774                if let ir::ExprKind::Binding {
775                    binding: ir::Binding::Window { field },
776                } = &expr.kind
777                    // Known when the target builds.
778                    && !matches!(field.as_str(), "menu_bar" | "webview" | "tv")
779                {
780                    diagnostics.push(
781                        Diagnostic::error(format!(
782                            "the `StatusItem`'s menu belongs to no window, so it cannot read `window.{field}`"
783                        ))
784                        .at(item.source.clone()),
785                    );
786                }
787            });
788        }
789    });
790}
791
792fn source_fingerprint(files: &[&std::path::Path]) -> String {
793    let mut bytes = Vec::new();
794    for file in files {
795        bytes.extend(file.display().to_string().into_bytes());
796        bytes.extend(std::fs::read(file).unwrap_or_default());
797    }
798    format!("{:016x}", bern_protocol::fingerprint_bytes(&bytes))
799}
800
801/// Generate the versioned descriptor of a component library from its
802/// sources. Library components are presentational: they take value
803/// parameters only and cannot own session state or call commands.
804pub fn compile_library(
805    library: &config::LibraryConfig,
806) -> Result<ir::ComponentLibrary, Vec<Diagnostic>> {
807    let mut diagnostics = Vec::new();
808    let mut values = Vec::new();
809    let mut components = Vec::new();
810    let crate_name = library.package.crate_name();
811    let home = String::new();
812    for file in &library.sources {
813        let items = match read(file) {
814            Ok(items) => items,
815            Err(errors) => {
816                diagnostics.extend(errors);
817                continue;
818            }
819        };
820        if !items.protocols.is_empty() || !items.sessions.is_empty() {
821            diagnostics.push(Diagnostic::error(format!(
822                "{}: component libraries cannot declare protocols or session state",
823                file.display
824            )));
825        }
826        let module = |relative: &Vec<String>| {
827            let mut module = vec![crate_name.clone()];
828            module.extend(file.module.iter().cloned());
829            module.extend(relative.iter().cloned());
830            module
831        };
832        for (path, item) in items.values {
833            values.push(Located {
834                file: file.display.clone(),
835                module: module(&path),
836                home: home.clone(),
837                item,
838            });
839        }
840        for (path, item) in items.components {
841            components.push(Located {
842                file: file.display.clone(),
843                module: module(&path),
844                home: home.clone(),
845                item,
846            });
847        }
848    }
849    let mut base = TypeTable::default();
850    let mentioned: Vec<&ast::TypeRef> = values
851        .iter()
852        .flat_map(|value| value_types(&value.item))
853        .chain(
854            components
855                .iter()
856                .flat_map(|component| component.item.params.iter().map(|param| &param.ty)),
857        )
858        .collect();
859    add_framework_types(&mut base, &mentioned);
860    let types = merge_ui_types(
861        &base,
862        &values,
863        (&library.languages, library.strict),
864        &mut diagnostics,
865    );
866    let mut signatures = BTreeMap::new();
867    for component in &components {
868        let mut params = Vec::new();
869        for param in &component.item.params {
870            match param_kind(param, None, None, None, &[], &types) {
871                Ok(kind) => {
872                    check_content_param(param, &kind, &component.file, &mut diagnostics);
873                    params.push((param.name.to_string(), kind));
874                }
875                Err(message) => diagnostics.push(
876                    Diagnostic::error(format!(
877                        "{message}; library components take value parameters only"
878                    ))
879                    .at(location(&component.file, param.span)),
880                ),
881            }
882        }
883        let name = component.item.name.to_string();
884        signatures.insert(
885            name.clone(),
886            Signature {
887                name,
888                module: component.module.clone(),
889                params,
890                content: content_params(&component.item),
891            },
892        );
893    }
894    let protocol = ProtocolDescriptor {
895        modules: Vec::new(),
896        events: Vec::new(),
897        descriptor_version: bern_protocol::DESCRIPTOR_VERSION,
898        namespace: "library".into(),
899        version: 0,
900        types: TypeTable::default(),
901        state: vec![],
902        commands: vec![],
903    };
904    let no_contracts = BTreeMap::new();
905    let mut checker = Checker::new(
906        &types,
907        &protocol,
908        &[],
909        &signatures,
910        &[],
911        &[],
912        &library.languages,
913        &library.default_language,
914        library.strict,
915        &no_contracts,
916        None,
917    );
918    let mut lowered = Vec::new();
919    for component in &components {
920        let name = component.item.name.to_string();
921        let signature = &signatures[&name];
922        if let Some(body) = checker.component(&component.file, &component.item, signature) {
923            lowered.push(ir::ComponentDef {
924                name,
925                module: component.module.clone(),
926                params: signature
927                    .params
928                    .iter()
929                    .map(|(name, kind)| ir::ParamDef {
930                        name: name.clone(),
931                        kind: kind.clone(),
932                        content: signature.content.contains(name),
933                    })
934                    .collect(),
935                body,
936                source: location(&component.file, component.item.span),
937            });
938        }
939    }
940    diagnostics.append(&mut checker.diagnostics);
941    if diagnostics.iter().any(Diagnostic::is_error) {
942        return Err(diagnostics);
943    }
944    let mut texts = std::mem::take(&mut checker.texts);
945    for def in &types.enums {
946        texts.extend(
947            def.variants
948                .iter()
949                .filter_map(|variant| variant.label.clone()),
950        );
951    }
952    let mut files: Vec<&std::path::Path> = vec![&library.manifest];
953    files.extend(library.sources.iter().map(|source| source.path.as_path()));
954    Ok(ir::ComponentLibrary {
955        descriptor_version: ir::LIBRARY_DESCRIPTOR_VERSION,
956        ir_version: IR_VERSION,
957        package: library.package.name.clone(),
958        version: library.version.clone(),
959        source_fingerprint: source_fingerprint(&files),
960        languages: library.languages.clone(),
961        types,
962        components: lowered,
963        texts,
964    })
965}
966
967/// Check that a library descriptor matches this application: versions and
968/// translations for every language the application requires.
969pub fn check_library(library: &ir::ComponentLibrary, config: &Config) -> Vec<Diagnostic> {
970    let mut diagnostics = Vec::new();
971    if library.descriptor_version != ir::LIBRARY_DESCRIPTOR_VERSION
972        || library.ir_version != IR_VERSION
973    {
974        diagnostics.push(Diagnostic::error(format!(
975            "library `{}` was described with descriptor {} / IR {}; this compiler uses {} / {}",
976            library.package,
977            library.descriptor_version,
978            library.ir_version,
979            ir::LIBRARY_DESCRIPTOR_VERSION,
980            IR_VERSION
981        )));
982    }
983    if config.strict {
984        for text in &library.texts {
985            let missing: Vec<_> = config
986                .languages
987                .iter()
988                .filter(|language| !text.translations.contains_key(*language))
989                .cloned()
990                .collect();
991            if !missing.is_empty() {
992                diagnostics.push(
993                    Diagnostic::error(format!(
994                        "Missing translation\nLibrary: {} {}\nText: {}\nMissing language: {}\nRequired languages: {}",
995                        library.package,
996                        library.version,
997                        text.key,
998                        missing.join(", "),
999                        config.languages.join(", ")
1000                    ))
1001                    .code("B102"),
1002                );
1003            }
1004        }
1005    }
1006    diagnostics
1007}
1008
1009/// One `#[bern::protocol]` module and its descriptor, lowered by the
1010/// shared code.
1011struct ProtocolSource {
1012    /// Path of the protocol module inside its file.
1013    path: Vec<String>,
1014    item: ast::ProtocolItem,
1015    descriptor: ProtocolDescriptor,
1016}
1017
1018impl ProtocolSource {
1019    /// Rust name of the protocol module.
1020    fn name(&self) -> String {
1021        self.item.module.ident.to_string()
1022    }
1023
1024    /// Rust path of the protocol module, declared in `file` of `package`.
1025    fn rust_path(&self, package: &config::Package, file: &SourceFile) -> String {
1026        let mut path = vec![package.crate_name()];
1027        path.extend(file.module.iter().cloned());
1028        path.extend(self.path.iter().cloned());
1029        path.push(self.name());
1030        path.join("::")
1031    }
1032}
1033
1034/// Read a protocol source, which holds exactly one protocol module; a
1035/// module's protocol may use the value types `imported` from the modules
1036/// it depends on.
1037fn read_protocol(
1038    file: &SourceFile,
1039    modules: &bern_syntax::build::Modules,
1040    imported: &TypeTable,
1041) -> Result<ProtocolSource, Vec<Diagnostic>> {
1042    let items = read_selected(file, modules)?;
1043    let Ok([(path, item)]) = <[_; 1]>::try_from(items.protocols) else {
1044        return Err(vec![Diagnostic::error(format!(
1045            "{} must contain exactly one #[bern::protocol] module",
1046            file.display
1047        ))]);
1048    };
1049    let descriptor = bern_syntax::lower::protocol_descriptor_with(&item, imported)
1050        .map_err(|error| syn_error(&file.display, error))?;
1051    Ok(ProtocolSource {
1052        path,
1053        item,
1054        descriptor,
1055    })
1056}
1057
1058/// Check the translations of the variant labels a protocol declares.
1059fn check_protocol_labels(
1060    item: &ast::ProtocolItem,
1061    file: &str,
1062    config: &Config,
1063    diagnostics: &mut Vec<Diagnostic>,
1064) {
1065    let languages = Some((config.languages.as_slice(), config.strict));
1066    for value in &item.values {
1067        if let ValueKind::Enum(variants) = &value.kind {
1068            for tr in variants.iter().filter_map(|variant| variant.label.as_ref()) {
1069                for (span, message) in bern_syntax::lower::check_languages(tr, languages) {
1070                    diagnostics.push(shared_diagnostic(file, span, &message));
1071                }
1072            }
1073        }
1074    }
1075}
1076
1077/// The application's protocol composed with its modules' into one
1078/// catalog, and the Rust paths of their protocol modules.
1079struct Protocols {
1080    protocol: ProtocolDescriptor,
1081    clients: Clients,
1082    /// Rust path of the application's protocol module.
1083    module: String,
1084    /// Rust paths of the modules' protocol modules, in application order.
1085    modules: Vec<String>,
1086}
1087
1088/// Read the protocols of the application and its modules and compose
1089/// them. A protocol source that cannot be read stops compilation with its
1090/// own errors alone; a catalog that does not compose stops it with
1091/// everything reported so far.
1092fn compose_protocols(
1093    config: &Config,
1094    diagnostics: &mut Vec<Diagnostic>,
1095) -> Result<Protocols, Vec<Diagnostic>> {
1096    let protocol_file = &config.protocol_source;
1097    let shell = read_protocol(protocol_file, &config.build_modules, &TypeTable::default())?;
1098    check_protocol_labels(&shell.item, &protocol_file.display, config, diagnostics);
1099    let mut clients = Clients {
1100        protocols: vec![(shell.name(), shell.descriptor.namespace.clone())],
1101    };
1102    let mut module_descriptors = Vec::new();
1103    let mut module_protocols = Vec::new();
1104    // The value types of each module read so far, its own and those it
1105    // took from the modules it depends on, which come before it.
1106    let mut module_types: BTreeMap<&str, TypeTable> = BTreeMap::new();
1107    for module in &config.modules {
1108        let file = &module.protocol_source;
1109        let mut imported = TypeTable::default();
1110        for dependency in &module.depends {
1111            if let Some(types) = module_types.get(dependency.as_str())
1112                && let Err(error) = imported.merge(types)
1113            {
1114                diagnostics.push(Diagnostic::error(format!(
1115                    "{}: the modules `{}` depends on declare one type in two shapes: {}",
1116                    file.display, module.id, error.message
1117                )));
1118            }
1119        }
1120        let source = read_protocol(file, &config.build_modules, &imported)?;
1121        module_types.insert(&module.id, source.descriptor.types.clone());
1122        if source.descriptor.namespace != module.id {
1123            diagnostics.push(Diagnostic::error(format!(
1124                "{}: the protocol of module `{}` must use `namespace = \"{}\"`, not `{}`",
1125                file.display, module.id, module.id, source.descriptor.namespace
1126            )));
1127        }
1128        check_protocol_labels(&source.item, &file.display, config, diagnostics);
1129        let name = source.name();
1130        if clients.protocols.iter().any(|(other, _)| *other == name) {
1131            diagnostics.push(Diagnostic::error(format!(
1132                "{}: protocol module `{name}` has the name of another protocol module; name it after the module",
1133                file.display
1134            )));
1135        }
1136        clients
1137            .protocols
1138            .push((name, source.descriptor.namespace.clone()));
1139        module_protocols.push(source.rust_path(&module.protocol_package, file));
1140        module_descriptors.push(source.descriptor);
1141    }
1142    check_event_routes(config, &shell.descriptor, &module_descriptors, diagnostics);
1143    let module = shell.rust_path(&config.protocol_package, protocol_file);
1144    let protocol = if module_descriptors.is_empty() {
1145        shell.descriptor
1146    } else {
1147        let parts: Vec<bern_protocol::ModulePart<'_>> = module_descriptors
1148            .iter()
1149            .zip(&config.modules)
1150            .map(|(descriptor, module)| bern_protocol::ModulePart {
1151                descriptor,
1152                optional: module.optional,
1153            })
1154            .collect();
1155        match compose(&shell.descriptor, &parts) {
1156            Ok(protocol) => protocol,
1157            Err(error) => {
1158                diagnostics.push(Diagnostic::error(error.0));
1159                return Err(std::mem::take(diagnostics));
1160            }
1161        }
1162    };
1163    Ok(Protocols {
1164        protocol,
1165        clients,
1166        module,
1167        modules: module_protocols,
1168    })
1169}
1170
1171/// A module answers only the events of modules it depends on; the
1172/// application answers any module's events, but not its own.
1173fn check_event_routes(
1174    config: &Config,
1175    shell: &ProtocolDescriptor,
1176    modules: &[ProtocolDescriptor],
1177    diagnostics: &mut Vec<Diagnostic>,
1178) {
1179    for (descriptor, module) in modules.iter().zip(&config.modules) {
1180        for command in &descriptor.commands {
1181            let Some(on) = &command.on else { continue };
1182            let emitter = on.split('.').next().unwrap_or_default();
1183            if !module
1184                .depends
1185                .iter()
1186                .any(|dependency| dependency == emitter)
1187            {
1188                diagnostics.push(Diagnostic::error(format!(
1189                    "{}: `{}` answers `{on}`, but module `{}` does not depend on `{emitter}`; add it to `depends`",
1190                    module.protocol_source.display, command.id, module.id
1191                )));
1192            }
1193        }
1194    }
1195    for command in &shell.commands {
1196        if let Some(on) = &command.on
1197            && on.split('.').next() == Some(shell.namespace.as_str())
1198        {
1199            diagnostics.push(Diagnostic::error(format!(
1200                "{}: `{}` answers the application's own event `{on}`; events reach the modules and the application above the emitter",
1201                config.protocol_source.display, command.id
1202            )));
1203        }
1204    }
1205}
1206
1207/// Declarations of the UI sources of the application and its modules.
1208#[derive(Default)]
1209struct Sources {
1210    values: Vec<Located<ast::ValueItem>>,
1211    sessions: Vec<Located<ast::SessionItem>>,
1212    players: Vec<Located<ast::PlayersItem>>,
1213    components: Vec<Located<ast::ComponentItem>>,
1214    features: Vec<Located<ast::FeaturesItem>>,
1215    /// The application's `style!` items, with the file declaring each.
1216    styles: Vec<(String, ast::StyleItem)>,
1217}
1218
1219/// Collect the declarations of every UI source: the application's, whose
1220/// namespace is `namespace`, then each module's. A source that cannot be
1221/// read is reported and skipped.
1222fn collect_sources(config: &Config, namespace: &str, diagnostics: &mut Vec<Diagnostic>) -> Sources {
1223    let mut sources = Sources::default();
1224    let files = config
1225        .ui_sources
1226        .iter()
1227        .map(|file| (file, namespace.to_string()))
1228        .chain(config.modules.iter().flat_map(|module| {
1229            module
1230                .ui_sources
1231                .iter()
1232                .map(move |file| (file, module.id.clone()))
1233        }));
1234    for (file, home) in files {
1235        let items = match read_selected(file, &config.build_modules) {
1236            Ok(items) => items,
1237            Err(errors) => {
1238                diagnostics.extend(errors);
1239                continue;
1240            }
1241        };
1242        if !items.protocols.is_empty() {
1243            diagnostics.push(Diagnostic::error(format!(
1244                "{} declares a protocol; protocols live in the [protocol] source",
1245                file.display
1246            )));
1247        }
1248        sources.values.extend(locate(file, &home, items.values));
1249        sources.sessions.extend(locate(file, &home, items.sessions));
1250        sources.players.extend(locate(file, &home, items.players));
1251        sources
1252            .components
1253            .extend(locate(file, &home, items.components));
1254        for (_, item) in items.styles {
1255            if home == namespace {
1256                sources.styles.push((file.display.clone(), item));
1257            } else {
1258                diagnostics.push(Diagnostic::error(format!(
1259                    "{}: modules cannot declare `style!`; the application's interface declares one for every module",
1260                    file.display
1261                )));
1262            }
1263        }
1264        sources.features.extend(locate(file, &home, items.features));
1265    }
1266    sources
1267}
1268
1269/// The `items` of `file`, a source of the application or module whose
1270/// namespace is `home`, each with the Rust module declaring it.
1271fn locate<T>(
1272    file: &SourceFile,
1273    home: &str,
1274    items: Vec<(Vec<String>, T)>,
1275) -> impl Iterator<Item = Located<T>> {
1276    items.into_iter().map(move |(path, item)| {
1277        let mut module = file.module.clone();
1278        module.extend(path);
1279        Located {
1280            file: file.display.clone(),
1281            module,
1282            home: home.to_string(),
1283            item,
1284        }
1285    })
1286}
1287
1288/// The handle name and players of the `#[bern::players]` catalog; an
1289/// application declares at most one.
1290fn player_catalog(
1291    declared: &[Located<ast::PlayersItem>],
1292    diagnostics: &mut Vec<Diagnostic>,
1293) -> (Option<String>, Vec<ir::PlayerDef>) {
1294    for extra in declared.iter().skip(1) {
1295        diagnostics.push(
1296            Diagnostic::error(
1297                "an application declares at most one #[bern::players] catalog; add players as fields",
1298            )
1299            .at(location(&extra.file, extra.item.span)),
1300        );
1301    }
1302    let Some(catalog) = declared.first() else {
1303        return (None, Vec::new());
1304    };
1305    let players = catalog
1306        .item
1307        .players
1308        .iter()
1309        .map(|player| ir::PlayerDef {
1310            id: player.name.to_string(),
1311            doc: player.doc.clone(),
1312        })
1313        .collect();
1314    (Some(catalog.item.name.to_string()), players)
1315}
1316
1317/// Component libraries: versioned descriptors generated from their
1318/// sources and checked against this application's configuration.
1319fn load_libraries(config: &Config, diagnostics: &mut Vec<Diagnostic>) -> Vec<ir::ComponentLibrary> {
1320    let mut libraries = Vec::new();
1321    for manifest in &config.libraries {
1322        match config::load_library(manifest).and_then(|library| compile_library(&library)) {
1323            Ok(library) => {
1324                diagnostics.extend(check_library(&library, config));
1325                libraries.push(library);
1326            }
1327            Err(errors) => diagnostics.extend(errors),
1328        }
1329    }
1330    libraries
1331}
1332
1333/// The application's type table: the protocol's types, the libraries'
1334/// and those the UI sources declare, with the framework types they use.
1335fn application_types(
1336    protocol: &ProtocolDescriptor,
1337    libraries: &[ir::ComponentLibrary],
1338    sources: &Sources,
1339    config: &Config,
1340    diagnostics: &mut Vec<Diagnostic>,
1341) -> TypeTable {
1342    let mut base_types = protocol.types.clone();
1343    for library in libraries {
1344        if let Err(error) = base_types.merge(&library.types) {
1345            diagnostics.push(Diagnostic::error(format!(
1346                "library `{}`: {}",
1347                library.package, error.message
1348            )));
1349        }
1350    }
1351    let mentioned: Vec<&ast::TypeRef> = sources
1352        .values
1353        .iter()
1354        .flat_map(|value| value_types(&value.item))
1355        .chain(
1356            sources
1357                .sessions
1358                .iter()
1359                .flat_map(|session| session.item.fields.iter().map(|field| &field.ty)),
1360        )
1361        .chain(
1362            sources
1363                .components
1364                .iter()
1365                .flat_map(|component| component.item.params.iter().map(|param| &param.ty)),
1366        )
1367        .collect();
1368    add_framework_types(&mut base_types, &mentioned);
1369    merge_ui_types(
1370        &base_types,
1371        &sources.values,
1372        (&config.languages, config.strict),
1373        diagnostics,
1374    )
1375}
1376
1377/// What the modules contribute to the application's slots. The enums
1378/// marked `pages = "slot"` gain a variant for every page the modules add
1379/// to that slot.
1380fn module_contributions(
1381    config: &Config,
1382    namespace: &str,
1383    values: &[Located<ast::ValueItem>],
1384    types: &mut TypeTable,
1385    diagnostics: &mut Vec<Diagnostic>,
1386) -> Contributions {
1387    let mut page_enums: BTreeMap<String, String> = BTreeMap::new();
1388    for value in values {
1389        if let Some(slot) = &value.item.pages
1390            && let Some(other) = page_enums.insert(slot.clone(), value.item.name.to_string())
1391        {
1392            diagnostics.push(
1393                Diagnostic::error(format!(
1394                    "page slot `{slot}` is already selected by `{other}`; one enum per page slot"
1395                ))
1396                .at(location(&value.file, value.item.span)),
1397            );
1398        }
1399    }
1400    let mut starts: BTreeMap<String, (String, String)> = BTreeMap::new();
1401    let mut contributions = Contributions {
1402        namespaces: std::iter::once(namespace.to_string())
1403            .chain(config.modules.iter().map(|module| module.id.clone()))
1404            .collect(),
1405        optional: config
1406            .modules
1407            .iter()
1408            .filter(|module| module.optional)
1409            .map(|module| module.id.clone())
1410            .collect(),
1411        ..Contributions::default()
1412    };
1413    for module in &config.modules {
1414        for page in &module.pages {
1415            let Some(enum_name) = page_enums.get(&page.slot) else {
1416                diagnostics.push(Diagnostic::error(format!(
1417                    "module `{}` adds page `{}` to slot `{}`, but no enum in the application's UI is marked `#[bern::value(pages = \"{}\")]`",
1418                    module.id, page.id, page.slot, page.slot
1419                )));
1420                continue;
1421            };
1422            let variant = ast::pascal_case(&page.id);
1423            let Some(def) = types.enums.iter_mut().find(|def| &def.name == enum_name) else {
1424                continue;
1425            };
1426            if def.variant_index(&variant).is_some() {
1427                diagnostics.push(Diagnostic::error(format!(
1428                    "module `{}` adds page `{}`, but `{enum_name}` already has a variant `{variant}`",
1429                    module.id, page.id
1430                )));
1431                continue;
1432            }
1433            if config.strict {
1434                for language in &config.languages {
1435                    if !page.label.contains_key(language) {
1436                        diagnostics.push(
1437                            Diagnostic::error(format!(
1438                                "Missing translation\nProperty: label of page `{}` in {}\nMissing language: {language}",
1439                                page.id,
1440                                module.manifest.display()
1441                            ))
1442                            .code("B102"),
1443                        );
1444                    }
1445                }
1446            }
1447            let added = VariantDef {
1448                name: variant.clone(),
1449                fields: Vec::new(),
1450                label: Some(LocalizedText {
1451                    key: bern_protocol::label_key(enum_name, &variant),
1452                    translations: page.label.clone(),
1453                    params: Vec::new(),
1454                }),
1455            };
1456            // A selection starts on the enum's first variant.
1457            if page.start {
1458                if let Some(first) =
1459                    starts.insert(enum_name.clone(), (module.id.clone(), page.id.clone()))
1460                {
1461                    diagnostics.push(Diagnostic::error(format!(
1462                        "pages `{}` of module `{}` and `{}` of module `{}` both start slot `{}`; one page per slot can",
1463                        first.1, first.0, page.id, module.id, page.slot
1464                    )));
1465                }
1466                def.variants.insert(0, added);
1467            } else {
1468                def.variants.push(added);
1469            }
1470            contributions
1471                .pages
1472                .entry(page.slot.clone())
1473                .or_insert_with(|| PageSlot {
1474                    enum_name: enum_name.clone(),
1475                    pages: Vec::new(),
1476                })
1477                .pages
1478                .push(ContributedPage {
1479                    module: module.id.clone(),
1480                    variant,
1481                    label: page.label.clone(),
1482                    icon: page.icon.clone(),
1483                    badge: page.badge.clone(),
1484                    when: page.when.clone(),
1485                    tab: page.tab,
1486                    component: page.component.clone(),
1487                });
1488        }
1489        for (slot, placed) in &module.contributes {
1490            for component in placed {
1491                contributions
1492                    .slots
1493                    .entry(slot.clone())
1494                    .or_default()
1495                    .push((module.id.clone(), component.clone()));
1496            }
1497        }
1498    }
1499    // The application's own contributions come first, and always show.
1500    for (slot, placed) in config.contributes.iter().rev() {
1501        let entries = contributions.slots.entry(slot.clone()).or_default();
1502        for component in placed.iter().rev() {
1503            entries.insert(0, (namespace.to_string(), component.clone()));
1504        }
1505    }
1506    contributions
1507}
1508
1509/// Lower the `#[bern::session]` structs, with each field's initial value.
1510fn lower_sessions(
1511    declared: &[Located<ast::SessionItem>],
1512    types: &TypeTable,
1513    diagnostics: &mut Vec<Diagnostic>,
1514) -> Vec<ir::SessionDef> {
1515    let mut sessions = Vec::new();
1516    for located in declared {
1517        let session = &located.item;
1518        let name = session.name.to_string();
1519        if sessions.iter().any(|def: &ir::SessionDef| def.name == name)
1520            || types.enum_def(&name).is_some()
1521            || types.record_def(&name).is_some()
1522        {
1523            diagnostics.push(
1524                Diagnostic::error(format!("type `{name}` is declared more than once"))
1525                    .at(location(&located.file, session.span)),
1526            );
1527            continue;
1528        }
1529        let mut fields = Vec::new();
1530        for field in &session.fields {
1531            let ty = match resolve_type(&field.ty, types) {
1532                Ok(ty) => ty,
1533                Err(message) => {
1534                    diagnostics
1535                        .push(Diagnostic::error(message).at(location(&located.file, field.span)));
1536                    continue;
1537                }
1538            };
1539            let initial = match &field.initial {
1540                Some(expr) => match initial_value(expr, &ty, types) {
1541                    Ok(value) => value,
1542                    Err(message) => {
1543                        diagnostics.push(
1544                            Diagnostic::error(message).at(location(&located.file, field.span)),
1545                        );
1546                        continue;
1547                    }
1548                },
1549                None => Value::default_for(&ty, types).unwrap_or(Value::Unit),
1550            };
1551            fields.push(ir::SessionFieldDef {
1552                name: field.name.to_string(),
1553                ty,
1554                initial,
1555            });
1556        }
1557        sessions.push(ir::SessionDef { name, fields });
1558    }
1559    sessions
1560}
1561
1562/// The window handle name and window ids of the root component, whose
1563/// body is a `WindowSet`. `None`, once reported, when the root is not
1564/// declared or its body is something else.
1565fn root_window_set(
1566    config: &Config,
1567    components: &[Located<ast::ComponentItem>],
1568    diagnostics: &mut Vec<Diagnostic>,
1569) -> Option<(String, Vec<String>)> {
1570    let Some(root) = components
1571        .iter()
1572        .find(|component| component.item.name == config.root)
1573    else {
1574        diagnostics.push(Diagnostic::error(format!(
1575            "root component `{}` from [ui].root is not declared in the listed sources",
1576            config.root
1577        )));
1578        return None;
1579    };
1580    let Some(windows) = root_windows(&root.item) else {
1581        diagnostics.push(
1582            Diagnostic::error(
1583                "the root component's body must be a `WindowSet(handle: …, primary: …)`",
1584            )
1585            .at(location(&root.file, root.item.span)),
1586        );
1587        return None;
1588    };
1589    Some(windows)
1590}
1591
1592/// The signature of every component, the libraries' first. `kind`
1593/// resolves a parameter, given the namespace of the application or
1594/// module declaring the component.
1595fn component_signatures(
1596    libraries: &[ir::ComponentLibrary],
1597    components: &[Located<ast::ComponentItem>],
1598    kind: impl Fn(&ast::Param, &str) -> Result<ir::ParamKind, String>,
1599    diagnostics: &mut Vec<Diagnostic>,
1600) -> BTreeMap<String, Signature> {
1601    let mut signatures = BTreeMap::new();
1602    for component in libraries.iter().flat_map(|library| &library.components) {
1603        signatures.insert(
1604            component.name.clone(),
1605            Signature {
1606                name: component.name.clone(),
1607                module: component.module.clone(),
1608                params: component
1609                    .params
1610                    .iter()
1611                    .map(|param| (param.name.clone(), param.kind.clone()))
1612                    .collect(),
1613                content: component
1614                    .params
1615                    .iter()
1616                    .filter(|param| param.content)
1617                    .map(|param| param.name.clone())
1618                    .collect(),
1619            },
1620        );
1621    }
1622    for component in components {
1623        let name = component.item.name.to_string();
1624        if signatures.contains_key(&name) {
1625            diagnostics.push(
1626                Diagnostic::error(format!(
1627                    "component `{name}` is declared twice; component names are unique per application"
1628                ))
1629                .at(location(&component.file, component.item.span)),
1630            );
1631            continue;
1632        }
1633        let mut params = Vec::new();
1634        for param in &component.item.params {
1635            match kind(param, &component.home) {
1636                Ok(kind) => {
1637                    check_content_param(param, &kind, &component.file, diagnostics);
1638                    params.push((param.name.to_string(), kind));
1639                }
1640                Err(message) => diagnostics
1641                    .push(Diagnostic::error(message).at(location(&component.file, param.span))),
1642            }
1643        }
1644        signatures.insert(
1645            name.clone(),
1646            Signature {
1647                name,
1648                module: component.module.clone(),
1649                params,
1650                content: content_params(&component.item),
1651            },
1652        );
1653    }
1654    signatures
1655}
1656
1657/// Every component the application places is declared by its own UI
1658/// sources, and every component a module places by that module's.
1659fn check_placed_components(
1660    config: &Config,
1661    namespace: &str,
1662    components: &[Located<ast::ComponentItem>],
1663    diagnostics: &mut Vec<Diagnostic>,
1664) {
1665    for component in config.contributes.values().flatten() {
1666        let home = components
1667            .iter()
1668            .find(|located| located.item.name == component.as_str())
1669            .map(|located| located.home.as_str());
1670        if home != Some(namespace) {
1671            diagnostics.push(Diagnostic::error(format!(
1672                "the application places component `{component}`, which its UI sources do not declare"
1673            )));
1674        }
1675    }
1676    for module in &config.modules {
1677        let placed = module
1678            .pages
1679            .iter()
1680            .map(|page| &page.component)
1681            .chain(module.contributes.values().flatten());
1682        for component in placed {
1683            let home = components
1684                .iter()
1685                .find(|located| located.item.name == component.as_str())
1686                .map(|located| located.home.as_str());
1687            if home != Some(module.id.as_str()) {
1688                diagnostics.push(Diagnostic::error(format!(
1689                    "module `{}` places component `{component}`, which its UI sources do not declare",
1690                    module.id
1691                )));
1692            }
1693        }
1694    }
1695}
1696
1697/// The feature names of each `#[bern::features]` contract, by contract.
1698/// A contract's name is unique among contracts and components.
1699fn declare_contracts(
1700    declared: &[Located<ast::FeaturesItem>],
1701    signatures: &BTreeMap<String, Signature>,
1702    diagnostics: &mut Vec<Diagnostic>,
1703) -> BTreeMap<String, Vec<String>> {
1704    let mut contract_features: BTreeMap<String, Vec<String>> = BTreeMap::new();
1705    for located in declared {
1706        let name = located.item.name.to_string();
1707        if contract_features.contains_key(&name) || signatures.contains_key(&name) {
1708            diagnostics.push(
1709                Diagnostic::error(format!("contract `{name}` is declared more than once"))
1710                    .at(location(&located.file, located.item.span)),
1711            );
1712            continue;
1713        }
1714        contract_features.insert(
1715            name,
1716            located
1717                .item
1718                .features
1719                .iter()
1720                .map(|feature| feature.name.to_string())
1721                .collect(),
1722        );
1723    }
1724    contract_features
1725}
1726
1727/// The component each contract scopes, by contract: every contract
1728/// scopes exactly one component, which names it in `features =`.
1729fn scope_contracts(
1730    sources: &Sources,
1731    contract_features: &BTreeMap<String, Vec<String>>,
1732    diagnostics: &mut Vec<Diagnostic>,
1733) -> BTreeMap<String, String> {
1734    let mut contract_scopes: BTreeMap<String, String> = BTreeMap::new();
1735    for component in &sources.components {
1736        let Some(path) = &component.item.features else {
1737            continue;
1738        };
1739        let contract = path
1740            .segments
1741            .last()
1742            .map(|segment| segment.ident.to_string())
1743            .unwrap_or_default();
1744        let span = path.segments.last().map(|segment| segment.ident.span());
1745        let at = location(&component.file, span.unwrap_or(component.item.span));
1746        if !contract_features.contains_key(&contract) {
1747            diagnostics.push(
1748                Diagnostic::error(format!(
1749                    "`{contract}` is not a `#[bern::features]` contract in the listed sources"
1750                ))
1751                .at(at),
1752            );
1753        } else if let Some(other) = contract_scopes.get(&contract) {
1754            diagnostics.push(
1755                Diagnostic::error(format!(
1756                    "contract `{contract}` already scopes component `{other}`; a contract has one scope"
1757                ))
1758                .at(at),
1759            );
1760        } else {
1761            contract_scopes.insert(contract, component.item.name.to_string());
1762        }
1763    }
1764    for located in &sources.features {
1765        let name = located.item.name.to_string();
1766        if !contract_scopes.contains_key(&name) {
1767            diagnostics.push(
1768                Diagnostic::error(format!(
1769                    "contract `{name}` scopes no component; add `#[component(features = {name})]` to the screen it describes"
1770                ))
1771                .at(location(&located.file, located.item.span)),
1772            );
1773        }
1774    }
1775    contract_scopes
1776}
1777
1778/// Let `checker` check the names components take from `style!`.
1779fn use_style(checker: &mut Checker<'_>, style: &ir::style::StyleSheet) {
1780    checker.variants = style::variants(style);
1781    checker.transitions = style
1782        .transitions
1783        .iter()
1784        .map(|transition| transition.name.clone())
1785        .collect();
1786    checker.tokens = style
1787        .motion
1788        .iter()
1789        .map(|token| token.name.clone())
1790        .collect();
1791    checker.patterns = style
1792        .patterns
1793        .iter()
1794        .map(|pattern| pattern.name.clone())
1795        .collect();
1796    checker.corners = style
1797        .corners
1798        .iter()
1799        .filter(|corner| corner.targets.is_empty())
1800        .map(|corner| (corner.name.clone(), corner.radius))
1801        .collect();
1802}
1803
1804/// Check and lower the application's components, after the libraries'.
1805/// The root's body comes back on its own: it becomes the window catalog,
1806/// not a component.
1807fn lower_components(
1808    checker: &mut Checker<'_>,
1809    config: &Config,
1810    components: &[Located<ast::ComponentItem>],
1811    libraries: &[ir::ComponentLibrary],
1812) -> (Vec<ir::ComponentDef>, Option<ir::Element>) {
1813    let signatures = checker.signatures;
1814    let mut lowered: Vec<ir::ComponentDef> = libraries
1815        .iter()
1816        .flat_map(|library| library.components.clone())
1817        .collect();
1818    let mut root_element = None;
1819    for component in components {
1820        let name = component.item.name.to_string();
1821        let Some(signature) = signatures.get(&name) else {
1822            continue;
1823        };
1824        let Some(body) = checker.component(&component.file, &component.item, signature) else {
1825            continue;
1826        };
1827        if name == config.root {
1828            if let ir::Node::Element(element) = body {
1829                root_element = Some(element);
1830            }
1831            continue;
1832        }
1833        lowered.push(ir::ComponentDef {
1834            name,
1835            module: component.module.clone(),
1836            params: signature
1837                .params
1838                .iter()
1839                .map(|(name, kind)| ir::ParamDef {
1840                    name: name.clone(),
1841                    kind: kind.clone(),
1842                    content: signature.content.contains(name),
1843                })
1844                .collect(),
1845            body,
1846            source: location(&component.file, component.item.span),
1847        });
1848    }
1849    (lowered, root_element)
1850}
1851
1852/// A component that shows a page reads the session holding the selection,
1853/// as if it declared it.
1854fn add_page_sessions(
1855    page_sessions: BTreeSet<(String, String)>,
1856    lowered: &mut [ir::ComponentDef],
1857    signatures: &mut BTreeMap<String, Signature>,
1858) {
1859    for (name, session) in page_sessions {
1860        let kind = ir::ParamKind::Session { session };
1861        if let Some(component) = lowered.iter_mut().find(|component| component.name == name)
1862            && !component.params.iter().any(|param| param.kind == kind)
1863        {
1864            component.params.push(ir::ParamDef {
1865                name: "bern_pages".into(),
1866                kind: kind.clone(),
1867                content: false,
1868            });
1869        }
1870        if let Some(signature) = signatures.get_mut(&name)
1871            && !signature
1872                .params
1873                .iter()
1874                .any(|(_, declared)| *declared == kind)
1875        {
1876            signature.params.push(("bern_pages".into(), kind));
1877        }
1878    }
1879}
1880
1881/// The components each lowered component places. Components cannot place
1882/// each other recursively, and neither they nor the windows of the `root`
1883/// window set place the root.
1884fn component_graph(
1885    components: &[ir::ComponentDef],
1886    root: Option<&ir::Element>,
1887    root_name: &str,
1888    diagnostics: &mut Vec<Diagnostic>,
1889) -> BTreeMap<String, Vec<String>> {
1890    let mut graph = BTreeMap::new();
1891    for component in components {
1892        let mut refs = Vec::new();
1893        component_refs(&component.body, &mut refs);
1894        graph.insert(component.name.clone(), refs);
1895    }
1896    if let Some(cycle) = find_cycle(&graph) {
1897        diagnostics.push(Diagnostic::error(format!(
1898            "components reference each other recursively ({}); recursive structures need a dedicated design",
1899            cycle.join(" → ")
1900        )));
1901    }
1902    let mut windows = Vec::new();
1903    if let Some(root) = root {
1904        component_refs(&ir::Node::Element(root.clone()), &mut windows);
1905    }
1906    if graph
1907        .values()
1908        .chain([&windows])
1909        .any(|refs| refs.iter().any(|name| name == root_name))
1910    {
1911        diagnostics.push(Diagnostic::error(
1912            "the root component cannot be placed by a component or a window",
1913        ));
1914    }
1915    graph
1916}
1917
1918/// The accent and appearance the windows show come from enums whose
1919/// variants name what they show.
1920fn check_accent_and_appearance(
1921    catalog: &ir::WindowCatalog,
1922    root: &ir::Element,
1923    types: &TypeTable,
1924    style: &ir::style::StyleSheet,
1925    diagnostics: &mut Vec<Diagnostic>,
1926) {
1927    if catalog
1928        .palette
1929        .as_ref()
1930        .is_some_and(|value| value.ty != Type::Text)
1931    {
1932        diagnostics.push(
1933            Diagnostic::error("`palette` is a text binding containing semantic color pairs")
1934                .at(root.source.clone()),
1935        );
1936    }
1937    let variants = |expr: &ir::Expr| match &expr.ty {
1938        Type::Enum { name } => types.enum_def(name).map(|def| {
1939            def.variants
1940                .iter()
1941                .map(|v| v.name.clone())
1942                .collect::<Vec<_>>()
1943        }),
1944        _ => None,
1945    };
1946    if let Some(accent) = &catalog.accent {
1947        match variants(accent) {
1948            Some(names) => {
1949                for name in names.iter().filter(|name| style.accent(name).is_none()) {
1950                    diagnostics.push(
1951                        Diagnostic::error(format!(
1952                            "accent `{name}` is not declared; add it to the `accents` block of `style!`"
1953                        ))
1954                        .at(root.source.clone()),
1955                    );
1956                }
1957            }
1958            None => diagnostics.push(
1959                Diagnostic::error("`accent` is an enum whose variants name accents of `style!`")
1960                    .at(root.source.clone()),
1961            ),
1962        }
1963    }
1964    if let Some(appearance) = &catalog.appearance {
1965        let fits = variants(appearance).is_some_and(|names| {
1966            names
1967                .iter()
1968                .all(|name| ["system", "light", "dark"].contains(&name.to_lowercase().as_str()))
1969        });
1970        if !fits {
1971            diagnostics.push(
1972                Diagnostic::error("`appearance` is an enum of `System`, `Light` and `Dark`")
1973                    .at(root.source.clone()),
1974            );
1975        }
1976    }
1977}
1978
1979/// The icon the system shows comes from an enum: `Default` is `[app.icon]`,
1980/// every other variant names an icon of `[app.alternate_icons]`.
1981fn check_icon_choice(
1982    catalog: &ir::WindowCatalog,
1983    root: &ir::Element,
1984    types: &TypeTable,
1985    alternates: &BTreeMap<String, BTreeMap<String, String>>,
1986    diagnostics: &mut Vec<Diagnostic>,
1987) {
1988    let Some(icon) = &catalog.icon else {
1989        return;
1990    };
1991    let variants = match &icon.ty {
1992        Type::Enum { name } => types.enum_def(name).map(|def| {
1993            def.variants
1994                .iter()
1995                .map(|variant| variant.name.clone())
1996                .collect::<Vec<_>>()
1997        }),
1998        _ => None,
1999    };
2000    let Some(variants) = variants else {
2001        diagnostics.push(
2002            Diagnostic::error(
2003                "`icon` is an enum: `Default` shows `[app.icon]`, and every other variant names an icon of `[app.alternate_icons]`",
2004            )
2005            .at(root.source.clone()),
2006        );
2007        return;
2008    };
2009    for variant in variants.iter().filter(|variant| *variant != "Default") {
2010        let name = bern_target_api::icon::alternate_name(variant);
2011        if !alternates.contains_key(&name) {
2012            diagnostics.push(
2013                Diagnostic::error(format!(
2014                    "icon `{variant}` names `[app.alternate_icons.{name}]` in Bern.toml, which is not declared"
2015                ))
2016                .at(root.source.clone()),
2017            );
2018        }
2019    }
2020}
2021
2022/// Lower the feature contracts that scope a component and prove what each
2023/// scope reaches.
2024fn prove_contracts(
2025    declared: &[Located<ast::FeaturesItem>],
2026    contract_scopes: &BTreeMap<String, String>,
2027    components: &[ir::ComponentDef],
2028    windows: Option<&ir::WindowCatalog>,
2029    protocol: &ProtocolDescriptor,
2030    diagnostics: &mut Vec<Diagnostic>,
2031) -> Vec<ir::FeatureContract> {
2032    let mut contracts: Vec<ir::FeatureContract> = declared
2033        .iter()
2034        .filter_map(|located| {
2035            let name = located.item.name.to_string();
2036            let component = contract_scopes.get(&name)?.clone();
2037            Some(features::lower(
2038                &features::Declared {
2039                    file: &located.file,
2040                    item: &located.item,
2041                },
2042                component,
2043                protocol,
2044                diagnostics,
2045            ))
2046        })
2047        .collect();
2048    features::prove(&mut contracts, components, windows, protocol, diagnostics);
2049    contracts
2050}
2051
2052/// Pictures come from declared resources, in library components and in
2053/// the `root` window set too.
2054fn check_resources(
2055    components: &[ir::ComponentDef],
2056    root: Option<&ir::Element>,
2057    resources: &[ir::ResourceDef],
2058    diagnostics: &mut Vec<Diagnostic>,
2059) {
2060    let root = root.cloned().map(ir::Node::Element);
2061    let trees = components
2062        .iter()
2063        .map(|component| &component.body)
2064        .chain(root.as_ref());
2065    for tree in trees {
2066        tree.walk(&mut |node| {
2067            let ir::Node::Element(element) = node else {
2068                return;
2069            };
2070            for prop in element.props.values() {
2071                if let ir::Prop::Resource { name } = prop
2072                    && !resources.iter().any(|resource| &resource.name == name)
2073                {
2074                    diagnostics.push(
2075                        Diagnostic::error(format!(
2076                            "resource `{name}` is not declared; list it under [resources] in Bern.toml"
2077                        ))
2078                        .at(element.source.clone()),
2079                    );
2080                }
2081            }
2082        });
2083    }
2084}
2085
2086/// Every text the application shows: the components', the libraries',
2087/// every variant label and the application's `name`. Each key names one
2088/// text, the first given.
2089fn collect_texts(
2090    mut texts: Vec<LocalizedText>,
2091    libraries: &[ir::ComponentLibrary],
2092    types: &TypeTable,
2093    name: &LocalizedText,
2094) -> Vec<LocalizedText> {
2095    for library in libraries {
2096        texts.extend(library.texts.iter().cloned());
2097    }
2098    // Every label is a resource.
2099    for def in &types.enums {
2100        for variant in &def.variants {
2101            if let Some(label) = &variant.label {
2102                texts.push(label.clone());
2103            }
2104        }
2105    }
2106    texts.push(name.clone());
2107    let mut keys = BTreeMap::new();
2108    texts.retain(|text| keys.insert(text.key.clone(), text.clone()).is_none());
2109    texts
2110}
2111
2112/// Collect, check and lower a project to IR.
2113pub fn compile(config: &Config) -> Result<Compiled, Vec<Diagnostic>> {
2114    let mut diagnostics = Vec::new();
2115
2116    // Declarations: protocols, UI sources, libraries, types and state.
2117    let Protocols {
2118        protocol,
2119        clients,
2120        module: protocol_module,
2121        modules: module_protocols,
2122    } = compose_protocols(config, &mut diagnostics)?;
2123    let sources = collect_sources(config, &protocol.namespace, &mut diagnostics);
2124    let (players_handle, players) = player_catalog(&sources.players, &mut diagnostics);
2125    let player_ids: Vec<String> = players.iter().map(|player| player.id.clone()).collect();
2126    let libraries = load_libraries(config, &mut diagnostics);
2127    let mut types = application_types(&protocol, &libraries, &sources, config, &mut diagnostics);
2128    let contributions = module_contributions(
2129        config,
2130        &protocol.namespace,
2131        &sources.values,
2132        &mut types,
2133        &mut diagnostics,
2134    );
2135    let sessions = lower_sessions(&sources.sessions, &types, &mut diagnostics);
2136    let Some((handle, window_ids)) = root_window_set(config, &sources.components, &mut diagnostics)
2137    else {
2138        return Err(diagnostics);
2139    };
2140    let mut signatures = component_signatures(
2141        &libraries,
2142        &sources.components,
2143        |param, home| {
2144            param_kind(
2145                param,
2146                Some((&clients, home)),
2147                Some(&handle),
2148                players_handle.as_deref(),
2149                &sessions,
2150                &types,
2151            )
2152        },
2153        &mut diagnostics,
2154    );
2155    check_placed_components(
2156        config,
2157        &protocol.namespace,
2158        &sources.components,
2159        &mut diagnostics,
2160    );
2161    let contract_features = declare_contracts(&sources.features, &signatures, &mut diagnostics);
2162    let contract_scopes = scope_contracts(&sources, &contract_features, &mut diagnostics);
2163
2164    // Components, checked against those declarations.
2165    let style = style::lower(&sources.styles, &mut diagnostics);
2166    let mut checker = Checker::new(
2167        &types,
2168        &protocol,
2169        &sessions,
2170        &signatures,
2171        &window_ids,
2172        &player_ids,
2173        &config.languages,
2174        &config.default_language,
2175        config.strict,
2176        &contract_features,
2177        config.layouts,
2178    );
2179    checker.contributions = contributions;
2180    use_style(&mut checker, &style);
2181    let (mut lowered, root_element) =
2182        lower_components(&mut checker, config, &sources.components, &libraries);
2183    diagnostics.append(&mut checker.diagnostics);
2184    let texts = std::mem::take(&mut checker.texts);
2185    let page_sessions = std::mem::take(&mut checker.page_sessions);
2186    add_page_sessions(page_sessions, &mut lowered, &mut signatures);
2187
2188    // Checks across the lowered components and the windows they fill.
2189    let graph = component_graph(
2190        &lowered,
2191        root_element.as_ref(),
2192        &config.root,
2193        &mut diagnostics,
2194    );
2195    let windows = root_element
2196        .as_ref()
2197        .and_then(|root| window_catalog(root, handle, &graph, &signatures, &mut diagnostics));
2198    if let (Some(catalog), Some(root)) = (&windows, &root_element) {
2199        check_accent_and_appearance(catalog, root, &types, &style, &mut diagnostics);
2200        check_icon_choice(
2201            catalog,
2202            root,
2203            &types,
2204            &config.alternate_icons,
2205            &mut diagnostics,
2206        );
2207    }
2208    let contracts = prove_contracts(
2209        &sources.features,
2210        &contract_scopes,
2211        &lowered,
2212        windows.as_ref(),
2213        &protocol,
2214        &mut diagnostics,
2215    );
2216    placement::check(&lowered, root_element.as_ref(), &mut diagnostics);
2217    check_resources(
2218        &lowered,
2219        root_element.as_ref(),
2220        &config.resources,
2221        &mut diagnostics,
2222    );
2223
2224    let name = LocalizedText {
2225        key: "app.name".into(),
2226        translations: config.name.clone(),
2227        params: vec![],
2228    };
2229    let texts = collect_texts(texts, &libraries, &types, &name);
2230    if diagnostics.iter().any(Diagnostic::is_error) {
2231        return Err(diagnostics);
2232    }
2233    let Some(windows) = windows else {
2234        return Err(vec![Diagnostic::error(
2235            "the root WindowSet could not be lowered",
2236        )]);
2237    };
2238    Ok(Compiled {
2239        app: ir::Application {
2240            ir_version: IR_VERSION,
2241            app: ir::AppInfo {
2242                id: config.app_id.clone(),
2243                bundle_id: config.bundle_id.clone(),
2244                name,
2245                version: config.version.clone(),
2246                icon: config.icon.clone(),
2247                platform_icons: config.platform_icons.clone(),
2248                alternate_icons: config.alternate_icons.clone(),
2249                publisher: config.publisher.clone(),
2250                files_folder: config.files_folder,
2251                background_audio: config.background_audio,
2252                overlays: config.overlays,
2253                photos: config.photos,
2254            },
2255            languages: ir::Languages {
2256                supported: config.languages.clone(),
2257                default: config.default_language.clone(),
2258                strict: config.strict,
2259            },
2260            theme: ir::Theme {
2261                mode: config.theme_mode,
2262                brand_seed: config.brand_seed,
2263            },
2264            protocol,
2265            types,
2266            sessions,
2267            windows,
2268            players,
2269            components: lowered,
2270            texts,
2271            resources: config.resources.clone(),
2272            layouts: config.layouts,
2273            contracts,
2274            style,
2275        },
2276        protocol_module,
2277        module_protocols,
2278        libraries,
2279        warnings: diagnostics,
2280    })
2281}
2282
2283#[cfg(test)]
2284mod tests;