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;