Skip to main content

graphcal_compiler/syntax/
desugar.rs

1//! Syntactic sugar desugaring pass (issue #481).
2//!
3//! Multi-declarations — `param a: T[I], const node b: U[I, J] = table[…]{…};` —
4//! are parsed as `DeclKind::Sugar(RawDeclSugar::Multi(MultiDecl))` to preserve
5//! source structure for surface-aware tools (formatter, LSP). This module
6//! expands them into N parallel ordinary declarations before semantic
7//! analysis, so lowering, TIR, resolver, and the runtime all see only
8//! single declarations.
9//!
10//! The desugar pass is invoked at the top of
11//! HIR lowering; everything downstream
12//! can assume `DeclKind::Sugar` does not appear in the AST.
13//!
14//! Note: today this pass mutates a `File<Raw>` in place, eliminating sugar
15//! variants by walking + flattening. A future commit will switch the API
16//! to `File<Raw> -> File<Desugared>` (the [`From`] impl in
17//! [`crate::desugar::convert`] is the engine for that transition) and
18//! pin all consumers to `File<Desugared>`, replacing the runtime
19//! [`unreachable_post_desugar`] panics with [`crate::syntax::phase::never`]
20//! on the [`Infallible`](core::convert::Infallible) `Sugar` payload.
21//!
22//! ## Span fidelity
23//!
24//! Each synthesized declaration carries:
25//! - `span` — from the slot header keyword through the closing `;` of the
26//!   multi-decl (so errors referencing the whole decl still land on the
27//!   surface).
28//! - `name.span` — pointing at the slot's name identifier in the source.
29//! - `type_ann.span` — pointing at the slot's type annotation in the source.
30//! - `value` — a synthesized `TableLiteral` whose span covers the original
31//!   `table[…] {…}` body; each entry's value carries the span of the source
32//!   cell it came from.
33
34/// Panic used in post-desugar exhaustive matches over `DeclKind`. Marks
35/// the invariant that [`desugar_multi_decls_in_file`] has already run.
36#[cold]
37#[track_caller]
38#[inline(never)]
39#[expect(
40    clippy::panic,
41    reason = "indicates a broken invariant — multi-decls must be desugared before this pass"
42)]
43pub fn unreachable_post_desugar() -> ! {
44    panic!(
45        "DeclKind::Sugar should have been removed by syntax::desugar::desugar_multi_decls_in_file"
46    )
47}
48
49use crate::syntax::ast::{
50    ConstNodeDecl, DeclKind, Declaration, Expr, ExprKind, File, MapEntry, MapEntryIndex,
51    MapEntryKey, MultiDecl, MultiHeaderCell, MultiSlotColumnSpan, MultiSlotKind, NodeDecl,
52    ParamDecl, TableIndexSpec,
53};
54use crate::syntax::names::{IndexName, IndexVariantName};
55use crate::syntax::non_empty::NonEmpty;
56use crate::syntax::phase::{Desugared, Raw};
57use crate::syntax::span::Spanned;
58
59fn multi_entry_keys(
60    mut prefix: Vec<MapEntryKey>,
61    row: MapEntryKey,
62    extra: Option<MapEntryKey>,
63) -> NonEmpty<MapEntryKey> {
64    if prefix.is_empty() {
65        let rest = extra.into_iter().collect();
66        NonEmpty::new(row, rest)
67    } else {
68        let first = prefix.remove(0);
69        prefix.push(row);
70        prefix.extend(extra);
71        NonEmpty::new(first, prefix)
72    }
73}
74
75/// Expand every multi-decl in `file` into its N constituent ordinary
76/// declarations and return the result as [`File<Desugared>`].
77///
78/// Consumes the raw file by value because the phase split is a type-level
79/// transformation: `File<Raw>` and `File<Desugared>` are distinct types and
80/// cannot share storage. The actual conversion logic lives in
81/// [`crate::desugar::convert`] (the `From<File<Raw>> for File<Desugared>`
82/// impl), which dispatches the multi-decl `Sugar` arm to
83/// [`expand_multi_decl`].
84#[must_use]
85pub fn desugar_multi_decls_in_file(file: File<Raw>) -> File<Desugared> {
86    file.into()
87}
88
89/// A declaration produced by multi-decl expansion.
90///
91/// Expansion can only yield the three slot kinds, so this enum makes the
92/// "never `Sugar`" invariant a type instead of a convention the desugar
93/// pass had to re-assert with a panic.
94#[derive(Debug)]
95pub enum ExpandedSlotDecl {
96    Param(ParamDecl, crate::syntax::span::Span),
97    Node(NodeDecl, crate::syntax::span::Span),
98    ConstNode(ConstNodeDecl, crate::syntax::span::Span),
99}
100
101impl ExpandedSlotDecl {
102    /// Re-wrap as a generic [`Declaration`] (used by tests that inspect the
103    /// expansion through the ordinary AST surface).
104    #[must_use]
105    pub fn into_declaration(self) -> Declaration {
106        let (kind, span) = match self {
107            Self::Param(p, span) => (DeclKind::Param(p), span),
108            Self::Node(n, span) => (DeclKind::Node(n), span),
109            Self::ConstNode(c, span) => (DeclKind::ConstNode(c), span),
110        };
111        Declaration {
112            attributes: vec![],
113            kind,
114            span,
115        }
116    }
117}
118
119/// Expand a single `MultiDecl` into its N constituent declarations.
120#[must_use]
121#[expect(
122    clippy::too_many_lines,
123    reason = "single cohesive routine for multi-decl expansion"
124)]
125pub fn expand_multi_decl(multi: &MultiDecl) -> Vec<ExpandedSlotDecl> {
126    let row_index_spec = multi.shared_axes.row_axis().clone();
127    let slice_axis_specs: &[TableIndexSpec] = multi.shared_axes.slice_axes();
128
129    let row_index_name = match &row_index_spec {
130        TableIndexSpec::Named(s) => Spanned::new(MapEntryIndex::Named(s.value.clone()), s.span),
131        TableIndexSpec::NatRange(n, sp) => Spanned::new(MapEntryIndex::NatRange(*n), *sp),
132    };
133
134    let mut out: Vec<ExpandedSlotDecl> = Vec::with_capacity(multi.slots.len());
135    for (slot_idx, slot) in multi.slots.iter().enumerate() {
136        let mut slot_entries: Vec<MapEntry> = Vec::new();
137        let mut slot_indexes: Vec<TableIndexSpec> = slice_axis_specs.to_vec();
138        slot_indexes.push(row_index_spec.clone());
139        let mut extra_axis_name: Option<Spanned<IndexName>> = None;
140
141        for slice in &multi.slices {
142            let col_span = &slice.column_layout[slot_idx];
143            match col_span {
144                MultiSlotColumnSpan::Single(col_idx) => {
145                    for row in &slice.rows {
146                        let row_key = MapEntryKey {
147                            index: row_index_name.clone(),
148                            variant: row.label.clone(),
149                        };
150                        slot_entries.push(MapEntry {
151                            keys: multi_entry_keys(slice.prefix_keys.clone(), row_key, None),
152                            value: row.values[*col_idx].clone(),
153                        });
154                    }
155                }
156                MultiSlotColumnSpan::Range {
157                    start,
158                    end,
159                    extra_axis,
160                } => {
161                    if extra_axis_name.is_none() {
162                        extra_axis_name = Some(extra_axis.clone());
163                    }
164                    let extra_index_name = Spanned::new(
165                        MapEntryIndex::Named(extra_axis.value.clone().into()),
166                        extra_axis.span,
167                    );
168                    let col_variants: Vec<Spanned<IndexVariantName>> = slice.header_cells
169                        [*start..*end]
170                        .iter()
171                        .filter_map(|c| match c {
172                            MultiHeaderCell::Variant { variant, .. } => Some(variant.clone()),
173                            MultiHeaderCell::Underscore { .. } => None,
174                        })
175                        .collect();
176                    for row in &slice.rows {
177                        for (local_col, col_variant) in col_variants.iter().enumerate() {
178                            let global_col = start + local_col;
179                            let row_key = MapEntryKey {
180                                index: row_index_name.clone(),
181                                variant: row.label.clone(),
182                            };
183                            let extra_key = MapEntryKey {
184                                index: extra_index_name.clone(),
185                                variant: col_variant.clone(),
186                            };
187                            slot_entries.push(MapEntry {
188                                keys: multi_entry_keys(
189                                    slice.prefix_keys.clone(),
190                                    row_key,
191                                    Some(extra_key),
192                                ),
193                                value: row.values[global_col].clone(),
194                            });
195                        }
196                    }
197                }
198            }
199        }
200
201        if let Some(extra) = extra_axis_name {
202            slot_indexes.push(TableIndexSpec::Named(Spanned::new(
203                extra.value.into(),
204                extra.span,
205            )));
206        }
207
208        let table_expr = Expr::new(
209            ExprKind::Sugar(crate::syntax::ast::RawExprSugar::TableLiteral {
210                indexes: slot_indexes,
211                entries: slot_entries,
212            }),
213            multi.table_expr_span,
214        );
215
216        // `span` covers the slot header through the closing `;` of the
217        // whole multi-decl so diagnostics land on the source surface.
218        let decl_span = slot.header_span.merge(multi.span);
219
220        out.push(match slot.kind {
221            MultiSlotKind::Param => ExpandedSlotDecl::Param(
222                ParamDecl {
223                    name: slot.name.clone(),
224                    type_ann: slot.type_ann.clone(),
225                    value: Some(table_expr),
226                },
227                decl_span,
228            ),
229            MultiSlotKind::Node => ExpandedSlotDecl::Node(
230                NodeDecl {
231                    visibility: slot.visibility,
232                    name: slot.name.clone(),
233                    type_ann: slot.type_ann.clone(),
234                    value: table_expr,
235                },
236                decl_span,
237            ),
238            MultiSlotKind::ConstNode => ExpandedSlotDecl::ConstNode(
239                ConstNodeDecl {
240                    visibility: slot.visibility,
241                    name: slot.name.clone(),
242                    type_ann: slot.type_ann.clone(),
243                    value: table_expr,
244                },
245                decl_span,
246            ),
247        });
248    }
249
250    out
251}