beck_syntax/
node.rs

1//! `Node` — the canonical AST, and an ordinary value.
2//!
3//! [`docs/02-syntax.md`](../../../../../docs/02-syntax.md) §2.2 fixes the shape:
4//!
5//! ```text
6//! model Node:
7//!     head: Sym | Lit
8//!     args: list[Node]
9//!     meta: Meta
10//! ```
11//!
12//! Everything else is derived. Both surfaces — the Python one and the S-expression one — read to
13//! *identical* `Node` trees; `beck fmt` prints either. That is the whole trick: "significant
14//! whitespace is only hard if your macros do string concatenation. Ours cannot."
15//!
16//! One representational decision the doc leaves implicit: `head` is a symbol *or* a literal, so an
17//! application whose callee is itself an expression has nowhere to put the callee. Those use the
18//! reserved head [`sym::CALL`] — `(call (. f g) x)` — which keeps the common case, and therefore
19//! the original sketch's notation, literal: `(update_at todos id ...)` is a symbol head with three
20//! arguments, exactly as written.
21
22use std::fmt;
23use std::sync::Arc;
24
25use beck_diag::Span;
26
27/// A hygiene scope. Fresh scopes are minted by the macro expander; the set a symbol carries is
28/// what decides which binding it refers to ([`crate::Symbol`]).
29#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct Scope(pub u32);
31
32/// A set of hygiene scopes, kept sorted and deduplicated so that subset tests are a merge.
33///
34/// The empty set is the source program's own scope, which is why an ordinary top-level definition
35/// is visible everywhere: `{} ⊆ S` for every `S`.
36#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct ScopeSet(Arc<[Scope]>);
38
39impl ScopeSet {
40    pub fn empty() -> ScopeSet {
41        ScopeSet(Arc::from([] as [Scope; 0]))
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.0.is_empty()
46    }
47
48    pub fn len(&self) -> usize {
49        self.0.len()
50    }
51
52    pub fn contains(&self, s: Scope) -> bool {
53        self.0.binary_search(&s).is_ok()
54    }
55
56    pub fn insert(&self, s: Scope) -> ScopeSet {
57        if self.contains(s) {
58            return self.clone();
59        }
60        let mut v = self.0.to_vec();
61        v.push(s);
62        v.sort_unstable();
63        ScopeSet(Arc::from(v))
64    }
65
66    pub fn remove(&self, s: Scope) -> ScopeSet {
67        if !self.contains(s) {
68            return self.clone();
69        }
70        let v: Vec<Scope> = self.0.iter().copied().filter(|x| *x != s).collect();
71        ScopeSet(Arc::from(v))
72    }
73
74    /// Add the scope if absent, remove it if present.
75    ///
76    /// This is the operation that makes hygiene work: the expander adds a fresh scope to a macro's
77    /// *input* and flips it on the *output*, so identifiers that came from the call site come back
78    /// to their original scopes while identifiers the template introduced acquire the new one.
79    pub fn flip(&self, s: Scope) -> ScopeSet {
80        if self.contains(s) {
81            self.remove(s)
82        } else {
83            self.insert(s)
84        }
85    }
86
87    /// `self ⊆ other`. A binding is a candidate for a reference exactly when this holds.
88    pub fn is_subset_of(&self, other: &ScopeSet) -> bool {
89        let (mut i, mut j) = (0, 0);
90        while i < self.0.len() {
91            if j >= other.0.len() {
92                return false;
93            }
94            match self.0[i].cmp(&other.0[j]) {
95                std::cmp::Ordering::Equal => {
96                    i += 1;
97                    j += 1;
98                }
99                std::cmp::Ordering::Greater => j += 1,
100                std::cmp::Ordering::Less => return false,
101            }
102        }
103        true
104    }
105}
106
107impl fmt::Debug for ScopeSet {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{{")?;
110        for (i, s) in self.0.iter().enumerate() {
111            if i > 0 {
112                write!(f, ",")?;
113            }
114            write!(f, "{}", s.0)?;
115        }
116        write!(f, "}}")
117    }
118}
119
120/// An identifier, with the hygiene scopes it was written (or introduced) in.
121#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
122pub struct Symbol {
123    pub name: Arc<str>,
124    pub scopes: ScopeSet,
125}
126
127impl Symbol {
128    pub fn new(name: impl AsRef<str>) -> Symbol {
129        Symbol {
130            name: Arc::from(name.as_ref()),
131            scopes: ScopeSet::empty(),
132        }
133    }
134
135    pub fn as_str(&self) -> &str {
136        &self.name
137    }
138
139    pub fn with_scopes(&self, scopes: ScopeSet) -> Symbol {
140        Symbol {
141            name: self.name.clone(),
142            scopes,
143        }
144    }
145}
146
147impl fmt::Debug for Symbol {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        if self.scopes.is_empty() {
150            write!(f, "{}", self.name)
151        } else {
152            write!(f, "{}{:?}", self.name, self.scopes)
153        }
154    }
155}
156
157impl fmt::Display for Symbol {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.write_str(&self.name)
160    }
161}
162
163/// A literal. Floats compare by bit pattern so that `Lit` — and therefore `Node` — can be `Eq`:
164/// two source files that differ only in `0.0` versus `-0.0` are different programs.
165#[derive(Clone, Debug)]
166pub enum Lit {
167    Int(i64),
168    Float(f64),
169    Str(Arc<str>),
170    Bool(bool),
171    /// `:keyword` — a self-evaluating name, used for record field labels and enum-ish tags. The
172    /// sketch writes `{:id id :text text}`, so keywords are in the core notation from the start.
173    Keyword(Arc<str>),
174}
175
176impl PartialEq for Lit {
177    fn eq(&self, other: &Self) -> bool {
178        match (self, other) {
179            (Lit::Int(a), Lit::Int(b)) => a == b,
180            (Lit::Float(a), Lit::Float(b)) => a.to_bits() == b.to_bits(),
181            (Lit::Str(a), Lit::Str(b)) => a == b,
182            (Lit::Bool(a), Lit::Bool(b)) => a == b,
183            (Lit::Keyword(a), Lit::Keyword(b)) => a == b,
184            _ => false,
185        }
186    }
187}
188
189impl Eq for Lit {}
190
191impl Lit {
192    pub fn type_name(&self) -> &'static str {
193        match self {
194            Lit::Int(_) => "int",
195            Lit::Float(_) => "float",
196            Lit::Str(_) => "str",
197            Lit::Bool(_) => "bool",
198            Lit::Keyword(_) => "keyword",
199        }
200    }
201}
202
203#[derive(Clone, Debug, PartialEq)]
204pub enum Head {
205    Sym(Symbol),
206    Lit(Lit),
207}
208
209/// Everything a `Node` knows about itself beyond its shape.
210#[derive(Clone, Debug, Default)]
211pub struct Meta {
212    pub span: Span,
213    /// The macro expansion chain this node came out of, innermost last. Empty for source code.
214    pub expansion: Vec<(Arc<str>, Span)>,
215    /// The `##` doc comment written immediately above this node, lines joined by `\n` with the
216    /// marker and one leading space stripped ([`crate::doc`]).
217    ///
218    /// Metadata rather than a form, for the same reason a span is: a doc comment is not part of a
219    /// node's identity ([`Node::structurally_eq`]), so every pass that matches on `def` or `model`
220    /// keeps working and a doc-only edit is not a change of meaning.
221    pub doc: Option<Arc<str>>,
222}
223
224impl Meta {
225    pub fn at(span: Span) -> Meta {
226        Meta {
227            span,
228            expansion: Vec::new(),
229            doc: None,
230        }
231    }
232}
233
234/// Structural equality, ignoring spans and expansion chains.
235///
236/// Salsa needs `Eq` to decide whether a re-executed query actually produced a different value, and
237/// *formatting is explicitly not part of a `Node`'s identity* (§2.2) — so equality is exactly
238/// [`Node::structurally_eq`], and a re-parse that moved a span does not invalidate anything
239/// downstream.
240impl PartialEq for Node {
241    fn eq(&self, other: &Self) -> bool {
242        self.structurally_eq(other)
243    }
244}
245
246impl Eq for Node {}
247
248#[derive(Clone, Debug)]
249pub struct Node {
250    pub head: Head,
251    pub args: Vec<Node>,
252    /// Whether this node was *written as an application*.
253    ///
254    /// §2.2's model has `args: list[Node]`, which leaves `(params)` and `params` indistinguishable
255    /// — and an empty parameter list is not the same thing as a reference to a variable called
256    /// `params`. Elixir solves this by giving a variable `nil` args where a call has a list; this
257    /// is the same distinction with a cheaper representation.
258    pub applied: bool,
259    pub meta: Meta,
260}
261
262/// The reserved heads. Named constants rather than string literals scattered through the compiler:
263/// a typo in `"paramss"` would otherwise be a silently-unmatched form.
264pub mod sym {
265    pub const MODULE: &str = "module";
266    pub const DEF: &str = "def";
267    pub const PARAMS: &str = "params";
268    /// A `def`'s type parameters — `def map[T, U](…)`. Always present on a `def`, empty when the
269    /// definition is monomorphic, so that the form has one shape (`docs/32` §32.7).
270    pub const TYPARAMS: &str = "typarams";
271    /// `*rest` inside a list — the tail binder of a list pattern (`docs/33` §33.5).
272    pub const REST: &str = "rest";
273    pub const RETURNS: &str = "returns";
274    pub const ANNOT: &str = ":";
275    pub const FN: &str = "fn";
276    pub const CALL: &str = "call";
277    pub const DOT: &str = ".";
278    pub const IF: &str = "if";
279    pub const LET: &str = "let";
280    pub const VAR: &str = "var";
281    pub const SET: &str = "set";
282    pub const DO: &str = "do";
283    pub const RETURN: &str = "return";
284    pub const MATCH: &str = "match";
285    pub const CASE: &str = "case";
286    pub const FOR: &str = "for";
287    pub const WHILE: &str = "while";
288    pub const LIST: &str = "list";
289    pub const MAP: &str = "map-lit";
290    pub const RECORD: &str = "record";
291    pub const MODEL: &str = "model";
292    pub const UNION: &str = "union";
293    pub const VARIANT: &str = "variant";
294    pub const FIELD: &str = "field";
295    pub const TYPE: &str = "type";
296    pub const NEWTYPE: &str = "newtype";
297    pub const TRAIT: &str = "trait";
298    pub const IMPL: &str = "impl";
299    pub const IMPORT: &str = "import";
300    pub const MACRO: &str = "macro";
301    pub const QUOTE: &str = "quote";
302    pub const UNQUOTE: &str = "unquote";
303    pub const SPLICE: &str = "unquote-splicing";
304    pub const DECORATE: &str = "decorate";
305    pub const ON: &str = "on";
306    /// `@render(client)` — where a component's `view` runs, which is a different question from
307    /// `@on`: placement says a tier *may* run it, rendering says which one does.
308    pub const RENDER: &str = "render";
309    pub const UI: &str = "ui";
310    /// `raise e` — fail with a value. Performs `raises(T)`, where `T` is the value's type.
311    pub const RAISE: &str = "raise";
312    /// `row Name = a, b` — a name for a bundle of effect atoms, usable in a `uses` clause.
313    pub const ROW: &str = "row";
314    /// `identity = external(issuer="https://login.acme.com")` — who authenticates this program's
315    /// clients ([`docs/10`](../../../../../docs/10-decisions.md) D6).
316    ///
317    /// A declaration rather than a runtime flag because the issuer is a **peer**: §6.5 derives the
318    /// cluster's egress rule from the hosts a program names, and an issuer nobody wrote is a host
319    /// the deployment cannot be told about — the same argument
320    /// [`adr/0013`](../../../../../docs/adr/0013-the-host-of-an-outbound-call-is-written-at-the-call-site.md)
321    /// makes about `http_fetch`, arriving at the runtime's own outbound call rather than a
322    /// program's.
323    pub const IDENTITY: &str = "identity";
324    /// `try: block` — run the block and reify a failure as a `Result[T, E]`.
325    ///
326    /// The handler is a *form*, so it is lexically scoped by construction rather than by a search
327    /// at run time — which POPL 2019 gives the general argument for and
328    /// [`docs/38`](../../../../../docs/38-literature-survey.md) §38.4 adopts. In a language where
329    /// effects decide placement, an accidentally intercepted effect would be an accidental
330    /// *re-placement*.
331    pub const TRY: &str = "try";
332    /// `parallel: block` — a scope whose bindings are its children.
333    ///
334    /// The scope is a *form*, for the same reason [`TRY`] is: a handler that owns its children has
335    /// to own them lexically, and a nursery whose membership were decided at run time would be a
336    /// dynamic search with the same objection ([`docs/38`](../../../../../docs/38-literature-survey.md)
337    /// §38.4). Both halves of §38.4's shape are here — the children are the scope's `let`s, and
338    /// there is no handle for one to escape in.
339    pub const PARALLEL: &str = "parallel";
340    pub const KW_ARG: &str = "kw";
341    pub const WILDCARD: &str = "_";
342    pub const SERVICE: &str = "service";
343    pub const STYLES: &str = "styles";
344    pub const DOCUMENT: &str = "document";
345
346    // ---- §21.2's test construct. A test is a log, a command and an expectation, so each of the
347    // three is a form of its own rather than a call the checker would have to recognise by name.
348    pub const TEST: &str = "test";
349    /// `(property "name" (params …) (do …))` — §11.10's generated-input sibling of `test`.
350    pub const PROPERTY: &str = "property";
351    /// `(given <list[Event]> <actor?>)` — the state, as the log that reaches it.
352    pub const GIVEN: &str = "given";
353    /// `(when <session|_> <command> …)` — proposals through the real `validate`.
354    pub const WHEN: &str = "when";
355    /// `(expect <Bool>)`.
356    pub const EXPECT: &str = "expect";
357    /// `(expect-contains <Str> <actor?>)` — `expect page contains "milk"`. The subject is always
358    /// the rendered page, for the actor named or the test's default one.
359    pub const EXPECT_CONTAINS: &str = "expect-contains";
360    pub const EXPECT_SNAPSHOT: &str = "expect-snapshot";
361    /// `(expect-fold <list[Event]> <actor?>)` — `expect state == fold_of [ … ]`.
362    pub const EXPECT_FOLD: &str = "expect-fold";
363    /// `(expect-place <name> <tier>)` — answered without running anything.
364    pub const EXPECT_PLACE: &str = "expect-place";
365    /// `(expect-flow <Type> <tier>)`.
366    pub const EXPECT_FLOW: &str = "expect-flow";
367    /// `(expect-wire "previous.becki")`.
368    pub const EXPECT_WIRE: &str = "expect-wire";
369    /// `(expect-effect "<atom>" (none|once|times <n>|with <expr>))` — §21.3 rule 4: verification is
370    /// a query over what happened, not an expectation set in advance.
371    pub const EXPECT_EFFECT: &str = "expect-effect";
372    /// `(stub "<atom>" <value>)` — §21.3 rule 2: name the effect, not the shape.
373    pub const STUB: &str = "stub";
374    /// `(stub "<atom>" (arms (case …) …))` — §21.3 rule 3. The arms have no scrutinee written
375    /// because only the checker knows what performs the effect, and therefore what its argument is.
376    pub const STUB_ARMS: &str = "arms";
377
378    /// Names the checker matches as *forms* before it resolves anything.
379    ///
380    /// A definition called one of these would be shadowed by the form and never called — silently,
381    /// because `record(x)` is a well-formed record literal whatever `record` is bound to. The
382    /// checker rejects such a definition by name rather than letting it be quietly unreachable.
383    pub const RESERVED_FORMS: &[&str] = &[
384        CALL, DO, FN, IF, LIST, MAP, MATCH, QUOTE, RECORD, RETURN, SET, UNQUOTE, SPLICE, KW_ARG,
385        DOT,
386    ];
387}
388
389impl Node {
390    pub fn sym(name: impl AsRef<str>, span: Span) -> Node {
391        Node {
392            head: Head::Sym(Symbol::new(name)),
393            args: Vec::new(),
394            applied: false,
395            meta: Meta::at(span),
396        }
397    }
398
399    pub fn symbol(sym: Symbol, span: Span) -> Node {
400        Node {
401            head: Head::Sym(sym),
402            args: Vec::new(),
403            applied: false,
404            meta: Meta::at(span),
405        }
406    }
407
408    pub fn lit(lit: Lit, span: Span) -> Node {
409        Node {
410            head: Head::Lit(lit),
411            args: Vec::new(),
412            applied: false,
413            meta: Meta::at(span),
414        }
415    }
416
417    pub fn form(head: impl AsRef<str>, args: Vec<Node>, span: Span) -> Node {
418        Node {
419            head: Head::Sym(Symbol::new(head)),
420            args,
421            applied: true,
422            meta: Meta::at(span),
423        }
424    }
425
426    pub fn form_sym(head: Symbol, args: Vec<Node>, span: Span) -> Node {
427        Node {
428            head: Head::Sym(head),
429            args,
430            applied: true,
431            meta: Meta::at(span),
432        }
433    }
434
435    pub fn span(&self) -> Span {
436        self.meta.span
437    }
438
439    pub fn head_sym(&self) -> Option<&Symbol> {
440        match &self.head {
441            Head::Sym(s) => Some(s),
442            Head::Lit(_) => None,
443        }
444    }
445
446    pub fn head_name(&self) -> Option<&str> {
447        self.head_sym().map(|s| s.as_str())
448    }
449
450    pub fn as_lit(&self) -> Option<&Lit> {
451        match &self.head {
452            Head::Lit(l) if !self.applied => Some(l),
453            _ => None,
454        }
455    }
456
457    pub fn as_str_lit(&self) -> Option<&str> {
458        match self.as_lit() {
459            Some(Lit::Str(s)) => Some(s),
460            _ => None,
461        }
462    }
463
464    pub fn as_keyword(&self) -> Option<&str> {
465        match self.as_lit() {
466            Some(Lit::Keyword(k)) => Some(k),
467            _ => None,
468        }
469    }
470
471    /// A bare identifier: symbol head, not applied.
472    pub fn as_var(&self) -> Option<&Symbol> {
473        match &self.head {
474            Head::Sym(s) if !self.applied => Some(s),
475            _ => None,
476        }
477    }
478
479    /// An application with this exact head name — `(params)` counts, a bare `params` does not.
480    pub fn is_form(&self, head: &str) -> bool {
481        self.applied && self.head_name() == Some(head)
482    }
483
484    /// `(head ...)` or a bare `head`.
485    pub fn has_head(&self, head: &str) -> bool {
486        self.head_name() == Some(head)
487    }
488
489    pub fn arg(&self, i: usize) -> Option<&Node> {
490        self.args.get(i)
491    }
492
493    /// Structural equality ignoring spans and expansion chains — what tests and the round-trip
494    /// property compare, since formatting is explicitly not part of a `Node`'s identity.
495    pub fn structurally_eq(&self, other: &Node) -> bool {
496        let heads = match (&self.head, &other.head) {
497            (Head::Sym(a), Head::Sym(b)) => a.name == b.name && a.scopes == b.scopes,
498            (Head::Lit(a), Head::Lit(b)) => a == b,
499            _ => false,
500        };
501        heads
502            && self.applied == other.applied
503            && self.args.len() == other.args.len()
504            && self
505                .args
506                .iter()
507                .zip(&other.args)
508                .all(|(a, b)| a.structurally_eq(b))
509    }
510
511    /// Rewrite every symbol in the tree. The expander's workhorse.
512    pub fn map_symbols(&self, f: &mut impl FnMut(&Symbol) -> Symbol) -> Node {
513        let head = match &self.head {
514            Head::Sym(s) => Head::Sym(f(s)),
515            Head::Lit(l) => Head::Lit(l.clone()),
516        };
517        Node {
518            head,
519            args: self.args.iter().map(|a| a.map_symbols(f)).collect(),
520            applied: self.applied,
521            meta: self.meta.clone(),
522        }
523    }
524
525    /// Add a scope to every symbol in the tree.
526    pub fn add_scope(&self, s: Scope) -> Node {
527        self.map_symbols(&mut |sym| sym.with_scopes(sym.scopes.insert(s)))
528    }
529
530    /// Flip a scope on every symbol in the tree (see [`ScopeSet::flip`]).
531    pub fn flip_scope(&self, s: Scope) -> Node {
532        self.map_symbols(&mut |sym| sym.with_scopes(sym.scopes.flip(s)))
533    }
534
535    /// Record that this subtree came out of a macro, for §4.5's expansion chain in diagnostics.
536    pub fn with_expansion(&self, name: Arc<str>, at: Span) -> Node {
537        let mut meta = self.meta.clone();
538        meta.expansion.push((name.clone(), at));
539        Node {
540            head: self.head.clone(),
541            args: self
542                .args
543                .iter()
544                .map(|a| a.with_expansion(name.clone(), at))
545                .collect(),
546            applied: self.applied,
547            meta,
548        }
549    }
550
551    pub fn node_count(&self) -> usize {
552        1 + self.args.iter().map(Node::node_count).sum::<usize>()
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn scope_sets_are_sorted_sets_with_subset_and_flip() {
562        let e = ScopeSet::empty();
563        let a = e.insert(Scope(3)).insert(Scope(1)).insert(Scope(3));
564        assert_eq!(a.len(), 2);
565        assert!(e.is_subset_of(&a));
566        assert!(!a.is_subset_of(&e));
567        assert!(a.is_subset_of(&a));
568
569        let flipped = a.flip(Scope(1));
570        assert!(!flipped.contains(Scope(1)));
571        assert!(flipped.flip(Scope(1)).is_subset_of(&a) && a.is_subset_of(&flipped.flip(Scope(1))));
572
573        let b = e.insert(Scope(2));
574        assert!(!b.is_subset_of(&a));
575        assert!(!a.is_subset_of(&b));
576    }
577
578    #[test]
579    fn flipping_a_scope_over_a_tree_is_an_involution() {
580        let n = Node::form(
581            "def",
582            vec![Node::sym("x", Span::NONE), Node::sym("y", Span::NONE)],
583            Span::NONE,
584        );
585        assert!(n
586            .flip_scope(Scope(7))
587            .flip_scope(Scope(7))
588            .structurally_eq(&n));
589        assert!(!n.flip_scope(Scope(7)).structurally_eq(&n));
590    }
591
592    #[test]
593    fn structural_equality_ignores_spans() {
594        let mut map = beck_diag::SourceMap::new();
595        let f = map.add("a", "xy");
596        let a = Node::sym("x", Span::new(f, 0..1));
597        let b = Node::sym("x", Span::new(f, 1..2));
598        assert!(a.structurally_eq(&b));
599    }
600}