beck_syntax/
lib.rs

1//! The Beck front end: two surfaces, one AST.
2//!
3//! ```text
4//!    surface/py.beck ──┐                                        ┌── printer.py    ──▶ .beck
5//!                      ├──▶  Reader  ──▶  Node (canonical AST) ─┤
6//!  surface/sx.beck  ───┘        ▲                               └── printer.sexpr ──▶ .sx
7//!                               │
8//!                     both readers produce identical Node trees
9//! ```
10//!
11//! ([`docs/02-syntax.md`](../../../../docs/02-syntax.md) §2.2.) The equivalence in that last line is
12//! not a slogan here: `print::tests::the_two_surfaces_are_the_same_language` reads the same
13//! definition through both readers and asserts the trees are structurally equal.
14
15pub mod doc;
16pub mod lexer;
17pub mod node;
18pub mod parser;
19pub mod print;
20pub mod security;
21pub mod sexpr;
22
23pub use node::{sym, Head, Lit, Meta, Node, Scope, ScopeSet, Symbol};
24
25use beck_diag::{Diagnostics, FileId};
26
27/// Read a source file in whichever surface its extension names.
28///
29/// `.beck` is the Python surface and the default; `.sx` is the canonical S-expression surface,
30/// which stays "documented and supported (it is invaluable for macro debugging, for the spec, and
31/// for generated code)" (§2.2).
32/// A file name, as the identifier a module is called.
33///
34/// A module name is printed into `(module <name> …)` and has to read back as a symbol, so it cannot
35/// simply be the file name: `01-counter.beck` would print a form the reader rejects, and
36/// `parse(print(parse(src))) == parse(src)` — the round-trip property §4.8 asks for — would fail on
37/// any file whose name is not already an identifier. Found by naming a corpus in file order.
38pub fn module_ident(name: &str) -> String {
39    let stem = name
40        .rsplit(['/', '\\'])
41        .next()
42        .unwrap_or(name)
43        .trim_end_matches(".becki")
44        .trim_end_matches(".beck")
45        .trim_end_matches(".sx");
46    let mut out = String::with_capacity(stem.len() + 1);
47    for (i, c) in stem.chars().enumerate() {
48        match c {
49            'a'..='z' | 'A'..='Z' | '_' => out.push(c),
50            '0'..='9' if i > 0 => out.push(c),
51            '0'..='9' => {
52                out.push('m');
53                out.push(c);
54            }
55            _ => out.push('_'),
56        }
57    }
58    if out.is_empty() {
59        out.push_str("main");
60    }
61    out
62}
63
64pub fn parse_file(file: FileId, name: &str, src: &str, diags: &mut Diagnostics) -> Node {
65    let mut parsed = parse_forms(file, name, src, diags);
66    // Doc comments are attached after parsing rather than lexed (see [`doc`]), so this is the one
67    // place both surfaces pass through and the one place the attachment has to happen.
68    doc::attach(&mut parsed, &doc::collect(src, doc::marker_for(name)));
69    parsed
70}
71
72fn parse_forms(file: FileId, name: &str, src: &str, diags: &mut Diagnostics) -> Node {
73    let module_name = module_ident(name);
74
75    // Before either surface reads a byte: what a source file is allowed to contain at all.
76    // One place for both notations — see [`security`].
77    security::scan(file, src, diags);
78
79    if name.ends_with(".sx") {
80        let forms = sexpr::read_all(file, src, diags);
81        // A file that already *is* a module is that module. `beck fmt --surface sexpr` prints
82        // `(module todo …)`, and wrapping that in a second module made the printer's own output
83        // unreadable by the checker — so `parse(print(parse(src)))` failed on the one surface §2.2
84        // calls canonical. Found by round-tripping the corpus; it predates Phase 2.
85        if forms.len() == 1 && forms[0].is_form(sym::MODULE) {
86            return forms.into_iter().next().expect("length checked");
87        }
88        let mut items = vec![Node::sym(&module_name, beck_diag::Span::NONE)];
89        items.extend(forms);
90        Node::form(sym::MODULE, items, beck_diag::Span::new(file, 0..src.len()))
91    } else {
92        parser::parse_module(file, &module_name, src, diags)
93    }
94}
95
96#[cfg(test)]
97mod module_name_tests {
98    use super::module_ident;
99
100    #[test]
101    fn a_printed_module_reads_back_as_that_module_rather_than_as_a_nested_one() {
102        use beck_diag::{Diagnostics, SourceMap};
103        let src = "def f() -> Int:\n    return 1\n";
104        let mut map = SourceMap::new();
105        let file = map.add("t.beck", src);
106        let mut d = Diagnostics::new();
107        let parsed = super::parse_file(file, "t.beck", src, &mut d);
108
109        let printed = super::print::to_sexpr_pretty(&parsed);
110        let file2 = map.add("t.sx", printed.clone());
111        let reread = super::parse_file(file2, "t.sx", &printed, &mut d);
112        assert!(!d.has_errors());
113        assert_eq!(
114            super::print::to_sexpr(&parsed),
115            super::print::to_sexpr(&reread),
116            "printed:\n{printed}"
117        );
118    }
119
120    #[test]
121    fn a_file_name_becomes_an_identifier_a_reader_can_read_back() {
122        assert_eq!(module_ident("todo.beck"), "todo");
123        assert_eq!(module_ident("src/orders.beck"), "orders");
124        assert_eq!(module_ident("a/b/c.sx"), "c");
125        assert_eq!(module_ident("orders.becki"), "orders");
126        // The ones that motivated this: a leading digit is not an identifier, and a hyphen is an
127        // operator.
128        assert_eq!(module_ident("01-counter.beck"), "m01_counter");
129        assert_eq!(module_ident("my app.beck"), "my_app");
130        assert_eq!(module_ident(""), "main");
131    }
132}