Skip to main content

ryo_source/pure/to_syn/
mod.rs

1//! Conversion from Pure types back to syn types.
2//!
3//! This enables code generation from PureFile after parallel modifications.
4//! All spans are set to `Span::call_site()` since PureFile doesn't preserve spans.
5//!
6//! # Module Structure
7//!
8//! - `helpers` - Common helper functions (ident, parse_path)
9//! - `attr` - PureAttribute, PureVis
10//! - `item` - PureItem, PureUse, PureUseTree
11//! - `function` - PureFn, PureParam, PureGenerics, PureBlock, PureStmt
12//! - `expr` - PureExpr, PurePattern, PureMatchArm
13//! - `types` - PureType
14//! - `structs` - PureStruct, PureFields, PureField, PureEnum, PureVariant
15//! - `impls` - PureImpl, PureImplItem
16//! - `other` - PureConst, PureStatic, PureTypeAlias, PureMod, PureTrait, PureTraitItem, PureMacro
17
18pub mod helpers;
19
20mod attr;
21mod expr;
22mod function;
23mod impls;
24mod item;
25mod other;
26mod structs;
27mod types;
28
29use std::cell::RefCell;
30
31use super::ast::PureFile;
32
33// ---------------------------------------------------------------------
34// PureExpr::Verbatim staging registry (B-3)
35//
36// PureFile::to_source clears the registry, drives `to_syn()` (which
37// pushes a `(idx, raw)` pair per encountered PureExpr::Verbatim and
38// emits a stub path expression `__ryo_verbatim_expr_<idx>`), and after
39// prettyplease finishes formatting, walks the registry to splice the
40// raw bytes back over each unique stub identifier. The registry lives
41// in thread-local state so we don't have to thread a mutable context
42// through the entire `ToSyn` recursion.
43//
44// Re-entrancy: nested PureFile::to_source calls would corrupt each
45// other's indices. This is fine for the current ryo codegen pipeline
46// where to_source is the outer-most boundary, and is documented as a
47// non-reentrancy contract on `PureFile::to_source`.
48// ---------------------------------------------------------------------
49
50thread_local! {
51    static VERBATIM_EXPR_REGISTRY: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
52    static VERBATIM_STMT_REGISTRY: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
53}
54
55/// Push a raw chunk into the registry and return its assigned index.
56/// Called from `PureExpr::Verbatim` lowering in `expr.rs`.
57pub(crate) fn register_verbatim_expr(raw: String) -> usize {
58    VERBATIM_EXPR_REGISTRY.with(|r| {
59        let mut v = r.borrow_mut();
60        let idx = v.len();
61        v.push(raw);
62        idx
63    })
64}
65
66/// Push a raw chunk into the stmt registry. Called from
67/// `PureStmt::Verbatim` lowering in `function.rs` (B-3-cont).
68pub(crate) fn register_verbatim_stmt(raw: String) -> usize {
69    VERBATIM_STMT_REGISTRY.with(|r| {
70        let mut v = r.borrow_mut();
71        let idx = v.len();
72        v.push(raw);
73        idx
74    })
75}
76
77/// Take and clear the current registry. Called from `to_source`
78/// after prettyplease has produced the formatted string so the splice
79/// pass can match each `__ryo_verbatim_expr_<idx>` stub.
80fn take_verbatim_expr_registry() -> Vec<String> {
81    VERBATIM_EXPR_REGISTRY.with(|r| std::mem::take(&mut *r.borrow_mut()))
82}
83
84/// Take and clear the stmt registry (B-3-cont).
85fn take_verbatim_stmt_registry() -> Vec<String> {
86    VERBATIM_STMT_REGISTRY.with(|r| std::mem::take(&mut *r.borrow_mut()))
87}
88
89/// Wipe both registries. Called from `to_source` before driving
90/// `to_syn()` so a previous failed/partial run cannot leak indices.
91fn clear_verbatim_expr_registry() {
92    VERBATIM_EXPR_REGISTRY.with(|r| r.borrow_mut().clear());
93    VERBATIM_STMT_REGISTRY.with(|r| r.borrow_mut().clear());
94}
95
96/// Format multiple files in-place using a single rustfmt invocation.
97///
98/// This is much faster than formatting each file individually, as it avoids
99/// spawning a new process for each file.
100///
101/// Returns Ok(()) if rustfmt succeeds, Err with message otherwise.
102pub fn format_files_with_rustfmt<P: AsRef<std::path::Path>>(files: &[P]) -> Result<(), String> {
103    use std::process::Command;
104
105    if files.is_empty() {
106        return Ok(());
107    }
108
109    let output = Command::new("rustfmt")
110        .args(["--edition", "2021"])
111        .args(files.iter().map(|p| p.as_ref()))
112        .output()
113        .map_err(|e| format!("Failed to spawn rustfmt: {}", e))?;
114
115    if output.status.success() {
116        Ok(())
117    } else {
118        Err(String::from_utf8_lossy(&output.stderr).to_string())
119    }
120}
121
122/// Trait for converting Pure types back to syn types.
123///
124/// All conversions are fallible because they may involve `syn::parse_str`
125/// for `Other` variants (expressions, types, patterns stored as strings).
126pub trait ToSyn {
127    /// `syn` output type produced from `self`.
128    type Output;
129    /// Convert `self` back into its `syn` representation.
130    fn to_syn(&self) -> Result<Self::Output, ToSynError>;
131}
132
133/// Error type for ToSyn conversions.
134#[derive(Debug, Clone)]
135pub enum ToSynError {
136    /// Failed to parse a path string (e.g., invalid syntax)
137    ParsePath {
138        /// Original input string that failed to parse.
139        input: String,
140        /// Underlying parser message.
141        message: String,
142    },
143    /// Failed to parse an expression
144    ParseExpr {
145        /// Original input string that failed to parse.
146        input: String,
147        /// Underlying parser message.
148        message: String,
149    },
150    /// Failed to parse a pattern
151    ParsePattern {
152        /// Original input string that failed to parse.
153        input: String,
154        /// Underlying parser message.
155        message: String,
156    },
157    /// Failed to parse a type
158    ParseType {
159        /// Original input string that failed to parse.
160        input: String,
161        /// Underlying parser message.
162        message: String,
163    },
164    /// Missing required value
165    MissingValue {
166        /// Context describing which value was missing.
167        context: String,
168    },
169    /// Other conversion error
170    Other {
171        /// Free-form error message.
172        message: String,
173    },
174}
175
176impl std::fmt::Display for ToSynError {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        match self {
179            Self::ParsePath { input, message } => {
180                write!(f, "Failed to parse path '{}': {}", input, message)
181            }
182            Self::ParseExpr { input, message } => {
183                write!(f, "Failed to parse expression '{}': {}", input, message)
184            }
185            Self::ParsePattern { input, message } => {
186                write!(f, "Failed to parse pattern '{}': {}", input, message)
187            }
188            Self::ParseType { input, message } => {
189                write!(f, "Failed to parse type '{}': {}", input, message)
190            }
191            Self::MissingValue { context } => {
192                write!(f, "Missing required value: {}", context)
193            }
194            Self::Other { message } => write!(f, "Conversion error: {}", message),
195        }
196    }
197}
198
199impl std::error::Error for ToSynError {}
200
201impl PureFile {
202    /// Convert to syn::File for code generation.
203    pub fn to_syn_file(&self) -> Result<syn::File, ToSynError> {
204        self.to_syn()
205    }
206
207    /// Generate source code string.
208    ///
209    /// Returns formatted source code using prettyplease. Falls back to
210    /// quote-based stringification if prettyplease panics.
211    ///
212    /// Note: This method does NOT run rustfmt. For final output, use
213    /// `format_files_with_rustfmt()` to batch-format written files.
214    ///
215    /// `PureItem::Verbatim` handling:
216    /// Each `Verbatim` at the top of `self.items` is staged out of the way
217    /// before lowering to `syn::File` by substituting a unique sentinel stub
218    /// `fn __ryo_verbatim_{N}() {}`. After prettyplease produces formatted
219    /// source for the rest of the file, the line containing each sentinel is
220    /// replaced with the raw bytes carried by the corresponding `Verbatim`.
221    /// This is the only path in ryo that preserves arbitrary raw source text
222    /// (DSL macro spacing, `//` line comments, blank lines, etc.) end-to-end
223    /// through the codegen pipeline.
224    pub fn to_source(&self) -> Result<String, ToSynError> {
225        // B-3: clear the thread-local PureExpr::Verbatim registry so
226        // a previous (possibly failed) call cannot leak indices into
227        // this run. The registry gets populated as `to_syn()` walks
228        // expressions below.
229        clear_verbatim_expr_registry();
230
231        // Stage Verbatim items into sentinel stubs and record the raw chunks
232        // in document order. Stubs use sequentially-indexed names so the
233        // post-process splice step can locate them.
234        let mut verbatims: Vec<String> = Vec::new();
235        let staged_items: Vec<crate::pure::PureItem> = self
236            .items
237            .iter()
238            .map(|item| {
239                if let crate::pure::PureItem::Verbatim(raw) = item {
240                    let idx = verbatims.len();
241                    verbatims.push(raw.clone());
242                    crate::pure::PureItem::Other(format!("fn __ryo_verbatim_{}() {{}}", idx))
243                } else {
244                    item.clone()
245                }
246            })
247            .collect();
248        let staged = Self {
249            attrs: self.attrs.clone(),
250            items: staged_items,
251        };
252
253        let file = staged.to_syn()?;
254
255        // Try prettyplease first, fall back to quote if it panics
256        let mut out = catch_unwind_silent(|| prettyplease::unparse(&file)).unwrap_or_else(|_| {
257            use quote::ToTokens;
258            file.to_token_stream().to_string()
259        });
260
261        // Splice raw bytes over each sentinel stub. We search for the unique
262        // identifier (not the surrounding `fn` syntax) because prettyplease
263        // may format the stub in either single-line or multi-line form.
264        for (idx, raw) in verbatims.iter().enumerate() {
265            let needle = format!("__ryo_verbatim_{}", idx);
266            if let Some(pos) = out.find(&needle) {
267                let bol = out[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
268                let eol = out[pos..].find('\n').map(|p| pos + p).unwrap_or(out.len());
269                let mut rebuilt = String::with_capacity(out.len() + raw.len());
270                rebuilt.push_str(&out[..bol]);
271                rebuilt.push_str(raw);
272                if !raw.ends_with('\n') {
273                    rebuilt.push('\n');
274                }
275                rebuilt.push_str(&out[eol..]);
276                out = rebuilt;
277            }
278        }
279
280        // B-3 expr-level splice: replace each unique
281        // `__ryo_verbatim_expr_<idx>` token with the raw bytes
282        // registered during the to_syn walk. Unlike the item-level
283        // splice, we replace only the identifier (not the surrounding
284        // line) so the raw bytes drop into the expression position
285        // verbatim, with the surrounding indent / punctuation that
286        // prettyplease laid out around the stub.
287        let expr_verbatims = take_verbatim_expr_registry();
288        for (idx, raw) in expr_verbatims.iter().enumerate() {
289            let needle = format!("__ryo_verbatim_expr_{}", idx);
290            if let Some(pos) = out.find(&needle) {
291                out.replace_range(pos..pos + needle.len(), raw);
292            }
293        }
294
295        // B-3-cont stmt-level splice: replace the entire line
296        // containing each `__ryo_verbatim_stmt_<idx>;` stub with the
297        // raw bytes. Line-splice (like the item-level splice) because
298        // a Stmt always occupies its own line in prettyplease output,
299        // and the user's raw is expected to carry its own statement
300        // terminator (`;` or block).
301        let stmt_verbatims = take_verbatim_stmt_registry();
302        for (idx, raw) in stmt_verbatims.iter().enumerate() {
303            let needle = format!("__ryo_verbatim_stmt_{}", idx);
304            if let Some(pos) = out.find(&needle) {
305                let bol = out[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
306                let eol = out[pos..].find('\n').map(|p| pos + p).unwrap_or(out.len());
307                let mut rebuilt = String::with_capacity(out.len() + raw.len());
308                rebuilt.push_str(&out[..bol]);
309                rebuilt.push_str(raw);
310                if !raw.ends_with('\n') {
311                    rebuilt.push('\n');
312                }
313                rebuilt.push_str(&out[eol..]);
314                out = rebuilt;
315            }
316        }
317
318        Ok(out)
319    }
320}
321
322/// Execute a closure, catching any panics without printing panic messages.
323///
324/// This is useful for handling expected panics from third-party libraries
325/// (like prettyplease's Expr::Verbatim) without polluting stderr.
326fn catch_unwind_silent<F, R>(f: F) -> std::thread::Result<R>
327where
328    F: FnOnce() -> R + std::panic::UnwindSafe,
329{
330    let prev_hook = std::panic::take_hook();
331    std::panic::set_hook(Box::new(|_| {}));
332    let result = std::panic::catch_unwind(f);
333    std::panic::set_hook(prev_hook);
334    result
335}
336
337impl ToSyn for PureFile {
338    type Output = syn::File;
339
340    fn to_syn(&self) -> Result<syn::File, ToSynError> {
341        Ok(syn::File {
342            shebang: None,
343            attrs: self
344                .attrs
345                .iter()
346                .map(|a| a.to_syn())
347                .collect::<Result<Vec<_>, _>>()?,
348            items: self
349                .items
350                .iter()
351                .map(|i| i.to_syn())
352                .collect::<Result<Vec<_>, _>>()?,
353        })
354    }
355}
356
357#[cfg(test)]
358mod tests;