Skip to main content

tramlines_core/grammar/
format.rs

1//! One line format: its shapes, its bans, and how a line matches it
2//! (TL_42, TL_74 to TL_81).
3
4use regex::Regex;
5
6use super::{RegexCache, Segment, Shape, TypeRegistry, shape_of, template};
7use crate::{Problem, Spanned, text};
8
9/// One shape of a variant format (grammar `format-variant`).
10#[derive(Debug, Clone)]
11pub struct FormatVariant {
12    /// The shape.
13    pub shape: Shape,
14    /// One conforming line, which a failure of this shape shows.
15    pub example: String,
16}
17
18/// A line format (grammar `format`), its shapes compiled against the types
19/// its layer sees.
20#[derive(Debug, Clone)]
21pub struct Format {
22    /// The shape of a format without variants.
23    pub shape: Option<Shape>,
24    /// The named shapes of a variant format, in the order a line tries them.
25    pub variants: Vec<(String, FormatVariant)>,
26    /// The name of the hole or group that captures the line's key.
27    pub key: Option<String>,
28    /// `prohibited-patterns`: what no bound line may match.
29    pub prohibited: Vec<Prohibition>,
30    /// One conforming line of a format without variants.
31    pub example: Option<String>,
32    /// The entry as written, for a printed registry and a layer on top.
33    pub raw: Spanned,
34}
35
36/// A key a shape captured, and the offset where the shape's match began,
37/// which is where the key's anchor goes.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Captured {
40    /// The key.
41    pub key: String,
42    /// The start of the match.
43    pub start: usize,
44}
45
46/// The key `name` that `shape` captures from `text`.
47pub fn captured(shape: &Shape, name: &str, text: &str) -> Option<Captured> {
48    let caps = shape.captures(text)?;
49    Some(Captured {
50        key: caps.name(name)?.as_str().to_string(),
51        start: caps.get(0).map_or(0, |m| m.start()),
52    })
53}
54
55/// The outcome of matching a line against a format.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum FormatMatch {
58    /// The line conforms; its key, if the format captures one.
59    Ok(Option<Captured>),
60    /// The line does not have the shape, with the reason.
61    Fail(String),
62    /// The line has the shape and matches a ban, with the reason.
63    Refused(String),
64}
65
66impl Format {
67    /// Every example the format carries: its own, or each variant's.
68    pub fn examples(&self) -> Vec<&str> {
69        match &self.example {
70            Some(e) => vec![e.as_str()],
71            None => self
72                .variants
73                .iter()
74                .map(|(_, v)| v.example.as_str())
75                .collect(),
76        }
77    }
78
79    /// Each shape of the format: its one shape, unnamed, or each variant's,
80    /// by the variant's name.
81    pub(crate) fn shapes(&self) -> impl Iterator<Item = (Option<&str>, &Shape)> {
82        self.shape.iter().map(|s| (None, s)).chain(
83            self.variants
84                .iter()
85                .map(|(name, v)| (Some(name.as_str()), &v.shape)),
86        )
87    }
88
89    /// How far `text` follows the format before it fails: the furthest
90    /// that any of its shapes reaches.
91    pub fn reach(&self, text: &str) -> usize {
92        self.shapes().map(|(_, s)| s.reach(text)).max().unwrap_or(0)
93    }
94
95    /// Whether a template of the format holds a hole of one of `types`.
96    pub(super) fn uses_any(&self, types: &[&str]) -> bool {
97        self.shapes().any(|(_, shape)| match shape {
98            Shape::Template(t) => t
99                .segments
100                .iter()
101                .any(|s| matches!(s, Segment::Hole { ty, .. } if types.contains(&ty.as_str()))),
102            _ => false,
103        })
104    }
105
106    /// The first example, which stands for the format where one is needed.
107    pub fn first_example(&self) -> &str {
108        self.example
109            .as_deref()
110            .or_else(|| self.variants.first().map(|(_, v)| v.example.as_str()))
111            .unwrap_or("")
112    }
113
114    /// Match `text`, squashed, against the format. A failure is built from
115    /// what the format holds, never from a sentence written to explain it:
116    /// where the shape fails and its example, or what a ban matched and the
117    /// example of the shape the line took.
118    // sokf-implements: TL_42
119    // sokf-implements: TL_74
120    // sokf-implements: TL_80
121    pub fn check(&self, text: &str) -> FormatMatch {
122        self.check_as(text, "line")
123    }
124
125    /// As `check`, for a unit that a failure calls `noun`, such as `item`.
126    pub fn check_as(&self, text: &str, noun: &str) -> FormatMatch {
127        let (shape, example) = match self.matching_as(text, noun) {
128            Ok(m) => m,
129            Err(reason) => return FormatMatch::Fail(reason),
130        };
131        if let Some(reason) = refused_as(text, &self.prohibited, noun, Some(example)) {
132            return FormatMatch::Refused(reason);
133        }
134        FormatMatch::Ok(self.key.as_ref().and_then(|k| captured(shape, k, text)))
135    }
136
137    /// The shape `text` has and its example, where it has one: the check
138    /// alone, with no failure built.
139    pub fn shape_for(&self, text: &str) -> Option<(&Shape, &str)> {
140        match &self.shape {
141            Some(shape) => shape
142                .matches(text, |_| None)
143                .then(|| (shape, self.example.as_deref().unwrap_or(""))),
144            None => self
145                .variants
146                .iter()
147                .find(|(_, v)| v.shape.matches(text, |_| None))
148                .map(|(_, v)| (&v.shape, v.example.as_str())),
149        }
150    }
151
152    /// Whether `text` has the format's shape and passes its bans, silently.
153    pub fn conforms(&self, text: &str) -> bool {
154        self.shape_for(text).is_some() && !self.prohibited.iter().any(|b| b.pattern.is_match(text))
155    }
156
157    /// The shape `text` has and its example, or why it has none. A variant
158    /// format tries its variants in order; where none matches, the one the
159    /// line follows furthest is the one it meant (TL_42).
160    // sokf-implements: TL_42
161    pub fn matching(&self, text: &str) -> std::result::Result<(&Shape, &str), String> {
162        self.matching_as(text, "line")
163    }
164
165    /// As `matching`, for a unit that a failure calls `noun`.
166    pub fn matching_as(
167        &self,
168        text: &str,
169        noun: &str,
170    ) -> std::result::Result<(&Shape, &str), String> {
171        if let Some(found) = self.shape_for(text) {
172            return Ok(found);
173        }
174        if let Some(shape) = &self.shape {
175            let example = self.example.as_deref().unwrap_or("");
176            return Err(format!(
177                "{}; {} {noun} of this kind reads {example}",
178                shape.failure(text),
179                text::article(noun)
180            ));
181        }
182        let reach: Vec<usize> = self
183            .variants
184            .iter()
185            .map(|(_, v)| v.shape.reach(text))
186            .collect();
187        let best = reach.iter().copied().max().unwrap_or(0);
188        let at: Vec<&(String, FormatVariant)> = self
189            .variants
190            .iter()
191            .zip(&reach)
192            .filter(|(_, r)| **r == best)
193            .map(|(v, _)| v)
194            .collect();
195        match at.as_slice() {
196            [] => Err("has no variants to match".into()),
197            [(name, v)] => Err(format!(
198                "does not match the `{name}` form: it {}; {} {noun} of this kind reads {}",
199                v.shape.failure(text),
200                text::article(noun),
201                v.example
202            )),
203            many => {
204                // A regex does not say where it fails; a template does.
205                let at = if best > 0 {
206                    format!(": each fails at column {}", template::column(text, best))
207                } else {
208                    String::new()
209                };
210                // Each form it names is one it may have meant, so each
211                // shows its example.
212                Err(format!(
213                    "matches none of the forms {}{at}; {} {noun} of this kind reads {}",
214                    text::code_list(many.iter().map(|(n, _)| n), ", "),
215                    text::article(noun),
216                    many.iter()
217                        .map(|(_, v)| v.example.as_str())
218                        .collect::<Vec<_>>()
219                        .join(" or ")
220                ))
221            }
222        }
223    }
224}
225
226/// The failure of the first ban in `bans` that `text` matches: the span it
227/// matched, the pattern, and, where the ban sits on a rule, that rule's
228/// example. A ban needs no sentence of its own, because its pattern is
229/// exactly what the author wrote.
230// sokf-implements: TL_75
231// sokf-implements: TL_76
232// sokf-implements: TL_81
233pub fn refused(text: &str, bans: &[Prohibition], example: Option<&str>) -> Option<String> {
234    refused_as(text, bans, "line", example)
235}
236
237/// As `refused`, for a unit that the failure calls `noun`. The reason names
238/// the span that a ban matched and gives the ban's message, which states
239/// the rule, rather than its pattern.
240// sokf-implements: TL_75
241// sokf-implements: TL_76
242pub fn refused_as(
243    text: &str,
244    bans: &[Prohibition],
245    noun: &str,
246    example: Option<&str>,
247) -> Option<String> {
248    bans.iter().find_map(|ban| {
249        let span = ban.pattern.find(text)?.as_str();
250        let message = ban.message.trim_end();
251        Some(match example {
252            Some(e) => {
253                let mut article = text::article(noun).to_string();
254                article[..1].make_ascii_uppercase();
255                format!("`{span}` is refused: {message} {article} {noun} of this kind reads {e}")
256            }
257            None => format!("`{span}` is refused: {message}"),
258        })
259    })
260}
261
262/// `Prohibition` is one entry of `prohibited-patterns`: a pattern that no
263/// governed text may match, and the message that states the rule as a fact
264/// about the part.
265#[derive(Debug, Clone)]
266pub struct Prohibition {
267    /// `pattern`: the regex.
268    pub pattern: Regex,
269    /// `message`: the rule, in one sentence.
270    pub message: String,
271}
272
273/// `prohibitions` reads the entries of `prohibited-patterns` under `node`.
274/// The grammar checks that each entry is a map of a `pattern` that compiles
275/// and a `message`, so an entry that is not is skipped.
276pub fn prohibitions(node: &Spanned, regexes: &RegexCache) -> Vec<Prohibition> {
277    node.get("prohibited-patterns")
278        .and_then(|b| b.as_seq())
279        .unwrap_or(&[])
280        .iter()
281        .filter_map(|ban| {
282            let pattern = regexes.get(ban.get("pattern")?.as_str()?).ok()?;
283            let message = ban.get("message")?.as_str()?.to_string();
284            Some(Prohibition { pattern, message })
285        })
286        .collect()
287}
288
289/// One shape of a format or a variant: `text`, `template` or `pattern`,
290/// exactly one, `text` a string.
291fn format_shape(
292    node: &Spanned,
293    types: &TypeRegistry,
294    regexes: &RegexCache,
295) -> std::result::Result<Shape, Problem> {
296    if node.get("text").is_some_and(|t| t.as_str().is_none()) {
297        return Err(Problem::new("TL_101", "a format's `text` is a string"));
298    }
299    // sokf-implements: TL_105
300    shape_of(node, types, regexes)?.ok_or_else(|| {
301        Problem::new(
302            "TL_105",
303            "a shape is one of `text`, `template` or `pattern`",
304        )
305    })
306}
307
308/// Lower a validated `format` entry against the types its layer sees. The
309/// problems are `(line, message)`: a shape that does not compile, an
310/// example where variants speak, none where they do not, and a key no
311/// shape captures.
312// sokf-implements: TL_42
313pub(super) fn lower_format(
314    entry: &Spanned,
315    types: &TypeRegistry,
316    regexes: &RegexCache,
317) -> std::result::Result<Format, Vec<(usize, Problem)>> {
318    let mut problems = Vec::new();
319    let mut variants = Vec::new();
320    if let Some(v) = entry.get("variants") {
321        for (name, variant) in v.as_map().unwrap_or(&[]) {
322            match format_shape(variant, types, regexes) {
323                Ok(shape) => variants.push((
324                    name.value.clone(),
325                    FormatVariant {
326                        shape,
327                        example: variant
328                            .get("example")
329                            .and_then(|d| d.as_str())
330                            .unwrap_or_default()
331                            .to_string(),
332                    },
333                )),
334                Err(e) => problems.push((
335                    variant.line,
336                    e.prefixed(&format!("variant `{}`: ", name.value)),
337                )),
338            }
339        }
340    }
341    let shape = if entry.get("variants").is_some() {
342        None
343    } else {
344        match format_shape(entry, types, regexes) {
345            Ok(s) => Some(s),
346            Err(e) => {
347                problems.push((entry.line, e));
348                None
349            }
350        }
351    };
352    let example = entry
353        .get("example")
354        .and_then(|d| d.as_str())
355        .map(str::to_string);
356    // sokf-implements: TL_40
357    // sokf-implements: TL_110
358    match (&example, entry.get("variants")) {
359        (Some(_), Some(_)) => problems.push((
360            entry.line,
361            Problem::new(
362                "TL_110",
363                "a format with `variants` has no `example` of its own: each variant's example speaks for its shape",
364            ),
365        )),
366        (None, None) => problems.push((
367            entry.line,
368            Problem::new("TL_40", "a format with one shape carries an `example`"),
369        )),
370        _ => {}
371    }
372    let format = Format {
373        shape,
374        variants,
375        key: entry
376            .get("key")
377            .and_then(|k| k.as_str())
378            .map(str::to_string),
379        prohibited: prohibitions(entry, regexes),
380        example,
381        raw: entry.clone(),
382    };
383    if let Some(k) = &format.key {
384        for (name, s) in format.shapes() {
385            if !s.has_group(k) {
386                let whose =
387                    name.map_or_else(|| "the shape".to_string(), |n| format!("variant `{n}`"));
388                problems.push((
389                    entry.line,
390                    Problem::new(
391                        "TL_31",
392                        format!("`key` `{k}` names no hole or named group of {whose}"),
393                    ),
394                ));
395            }
396        }
397    }
398    if !problems.is_empty() {
399        return Err(problems);
400    }
401    Ok(format)
402}