Skip to main content

graphcal_compiler/syntax/ast/
format_equivalent.rs

1//! Format-insensitive structural equality for the [`Raw`] syntax tree.
2//!
3//! The formatter rewrites source text and must then guarantee that it changed
4//! only *formatting* — never the program. [`FormatEquivalent`] is the single
5//! place that defines what "same program" means at the syntax level: it
6//! compares two [`File<Raw>`](crate::syntax::ast::File) trees while ignoring
7//! source spans.
8//!
9//! # Why a dedicated trait instead of `PartialEq`
10//!
11//! Spans are load-bearing for diagnostics, so the AST's normal equality (where
12//! present) considers them — two `Spanned` values that differ only in span are
13//! deliberately *not* `PartialEq`-equal. The formatter needs the opposite
14//! relation: trees that differ *only* in formatting are equivalent. Giving that
15//! relation its own trait keeps the "ignore spans" decision in one auditable
16//! location instead of scattering span normalization or ad-hoc `==` overrides
17//! through the codebase.
18//!
19//! # Extension point for future formatter transformations
20//!
21//! The formatter may eventually reorder nodes — e.g. sorting declarations, or
22//! canonicalizing the order of map entries. When that happens, the *only* code
23//! that changes is the relevant impl in this module: [`File`]'s impl would
24//! compare declaration multisets instead of zipped sequences, and so on. The
25//! formatter's self-check, and every other caller, stays untouched. This module
26//! intentionally owns the definition of formatter equivalence so that
27//! flexibility lives in one place.
28//!
29//! # Completeness is compiler-enforced
30//!
31//! Every impl destructures its type *exhaustively* — no `..` rest patterns and
32//! no `_ =>` variant wildcards — binding span-only fields to `_`. Adding a
33//! field or a variant to the AST therefore fails to compile here until it is
34//! accounted for. The equivalence check can never silently go stale and miss a
35//! meaning-changing edit.
36
37use crate::syntax::ast::{
38    AssertBody, AssertDecl, Attribute, AttributeArg, BaseDimDecl, ConstNodeDecl, DagDecl, DeclKind,
39    Declaration, DimDecl, DimExpr, DimExprItem, DimTerm, DomainBound, Encoding, Expr, ExprKind,
40    FieldDecl, FieldInit, FigureDecl, File, ForBinding, ForBindingIndex, GenericArg, GenericParam,
41    Ident, IdentPath, ImportDecl, ImportItem, ImportKind, IncludeDecl, IndexArg, IndexDecl,
42    IndexDeclKind, IndexExpr, LayerDecl, MapEntry, MapEntryKey, MarkSpec, MatchArm, MatchPattern,
43    ModulePath, MultiDataRow, MultiDecl, MultiDeclSharedAxes, MultiDeclSlice, MultiDeclSlot,
44    MultiHeaderCell, MultiSlotAxis, MultiSlotColumnSpan, NatExpr, NodeDecl, ParamBinding,
45    ParamDecl, PatternBinding, PlotDecl, PlotField, RawDeclSugar, RawExprSugar, TableIndexSpec,
46    TypeDecl, TypeDeclBody, TypeExpr, TypeExprKind, UnionMember, UnitDecl, UnitDef, UnitExpr,
47    UnitExprItem, UnresolvedRef,
48};
49use crate::syntax::names::{
50    ConstructorName, DeclName, DimName, FieldName, GenericParamName, IndexName, IndexVariantName,
51    ModuleAliasName, NamePath, PlotPropertyName, ScopedName, StructTypeName, UnitName,
52};
53use crate::syntax::non_empty::NonEmpty;
54use crate::syntax::span::Spanned;
55
56/// Structural equality of two [`Raw`](crate::syntax::phase::Raw) syntax trees
57/// modulo formatting — currently, modulo source spans.
58///
59/// Returns `true` when `self` and `other` denote the same program. This is the
60/// invariant the formatter must uphold: formatting changes spans (and, in the
61/// future, possibly node order) but never meaning.
62pub trait FormatEquivalent {
63    /// Returns `true` if `self` and `other` are equivalent up to formatting.
64    fn format_equivalent(&self, other: &Self) -> bool;
65}
66
67// ---------------------------------------------------------------------------
68// Leaves: types that carry no spans, compared by their normal equality.
69// ---------------------------------------------------------------------------
70
71/// Implements [`FormatEquivalent`] via `PartialEq` for span-free leaf types.
72///
73/// These types contain no source positions, so format-equivalence collapses to
74/// ordinary equality. Restricting this shortcut to a hand-listed set keeps
75/// span-bearing types from accidentally opting into a span-sensitive compare.
76macro_rules! format_equivalent_via_eq {
77    ($($t:ty),+ $(,)?) => {
78        $(impl FormatEquivalent for $t {
79            fn format_equivalent(&self, other: &Self) -> bool {
80                self == other
81            }
82        })+
83    };
84}
85
86format_equivalent_via_eq!(
87    bool,
88    i32,
89    i64,
90    crate::syntax::dimension::Rational,
91    u64,
92    usize,
93    String,
94    // Identifier newtypes — written identity only, never a span.
95    DeclName,
96    DimName,
97    UnitName,
98    IndexName,
99    IndexVariantName,
100    FieldName,
101    ConstructorName,
102    StructTypeName,
103    GenericParamName,
104    PlotPropertyName,
105    ScopedName,
106    crate::syntax::names::LocalName,
107    NamePath,
108    ModuleAliasName,
109    crate::syntax::names::UnitRef,
110    // Closed enums with no payload spans.
111    crate::syntax::ast::BinOp,
112    crate::syntax::ast::UnaryOp,
113    crate::syntax::ast::MulDivOp,
114    crate::syntax::ast::Visibility,
115    crate::syntax::ast::BindableVisibility,
116    crate::syntax::ast::UnitConstness,
117    crate::syntax::ast::MarkType,
118    crate::syntax::ast::EncodingChannel,
119    crate::syntax::ast::MultiSlotKind,
120    crate::syntax::ast::GenericConstraint,
121    crate::syntax::ast::DomainBoundKind,
122    crate::syntax::ast::ImportItemNamespace,
123    crate::syntax::ast::MapEntryIndex,
124);
125
126/// Numeric literals compare bit-for-bit.
127///
128/// Bit comparison (rather than `==`) makes the relation reflexive even for the
129/// degenerate `NaN` case and sidesteps the `clippy::float_cmp` lint — two
130/// literals are format-equivalent exactly when they are the same literal.
131impl FormatEquivalent for f64 {
132    fn format_equivalent(&self, other: &Self) -> bool {
133        self.to_bits() == other.to_bits()
134    }
135}
136
137// ---------------------------------------------------------------------------
138// Generic containers.
139// ---------------------------------------------------------------------------
140
141impl<T: FormatEquivalent> FormatEquivalent for Box<T> {
142    fn format_equivalent(&self, other: &Self) -> bool {
143        (**self).format_equivalent(&**other)
144    }
145}
146
147impl<T: FormatEquivalent> FormatEquivalent for Option<T> {
148    fn format_equivalent(&self, other: &Self) -> bool {
149        match (self, other) {
150            (Some(a), Some(b)) => a.format_equivalent(b),
151            (None, None) => true,
152            (Some(_), None) | (None, Some(_)) => false,
153        }
154    }
155}
156
157impl<T: FormatEquivalent> FormatEquivalent for [T] {
158    fn format_equivalent(&self, other: &Self) -> bool {
159        self.len() == other.len()
160            && self
161                .iter()
162                .zip(other.iter())
163                .all(|(a, b)| a.format_equivalent(b))
164    }
165}
166
167impl<T: FormatEquivalent> FormatEquivalent for Vec<T> {
168    fn format_equivalent(&self, other: &Self) -> bool {
169        self.as_slice().format_equivalent(other.as_slice())
170    }
171}
172
173impl<T: FormatEquivalent> FormatEquivalent for NonEmpty<T> {
174    fn format_equivalent(&self, other: &Self) -> bool {
175        self.as_slice().format_equivalent(other.as_slice())
176    }
177}
178
179/// A spanned value is equivalent to another when their payloads are — the span
180/// is the formatting difference this whole trait exists to ignore.
181impl<T: FormatEquivalent> FormatEquivalent for Spanned<T> {
182    fn format_equivalent(&self, other: &Self) -> bool {
183        let Self { value, span: _ } = self;
184        let Self {
185            value: other_value,
186            span: _,
187        } = other;
188        value.format_equivalent(other_value)
189    }
190}
191
192// ---------------------------------------------------------------------------
193// `common.rs` nodes.
194// ---------------------------------------------------------------------------
195
196impl FormatEquivalent for Ident {
197    fn format_equivalent(&self, other: &Self) -> bool {
198        let Self { name, span: _ } = self;
199        let Self {
200            name: other_name,
201            span: _,
202        } = other;
203        name == other_name
204    }
205}
206
207impl FormatEquivalent for Attribute {
208    fn format_equivalent(&self, other: &Self) -> bool {
209        let Self {
210            name,
211            args,
212            span: _,
213        } = self;
214        let Self {
215            name: other_name,
216            args: other_args,
217            span: _,
218        } = other;
219        name.format_equivalent(other_name) && args.format_equivalent(other_args)
220    }
221}
222
223impl FormatEquivalent for AttributeArg {
224    fn format_equivalent(&self, other: &Self) -> bool {
225        match self {
226            Self::Path { segments, span: _ } => {
227                let Self::Path {
228                    segments: other_segments,
229                    span: _,
230                } = other
231                else {
232                    return false;
233                };
234                segments.format_equivalent(other_segments)
235            }
236            Self::RangeStep { step, span: _ } => {
237                let Self::RangeStep {
238                    step: other_step,
239                    span: _,
240                } = other
241                else {
242                    return false;
243                };
244                step == other_step
245            }
246            Self::Group { elements, span: _ } => {
247                let Self::Group {
248                    elements: other_elements,
249                    span: _,
250                } = other
251                else {
252                    return false;
253                };
254                elements.format_equivalent(other_elements)
255            }
256        }
257    }
258}
259
260impl FormatEquivalent for ImportKind {
261    fn format_equivalent(&self, other: &Self) -> bool {
262        match self {
263            Self::Selective(items) => {
264                let Self::Selective(other_items) = other else {
265                    return false;
266                };
267                items.format_equivalent(other_items)
268            }
269            Self::Module { alias } => {
270                let Self::Module { alias: other_alias } = other else {
271                    return false;
272                };
273                alias.format_equivalent(other_alias)
274            }
275        }
276    }
277}
278
279impl FormatEquivalent for ModulePath {
280    fn format_equivalent(&self, other: &Self) -> bool {
281        let Self { segments, span: _ } = self;
282        let Self {
283            segments: other_segments,
284            span: _,
285        } = other;
286        segments.format_equivalent(other_segments)
287    }
288}
289
290impl FormatEquivalent for ImportItem {
291    fn format_equivalent(&self, other: &Self) -> bool {
292        let Self {
293            attributes,
294            is_pub,
295            namespace,
296            name,
297            alias,
298        } = self;
299        let Self {
300            attributes: other_attributes,
301            is_pub: other_is_pub,
302            namespace: other_namespace,
303            name: other_name,
304            alias: other_alias,
305        } = other;
306        attributes.format_equivalent(other_attributes)
307            && is_pub.format_equivalent(other_is_pub)
308            && namespace.format_equivalent(other_namespace)
309            && name.format_equivalent(other_name)
310            && alias.format_equivalent(other_alias)
311    }
312}
313
314// ---------------------------------------------------------------------------
315// `decl.rs` nodes.
316// ---------------------------------------------------------------------------
317
318impl FormatEquivalent for File {
319    fn format_equivalent(&self, other: &Self) -> bool {
320        // Extension point: to allow the formatter to reorder declarations,
321        // compare `declarations` as a multiset here instead of positionally.
322        let Self { declarations } = self;
323        let Self {
324            declarations: other_declarations,
325        } = other;
326        declarations.format_equivalent(other_declarations)
327    }
328}
329
330impl FormatEquivalent for Declaration {
331    fn format_equivalent(&self, other: &Self) -> bool {
332        let Self {
333            attributes,
334            kind,
335            span: _,
336        } = self;
337        let Self {
338            attributes: other_attributes,
339            kind: other_kind,
340            span: _,
341        } = other;
342        attributes.format_equivalent(other_attributes) && kind.format_equivalent(other_kind)
343    }
344}
345
346impl FormatEquivalent for DeclKind {
347    fn format_equivalent(&self, other: &Self) -> bool {
348        match self {
349            Self::Param(a) => {
350                let Self::Param(b) = other else { return false };
351                a.format_equivalent(b)
352            }
353            Self::Node(a) => {
354                let Self::Node(b) = other else { return false };
355                a.format_equivalent(b)
356            }
357            Self::ConstNode(a) => {
358                let Self::ConstNode(b) = other else {
359                    return false;
360                };
361                a.format_equivalent(b)
362            }
363            Self::BaseDimension(a) => {
364                let Self::BaseDimension(b) = other else {
365                    return false;
366                };
367                a.format_equivalent(b)
368            }
369            Self::Dimension(a) => {
370                let Self::Dimension(b) = other else {
371                    return false;
372                };
373                a.format_equivalent(b)
374            }
375            Self::Unit(a) => {
376                let Self::Unit(b) = other else { return false };
377                a.format_equivalent(b)
378            }
379            Self::Type(a) => {
380                let Self::Type(b) = other else { return false };
381                a.format_equivalent(b)
382            }
383            Self::Index(a) => {
384                let Self::Index(b) = other else { return false };
385                a.format_equivalent(b)
386            }
387            Self::Import(a) => {
388                let Self::Import(b) = other else { return false };
389                a.format_equivalent(b)
390            }
391            Self::Include(a) => {
392                let Self::Include(b) = other else {
393                    return false;
394                };
395                a.format_equivalent(b)
396            }
397            Self::Dag(a) => {
398                let Self::Dag(b) = other else { return false };
399                a.format_equivalent(b)
400            }
401            Self::Assert(a) => {
402                let Self::Assert(b) = other else { return false };
403                a.format_equivalent(b)
404            }
405            Self::Plot(a) => {
406                let Self::Plot(b) = other else { return false };
407                a.format_equivalent(b)
408            }
409            Self::Figure(a) => {
410                let Self::Figure(b) = other else { return false };
411                a.format_equivalent(b)
412            }
413            Self::Layer(a) => {
414                let Self::Layer(b) = other else { return false };
415                a.format_equivalent(b)
416            }
417            Self::Sugar(a) => {
418                let Self::Sugar(b) = other else { return false };
419                a.format_equivalent(b)
420            }
421        }
422    }
423}
424
425impl FormatEquivalent for RawDeclSugar {
426    fn format_equivalent(&self, other: &Self) -> bool {
427        let Self::Multi(a) = self;
428        let Self::Multi(b) = other;
429        a.format_equivalent(b)
430    }
431}
432
433impl FormatEquivalent for ParamDecl {
434    fn format_equivalent(&self, other: &Self) -> bool {
435        let Self {
436            name,
437            type_ann,
438            value,
439        } = self;
440        let Self {
441            name: other_name,
442            type_ann: other_type_ann,
443            value: other_value,
444        } = other;
445        name.format_equivalent(other_name)
446            && type_ann.format_equivalent(other_type_ann)
447            && value.format_equivalent(other_value)
448    }
449}
450
451impl FormatEquivalent for NodeDecl {
452    fn format_equivalent(&self, other: &Self) -> bool {
453        let Self {
454            visibility,
455            name,
456            type_ann,
457            value,
458        } = self;
459        let Self {
460            visibility: other_visibility,
461            name: other_name,
462            type_ann: other_type_ann,
463            value: other_value,
464        } = other;
465        visibility.format_equivalent(other_visibility)
466            && name.format_equivalent(other_name)
467            && type_ann.format_equivalent(other_type_ann)
468            && value.format_equivalent(other_value)
469    }
470}
471
472impl FormatEquivalent for ConstNodeDecl {
473    fn format_equivalent(&self, other: &Self) -> bool {
474        let Self {
475            visibility,
476            name,
477            type_ann,
478            value,
479        } = self;
480        let Self {
481            visibility: other_visibility,
482            name: other_name,
483            type_ann: other_type_ann,
484            value: other_value,
485        } = other;
486        visibility.format_equivalent(other_visibility)
487            && name.format_equivalent(other_name)
488            && type_ann.format_equivalent(other_type_ann)
489            && value.format_equivalent(other_value)
490    }
491}
492
493impl FormatEquivalent for BaseDimDecl {
494    fn format_equivalent(&self, other: &Self) -> bool {
495        let Self { visibility, name } = self;
496        let Self {
497            visibility: other_visibility,
498            name: other_name,
499        } = other;
500        visibility.format_equivalent(other_visibility) && name.format_equivalent(other_name)
501    }
502}
503
504impl FormatEquivalent for DimDecl {
505    fn format_equivalent(&self, other: &Self) -> bool {
506        let Self {
507            visibility,
508            name,
509            definition,
510        } = self;
511        let Self {
512            visibility: other_visibility,
513            name: other_name,
514            definition: other_definition,
515        } = other;
516        visibility.format_equivalent(other_visibility)
517            && name.format_equivalent(other_name)
518            && definition.format_equivalent(other_definition)
519    }
520}
521
522impl FormatEquivalent for UnitDecl {
523    fn format_equivalent(&self, other: &Self) -> bool {
524        let Self {
525            visibility,
526            constness,
527            name,
528            dim_type,
529            definition,
530        } = self;
531        let Self {
532            visibility: other_visibility,
533            constness: other_constness,
534            name: other_name,
535            dim_type: other_dim_type,
536            definition: other_definition,
537        } = other;
538        visibility.format_equivalent(other_visibility)
539            && constness.format_equivalent(other_constness)
540            && name.format_equivalent(other_name)
541            && dim_type.format_equivalent(other_dim_type)
542            && definition.format_equivalent(other_definition)
543    }
544}
545
546impl FormatEquivalent for UnitDef {
547    fn format_equivalent(&self, other: &Self) -> bool {
548        let Self {
549            scale_expr,
550            unit_expr,
551            span: _,
552        } = self;
553        let Self {
554            scale_expr: other_scale_expr,
555            unit_expr: other_unit_expr,
556            span: _,
557        } = other;
558        scale_expr.format_equivalent(other_scale_expr)
559            && unit_expr.format_equivalent(other_unit_expr)
560    }
561}
562
563impl FormatEquivalent for TypeDecl {
564    fn format_equivalent(&self, other: &Self) -> bool {
565        let Self {
566            visibility,
567            name,
568            generic_params,
569            body,
570        } = self;
571        let Self {
572            visibility: other_visibility,
573            name: other_name,
574            generic_params: other_generic_params,
575            body: other_body,
576        } = other;
577        visibility.format_equivalent(other_visibility)
578            && name.format_equivalent(other_name)
579            && generic_params.format_equivalent(other_generic_params)
580            && body.format_equivalent(other_body)
581    }
582}
583
584impl FormatEquivalent for TypeDeclBody {
585    fn format_equivalent(&self, other: &Self) -> bool {
586        match self {
587            Self::Required => matches!(other, Self::Required),
588            Self::Constructors(members) => {
589                let Self::Constructors(other_members) = other else {
590                    return false;
591                };
592                members.format_equivalent(other_members)
593            }
594        }
595    }
596}
597
598impl FormatEquivalent for UnionMember {
599    fn format_equivalent(&self, other: &Self) -> bool {
600        let Self {
601            name,
602            payload,
603            span: _,
604        } = self;
605        let Self {
606            name: other_name,
607            payload: other_payload,
608            span: _,
609        } = other;
610        name.format_equivalent(other_name) && payload.format_equivalent(other_payload)
611    }
612}
613
614impl FormatEquivalent for FieldDecl {
615    fn format_equivalent(&self, other: &Self) -> bool {
616        let Self { name, type_ann } = self;
617        let Self {
618            name: other_name,
619            type_ann: other_type_ann,
620        } = other;
621        name.format_equivalent(other_name) && type_ann.format_equivalent(other_type_ann)
622    }
623}
624
625impl FormatEquivalent for IndexDecl {
626    fn format_equivalent(&self, other: &Self) -> bool {
627        let Self {
628            visibility,
629            name,
630            kind,
631        } = self;
632        let Self {
633            visibility: other_visibility,
634            name: other_name,
635            kind: other_kind,
636        } = other;
637        visibility.format_equivalent(other_visibility)
638            && name.format_equivalent(other_name)
639            && kind.format_equivalent(other_kind)
640    }
641}
642
643impl FormatEquivalent for IndexDeclKind {
644    fn format_equivalent(&self, other: &Self) -> bool {
645        match self {
646            Self::Named { variants } => {
647                let Self::Named {
648                    variants: other_variants,
649                } = other
650                else {
651                    return false;
652                };
653                variants.format_equivalent(other_variants)
654            }
655            Self::Range { start, end, step } => {
656                let Self::Range {
657                    start: other_start,
658                    end: other_end,
659                    step: other_step,
660                } = other
661                else {
662                    return false;
663                };
664                start.format_equivalent(other_start)
665                    && end.format_equivalent(other_end)
666                    && step.format_equivalent(other_step)
667            }
668            Self::RequiredNamed => matches!(other, Self::RequiredNamed),
669            Self::RequiredRange { dimension } => {
670                let Self::RequiredRange {
671                    dimension: other_dimension,
672                } = other
673                else {
674                    return false;
675                };
676                dimension.format_equivalent(other_dimension)
677            }
678        }
679    }
680}
681
682impl FormatEquivalent for GenericParam {
683    fn format_equivalent(&self, other: &Self) -> bool {
684        let Self {
685            name,
686            constraint,
687            default,
688        } = self;
689        let Self {
690            name: other_name,
691            constraint: other_constraint,
692            default: other_default,
693        } = other;
694        name.format_equivalent(other_name)
695            && constraint.format_equivalent(other_constraint)
696            && default.format_equivalent(other_default)
697    }
698}
699
700impl FormatEquivalent for ImportDecl {
701    fn format_equivalent(&self, other: &Self) -> bool {
702        let Self {
703            visibility,
704            path,
705            kind,
706        } = self;
707        let Self {
708            visibility: other_visibility,
709            path: other_path,
710            kind: other_kind,
711        } = other;
712        visibility.format_equivalent(other_visibility)
713            && path.format_equivalent(other_path)
714            && kind.format_equivalent(other_kind)
715    }
716}
717
718impl FormatEquivalent for IncludeDecl {
719    fn format_equivalent(&self, other: &Self) -> bool {
720        let Self {
721            visibility,
722            path,
723            param_bindings,
724            kind,
725        } = self;
726        let Self {
727            visibility: other_visibility,
728            path: other_path,
729            param_bindings: other_param_bindings,
730            kind: other_kind,
731        } = other;
732        visibility.format_equivalent(other_visibility)
733            && path.format_equivalent(other_path)
734            && param_bindings.format_equivalent(other_param_bindings)
735            && kind.format_equivalent(other_kind)
736    }
737}
738
739impl FormatEquivalent for DagDecl {
740    fn format_equivalent(&self, other: &Self) -> bool {
741        let Self {
742            visibility,
743            name,
744            body,
745            span: _,
746        } = self;
747        let Self {
748            visibility: other_visibility,
749            name: other_name,
750            body: other_body,
751            span: _,
752        } = other;
753        visibility.format_equivalent(other_visibility)
754            && name.format_equivalent(other_name)
755            && body.format_equivalent(other_body)
756    }
757}
758
759impl FormatEquivalent for AssertDecl {
760    fn format_equivalent(&self, other: &Self) -> bool {
761        let Self {
762            visibility,
763            name,
764            body,
765        } = self;
766        let Self {
767            visibility: other_visibility,
768            name: other_name,
769            body: other_body,
770        } = other;
771        visibility.format_equivalent(other_visibility)
772            && name.format_equivalent(other_name)
773            && body.format_equivalent(other_body)
774    }
775}
776
777impl FormatEquivalent for AssertBody {
778    fn format_equivalent(&self, other: &Self) -> bool {
779        match self {
780            Self::Expr(expr) => {
781                let Self::Expr(other_expr) = other else {
782                    return false;
783                };
784                expr.format_equivalent(other_expr)
785            }
786            Self::Tolerance {
787                actual,
788                expected,
789                tolerance,
790                is_relative,
791            } => {
792                let Self::Tolerance {
793                    actual: other_actual,
794                    expected: other_expected,
795                    tolerance: other_tolerance,
796                    is_relative: other_is_relative,
797                } = other
798                else {
799                    return false;
800                };
801                actual.format_equivalent(other_actual)
802                    && expected.format_equivalent(other_expected)
803                    && tolerance.format_equivalent(other_tolerance)
804                    && is_relative.format_equivalent(other_is_relative)
805            }
806        }
807    }
808}
809
810impl FormatEquivalent for PlotDecl {
811    fn format_equivalent(&self, other: &Self) -> bool {
812        let Self {
813            visibility,
814            name,
815            mark,
816            encodings,
817            properties,
818        } = self;
819        let Self {
820            visibility: other_visibility,
821            name: other_name,
822            mark: other_mark,
823            encodings: other_encodings,
824            properties: other_properties,
825        } = other;
826        visibility.format_equivalent(other_visibility)
827            && name.format_equivalent(other_name)
828            && mark.format_equivalent(other_mark)
829            && encodings.format_equivalent(other_encodings)
830            && properties.format_equivalent(other_properties)
831    }
832}
833
834impl FormatEquivalent for MarkSpec {
835    fn format_equivalent(&self, other: &Self) -> bool {
836        let Self {
837            mark_type,
838            mark_type_span: _,
839            properties,
840            span: _,
841        } = self;
842        let Self {
843            mark_type: other_mark_type,
844            mark_type_span: _,
845            properties: other_properties,
846            span: _,
847        } = other;
848        mark_type.format_equivalent(other_mark_type)
849            && properties.format_equivalent(other_properties)
850    }
851}
852
853impl FormatEquivalent for Encoding {
854    fn format_equivalent(&self, other: &Self) -> bool {
855        let Self {
856            channel,
857            channel_span: _,
858            value,
859            span: _,
860        } = self;
861        let Self {
862            channel: other_channel,
863            channel_span: _,
864            value: other_value,
865            span: _,
866        } = other;
867        channel.format_equivalent(other_channel) && value.format_equivalent(other_value)
868    }
869}
870
871impl FormatEquivalent for PlotField {
872    fn format_equivalent(&self, other: &Self) -> bool {
873        let Self {
874            name,
875            value,
876            span: _,
877        } = self;
878        let Self {
879            name: other_name,
880            value: other_value,
881            span: _,
882        } = other;
883        name.format_equivalent(other_name) && value.format_equivalent(other_value)
884    }
885}
886
887impl FormatEquivalent for FigureDecl {
888    fn format_equivalent(&self, other: &Self) -> bool {
889        let Self {
890            visibility,
891            name,
892            plot_names,
893            fields,
894        } = self;
895        let Self {
896            visibility: other_visibility,
897            name: other_name,
898            plot_names: other_plot_names,
899            fields: other_fields,
900        } = other;
901        visibility.format_equivalent(other_visibility)
902            && name.format_equivalent(other_name)
903            && plot_names.format_equivalent(other_plot_names)
904            && fields.format_equivalent(other_fields)
905    }
906}
907
908impl FormatEquivalent for LayerDecl {
909    fn format_equivalent(&self, other: &Self) -> bool {
910        let Self {
911            visibility,
912            name,
913            plot_names,
914            fields,
915        } = self;
916        let Self {
917            visibility: other_visibility,
918            name: other_name,
919            plot_names: other_plot_names,
920            fields: other_fields,
921        } = other;
922        visibility.format_equivalent(other_visibility)
923            && name.format_equivalent(other_name)
924            && plot_names.format_equivalent(other_plot_names)
925            && fields.format_equivalent(other_fields)
926    }
927}
928
929impl FormatEquivalent for MultiDecl {
930    fn format_equivalent(&self, other: &Self) -> bool {
931        let Self {
932            slots,
933            shared_axes,
934            slot_axes,
935            slices,
936            span: _,
937            table_expr_span: _,
938        } = self;
939        let Self {
940            slots: other_slots,
941            shared_axes: other_shared_axes,
942            slot_axes: other_slot_axes,
943            slices: other_slices,
944            span: _,
945            table_expr_span: _,
946        } = other;
947        slots.format_equivalent(other_slots)
948            && shared_axes.format_equivalent(other_shared_axes)
949            && slot_axes.format_equivalent(other_slot_axes)
950            && slices.format_equivalent(other_slices)
951    }
952}
953
954impl FormatEquivalent for MultiDeclSlot {
955    fn format_equivalent(&self, other: &Self) -> bool {
956        let Self {
957            visibility,
958            kind,
959            kind_span: _,
960            name,
961            type_ann,
962            header_span: _,
963        } = self;
964        let Self {
965            visibility: other_visibility,
966            kind: other_kind,
967            kind_span: _,
968            name: other_name,
969            type_ann: other_type_ann,
970            header_span: _,
971        } = other;
972        visibility.format_equivalent(other_visibility)
973            && kind.format_equivalent(other_kind)
974            && name.format_equivalent(other_name)
975            && type_ann.format_equivalent(other_type_ann)
976    }
977}
978
979impl FormatEquivalent for MultiSlotAxis {
980    fn format_equivalent(&self, other: &Self) -> bool {
981        match self {
982            Self::Underscore => matches!(other, Self::Underscore),
983            Self::Axis(axis) => {
984                let Self::Axis(other_axis) = other else {
985                    return false;
986                };
987                axis.format_equivalent(other_axis)
988            }
989        }
990    }
991}
992
993impl FormatEquivalent for MultiSlotColumnSpan {
994    fn format_equivalent(&self, other: &Self) -> bool {
995        match self {
996            Self::Single(col) => {
997                let Self::Single(other_col) = other else {
998                    return false;
999                };
1000                col.format_equivalent(other_col)
1001            }
1002            Self::Range {
1003                start,
1004                end,
1005                extra_axis,
1006            } => {
1007                let Self::Range {
1008                    start: other_start,
1009                    end: other_end,
1010                    extra_axis: other_extra_axis,
1011                } = other
1012                else {
1013                    return false;
1014                };
1015                start.format_equivalent(other_start)
1016                    && end.format_equivalent(other_end)
1017                    && extra_axis.format_equivalent(other_extra_axis)
1018            }
1019        }
1020    }
1021}
1022
1023impl FormatEquivalent for MultiDeclSlice {
1024    fn format_equivalent(&self, other: &Self) -> bool {
1025        let Self {
1026            prefix_keys,
1027            header_cells,
1028            header_span: _,
1029            column_layout,
1030            rows,
1031        } = self;
1032        let Self {
1033            prefix_keys: other_prefix_keys,
1034            header_cells: other_header_cells,
1035            header_span: _,
1036            column_layout: other_column_layout,
1037            rows: other_rows,
1038        } = other;
1039        prefix_keys.format_equivalent(other_prefix_keys)
1040            && header_cells.format_equivalent(other_header_cells)
1041            && column_layout.format_equivalent(other_column_layout)
1042            && rows.format_equivalent(other_rows)
1043    }
1044}
1045
1046impl FormatEquivalent for MultiHeaderCell {
1047    fn format_equivalent(&self, other: &Self) -> bool {
1048        match self {
1049            Self::Underscore { span: _ } => matches!(other, Self::Underscore { .. }),
1050            Self::Variant {
1051                axis,
1052                variant,
1053                span: _,
1054            } => {
1055                let Self::Variant {
1056                    axis: other_axis,
1057                    variant: other_variant,
1058                    span: _,
1059                } = other
1060                else {
1061                    return false;
1062                };
1063                axis.format_equivalent(other_axis) && variant.format_equivalent(other_variant)
1064            }
1065        }
1066    }
1067}
1068
1069impl FormatEquivalent for MultiDataRow {
1070    fn format_equivalent(&self, other: &Self) -> bool {
1071        let Self {
1072            label,
1073            values,
1074            span: _,
1075        } = self;
1076        let Self {
1077            label: other_label,
1078            values: other_values,
1079            span: _,
1080        } = other;
1081        label.format_equivalent(other_label) && values.format_equivalent(other_values)
1082    }
1083}
1084
1085// ---------------------------------------------------------------------------
1086// `value.rs` nodes.
1087// ---------------------------------------------------------------------------
1088
1089impl FormatEquivalent for TypeExpr {
1090    fn format_equivalent(&self, other: &Self) -> bool {
1091        let Self {
1092            kind,
1093            constraints,
1094            span: _,
1095        } = self;
1096        let Self {
1097            kind: other_kind,
1098            constraints: other_constraints,
1099            span: _,
1100        } = other;
1101        kind.format_equivalent(other_kind) && constraints.format_equivalent(other_constraints)
1102    }
1103}
1104
1105impl FormatEquivalent for TypeExprKind {
1106    fn format_equivalent(&self, other: &Self) -> bool {
1107        match self {
1108            Self::Dimensionless => matches!(other, Self::Dimensionless),
1109            Self::Bool => matches!(other, Self::Bool),
1110            Self::Int => matches!(other, Self::Int),
1111            Self::Datetime => matches!(other, Self::Datetime),
1112            Self::DatetimeApplication { type_args } => {
1113                let Self::DatetimeApplication {
1114                    type_args: other_type_args,
1115                } = other
1116                else {
1117                    return false;
1118                };
1119                type_args.format_equivalent(other_type_args)
1120            }
1121            Self::DimExpr(dim) => {
1122                let Self::DimExpr(other_dim) = other else {
1123                    return false;
1124                };
1125                dim.format_equivalent(other_dim)
1126            }
1127            Self::Indexed { base, indexes } => {
1128                let Self::Indexed {
1129                    base: other_base,
1130                    indexes: other_indexes,
1131                } = other
1132                else {
1133                    return false;
1134                };
1135                base.format_equivalent(other_base) && indexes.format_equivalent(other_indexes)
1136            }
1137            Self::TypeApplication { name, type_args } => {
1138                let Self::TypeApplication {
1139                    name: other_name,
1140                    type_args: other_type_args,
1141                } = other
1142                else {
1143                    return false;
1144                };
1145                name.format_equivalent(other_name) && type_args.format_equivalent(other_type_args)
1146            }
1147        }
1148    }
1149}
1150
1151impl FormatEquivalent for DomainBound {
1152    fn format_equivalent(&self, other: &Self) -> bool {
1153        let Self {
1154            kind,
1155            kind_span: _,
1156            value,
1157            span: _,
1158        } = self;
1159        let Self {
1160            kind: other_kind,
1161            kind_span: _,
1162            value: other_value,
1163            span: _,
1164        } = other;
1165        kind.format_equivalent(other_kind) && value.format_equivalent(other_value)
1166    }
1167}
1168
1169impl FormatEquivalent for IndexExpr {
1170    fn format_equivalent(&self, other: &Self) -> bool {
1171        match self {
1172            Self::Name(name) => {
1173                let Self::Name(other_name) = other else {
1174                    return false;
1175                };
1176                name.format_equivalent(other_name)
1177            }
1178            Self::NatExpr(nat) => {
1179                let Self::NatExpr(other_nat) = other else {
1180                    return false;
1181                };
1182                nat.format_equivalent(other_nat)
1183            }
1184        }
1185    }
1186}
1187
1188impl FormatEquivalent for DimExpr {
1189    fn format_equivalent(&self, other: &Self) -> bool {
1190        let Self { terms, span: _ } = self;
1191        let Self {
1192            terms: other_terms,
1193            span: _,
1194        } = other;
1195        terms.format_equivalent(other_terms)
1196    }
1197}
1198
1199impl FormatEquivalent for DimExprItem {
1200    fn format_equivalent(&self, other: &Self) -> bool {
1201        let Self { op, term } = self;
1202        let Self {
1203            op: other_op,
1204            term: other_term,
1205        } = other;
1206        op.format_equivalent(other_op) && term.format_equivalent(other_term)
1207    }
1208}
1209
1210impl FormatEquivalent for DimTerm {
1211    fn format_equivalent(&self, other: &Self) -> bool {
1212        let Self {
1213            name,
1214            power,
1215            span: _,
1216        } = self;
1217        let Self {
1218            name: other_name,
1219            power: other_power,
1220            span: _,
1221        } = other;
1222        name.format_equivalent(other_name) && power.format_equivalent(other_power)
1223    }
1224}
1225
1226impl FormatEquivalent for UnitExpr {
1227    fn format_equivalent(&self, other: &Self) -> bool {
1228        let Self { terms, span: _ } = self;
1229        let Self {
1230            terms: other_terms,
1231            span: _,
1232        } = other;
1233        terms.format_equivalent(other_terms)
1234    }
1235}
1236
1237impl FormatEquivalent for UnitExprItem {
1238    fn format_equivalent(&self, other: &Self) -> bool {
1239        let Self { op, name, power } = self;
1240        let Self {
1241            op: other_op,
1242            name: other_name,
1243            power: other_power,
1244        } = other;
1245        op.format_equivalent(other_op)
1246            && name.format_equivalent(other_name)
1247            && power.format_equivalent(other_power)
1248    }
1249}
1250
1251impl FormatEquivalent for UnresolvedRef {
1252    fn format_equivalent(&self, other: &Self) -> bool {
1253        let Self::Path(a) = self;
1254        let Self::Path(b) = other;
1255        a.format_equivalent(b)
1256    }
1257}
1258
1259impl FormatEquivalent for IdentPath {
1260    fn format_equivalent(&self, other: &Self) -> bool {
1261        let Self { segments } = self;
1262        let Self {
1263            segments: other_segments,
1264        } = other;
1265        segments.format_equivalent(other_segments)
1266    }
1267}
1268
1269impl FormatEquivalent for ParamBinding {
1270    fn format_equivalent(&self, other: &Self) -> bool {
1271        let Self {
1272            name,
1273            value,
1274            span: _,
1275        } = self;
1276        let Self {
1277            name: other_name,
1278            value: other_value,
1279            span: _,
1280        } = other;
1281        name.format_equivalent(other_name) && value.format_equivalent(other_value)
1282    }
1283}
1284
1285impl FormatEquivalent for TableIndexSpec {
1286    fn format_equivalent(&self, other: &Self) -> bool {
1287        match self {
1288            Self::Named(name) => {
1289                let Self::Named(other_name) = other else {
1290                    return false;
1291                };
1292                name.format_equivalent(other_name)
1293            }
1294            Self::NatRange(size, _span) => {
1295                let Self::NatRange(other_size, _other_span) = other else {
1296                    return false;
1297                };
1298                size.format_equivalent(other_size)
1299            }
1300        }
1301    }
1302}
1303
1304impl FormatEquivalent for MultiDeclSharedAxes {
1305    fn format_equivalent(&self, other: &Self) -> bool {
1306        // Private fields; compare through the public accessors. The shape
1307        // (slice axes + a distinguished row axis) is fixed, so positional
1308        // comparison is total.
1309        self.slice_axes().format_equivalent(other.slice_axes())
1310            && self.row_axis().format_equivalent(other.row_axis())
1311    }
1312}
1313
1314impl FormatEquivalent for MapEntryKey {
1315    fn format_equivalent(&self, other: &Self) -> bool {
1316        let Self { index, variant } = self;
1317        let Self {
1318            index: other_index,
1319            variant: other_variant,
1320        } = other;
1321        index.format_equivalent(other_index) && variant.format_equivalent(other_variant)
1322    }
1323}
1324
1325impl FormatEquivalent for MapEntry {
1326    fn format_equivalent(&self, other: &Self) -> bool {
1327        let Self { keys, value } = self;
1328        let Self {
1329            keys: other_keys,
1330            value: other_value,
1331        } = other;
1332        keys.format_equivalent(other_keys) && value.format_equivalent(other_value)
1333    }
1334}
1335
1336impl FormatEquivalent for ForBinding {
1337    fn format_equivalent(&self, other: &Self) -> bool {
1338        let Self { var, index } = self;
1339        let Self {
1340            var: other_var,
1341            index: other_index,
1342        } = other;
1343        var.format_equivalent(other_var) && index.format_equivalent(other_index)
1344    }
1345}
1346
1347impl FormatEquivalent for ForBindingIndex {
1348    fn format_equivalent(&self, other: &Self) -> bool {
1349        match self {
1350            Self::Named(name) => {
1351                let Self::Named(other_name) = other else {
1352                    return false;
1353                };
1354                name.format_equivalent(other_name)
1355            }
1356            Self::Range { arg, span: _ } => {
1357                let Self::Range {
1358                    arg: other_arg,
1359                    span: _,
1360                } = other
1361                else {
1362                    return false;
1363                };
1364                arg.format_equivalent(other_arg)
1365            }
1366        }
1367    }
1368}
1369
1370impl FormatEquivalent for NatExpr {
1371    fn format_equivalent(&self, other: &Self) -> bool {
1372        match self {
1373            Self::Literal(value, _span) => {
1374                let Self::Literal(other_value, _other_span) = other else {
1375                    return false;
1376                };
1377                value.format_equivalent(other_value)
1378            }
1379            Self::Var(ident) => {
1380                let Self::Var(other_ident) = other else {
1381                    return false;
1382                };
1383                ident.format_equivalent(other_ident)
1384            }
1385            Self::Add(lhs, rhs, _span) => {
1386                let Self::Add(other_lhs, other_rhs, _other_span) = other else {
1387                    return false;
1388                };
1389                lhs.format_equivalent(other_lhs) && rhs.format_equivalent(other_rhs)
1390            }
1391            Self::Mul(lhs, rhs, _span) => {
1392                let Self::Mul(other_lhs, other_rhs, _other_span) = other else {
1393                    return false;
1394                };
1395                lhs.format_equivalent(other_lhs) && rhs.format_equivalent(other_rhs)
1396            }
1397        }
1398    }
1399}
1400
1401impl FormatEquivalent for GenericArg {
1402    fn format_equivalent(&self, other: &Self) -> bool {
1403        match self {
1404            Self::Type(type_expr) => {
1405                let Self::Type(other_type_expr) = other else {
1406                    return false;
1407                };
1408                type_expr.format_equivalent(other_type_expr)
1409            }
1410            Self::Nat(nat) => {
1411                let Self::Nat(other_nat) = other else {
1412                    return false;
1413                };
1414                nat.format_equivalent(other_nat)
1415            }
1416        }
1417    }
1418}
1419
1420impl FormatEquivalent for IndexArg {
1421    fn format_equivalent(&self, other: &Self) -> bool {
1422        match self {
1423            Self::Variant { index, variant } => {
1424                let Self::Variant {
1425                    index: other_index,
1426                    variant: other_variant,
1427                } = other
1428                else {
1429                    return false;
1430                };
1431                index.format_equivalent(other_index) && variant.format_equivalent(other_variant)
1432            }
1433            Self::Var(ident) => {
1434                let Self::Var(other_ident) = other else {
1435                    return false;
1436                };
1437                ident.format_equivalent(other_ident)
1438            }
1439            Self::Expr(expr) => {
1440                let Self::Expr(other_expr) = other else {
1441                    return false;
1442                };
1443                expr.format_equivalent(other_expr)
1444            }
1445        }
1446    }
1447}
1448
1449impl FormatEquivalent for FieldInit {
1450    fn format_equivalent(&self, other: &Self) -> bool {
1451        let Self { name, value } = self;
1452        let Self {
1453            name: other_name,
1454            value: other_value,
1455        } = other;
1456        name.format_equivalent(other_name) && value.format_equivalent(other_value)
1457    }
1458}
1459
1460impl FormatEquivalent for MatchArm {
1461    fn format_equivalent(&self, other: &Self) -> bool {
1462        let Self {
1463            pattern,
1464            body,
1465            span: _,
1466        } = self;
1467        let Self {
1468            pattern: other_pattern,
1469            body: other_body,
1470            span: _,
1471        } = other;
1472        pattern.format_equivalent(other_pattern) && body.format_equivalent(other_body)
1473    }
1474}
1475
1476impl FormatEquivalent for MatchPattern {
1477    fn format_equivalent(&self, other: &Self) -> bool {
1478        match self {
1479            Self::Path {
1480                path,
1481                bindings,
1482                span: _,
1483            } => {
1484                let Self::Path {
1485                    path: other_path,
1486                    bindings: other_bindings,
1487                    span: _,
1488                } = other
1489                else {
1490                    return false;
1491                };
1492                path.format_equivalent(other_path) && bindings.format_equivalent(other_bindings)
1493            }
1494            Self::Constructor {
1495                name,
1496                bindings,
1497                span: _,
1498            } => {
1499                let Self::Constructor {
1500                    name: other_name,
1501                    bindings: other_bindings,
1502                    span: _,
1503                } = other
1504                else {
1505                    return false;
1506                };
1507                name.format_equivalent(other_name) && bindings.format_equivalent(other_bindings)
1508            }
1509            Self::IndexLabel {
1510                index,
1511                variant,
1512                span: _,
1513            } => {
1514                let Self::IndexLabel {
1515                    index: other_index,
1516                    variant: other_variant,
1517                    span: _,
1518                } = other
1519                else {
1520                    return false;
1521                };
1522                index.format_equivalent(other_index) && variant.format_equivalent(other_variant)
1523            }
1524        }
1525    }
1526}
1527
1528impl FormatEquivalent for PatternBinding {
1529    fn format_equivalent(&self, other: &Self) -> bool {
1530        match self {
1531            Self::Bind { field, var } => {
1532                let Self::Bind {
1533                    field: other_field,
1534                    var: other_var,
1535                } = other
1536                else {
1537                    return false;
1538                };
1539                field.format_equivalent(other_field) && var.format_equivalent(other_var)
1540            }
1541            Self::Wildcard { field, span: _ } => {
1542                let Self::Wildcard {
1543                    field: other_field,
1544                    span: _,
1545                } = other
1546                else {
1547                    return false;
1548                };
1549                field.format_equivalent(other_field)
1550            }
1551        }
1552    }
1553}
1554
1555impl FormatEquivalent for RawExprSugar {
1556    fn format_equivalent(&self, other: &Self) -> bool {
1557        let Self::TableLiteral { indexes, entries } = self;
1558        let Self::TableLiteral {
1559            indexes: other_indexes,
1560            entries: other_entries,
1561        } = other;
1562        indexes.format_equivalent(other_indexes) && entries.format_equivalent(other_entries)
1563    }
1564}
1565
1566impl FormatEquivalent for Expr {
1567    fn format_equivalent(&self, other: &Self) -> bool {
1568        // Mirrors the manual `Clone`: route each tree level through the
1569        // stack-growth guard so deep left-nested operator chains do not
1570        // overflow. The span is ignored — that is the whole point.
1571        crate::stack::with_stack_growth(|| self.kind.format_equivalent(&other.kind))
1572    }
1573}
1574
1575impl FormatEquivalent for ExprKind {
1576    #[expect(
1577        clippy::too_many_lines,
1578        reason = "one arm per ExprKind variant; exhaustiveness is the point"
1579    )]
1580    fn format_equivalent(&self, other: &Self) -> bool {
1581        match self {
1582            Self::Number(value) => {
1583                let Self::Number(other_value) = other else {
1584                    return false;
1585                };
1586                value.format_equivalent(other_value)
1587            }
1588            Self::Integer(value) => {
1589                let Self::Integer(other_value) = other else {
1590                    return false;
1591                };
1592                value.format_equivalent(other_value)
1593            }
1594            Self::Bool(value) => {
1595                let Self::Bool(other_value) = other else {
1596                    return false;
1597                };
1598                value.format_equivalent(other_value)
1599            }
1600            Self::StringLiteral(value) => {
1601                let Self::StringLiteral(other_value) = other else {
1602                    return false;
1603                };
1604                value.format_equivalent(other_value)
1605            }
1606            Self::GraphRef(name) => {
1607                let Self::GraphRef(other_name) = other else {
1608                    return false;
1609                };
1610                name.format_equivalent(other_name)
1611            }
1612            Self::BinOp { op, lhs, rhs } => {
1613                let Self::BinOp {
1614                    op: other_op,
1615                    lhs: other_lhs,
1616                    rhs: other_rhs,
1617                } = other
1618                else {
1619                    return false;
1620                };
1621                op.format_equivalent(other_op)
1622                    && lhs.format_equivalent(other_lhs)
1623                    && rhs.format_equivalent(other_rhs)
1624            }
1625            Self::UnaryOp { op, operand } => {
1626                let Self::UnaryOp {
1627                    op: other_op,
1628                    operand: other_operand,
1629                } = other
1630                else {
1631                    return false;
1632                };
1633                op.format_equivalent(other_op) && operand.format_equivalent(other_operand)
1634            }
1635            Self::FnCall {
1636                callee,
1637                type_args,
1638                args,
1639            } => {
1640                let Self::FnCall {
1641                    callee: other_callee,
1642                    type_args: other_type_args,
1643                    args: other_args,
1644                } = other
1645                else {
1646                    return false;
1647                };
1648                callee.format_equivalent(other_callee)
1649                    && type_args.format_equivalent(other_type_args)
1650                    && args.format_equivalent(other_args)
1651            }
1652            Self::If {
1653                condition,
1654                then_branch,
1655                else_branch,
1656            } => {
1657                let Self::If {
1658                    condition: other_condition,
1659                    then_branch: other_then_branch,
1660                    else_branch: other_else_branch,
1661                } = other
1662                else {
1663                    return false;
1664                };
1665                condition.format_equivalent(other_condition)
1666                    && then_branch.format_equivalent(other_then_branch)
1667                    && else_branch.format_equivalent(other_else_branch)
1668            }
1669            Self::UnitLiteral { value, unit } => {
1670                let Self::UnitLiteral {
1671                    value: other_value,
1672                    unit: other_unit,
1673                } = other
1674                else {
1675                    return false;
1676                };
1677                value.format_equivalent(other_value) && unit.format_equivalent(other_unit)
1678            }
1679            Self::Convert { expr, target } => {
1680                let Self::Convert {
1681                    expr: other_expr,
1682                    target: other_target,
1683                } = other
1684                else {
1685                    return false;
1686                };
1687                expr.format_equivalent(other_expr) && target.format_equivalent(other_target)
1688            }
1689            Self::DisplayTimezone { expr, timezone } => {
1690                let Self::DisplayTimezone {
1691                    expr: other_expr,
1692                    timezone: other_timezone,
1693                } = other
1694                else {
1695                    return false;
1696                };
1697                expr.format_equivalent(other_expr) && timezone.format_equivalent(other_timezone)
1698            }
1699            Self::FieldAccess { expr, field } => {
1700                let Self::FieldAccess {
1701                    expr: other_expr,
1702                    field: other_field,
1703                } = other
1704                else {
1705                    return false;
1706                };
1707                expr.format_equivalent(other_expr) && field.format_equivalent(other_field)
1708            }
1709            Self::ConstructorCall {
1710                callee,
1711                generic_args,
1712                fields,
1713            } => {
1714                let Self::ConstructorCall {
1715                    callee: other_callee,
1716                    generic_args: other_generic_args,
1717                    fields: other_fields,
1718                } = other
1719                else {
1720                    return false;
1721                };
1722                callee.format_equivalent(other_callee)
1723                    && generic_args.format_equivalent(other_generic_args)
1724                    && fields.format_equivalent(other_fields)
1725            }
1726            Self::MapLiteral { entries } => {
1727                let Self::MapLiteral {
1728                    entries: other_entries,
1729                } = other
1730                else {
1731                    return false;
1732                };
1733                entries.format_equivalent(other_entries)
1734            }
1735            Self::ForComp { bindings, body } => {
1736                let Self::ForComp {
1737                    bindings: other_bindings,
1738                    body: other_body,
1739                } = other
1740                else {
1741                    return false;
1742                };
1743                bindings.format_equivalent(other_bindings) && body.format_equivalent(other_body)
1744            }
1745            Self::IndexAccess { expr, args } => {
1746                let Self::IndexAccess {
1747                    expr: other_expr,
1748                    args: other_args,
1749                } = other
1750                else {
1751                    return false;
1752                };
1753                expr.format_equivalent(other_expr) && args.format_equivalent(other_args)
1754            }
1755            Self::Scan {
1756                source,
1757                init,
1758                acc_name,
1759                val_name,
1760                body,
1761            } => {
1762                let Self::Scan {
1763                    source: other_source,
1764                    init: other_init,
1765                    acc_name: other_acc_name,
1766                    val_name: other_val_name,
1767                    body: other_body,
1768                } = other
1769                else {
1770                    return false;
1771                };
1772                source.format_equivalent(other_source)
1773                    && init.format_equivalent(other_init)
1774                    && acc_name.format_equivalent(other_acc_name)
1775                    && val_name.format_equivalent(other_val_name)
1776                    && body.format_equivalent(other_body)
1777            }
1778            Self::Unfold {
1779                init,
1780                prev_name,
1781                curr_name,
1782                body,
1783            } => {
1784                let Self::Unfold {
1785                    init: other_init,
1786                    prev_name: other_prev_name,
1787                    curr_name: other_curr_name,
1788                    body: other_body,
1789                } = other
1790                else {
1791                    return false;
1792                };
1793                init.format_equivalent(other_init)
1794                    && prev_name.format_equivalent(other_prev_name)
1795                    && curr_name.format_equivalent(other_curr_name)
1796                    && body.format_equivalent(other_body)
1797            }
1798            Self::Match { scrutinee, arms } => {
1799                let Self::Match {
1800                    scrutinee: other_scrutinee,
1801                    arms: other_arms,
1802                } = other
1803                else {
1804                    return false;
1805                };
1806                scrutinee.format_equivalent(other_scrutinee) && arms.format_equivalent(other_arms)
1807            }
1808            Self::InlineDagRef { path, args, output } => {
1809                let Self::InlineDagRef {
1810                    path: other_path,
1811                    args: other_args,
1812                    output: other_output,
1813                } = other
1814                else {
1815                    return false;
1816                };
1817                path.format_equivalent(other_path)
1818                    && args.format_equivalent(other_args)
1819                    && output.format_equivalent(other_output)
1820            }
1821            Self::UnresolvedRef(reference) => {
1822                let Self::UnresolvedRef(other_reference) = other else {
1823                    return false;
1824                };
1825                reference.format_equivalent(other_reference)
1826            }
1827            Self::Sugar(sugar) => {
1828                let Self::Sugar(other_sugar) = other else {
1829                    return false;
1830                };
1831                sugar.format_equivalent(other_sugar)
1832            }
1833        }
1834    }
1835}
1836
1837#[cfg(test)]
1838mod tests {
1839    use super::FormatEquivalent;
1840    use crate::syntax::ast::File;
1841    use crate::syntax::parser::Parser;
1842
1843    fn parse(source: &str) -> File {
1844        Parser::new(source)
1845            .parse_file()
1846            .unwrap_or_else(|err| panic!("test source should parse: {err:?}\n---\n{source}"))
1847    }
1848
1849    /// Two parses of the same text are equivalent — reflexivity over real spans.
1850    #[test]
1851    fn identical_sources_are_equivalent() {
1852        let source = "node x: Dimensionless = 1.0 + 2.0 * 3.0;\n";
1853        assert!(parse(source).format_equivalent(&parse(source)));
1854    }
1855
1856    /// Layout-only differences (whitespace, newlines) move every span but must
1857    /// not change equivalence — this is the property the formatter relies on.
1858    #[test]
1859    fn whitespace_differences_are_equivalent() {
1860        let dense = "node x:Dimensionless=1.0+2.0*3.0;node y:Dimensionless=@x;";
1861        let spaced = "
1862            node x: Dimensionless = 1.0 + 2.0 * 3.0;
1863
1864            node y: Dimensionless = @x;
1865        ";
1866        assert!(parse(dense).format_equivalent(&parse(spaced)));
1867    }
1868
1869    /// Comments live in source metadata, not the AST, so they never affect
1870    /// equivalence.
1871    #[test]
1872    fn comment_differences_are_equivalent() {
1873        let bare = "node x: Dimensionless = 1.0;\n";
1874        let commented = "// leading\nnode x: Dimensionless = 1.0; // trailing\n";
1875        assert!(parse(bare).format_equivalent(&parse(commented)));
1876    }
1877
1878    #[test]
1879    fn number_literal_difference_is_not_equivalent() {
1880        let a = "node x: Dimensionless = 1.0;\n";
1881        let b = "node x: Dimensionless = 2.0;\n";
1882        assert!(!parse(a).format_equivalent(&parse(b)));
1883    }
1884
1885    #[test]
1886    fn operator_difference_is_not_equivalent() {
1887        let a = "node x: Dimensionless = 1.0 + 2.0;\n";
1888        let b = "node x: Dimensionless = 1.0 - 2.0;\n";
1889        assert!(!parse(a).format_equivalent(&parse(b)));
1890    }
1891
1892    #[test]
1893    fn name_difference_is_not_equivalent() {
1894        let a = "node x: Dimensionless = 1.0;\n";
1895        let b = "node y: Dimensionless = 1.0;\n";
1896        assert!(!parse(a).format_equivalent(&parse(b)));
1897    }
1898
1899    /// Operand order is structural: `a - b` differs from `b - a` even though
1900    /// the spans cover the same ranges.
1901    #[test]
1902    fn operand_order_is_not_equivalent() {
1903        let a = "node x: Dimensionless = 1.0 - 2.0;\n";
1904        let b = "node x: Dimensionless = 2.0 - 1.0;\n";
1905        assert!(!parse(a).format_equivalent(&parse(b)));
1906    }
1907
1908    /// A dropped declaration must be caught — the canonical "formatting changed
1909    /// the program" failure.
1910    #[test]
1911    fn missing_declaration_is_not_equivalent() {
1912        let a = "node x: Dimensionless = 1.0;\nnode y: Dimensionless = 2.0;\n";
1913        let b = "node x: Dimensionless = 1.0;\n";
1914        assert!(!parse(a).format_equivalent(&parse(b)));
1915    }
1916}