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    /// The ordinary `#` comments written around this node ([`crate::doc`]).
223    ///
224    /// Boxed and absent by default, because most nodes have none and every node pays for the
225    /// field. Metadata for the same reason `doc` is: a comment is not part of a node's identity,
226    /// so adding one does not invalidate a memo or move an interface digest.
227    pub comments: Option<Box<Comments>>,
228}
229
230/// The ordinary comments attached to one node, in the three positions a comment can hold.
231///
232/// A doc comment is a single run above a declaration and is [`Meta::doc`]. These are the rest, and
233/// they are kept because `beck fmt` prints from the tree: what the tree does not carry, the
234/// formatter deletes.
235#[derive(Clone, Debug, Default, PartialEq, Eq)]
236pub struct Comments {
237    /// Full-line comments immediately above, in source order.
238    pub before: Vec<Arc<str>>,
239    /// A comment at the end of this node's own line.
240    pub trailing: Option<Arc<str>>,
241    /// Full-line comments below it with nothing after them in the block — the end of a body, or
242    /// the end of the file. They attach *backwards* because there is no node beneath to hold them.
243    pub after: Vec<Arc<str>>,
244}
245
246impl Meta {
247    pub fn at(span: Span) -> Meta {
248        Meta {
249            span,
250            expansion: Vec::new(),
251            doc: None,
252            comments: None,
253        }
254    }
255}
256
257/// Structural equality, ignoring spans and expansion chains.
258///
259/// Salsa needs `Eq` to decide whether a re-executed query actually produced a different value, and
260/// *formatting is explicitly not part of a `Node`'s identity* (§2.2) — so equality is exactly
261/// [`Node::structurally_eq`], and a re-parse that moved a span does not invalidate anything
262/// downstream.
263impl PartialEq for Node {
264    fn eq(&self, other: &Self) -> bool {
265        self.structurally_eq(other)
266    }
267}
268
269impl Eq for Node {}
270
271#[derive(Clone, Debug)]
272pub struct Node {
273    pub head: Head,
274    pub args: Vec<Node>,
275    /// Whether this node was *written as an application*.
276    ///
277    /// §2.2's model has `args: list[Node]`, which leaves `(params)` and `params` indistinguishable
278    /// — and an empty parameter list is not the same thing as a reference to a variable called
279    /// `params`. Elixir solves this by giving a variable `nil` args where a call has a list; this
280    /// is the same distinction with a cheaper representation.
281    pub applied: bool,
282    pub meta: Meta,
283}
284
285/// The reserved heads. Named constants rather than string literals scattered through the compiler:
286/// a typo in `"paramss"` would otherwise be a silently-unmatched form.
287pub mod sym {
288    pub const MODULE: &str = "module";
289    pub const DEF: &str = "def";
290    pub const PARAMS: &str = "params";
291    /// A `def`'s type parameters — `def map[T, U](…)`. Always present on a `def`, empty when the
292    /// definition is monomorphic, so that the form has one shape (`docs/27` §27.2).
293    pub const TYPARAMS: &str = "typarams";
294    /// `*rest` inside a list — the tail binder of a list pattern (`docs/27` §27.3).
295    pub const REST: &str = "rest";
296    pub const RETURNS: &str = "returns";
297    pub const ANNOT: &str = ":";
298    pub const FN: &str = "fn";
299    pub const CALL: &str = "call";
300    pub const DOT: &str = ".";
301    pub const IF: &str = "if";
302    pub const LET: &str = "let";
303    pub const VAR: &str = "var";
304    pub const SET: &str = "set";
305    pub const DO: &str = "do";
306    pub const RETURN: &str = "return";
307    pub const MATCH: &str = "match";
308    pub const CASE: &str = "case";
309    pub const FOR: &str = "for";
310    pub const WHILE: &str = "while";
311    pub const LIST: &str = "list";
312    pub const MAP: &str = "map-lit";
313    pub const RECORD: &str = "record";
314    pub const MODEL: &str = "model";
315    pub const UNION: &str = "union";
316    pub const VARIANT: &str = "variant";
317    pub const FIELD: &str = "field";
318    pub const TYPE: &str = "type";
319    pub const NEWTYPE: &str = "newtype";
320    pub const TRAIT: &str = "trait";
321    pub const IMPL: &str = "impl";
322    pub const IMPORT: &str = "import";
323    pub const MACRO: &str = "macro";
324    /// `typed macro f(x):` — a macro the **checker** expands, so its body can ask what a
325    /// call site's expressions were inferred to be (`docs/02` §2.4). A separate head
326    /// because the expander must leave it alone: an untyped expansion runs before there is
327    /// anything to ask.
328    pub const TYPED_MACRO: &str = "typed-macro";
329    pub const QUOTE: &str = "quote";
330
331    /// What an expansion that failed leaves where the call was.
332    ///
333    /// The alternative is leaving the call, and then the checker cannot find the macro's name and
334    /// says so — a second, contradictory error about a name that *was* found and refused. The
335    /// checker gives this a fresh type variable, so nothing downstream cascades either.
336    ///
337    /// **Spelled so that no program can write it.** A head the checker matches on and a name a
338    /// program may use are the same namespace, which is what [`RESERVED_FORMS`] exists to police;
339    /// a marker only the compiler ever builds should not cost a word, and `<` cannot begin an
340    /// identifier.
341    pub const REFUSED: &str = "<refused>";
342    pub const UNQUOTE: &str = "unquote";
343    pub const SPLICE: &str = "unquote-splicing";
344    pub const DECORATE: &str = "decorate";
345    pub const ON: &str = "on";
346    /// `@render(client)` — where a component's `view` runs, which is a different question from
347    /// `@on`: placement says a tier *may* run it, rendering says which one does.
348    pub const RENDER: &str = "render";
349    pub const UI: &str = "ui";
350    /// `raise e` — fail with a value. Performs `raises(T)`, where `T` is the value's type.
351    pub const RAISE: &str = "raise";
352    /// `row Name = a, b` — a name for a bundle of effect atoms, usable in a `uses` clause.
353    pub const ROW: &str = "row";
354    /// `identity = external(issuer="https://login.acme.com")` — who authenticates this program's
355    /// clients ([`docs/10`](../../../../../docs/10-decisions.md) D6).
356    ///
357    /// A declaration rather than a runtime flag because the issuer is a **peer**: §6.5 derives the
358    /// cluster's egress rule from the hosts a program names, and an issuer nobody wrote is a host
359    /// the deployment cannot be told about — the same argument
360    /// [`adr/0013`](../../../../../docs/adr/0013-the-host-of-an-outbound-call-is-written-at-the-call-site.md)
361    /// makes about `http_fetch`, arriving at the runtime's own outbound call rather than a
362    /// program's.
363    pub const IDENTITY: &str = "identity";
364    /// `try: block` — run the block and reify a failure as a `Result[T, E]`.
365    ///
366    /// The handler is a *form*, so it is lexically scoped by construction rather than by a search
367    /// at run time — which POPL 2019 gives the general argument for and
368    /// [`docs/38`](../../../../../docs/38-literature-survey.md) §38.4 adopts. In a language where
369    /// effects decide placement, an accidentally intercepted effect would be an accidental
370    /// *re-placement*.
371    pub const TRY: &str = "try";
372    /// `parallel: block` — a scope whose bindings are its children.
373    ///
374    /// The scope is a *form*, for the same reason [`TRY`] is: a handler that owns its children has
375    /// to own them lexically, and a nursery whose membership were decided at run time would be a
376    /// dynamic search with the same objection ([`docs/38`](../../../../../docs/38-literature-survey.md)
377    /// §38.4). Both halves of §38.4's shape are here — the children are the scope's `let`s, and
378    /// there is no handle for one to escape in.
379    pub const PARALLEL: &str = "parallel";
380    pub const KW_ARG: &str = "kw";
381    pub const WILDCARD: &str = "_";
382    pub const SERVICE: &str = "service";
383    pub const STYLES: &str = "styles";
384    pub const DOCUMENT: &str = "document";
385
386    // ---- §21.2's test construct. A test is a log, a command and an expectation, so each of the
387    // three is a form of its own rather than a call the checker would have to recognise by name.
388    pub const TEST: &str = "test";
389    /// `(property "name" (params …) (do …))` — §11.10's generated-input sibling of `test`.
390    pub const PROPERTY: &str = "property";
391    /// `(given <list[Event]> <actor?>)` — the state, as the log that reaches it.
392    pub const GIVEN: &str = "given";
393    /// `(when <session|_> <command> …)` — proposals through the real `validate`.
394    pub const WHEN: &str = "when";
395    /// `(at "ana" "/done")` — a session slot that names a **route** as well as an actor.
396    ///
397    /// The slot is one node with two shapes rather than two slots, because every form that has one
398    /// already treats it as optional: a bare string is `session("ana")` and this is
399    /// `session("ana", "/done")`. A second positional slot would have made "no actor, a route" and
400    /// "an actor, no route" the same arity and told them apart by which one happened to be a
401    /// string.
402    pub const AT: &str = "at";
403    /// `(expect <Bool>)`.
404    pub const EXPECT: &str = "expect";
405    /// `(expect-contains <Str> <actor?>)` — `expect page contains "milk"`. The subject is always
406    /// the rendered page, for the actor named or the test's default one.
407    pub const EXPECT_CONTAINS: &str = "expect-contains";
408    pub const EXPECT_SNAPSHOT: &str = "expect-snapshot";
409    /// `(expect-fold <list[Event]> <actor?>)` — `expect state == fold_of [ … ]`.
410    pub const EXPECT_FOLD: &str = "expect-fold";
411    /// `(expect-place <name> <tier>)` — answered without running anything.
412    pub const EXPECT_PLACE: &str = "expect-place";
413    /// `(expect-flow <Type> <tier>)`.
414    pub const EXPECT_FLOW: &str = "expect-flow";
415    /// `(expect-wire "previous.becki")`.
416    pub const EXPECT_WIRE: &str = "expect-wire";
417    /// `(expect-effect "<atom>" (none|once|times <n>|with <expr>))` — §21.3 rule 4: verification is
418    /// a query over what happened, not an expectation set in advance.
419    pub const EXPECT_EFFECT: &str = "expect-effect";
420    /// `(stub "<atom>" <value>)` — §21.3 rule 2: name the effect, not the shape.
421    pub const STUB: &str = "stub";
422    /// `(stub "<atom>" (arms (case …) …))` — §21.3 rule 3. The arms have no scrutinee written
423    /// because only the checker knows what performs the effect, and therefore what its argument is.
424    pub const STUB_ARMS: &str = "arms";
425
426    /// Names the checker matches as *forms* before it resolves anything.
427    ///
428    /// A definition called one of these would be shadowed by the form and never called — silently,
429    /// because `record(x)` is a well-formed record literal whatever `record` is bound to. The
430    /// checker rejects such a definition by name rather than letting it be quietly unreachable.
431    pub const RESERVED_FORMS: &[&str] = &[
432        CALL, DO, FN, IF, LIST, MAP, MATCH, QUOTE, RECORD, RETURN, SET, UNQUOTE, SPLICE, KW_ARG,
433        DOT,
434    ];
435}
436
437impl Node {
438    pub fn sym(name: impl AsRef<str>, span: Span) -> Node {
439        Node {
440            head: Head::Sym(Symbol::new(name)),
441            args: Vec::new(),
442            applied: false,
443            meta: Meta::at(span),
444        }
445    }
446
447    pub fn symbol(sym: Symbol, span: Span) -> Node {
448        Node {
449            head: Head::Sym(sym),
450            args: Vec::new(),
451            applied: false,
452            meta: Meta::at(span),
453        }
454    }
455
456    pub fn lit(lit: Lit, span: Span) -> Node {
457        Node {
458            head: Head::Lit(lit),
459            args: Vec::new(),
460            applied: false,
461            meta: Meta::at(span),
462        }
463    }
464
465    pub fn form(head: impl AsRef<str>, args: Vec<Node>, span: Span) -> Node {
466        Node {
467            head: Head::Sym(Symbol::new(head)),
468            args,
469            applied: true,
470            meta: Meta::at(span),
471        }
472    }
473
474    pub fn form_sym(head: Symbol, args: Vec<Node>, span: Span) -> Node {
475        Node {
476            head: Head::Sym(head),
477            args,
478            applied: true,
479            meta: Meta::at(span),
480        }
481    }
482
483    pub fn span(&self) -> Span {
484        self.meta.span
485    }
486
487    pub fn head_sym(&self) -> Option<&Symbol> {
488        match &self.head {
489            Head::Sym(s) => Some(s),
490            Head::Lit(_) => None,
491        }
492    }
493
494    pub fn head_name(&self) -> Option<&str> {
495        self.head_sym().map(|s| s.as_str())
496    }
497
498    pub fn as_lit(&self) -> Option<&Lit> {
499        match &self.head {
500            Head::Lit(l) if !self.applied => Some(l),
501            _ => None,
502        }
503    }
504
505    pub fn as_str_lit(&self) -> Option<&str> {
506        match self.as_lit() {
507            Some(Lit::Str(s)) => Some(s),
508            _ => None,
509        }
510    }
511
512    pub fn as_keyword(&self) -> Option<&str> {
513        match self.as_lit() {
514            Some(Lit::Keyword(k)) => Some(k),
515            _ => None,
516        }
517    }
518
519    /// A bare identifier: symbol head, not applied.
520    pub fn as_var(&self) -> Option<&Symbol> {
521        match &self.head {
522            Head::Sym(s) if !self.applied => Some(s),
523            _ => None,
524        }
525    }
526
527    /// An application with this exact head name — `(params)` counts, a bare `params` does not.
528    pub fn is_form(&self, head: &str) -> bool {
529        self.applied && self.head_name() == Some(head)
530    }
531
532    /// `(head ...)` or a bare `head`.
533    pub fn has_head(&self, head: &str) -> bool {
534        self.head_name() == Some(head)
535    }
536
537    pub fn arg(&self, i: usize) -> Option<&Node> {
538        self.args.get(i)
539    }
540
541    /// Structural equality ignoring spans and expansion chains — what tests and the round-trip
542    /// property compare, since formatting is explicitly not part of a `Node`'s identity.
543    pub fn structurally_eq(&self, other: &Node) -> bool {
544        let heads = match (&self.head, &other.head) {
545            (Head::Sym(a), Head::Sym(b)) => a.name == b.name && a.scopes == b.scopes,
546            (Head::Lit(a), Head::Lit(b)) => a == b,
547            _ => false,
548        };
549        heads
550            && self.applied == other.applied
551            && self.args.len() == other.args.len()
552            && self
553                .args
554                .iter()
555                .zip(&other.args)
556                .all(|(a, b)| a.structurally_eq(b))
557    }
558
559    /// Rewrite every symbol in the tree. The expander's workhorse.
560    pub fn map_symbols(&self, f: &mut impl FnMut(&Symbol) -> Symbol) -> Node {
561        let head = match &self.head {
562            Head::Sym(s) => Head::Sym(f(s)),
563            Head::Lit(l) => Head::Lit(l.clone()),
564        };
565        Node {
566            head,
567            args: self.args.iter().map(|a| a.map_symbols(f)).collect(),
568            applied: self.applied,
569            meta: self.meta.clone(),
570        }
571    }
572
573    /// Add a scope to every symbol in the tree.
574    pub fn add_scope(&self, s: Scope) -> Node {
575        self.map_symbols(&mut |sym| sym.with_scopes(sym.scopes.insert(s)))
576    }
577
578    /// Flip a scope on every symbol in the tree (see [`ScopeSet::flip`]).
579    pub fn flip_scope(&self, s: Scope) -> Node {
580        self.map_symbols(&mut |sym| sym.with_scopes(sym.scopes.flip(s)))
581    }
582
583    /// Record that this subtree came out of a macro, for §4.5's expansion chain in diagnostics.
584    pub fn with_expansion(&self, name: Arc<str>, at: Span) -> Node {
585        let mut meta = self.meta.clone();
586        meta.expansion.push((name.clone(), at));
587        Node {
588            head: self.head.clone(),
589            args: self
590                .args
591                .iter()
592                .map(|a| a.with_expansion(name.clone(), at))
593                .collect(),
594            applied: self.applied,
595            meta,
596        }
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603
604    #[test]
605    fn scope_sets_are_sorted_sets_with_subset_and_flip() {
606        let e = ScopeSet::empty();
607        let a = e.insert(Scope(3)).insert(Scope(1)).insert(Scope(3));
608        assert_eq!(a.len(), 2);
609        assert!(e.is_subset_of(&a));
610        assert!(!a.is_subset_of(&e));
611        assert!(a.is_subset_of(&a));
612
613        let flipped = a.flip(Scope(1));
614        assert!(!flipped.contains(Scope(1)));
615        assert!(flipped.flip(Scope(1)).is_subset_of(&a) && a.is_subset_of(&flipped.flip(Scope(1))));
616
617        let b = e.insert(Scope(2));
618        assert!(!b.is_subset_of(&a));
619        assert!(!a.is_subset_of(&b));
620    }
621
622    #[test]
623    fn flipping_a_scope_over_a_tree_is_an_involution() {
624        let n = Node::form(
625            "def",
626            vec![Node::sym("x", Span::NONE), Node::sym("y", Span::NONE)],
627            Span::NONE,
628        );
629        assert!(n
630            .flip_scope(Scope(7))
631            .flip_scope(Scope(7))
632            .structurally_eq(&n));
633        assert!(!n.flip_scope(Scope(7)).structurally_eq(&n));
634    }
635
636    #[test]
637    fn structural_equality_ignores_spans() {
638        let mut map = beck_diag::SourceMap::new();
639        let f = map.add("a", "xy");
640        let a = Node::sym("x", Span::new(f, 0..1));
641        let b = Node::sym("x", Span::new(f, 1..2));
642        assert!(a.structurally_eq(&b));
643    }
644}