Skip to main content

bern_syntax/
lower.rs

1//! Lowering of declarations to protocol schema, shared by the macros and
2//! the build tool so both derive identical descriptors.
3
4use crate::{ProtocolItem, TypeRef, ValueKind};
5use bern_protocol::format::{self, Use};
6use bern_protocol::{
7    CommandPolicy, CommandSpec, Concurrency, DESCRIPTOR_VERSION, EnumDef, EventSpec, ExternalNames,
8    FieldDef, LocalizedText, OperationKind, ParamSpec, ProtocolDescriptor, RecordDef, Retry, Scope,
9    StateField, Type, TypeTable, VariantDef,
10};
11use proc_macro2::Span;
12
13/// Resolve a syntax type against a self-contained type table.
14pub fn resolve(ty: &TypeRef, types: &TypeTable, span: Span) -> syn::Result<Type> {
15    Ok(match ty {
16        TypeRef::Unit => Type::Unit,
17        TypeRef::Bool => Type::Bool,
18        TypeRef::Int => Type::Int,
19        TypeRef::Float => Type::Float,
20        TypeRef::Text => Type::Text,
21        TypeRef::Optional(inner) => Type::optional(resolve(inner, types, span)?),
22        TypeRef::List(inner) => Type::list(resolve(inner, types, span)?),
23        TypeRef::Named(path) => {
24            let name = path.last().unwrap();
25            if path.len() > 1 {
26                return Err(syn::Error::new(
27                    span,
28                    format!(
29                        "refer to `{name}` by its name; protocol types are declared in the protocol module"
30                    ),
31                ));
32            }
33            if types.enum_def(name).is_some() {
34                Type::Enum { name: name.clone() }
35            } else if types.record_def(name).is_some() {
36                Type::Record { name: name.clone() }
37            } else {
38                return Err(syn::Error::new(
39                    span,
40                    format!(
41                        "type `{name}` is not declared in this protocol; declare it here \
42                         with `#[value]`, or depend on the module whose protocol declares it"
43                    ),
44                ));
45            }
46        }
47    })
48}
49
50/// Whether text is the whole of a type: text, or optional or listed text.
51pub fn is_text(ty: &Type) -> bool {
52    match ty {
53        Type::Text => true,
54        Type::Optional { inner } | Type::List { inner } => is_text(inner),
55        _ => false,
56    }
57}
58
59/// `#[content]` of a field, which only text takes.
60pub fn content(field: &crate::FieldItem, ty: &Type) -> syn::Result<bool> {
61    if field.content && !is_text(ty) {
62        return Err(syn::Error::new(
63            field.span,
64            format!(
65                "`#[content]` marks text that is data, such as a title; `{}` is {}",
66                field.name,
67                ty.display()
68            ),
69        ));
70    }
71    Ok(field.content)
72}
73
74/// Whether `ty` is the framework's calendar date.
75pub fn is_date(ty: &Type) -> bool {
76    matches!(ty, Type::Record { name } if name == format::CALENDAR_DATE)
77}
78
79/// A variant's label. Its placeholders are the variant's fields, shown as
80/// the label's text says: text that is data (`#[content]`), numbers, enums
81/// by their labels, dates with `{field, date, …}`.
82pub fn label(
83    enum_name: &str,
84    variant: &str,
85    tr: &crate::Tr,
86    fields: &[FieldDef],
87) -> syn::Result<LocalizedText> {
88    let mut params: Vec<String> = Vec::new();
89    for entry in &tr.entries {
90        let pieces =
91            format::parse(&entry.text).map_err(|message| syn::Error::new(entry.span, message))?;
92        for (name, how) in format::uses(&pieces) {
93            let Some(field) = fields.iter().find(|field| field.name == name) else {
94                return Err(syn::Error::new(
95                    entry.span,
96                    format!(
97                        "`{{{name}}}` is not a field of `{enum_name}::{variant}`; a label shows the variant's own fields"
98                    ),
99                ));
100            };
101            let fits = match how {
102                Use::Value => match &field.ty {
103                    Type::Text => {
104                        if !field.content {
105                            return Err(syn::Error::new(
106                                entry.span,
107                                format!(
108                                    "the label of `{enum_name}::{variant}` shows `{name}`, text without translations; mark the field `#[content]` if it is data such as a title, or make it a labelled enum"
109                                ),
110                            ));
111                        }
112                        true
113                    }
114                    Type::Int | Type::Float | Type::Enum { .. } => true,
115                    _ => false,
116                },
117                Use::Plural | Use::Duration => matches!(field.ty, Type::Int | Type::Float),
118                Use::Date => is_date(&field.ty),
119            };
120            if !fits {
121                return Err(syn::Error::new(
122                    entry.span,
123                    format!(
124                        "`{name}` of `{enum_name}::{variant}` is {}, which `{{{name}{}}}` cannot show",
125                        field.ty.display(),
126                        match how {
127                            Use::Value => String::new(),
128                            other => format!(", {}, …", other.name()),
129                        }
130                    ),
131                ));
132            }
133            if !params.contains(&name) {
134                params.push(name);
135            }
136        }
137    }
138    Ok(LocalizedText {
139        key: bern_protocol::label_key(enum_name, variant),
140        translations: tr
141            .entries
142            .iter()
143            .map(|entry| (entry.language.clone(), entry.text.clone()))
144            .collect(),
145        params,
146    })
147}
148
149/// Every type written in a protocol: value fields, state and parameters.
150fn protocol_types(protocol: &ProtocolItem) -> Vec<&TypeRef> {
151    let mut types = Vec::new();
152    for value in &protocol.values {
153        match &value.kind {
154            ValueKind::Enum(variants) => {
155                for variant in variants {
156                    types.extend(variant.fields.iter().map(|field| &field.ty));
157                }
158            }
159            ValueKind::Record(fields) => types.extend(fields.iter().map(|field| &field.ty)),
160        }
161    }
162    types.extend(protocol.state.fields.iter().map(|field| &field.ty));
163    for method in &protocol.commands.methods {
164        types.extend(method.params.iter().map(|param| &param.ty));
165        types.push(&method.result);
166    }
167    for method in protocol.events.iter().flat_map(|events| &events.methods) {
168        types.extend(method.params.iter().map(|param| &param.ty));
169    }
170    for method in protocol.queries.iter().flat_map(|queries| &queries.methods) {
171        types.extend(method.params.iter().map(|param| &param.ty));
172        types.push(&method.result);
173    }
174    types
175}
176
177/// Collect the protocol's types, state and commands into a descriptor.
178pub fn protocol_descriptor(protocol: &ProtocolItem) -> syn::Result<ProtocolDescriptor> {
179    protocol_descriptor_with(protocol, &TypeTable::default())
180}
181
182/// The named types `ty` refers to, in order.
183fn named_types<'a>(ty: &'a Type, names: &mut Vec<&'a str>) {
184    match ty {
185        Type::Record { name } | Type::Enum { name } => names.push(name),
186        Type::Optional { inner } | Type::List { inner } => named_types(inner, names),
187        _ => {}
188    }
189}
190
191/// [`protocol_descriptor`] for the protocol of a module, which may also
192/// use the value types of the protocols of the modules it depends on:
193/// `imported`. Each it uses is copied into its own table, with the types
194/// that one uses in turn, so the descriptor stays self-contained.
195pub fn protocol_descriptor_with(
196    protocol: &ProtocolItem,
197    imported: &TypeTable,
198) -> syn::Result<ProtocolDescriptor> {
199    // Names first, so field types can refer to any declared value.
200    let mut types = TypeTable::default();
201    for value in &protocol.values {
202        for framework in [bern_media::MEDIA_SOURCE, format::CALENDAR_DATE] {
203            if value.name == framework {
204                return Err(syn::Error::new(
205                    value.span,
206                    format!(
207                        "`{framework}` is a framework type; use it from `bern::prelude` instead of declaring it"
208                    ),
209                ));
210            }
211        }
212    }
213    // Framework value types join the table when the protocol uses them.
214    let used = protocol_types(protocol);
215    if used
216        .iter()
217        .any(|ty| crate::mentions(ty, bern_media::MEDIA_SOURCE))
218    {
219        types.records.push(bern_media::media_source_def());
220    }
221    if used
222        .iter()
223        .any(|ty| crate::mentions(ty, format::CALENDAR_DATE))
224    {
225        types.records.push(format::calendar_date_def());
226    }
227    for value in &protocol.values {
228        match &value.kind {
229            ValueKind::Enum(_) => types.enums.push(EnumDef {
230                name: value.name.to_string(),
231                variants: vec![],
232            }),
233            ValueKind::Record(_) => types.records.push(RecordDef {
234                name: value.name.to_string(),
235                fields: vec![],
236            }),
237        }
238    }
239    // The imported types it names, and those they name, after its own.
240    let mut wanted: Vec<String> = Vec::new();
241    for ty in &used {
242        crate::named(ty, &mut |name| wanted.push(name.to_string()));
243    }
244    let mut next = 0;
245    while next < wanted.len() {
246        let name = wanted[next].clone();
247        next += 1;
248        if types.enum_def(&name).is_some() || types.record_def(&name).is_some() {
249            continue;
250        }
251        let mut uses = Vec::new();
252        if let Some(def) = imported.enum_def(&name) {
253            for field in def.variants.iter().flat_map(|variant| &variant.fields) {
254                named_types(&field.ty, &mut uses);
255            }
256            types.enums.push(def.clone());
257        } else if let Some(def) = imported.record_def(&name) {
258            for field in &def.fields {
259                named_types(&field.ty, &mut uses);
260            }
261            types.records.push(def.clone());
262        }
263        wanted.extend(uses.into_iter().map(str::to_string));
264    }
265    let lookup = types.clone();
266    for value in &protocol.values {
267        let name = value.name.to_string();
268        match &value.kind {
269            ValueKind::Enum(variants) => {
270                let mut defs = Vec::new();
271                for variant in variants {
272                    let mut fields = Vec::new();
273                    for field in &variant.fields {
274                        let ty = resolve(&field.ty, &lookup, field.span)?;
275                        fields.push(FieldDef {
276                            name: field.name.to_string(),
277                            content: content(field, &ty)?,
278                            ty,
279                            doc: field.doc.clone(),
280                        });
281                    }
282                    let text = match &variant.label {
283                        Some(tr) => Some(label(&name, &variant.name.to_string(), tr, &fields)?),
284                        None => None,
285                    };
286                    defs.push(VariantDef {
287                        name: variant.name.to_string(),
288                        fields,
289                        label: text,
290                    });
291                }
292                types
293                    .enums
294                    .iter_mut()
295                    .find(|def| def.name == name)
296                    .unwrap()
297                    .variants = defs;
298            }
299            ValueKind::Record(fields) => {
300                let mut defs = Vec::new();
301                for field in fields {
302                    let ty = resolve(&field.ty, &lookup, field.span)?;
303                    defs.push(FieldDef {
304                        name: field.name.to_string(),
305                        content: content(field, &ty)?,
306                        ty,
307                        doc: field.doc.clone(),
308                    });
309                }
310                types
311                    .records
312                    .iter_mut()
313                    .find(|def| def.name == name)
314                    .unwrap()
315                    .fields = defs;
316            }
317        }
318    }
319    let mut state = Vec::new();
320    for field in &protocol.state.fields {
321        let ty = resolve(&field.ty, &types, field.span)?;
322        state.push(StateField {
323            name: field.name.to_string(),
324            content: content(field, &ty)?,
325            ty,
326            doc: field.doc.clone(),
327        });
328    }
329    let namespace = protocol.args.namespace.clone();
330    let mut commands = Vec::new();
331    for method in &protocol.commands.methods {
332        let name = method.name.to_string();
333        let mut params = Vec::new();
334        for param in &method.params {
335            params.push(ParamSpec {
336                name: param.name.to_string(),
337                ty: resolve(&param.ty, &types, param.name.span())?,
338                doc: String::new(),
339            });
340        }
341        commands.push(CommandSpec {
342            id: format!("{namespace}.{name}"),
343            external: ExternalNames::derive(&namespace, &name),
344            reads: vec![],
345            on: method
346                .on
347                .as_ref()
348                .map(|(module, event)| format!("{module}.{event}")),
349            name,
350            doc: method.doc.clone(),
351            kind: OperationKind::Command,
352            scope: Scope::Engine,
353            params,
354            result: resolve(&method.result, &types, method.span)?,
355            errors: method.errors.clone(),
356            policy: CommandPolicy {
357                retry: if method.idempotent {
358                    Retry::Idempotent
359                } else {
360                    Retry::Never
361                },
362                ..CommandPolicy::default()
363            },
364        });
365    }
366    // Queries follow the commands in the catalog, so adding one never
367    // renumbers a command.
368    for method in protocol.queries.iter().flat_map(|queries| &queries.methods) {
369        let name = method.name.to_string();
370        let mut params = Vec::new();
371        for param in &method.params {
372            params.push(ParamSpec {
373                name: param.name.to_string(),
374                ty: resolve(&param.ty, &types, param.name.span())?,
375                doc: String::new(),
376            });
377        }
378        let mut reads = Vec::new();
379        for field in &method.reads {
380            if !state.iter().any(|state| *field == state.name) {
381                return Err(syn::Error::new(
382                    field.span(),
383                    format!(
384                        "`{field}` is not a field of the protocol state; `#[reads(…)]` names \
385                         the state fields the query's answer depends on"
386                    ),
387                ));
388            }
389            reads.push(field.to_string());
390        }
391        commands.push(CommandSpec {
392            id: format!("{namespace}.{name}"),
393            external: ExternalNames::derive(&namespace, &name),
394            reads,
395            on: None,
396            name,
397            doc: method.doc.clone(),
398            kind: OperationKind::Query,
399            scope: Scope::Engine,
400            params,
401            result: resolve(&method.result, &types, method.span)?,
402            errors: method.errors.clone(),
403            // Reads can always be repeated; answers may overlap and a
404            // superseded request is cancelled.
405            policy: CommandPolicy {
406                concurrency: Concurrency::Overlapping,
407                retry: Retry::Idempotent,
408                cancellable: true,
409            },
410        });
411    }
412    let mut events = Vec::new();
413    for event in protocol.events.iter().flat_map(|events| &events.methods) {
414        let mut params = Vec::new();
415        for param in &event.params {
416            params.push(ParamSpec {
417                name: param.name.to_string(),
418                ty: resolve(&param.ty, &types, param.name.span())?,
419                doc: String::new(),
420            });
421        }
422        events.push(EventSpec {
423            name: event.name.to_string(),
424            doc: event.doc.clone(),
425            params,
426        });
427    }
428    let descriptor = ProtocolDescriptor {
429        modules: Vec::new(),
430        events,
431        descriptor_version: DESCRIPTOR_VERSION,
432        namespace,
433        version: protocol.args.version,
434        types,
435        state,
436        commands,
437    };
438    descriptor
439        .validate()
440        .map_err(|error| syn::Error::new(protocol.args.span, error.0))?;
441    Ok(descriptor)
442}
443
444/// Problems with one `tr!`: duplicate or unknown language tags, missing
445/// translations in strict mode, plural forms a language lacks, and
446/// placeholder/argument mismatches. `languages` is `(supported, strict)`
447/// when a project configuration exists.
448pub fn check_translations(
449    tr: &crate::Tr,
450    languages: Option<(&[String], bool)>,
451) -> Vec<(Span, String)> {
452    let mut problems = check_languages(tr, languages);
453    problems.extend(check_arguments(tr));
454    problems
455}
456
457/// The language side of [`check_translations`]: tags, missing languages
458/// and the plural forms each language needs. Enum labels, whose
459/// placeholders are their variant's fields, take only these checks.
460pub fn check_languages(
461    tr: &crate::Tr,
462    languages: Option<(&[String], bool)>,
463) -> Vec<(Span, String)> {
464    use std::collections::BTreeSet;
465    let mut problems = Vec::new();
466    let mut seen = BTreeSet::new();
467    for entry in &tr.entries {
468        if !seen.insert(entry.language.as_str()) {
469            problems.push((
470                entry.span,
471                format!(
472                    "error[B101]: language `{}` is translated twice",
473                    entry.language
474                ),
475            ));
476        }
477        if let Some((supported, _)) = languages
478            && !supported.contains(&entry.language)
479        {
480            problems.push((
481                entry.span,
482                format!(
483                    "error[B101]: language tag `{}` is not in [languages].supported ({}); check the spelling",
484                    entry.language,
485                    supported.join(", ")
486                ),
487            ));
488        }
489    }
490    if let Some((supported, true)) = languages {
491        let missing: Vec<_> = supported
492            .iter()
493            .filter(|language| !seen.contains(language.as_str()))
494            .cloned()
495            .collect();
496        if !missing.is_empty() {
497            problems.push((
498                tr.span,
499                format!(
500                    "error[B102]: Missing translation\nMissing language: {}\nRequired languages: {}",
501                    missing.join(", "),
502                    supported.join(", ")
503                ),
504            ));
505        }
506    }
507    for entry in &tr.entries {
508        if let Ok(pieces) = format::parse(&entry.text) {
509            let missing = format::missing_plurals(&pieces, &entry.language);
510            if !missing.is_empty() {
511                problems.push((
512                    entry.span,
513                    format!(
514                        "error[B104]: the `{}` text needs the plural branch {}, a form every number in the language takes",
515                        entry.language,
516                        missing.join(", ")
517                    ),
518                ));
519            }
520        }
521    }
522    problems
523}
524
525/// The argument side of [`check_translations`]: every placeholder has an
526/// argument, every translation uses the same ones, every argument is used.
527fn check_arguments(tr: &crate::Tr) -> Vec<(Span, String)> {
528    use std::collections::BTreeSet;
529    let mut problems = Vec::new();
530    let declared: BTreeSet<String> = tr.args.iter().map(|arg| arg.name.to_string()).collect();
531    let mut used = BTreeSet::new();
532    let mut reference: Option<BTreeSet<String>> = None;
533    for entry in &tr.entries {
534        match LocalizedText::placeholders(&entry.text) {
535            Err(message) => problems.push((entry.span, format!("error[B104]: {message}"))),
536            Ok(names) => {
537                let names: BTreeSet<String> = names.into_iter().collect();
538                for name in &names {
539                    if !declared.contains(name) {
540                        problems.push((
541                            entry.span,
542                            format!(
543                                "error[B104]: placeholder `{{{name}}}` has no argument; add `; {name}: value`"
544                            ),
545                        ));
546                    }
547                }
548                match &reference {
549                    None => reference = Some(names.clone()),
550                    Some(first) if *first != names => problems.push((
551                        entry.span,
552                        "error[B104]: every translation must use the same placeholders".to_string(),
553                    )),
554                    _ => {}
555                }
556                used.extend(names);
557            }
558        }
559    }
560    for arg in &tr.args {
561        if !used.contains(&arg.name.to_string()) {
562            problems.push((
563                arg.name.span(),
564                format!(
565                    "error[B104]: argument `{}` is not used by any translation",
566                    arg.name
567                ),
568            ));
569        }
570    }
571    problems
572}