Skip to main content

lang_forge/
sketch.rs

1//! [`Sketch`]: a sketch whose parts are several files (LSF2 §3, ISSUES M13).
2
3use alloc::{
4    boxed::Box,
5    collections::BTreeSet,
6    format,
7    string::{String, ToString},
8    vec::Vec,
9};
10
11use diag_lang::{Diagnostic, Label, SourceMap};
12use syntax_lang::Span;
13
14use crate::{Error, Language, codes, error::Report, noml};
15
16/// The most files one sketch may have (LSF2 §1.8).
17const MAX_FILES: usize = 1024;
18
19/// The most bytes all files of a sketch may have together (LSF2 §1.8).
20const MAX_TOTAL: usize = 64 << 20;
21
22/// A sketch made of several files: the entry and the parts its
23/// `[sketch] modules` lists (LSF2 §3.2).
24///
25/// Add the entry first, then its parts, each under its project-relative path
26/// (`/`-separated, no `..` that leaves the root, portable to every OS). The
27/// files go into a [`SourceMap`] in the order they are added, so every
28/// diagnostic forging reports — and every span in the forged language's
29/// warnings — points into the right file: render them with
30/// [`source_map`](Sketch::source_map). A one-file sketch forged from a
31/// `Sketch` gets the same spans as from [`Language::from_lsf`], since the
32/// first file starts at position 0.
33///
34/// The parts merge as if the files were one document: a table may be
35/// continued in another file (`[rules]` in two files adds rules to one
36/// table), and a key or table defined twice, in one file or two, is an error
37/// naming both places. When rules come from more than one file,
38/// `[language] start` is required.
39///
40/// # Examples
41///
42/// ```
43/// use lang_forge::{Language, Sketch};
44///
45/// let mut sketch = Sketch::new();
46/// sketch.add("calc.lsf", r#"
47/// [sketch]
48/// format = 2
49/// modules = ["rules/expr.lsf"]
50///
51/// [language]
52/// name = "calc"
53/// version = "1.0.0"
54/// start = "program"
55///
56/// [rules]
57/// program = "stmts:stmt*"
58/// stmt = "value:expr ';'"
59/// "#)?;
60/// sketch.add("rules/expr.lsf", r#"
61/// [sketch]
62/// format = 2
63/// kind = "part"
64///
65/// [rules.expr]
66/// operand = "NUMBER"
67/// levels = [{ left = ["+"] }]
68/// "#)?;
69///
70/// let calc = Language::from_sketch(&sketch)?;
71/// assert!(!calc.parse("1 + 2; 3;").has_errors());
72/// # Ok::<(), lang_forge::Error>(())
73/// ```
74///
75/// A problem in a part is reported in that part:
76///
77/// ```
78/// use lang_forge::{Language, Sketch};
79///
80/// let mut sketch = Sketch::new();
81/// sketch.add("a.lsf", "[sketch]\nformat = 2\nmodules = [\"b.lsf\"]\n[language]\nname = \"a\"\nversion = \"1.0.0\"\nstart = \"x\"\n[rules]\nx = \"y\"\n")?;
82/// sketch.add("b.lsf", "[sketch]\nformat = 2\nkind = \"part\"\n[rules]\ny = \"undefined_rule\"\n")?;
83/// let err = Language::from_sketch(&sketch).unwrap_err();
84/// assert_eq!(err.to_string(), "b.lsf:5:6: undefined rule `undefined_rule`");
85/// # Ok::<(), lang_forge::Error>(())
86/// ```
87#[derive(Clone, Debug, Default)]
88pub struct Sketch {
89    map: SourceMap,
90    /// The normalized path of each file, in the order added.
91    paths: Vec<String>,
92    total: usize,
93}
94
95impl Sketch {
96    /// An empty sketch.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use lang_forge::Sketch;
102    ///
103    /// let mut sketch = Sketch::new();
104    /// assert!(sketch.is_empty());
105    /// sketch.add("main.lsf", "[language]\nname = \"m\"\n[rules]\nm = \"IDENT\"\n")?;
106    /// assert_eq!(sketch.len(), 1);
107    /// assert_eq!(sketch.source_map().iter().next().map(|(_, f)| f.name()), Some("main.lsf"));
108    /// # Ok::<(), lang_forge::Error>(())
109    /// ```
110    #[must_use]
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Adds a file under its project-relative `path`. The first file added
116    /// is the entry.
117    ///
118    /// # Errors
119    ///
120    /// Returns an [`Error`] if the path is not a portable project-relative
121    /// path (`LSF8001`, `LSF8002`), is already in the sketch, or differs
122    /// from one only by ASCII case (`LSF2005`, `LSF8003`), or if the file is
123    /// larger than 8 MiB (`LSF9001`), the sketch would pass 64 MiB in total
124    /// (`LSF9002`), or it would have more than 1024 files (`LSF9003`). The
125    /// sketch is unchanged.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use lang_forge::Sketch;
131    ///
132    /// let mut sketch = Sketch::new();
133    /// assert!(sketch.add("lang/main.lsf", "").is_ok());
134    /// assert!(sketch.add("../outside.lsf", "").is_err());
135    /// assert!(sketch.add("C:/abs.lsf", "").is_err());
136    /// assert!(sketch.add("lang/main.lsf", "").is_err());
137    /// assert_eq!(sketch.len(), 1);
138    /// ```
139    pub fn add(&mut self, path: &str, text: impl Into<Box<str>>) -> Result<(), Error> {
140        let text: Box<str> = text.into();
141        let fail = |code, message: String| {
142            Err(Error::located(
143                Vec::from([crate::error::coded(code, Span::empty(0), message)]),
144                1,
145                1,
146            ))
147        };
148        let normalized = match normalize(path) {
149            Ok(p) => p,
150            Err((code, message)) => return fail(code, message),
151        };
152        if self.paths.contains(&normalized) {
153            return fail(
154                codes::FILE_TWICE,
155                format!("`{normalized}` is already in the sketch"),
156            );
157        }
158        if let Some(other) = self
159            .paths
160            .iter()
161            .find(|p| p.eq_ignore_ascii_case(&normalized))
162        {
163            return fail(
164                codes::PATH_CASE,
165                format!(
166                    "`{normalized}` and `{other}` differ only by case, which is one file on Windows and macOS"
167                ),
168            );
169        }
170        if text.len() > noml::MAX_SCHEMATIC {
171            return fail(
172                codes::DOCUMENT_TOO_LARGE,
173                format!(
174                    "`{normalized}` is larger than {} MiB; a sketch file is at most that",
175                    noml::MAX_SCHEMATIC >> 20
176                ),
177            );
178        }
179        if self.total + text.len() > MAX_TOTAL {
180            return fail(
181                codes::SKETCH_TOO_LARGE,
182                format!("a sketch is at most {} MiB in total", MAX_TOTAL >> 20),
183            );
184        }
185        if self.paths.len() >= MAX_FILES {
186            return fail(
187                codes::TOO_MANY_FILES,
188                format!("a sketch has at most {MAX_FILES} files"),
189            );
190        }
191        let len = text.len();
192        if self.map.add(normalized.as_str(), text).is_err() {
193            return fail(
194                codes::SKETCH_TOO_LARGE,
195                String::from("the sketch does not fit in the source map"),
196            );
197        }
198        self.total += len;
199        self.paths.push(normalized);
200        Ok(())
201    }
202
203    /// The sketch's files, for rendering diagnostics with
204    /// [`Renderer`](diag_lang::Renderer).
205    #[must_use]
206    pub fn source_map(&self) -> &SourceMap {
207        &self.map
208    }
209
210    /// The number of files.
211    #[must_use]
212    pub fn len(&self) -> usize {
213        self.paths.len()
214    }
215
216    /// Whether no file has been added.
217    #[must_use]
218    pub fn is_empty(&self) -> bool {
219        self.paths.is_empty()
220    }
221
222    /// The text and global base offset of file `i`.
223    fn file(&self, i: usize) -> Option<(&str, u32)> {
224        let (_, file) = self.map.iter().nth(i)?;
225        Some((file.text(), file.span().start().to_u32()))
226    }
227}
228
229/// Normalizes and validates a project-relative path (LSF2 §3.4).
230fn normalize(path: &str) -> Result<String, (diag_lang::Code, String)> {
231    let bad = |code, why: &str| Err((code, format!("path `{path}` {why}")));
232    if path.is_empty() {
233        return bad(codes::PATH_PORTABLE, "is empty");
234    }
235    if path.starts_with('/') || path.contains('\\') || path.as_bytes().get(1) == Some(&b':') {
236        return bad(
237            codes::PATH_PORTABLE,
238            "must be project-relative and `/`-separated",
239        );
240    }
241    let mut parts: Vec<&str> = Vec::new();
242    for part in path.split('/') {
243        match part {
244            "" | "." => return bad(codes::PATH_PORTABLE, "has an empty or `.` component"),
245            ".." => {
246                if parts.pop().is_none() {
247                    return bad(codes::PATH_ESCAPES, "leaves the project root");
248                }
249            }
250            _ => {
251                if part.len() > 255
252                    || part.chars().any(|c| {
253                        c.is_control() || matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*')
254                    })
255                    || part.ends_with('.')
256                    || part.ends_with(' ')
257                    || is_reserved_name(part)
258                {
259                    return bad(
260                        codes::PATH_PORTABLE,
261                        "is not portable to every operating system",
262                    );
263                }
264                parts.push(part);
265            }
266        }
267    }
268    if parts.is_empty() {
269        return bad(codes::PATH_PORTABLE, "names no file");
270    }
271    Ok(parts.join("/"))
272}
273
274/// Windows reserved device names, with any extension.
275fn is_reserved_name(part: &str) -> bool {
276    let stem = part.split('.').next().unwrap_or(part).to_ascii_uppercase();
277    matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
278        || ((stem.starts_with("COM") || stem.starts_with("LPT"))
279            && stem.len() == 4
280            && stem.as_bytes()[3].is_ascii_digit()
281            && stem.as_bytes()[3] != b'0')
282}
283
284/// A diagnostic with every span moved by `base`.
285fn shift(d: &Diagnostic, base: u32) -> Diagnostic {
286    let move_span = |s: Span| Span::new(s.start().to_u32() + base, s.end().to_u32() + base);
287    let primary = d.primary();
288    let label = if primary.message().is_empty() {
289        Label::unlabelled(move_span(primary.span()))
290    } else {
291        Label::new(move_span(primary.span()), primary.message())
292    };
293    let mut out = Diagnostic::new(d.severity(), d.message(), label);
294    for s in d.secondary() {
295        out = out.with_secondary(Label::new(move_span(s.span()), s.message()));
296    }
297    for n in d.notes() {
298        out = out.with_note(n);
299    }
300    for h in d.help() {
301        out = out.with_help(h);
302    }
303    if let Some(code) = d.code() {
304        out = out.with_code(code);
305    }
306    out
307}
308
309impl Language {
310    /// Forges a language from a [`Sketch`] of one or more files (LSF2 §3).
311    ///
312    /// The first file is the entry. A format-2 entry lists its parts in
313    /// `[sketch] modules`, and every part must be in the sketch with
314    /// `[sketch] kind = "part"`. A one-file format-1 sketch is forged exactly
315    /// as [`from_lsf`](Language::from_lsf) forges it.
316    ///
317    /// # Errors
318    ///
319    /// Returns an [`Error`] whose diagnostics point into the sketch's
320    /// [`source_map`](Sketch::source_map): everything `from_lsf` reports,
321    /// plus a module that is not in the sketch or listed twice (`LSF2023`,
322    /// `LSF2005`), a file no module lists (`LSF2023`), a part that is not
323    /// marked `kind = "part"` or has `[language]` or `modules` (`LSF2024`,
324    /// `LSF2022`, `LSF2003`), a key defined in two files (`LSF2002`), and a
325    /// missing `[language] start` when rules span files (`LSF2006`).
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use lang_forge::{Language, Sketch};
331    ///
332    /// let mut sketch = Sketch::new();
333    /// sketch.add("words.lsf", "[sketch]\nformat = 2\nmodules = [\"lexer.lsf\"]\n\
334    ///     [language]\nname = \"words\"\nversion = \"1.0.0\"\n[rules]\nfile = \"TAG*\"\n")?;
335    /// sketch.add("lexer.lsf", "[sketch]\nformat = 2\nkind = \"part\"\n\
336    ///     [lexer.tokens]\nTAG = { regex = \"#[a-z]+\" }\n")?;
337    /// assert_eq!(sketch.len(), 2);
338    ///
339    /// let words = Language::from_sketch(&sketch)?;
340    /// assert!(!words.parse("#a #bc").has_errors());
341    /// # Ok::<(), lang_forge::Error>(())
342    /// ```
343    pub fn from_sketch(sketch: &Sketch) -> Result<Self, Error> {
344        let map = &sketch.map;
345        let mut report = Report::default();
346        let n = sketch.len();
347        let rank_of = |span: Span| -> u32 {
348            let at = span.start();
349            map.locate(at).map_or(0, |(id, _)| {
350                map.iter().position(|(i, _)| i == id).unwrap_or(0) as u32
351            })
352        };
353        let Some((entry_text, entry_base)) = sketch.file(0) else {
354            report.error(codes::MISSING, Span::empty(0), "the sketch has no files");
355            return Err(report.into_sketch_error(map, &rank_of));
356        };
357        // Read every file; spans move into the source map's positions.
358        let mut roots = Vec::with_capacity(n);
359        for i in 0..n {
360            let Some((text, base)) = sketch.file(i) else {
361                continue;
362            };
363            match noml::read(text) {
364                Ok(root) => roots.push(Some(root)),
365                Err(d) => {
366                    report.diagnostic(shift(&d, base));
367                    roots.push(None);
368                }
369            }
370        }
371        if !report.is_clean() {
372            return Err(report.into_sketch_error(map, &rank_of));
373        }
374        let mut roots: Vec<noml::Table<'_>> = roots.into_iter().flatten().collect();
375        let format2 = roots[0].entries.iter().any(|e| e.key == "sketch");
376        if !format2 {
377            if n > 1 {
378                report.error(
379                    codes::MODULES_OUTSIDE_ENTRY,
380                    Span::new(entry_base, entry_base),
381                    "a format-1 sketch is one file; give the entry `[sketch] format = 2` and `modules` to split it",
382                );
383                return Err(report.into_sketch_error(map, &rank_of));
384            }
385            return Language::from_lsf(entry_text);
386        }
387        for (i, root) in roots.iter_mut().enumerate() {
388            if let Some((_, base)) = sketch.file(i) {
389                root.rebase(base);
390            }
391        }
392
393        // The modules the entry lists.
394        let listed: Vec<(String, Span)> = crate::spec2::modules(&roots[0])
395            .into_iter()
396            .map(|(p, s)| (p.to_string(), s))
397            .collect();
398        let mut order: Vec<usize> = Vec::new();
399        let mut seen: BTreeSet<usize> = BTreeSet::new();
400        for (path, span) in &listed {
401            match normalize(path) {
402                Ok(p) => match sketch.paths.iter().position(|q| *q == p) {
403                    Some(0) => report.error(
404                        codes::FILE_TWICE,
405                        *span,
406                        "the entry cannot list itself as a module",
407                    ),
408                    Some(i) if !seen.insert(i) => {
409                        report.error(
410                            codes::FILE_TWICE,
411                            *span,
412                            format!("module `{p}` is listed twice"),
413                        );
414                    }
415                    Some(i) => order.push(i),
416                    None => report.error(
417                        codes::MODULE_NOT_FOUND,
418                        *span,
419                        format!("module `{p}` is not in the sketch"),
420                    ),
421                },
422                Err((code, message)) => report.error(code, *span, message),
423            }
424        }
425        for (i, path) in sketch.paths.iter().enumerate().skip(1) {
426            if !seen.contains(&i) {
427                let base = sketch.file(i).map_or(0, |(_, b)| b);
428                report.error(
429                    codes::MODULE_NOT_FOUND,
430                    Span::new(base, base),
431                    format!(
432                        "`{path}` is in the sketch, but the entry's `modules` does not list it"
433                    ),
434                );
435            }
436        }
437        // Parts: `[sketch] kind = "part"`, no `[language]`, no `modules`.
438        let mut rules_files = 0;
439        for (i, root) in roots.iter().enumerate() {
440            if root.entries.iter().any(|e| e.key == "rules") {
441                rules_files += 1;
442            }
443            if i == 0 {
444                continue;
445            }
446            let base = sketch.file(i).map_or(0, |(_, b)| b);
447            let at = Span::new(base, base);
448            let sketch_table = root.entries.iter().find(|e| e.key == "sketch");
449            let kind = sketch_table.and_then(|e| match &e.value.kind {
450                noml::ValueKind::Table(t) => {
451                    t.entries
452                        .iter()
453                        .find(|k| k.key == "kind")
454                        .and_then(|k| match &k.value.kind {
455                            noml::ValueKind::Str(s) => Some(s.text.to_string()),
456                            _ => None,
457                        })
458                }
459                _ => None,
460            });
461            if kind.as_deref() != Some("part") {
462                report.error(
463                    codes::NOT_A_PART,
464                    sketch_table.map_or(at, |e| e.key_span),
465                    format!(
466                        "module `{}` must say `[sketch] kind = \"part\"`",
467                        sketch.paths[i]
468                    ),
469                );
470            }
471            if let Some(language) = root.entries.iter().find(|e| e.key == "language") {
472                report.error(
473                    codes::FILE_ROLE,
474                    language.key_span,
475                    "a part cannot have [language]; it belongs in the entry",
476                );
477            }
478            if !crate::spec2::modules(root).is_empty() {
479                report.error(
480                    codes::MODULES_OUTSIDE_ENTRY,
481                    at,
482                    "`modules` is only allowed in the entry file",
483                );
484            }
485        }
486        if rules_files > 1 {
487            let start_given = roots[0].entries.iter().any(|e| {
488                e.key == "language"
489                    && matches!(&e.value.kind, noml::ValueKind::Table(t) if t.entries.iter().any(|k| k.key == "start"))
490            });
491            if !start_given {
492                report.error(
493                    codes::START_REQUIRED,
494                    Span::new(entry_base, entry_base),
495                    "[rules] spans several files, so `[language] start` is required",
496                );
497            }
498        }
499        if !report.is_clean() {
500            return Err(report.into_sketch_error(map, &rank_of));
501        }
502
503        // Merge the parts into the entry, in `modules` order.
504        let mut taken: Vec<Option<noml::Table<'_>>> = roots.into_iter().map(Some).collect();
505        let Some(mut merged) = taken[0].take() else {
506            return Err(report.into_sketch_error(map, &rank_of));
507        };
508        for &i in &order {
509            let Some(mut part) = taken[i].take() else {
510                continue;
511            };
512            // Each file has its own `[sketch]`.
513            part.entries.retain(|e| e.key != "sketch");
514            let mut twice = |key: &str, first: Span, second: Span| {
515                report.diagnostic(
516                    Diagnostic::new(
517                        diag_lang::Severity::Error,
518                        format!("`{key}` is defined twice in this sketch"),
519                        Label::new(second, "defined again here"),
520                    )
521                    .with_secondary(Label::new(first, "first defined here"))
522                    .with_code(codes::DEFINED_TWICE),
523                );
524            };
525            merged.merge(part, &mut twice);
526        }
527        let spec = crate::schematic::interpret(merged, &mut report);
528        let locate = |span: Span| -> (u32, u32) {
529            map.line_col(span.start())
530                .map_or((1, 1), |(_, lc)| (lc.line, lc.col))
531        };
532        let grammar = spec.and_then(|spec| crate::grammar2::compile(&spec, &locate, &mut report));
533        match grammar {
534            Some(mut grammar) if report.is_clean() => {
535                if let Some(extra) = grammar.extra.as_mut() {
536                    extra.warnings = report.into_sketch_warnings(&rank_of).into();
537                }
538                Ok(Language::from_grammar(grammar))
539            }
540            _ => {
541                if report.is_clean() {
542                    report.error(
543                        codes::MISSING,
544                        Span::empty(0),
545                        "the sketch could not be forged",
546                    );
547                }
548                Err(report.into_sketch_error(map, &rank_of))
549            }
550        }
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_normalize_paths() {
560        assert_eq!(normalize("a/b/../c.lsf").as_deref().ok(), Some("a/c.lsf"));
561        assert!(normalize("../x.lsf").is_err());
562        assert!(normalize("a//b").is_err());
563        assert!(normalize("./a").is_err());
564        assert!(normalize("/a").is_err());
565        assert!(normalize("a\\b").is_err());
566        assert!(normalize("CON.lsf").is_err());
567        assert!(normalize("com1").is_err());
568        assert!(normalize("com0").is_ok());
569        assert!(normalize("a?.lsf").is_err());
570        assert!(normalize("a.").is_err());
571        assert!(normalize("x/..").is_err());
572    }
573}