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}