1use proc_macro2::Span;
4use syn::token;
5
6use super::helpers::{ident, try_parse_path};
7use super::{ToSyn, ToSynError};
8use crate::pure::ast::{MacroDelimiter, PureExpr, PureMatchArm, PurePattern};
9
10fn make_label(name: &str) -> syn::Label {
12 syn::Label {
13 name: syn::Lifetime {
14 apostrophe: Span::call_site(),
15 ident: proc_macro2::Ident::new(name, Span::call_site()),
16 },
17 colon_token: token::Colon::default(),
18 }
19}
20
21fn make_lifetime(name: &str) -> syn::Lifetime {
23 syn::Lifetime {
24 apostrophe: Span::call_site(),
25 ident: proc_macro2::Ident::new(name, Span::call_site()),
26 }
27}
28
29impl ToSyn for PurePattern {
30 type Output = syn::Pat;
31
32 fn to_syn(&self) -> Result<syn::Pat, ToSynError> {
33 match self {
34 PurePattern::Ident {
35 name,
36 is_mut,
37 by_ref,
38 } => Ok(syn::Pat::Ident(syn::PatIdent {
39 attrs: vec![],
40 by_ref: if *by_ref {
41 Some(token::Ref::default())
42 } else {
43 None
44 },
45 mutability: if *is_mut {
46 Some(token::Mut::default())
47 } else {
48 None
49 },
50 ident: ident(name),
51 subpat: None,
52 })),
53 PurePattern::Wild => Ok(syn::Pat::Wild(syn::PatWild {
54 attrs: vec![],
55 underscore_token: token::Underscore::default(),
56 })),
57 PurePattern::Tuple(pats) => Ok(syn::Pat::Tuple(syn::PatTuple {
58 attrs: vec![],
59 paren_token: token::Paren::default(),
60 elems: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
61 })),
62 PurePattern::Struct { path, fields, rest } => {
63 let is_tuple_struct = !*rest
69 && !fields.is_empty()
70 && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
71 if is_tuple_struct {
72 Ok(syn::Pat::TupleStruct(syn::PatTupleStruct {
73 attrs: vec![],
74 qself: None,
75 path: try_parse_path(path)?,
76 paren_token: token::Paren::default(),
77 elems: fields
78 .iter()
79 .map(|(_, pat)| pat.to_syn())
80 .collect::<Result<_, _>>()?,
81 }))
82 } else {
83 let syn_fields = fields
94 .iter()
95 .map(|(name, pat)| {
96 let is_shorthand = matches!(
110 pat,
111 PurePattern::Ident { name: pat_name, .. }
112 if pat_name == name
113 );
114 Ok(syn::FieldPat {
115 attrs: vec![],
116 member: syn::Member::Named(ident(name)),
117 colon_token: if is_shorthand {
118 None
119 } else {
120 Some(token::Colon::default())
121 },
122 pat: Box::new(pat.to_syn()?),
123 })
124 })
125 .collect::<Result<_, ToSynError>>()?;
126 Ok(syn::Pat::Struct(syn::PatStruct {
127 attrs: vec![],
128 qself: None,
129 path: try_parse_path(path)?,
130 brace_token: token::Brace::default(),
131 fields: syn_fields,
132 rest: if *rest {
133 Some(syn::PatRest {
134 attrs: vec![],
135 dot2_token: token::DotDot::default(),
136 })
137 } else {
138 None
139 },
140 }))
141 }
142 }
143 PurePattern::Ref { is_mut, pattern } => Ok(syn::Pat::Reference(syn::PatReference {
144 attrs: vec![],
145 and_token: token::And::default(),
146 mutability: if *is_mut {
147 Some(token::Mut::default())
148 } else {
149 None
150 },
151 pat: Box::new(pattern.to_syn()?),
152 })),
153 PurePattern::Lit(lit) => match syn::parse_str::<syn::Expr>(lit) {
154 Ok(syn::Expr::Lit(lit_expr)) => Ok(syn::Pat::Lit(lit_expr)),
155 Ok(_) => Err(ToSynError::ParsePattern {
156 input: lit.clone(),
157 message: "Expected literal pattern but got non-literal expression".to_string(),
158 }),
159 Err(e) => Err(ToSynError::ParsePattern {
160 input: lit.clone(),
161 message: e.to_string(),
162 }),
163 },
164 PurePattern::Or(pats) => Ok(syn::Pat::Or(syn::PatOr {
165 attrs: vec![],
166 leading_vert: None,
167 cases: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
168 })),
169 PurePattern::Path(path) => {
170 debug_assert!(
173 !path.contains('(') && !path.contains('{') && !path.contains(".."),
174 "PurePattern::Path received a complex pattern '{}'. Use PurePattern::Other instead.",
175 path
176 );
177 Ok(syn::Pat::Path(syn::ExprPath {
178 attrs: vec![],
179 qself: None,
180 path: try_parse_path(path)?,
181 }))
182 }
183 PurePattern::Range {
184 start,
185 end,
186 inclusive,
187 } => {
188 let start_expr = start
189 .as_ref()
190 .map(|s| {
191 syn::parse_str::<syn::Expr>(s).map_err(|e| ToSynError::ParseExpr {
192 input: s.clone(),
193 message: e.to_string(),
194 })
195 })
196 .transpose()?;
197 let end_expr = end
198 .as_ref()
199 .map(|s| {
200 syn::parse_str::<syn::Expr>(s).map_err(|e| ToSynError::ParseExpr {
201 input: s.clone(),
202 message: e.to_string(),
203 })
204 })
205 .transpose()?;
206 let limits = if *inclusive {
207 syn::RangeLimits::Closed(token::DotDotEq::default())
208 } else {
209 syn::RangeLimits::HalfOpen(token::DotDot::default())
210 };
211 Ok(syn::Pat::Range(syn::ExprRange {
212 attrs: vec![],
213 start: start_expr.map(Box::new),
214 limits,
215 end: end_expr.map(Box::new),
216 }))
217 }
218 PurePattern::Slice(pats) => Ok(syn::Pat::Slice(syn::PatSlice {
219 attrs: vec![],
220 bracket_token: token::Bracket::default(),
221 elems: pats.iter().map(|p| p.to_syn()).collect::<Result<_, _>>()?,
222 })),
223 PurePattern::Rest => Ok(syn::Pat::Rest(syn::PatRest {
224 attrs: vec![],
225 dot2_token: token::DotDot::default(),
226 })),
227 PurePattern::Other(s) => {
228 let tokens: proc_macro2::TokenStream =
229 s.parse()
230 .map_err(|e: proc_macro2::LexError| ToSynError::ParsePattern {
231 input: s.clone(),
232 message: format!("{}", e),
233 })?;
234 Ok(syn::Pat::Verbatim(tokens))
235 }
236 }
237 }
238}
239
240impl ToSyn for PureExpr {
241 type Output = syn::Expr;
242
243 fn to_syn(&self) -> Result<syn::Expr, ToSynError> {
244 match self {
245 PureExpr::Lit(lit) => syn::parse_str(lit).map_err(|e| ToSynError::ParseExpr {
246 input: lit.clone(),
247 message: e.to_string(),
248 }),
249 PureExpr::Path(path) => Ok(syn::Expr::Path(syn::ExprPath {
250 attrs: vec![],
251 qself: None,
252 path: try_parse_path(path)?,
253 })),
254 PureExpr::Binary { op, left, right } => {
255 if op == "=" {
257 Ok(syn::Expr::Assign(syn::ExprAssign {
258 attrs: vec![],
259 left: Box::new(left.to_syn()?),
260 eq_token: token::Eq::default(),
261 right: Box::new(right.to_syn()?),
262 }))
263 } else {
264 Ok(syn::Expr::Binary(syn::ExprBinary {
265 attrs: vec![],
266 left: Box::new(left.to_syn()?),
267 op: syn::parse_str(op).map_err(|e| ToSynError::ParseExpr {
268 input: op.clone(),
269 message: format!("Failed to parse binary operator: {}", e),
270 })?,
271 right: Box::new(right.to_syn()?),
272 }))
273 }
274 }
275 PureExpr::Unary { op, expr } => Ok(syn::Expr::Unary(syn::ExprUnary {
276 attrs: vec![],
277 op: syn::parse_str(op).map_err(|e| ToSynError::ParseExpr {
278 input: op.clone(),
279 message: format!("Failed to parse unary operator: {}", e),
280 })?,
281 expr: Box::new(expr.to_syn()?),
282 })),
283 PureExpr::Call { func, args } => Ok(syn::Expr::Call(syn::ExprCall {
284 attrs: vec![],
285 func: Box::new(func.to_syn()?),
286 paren_token: token::Paren::default(),
287 args: args.iter().map(|a| a.to_syn()).collect::<Result<_, _>>()?,
288 })),
289 PureExpr::MethodCall {
290 receiver,
291 method,
292 turbofish,
293 args,
294 } => Ok(syn::Expr::MethodCall(syn::ExprMethodCall {
295 attrs: vec![],
296 receiver: Box::new(receiver.to_syn()?),
297 dot_token: token::Dot::default(),
298 method: ident(method),
299 turbofish: turbofish.as_ref().and_then(|t| {
300 let args_str = format!("::<{}>", t);
302 syn::parse_str::<syn::AngleBracketedGenericArguments>(&args_str).ok()
303 }),
304 paren_token: token::Paren::default(),
305 args: args.iter().map(|a| a.to_syn()).collect::<Result<_, _>>()?,
306 })),
307 PureExpr::Field { expr, field } => Ok(syn::Expr::Field(syn::ExprField {
308 attrs: vec![],
309 base: Box::new(expr.to_syn()?),
310 dot_token: token::Dot::default(),
311 member: if let Ok(index) = field.parse::<u32>() {
312 syn::Member::Unnamed(syn::Index {
313 index,
314 span: Span::call_site(),
315 })
316 } else {
317 syn::Member::Named(ident(field))
318 },
319 })),
320 PureExpr::Index { expr, index } => Ok(syn::Expr::Index(syn::ExprIndex {
321 attrs: vec![],
322 expr: Box::new(expr.to_syn()?),
323 bracket_token: token::Bracket::default(),
324 index: Box::new(index.to_syn()?),
325 })),
326 PureExpr::Block { label, block } => Ok(syn::Expr::Block(syn::ExprBlock {
327 attrs: vec![],
328 label: label.as_ref().map(|l| make_label(l)),
329 block: block.to_syn()?,
330 })),
331 PureExpr::If {
332 cond,
333 then_branch,
334 else_branch,
335 } => {
336 let else_b = else_branch
337 .as_ref()
338 .map(|e| Ok((token::Else::default(), Box::new(e.to_syn()?))))
339 .transpose()?;
340 Ok(syn::Expr::If(syn::ExprIf {
341 attrs: vec![],
342 if_token: token::If::default(),
343 cond: Box::new(cond.to_syn()?),
344 then_branch: then_branch.to_syn()?,
345 else_branch: else_b,
346 }))
347 }
348 PureExpr::Match { expr, arms } => Ok(syn::Expr::Match(syn::ExprMatch {
349 attrs: vec![],
350 match_token: token::Match::default(),
351 expr: Box::new(expr.to_syn()?),
352 brace_token: token::Brace::default(),
353 arms: arms
354 .iter()
355 .map(|a| a.to_syn())
356 .collect::<Result<Vec<_>, _>>()?,
357 })),
358 PureExpr::Loop { label, body } => Ok(syn::Expr::Loop(syn::ExprLoop {
359 attrs: vec![],
360 label: label.as_ref().map(|l| make_label(l)),
361 loop_token: token::Loop::default(),
362 body: body.to_syn()?,
363 })),
364 PureExpr::While { label, cond, body } => Ok(syn::Expr::While(syn::ExprWhile {
365 attrs: vec![],
366 label: label.as_ref().map(|l| make_label(l)),
367 while_token: token::While::default(),
368 cond: Box::new(cond.to_syn()?),
369 body: body.to_syn()?,
370 })),
371 PureExpr::For {
372 label,
373 pat,
374 expr,
375 body,
376 } => Ok(syn::Expr::ForLoop(syn::ExprForLoop {
377 attrs: vec![],
378 label: label.as_ref().map(|l| make_label(l)),
379 for_token: token::For::default(),
380 pat: Box::new(pat.to_syn()?),
381 in_token: token::In::default(),
382 expr: Box::new(expr.to_syn()?),
383 body: body.to_syn()?,
384 })),
385 PureExpr::Return(expr) => {
386 let ret_expr = expr
387 .as_ref()
388 .map(|e| Ok(Box::new(e.to_syn()?)))
389 .transpose()?;
390 Ok(syn::Expr::Return(syn::ExprReturn {
391 attrs: vec![],
392 return_token: token::Return::default(),
393 expr: ret_expr,
394 }))
395 }
396 PureExpr::Break { label, expr } => {
397 let break_expr = expr
398 .as_ref()
399 .map(|e| Ok(Box::new(e.to_syn()?)))
400 .transpose()?;
401 Ok(syn::Expr::Break(syn::ExprBreak {
402 attrs: vec![],
403 break_token: token::Break::default(),
404 label: label.as_ref().map(|l| make_lifetime(l)),
405 expr: break_expr,
406 }))
407 }
408 PureExpr::Continue { label } => Ok(syn::Expr::Continue(syn::ExprContinue {
409 attrs: vec![],
410 continue_token: token::Continue::default(),
411 label: label.as_ref().map(|l| make_lifetime(l)),
412 })),
413 PureExpr::Closure {
414 is_async,
415 is_move,
416 params,
417 ret,
418 body,
419 } => {
420 let inputs = params
421 .iter()
422 .map(|cp| {
423 let pat = cp.pattern.to_syn()?;
424 match &cp.ty {
425 Some(ty) => Ok(syn::Pat::Type(syn::PatType {
426 attrs: vec![],
427 pat: Box::new(pat),
428 colon_token: token::Colon::default(),
429 ty: Box::new(ty.to_syn()?),
430 })),
431 None => Ok(pat),
432 }
433 })
434 .collect::<Result<_, ToSynError>>()?;
435 let output = match ret {
436 Some(ty) => {
437 syn::ReturnType::Type(token::RArrow::default(), Box::new(ty.to_syn()?))
438 }
439 None => syn::ReturnType::Default,
440 };
441 Ok(syn::Expr::Closure(syn::ExprClosure {
442 attrs: vec![],
443 lifetimes: None,
444 constness: None,
445 movability: None,
446 asyncness: if *is_async {
447 Some(token::Async::default())
448 } else {
449 None
450 },
451 capture: if *is_move {
452 Some(token::Move::default())
453 } else {
454 None
455 },
456 or1_token: token::Or::default(),
457 inputs,
458 or2_token: token::Or::default(),
459 output,
460 body: Box::new(body.to_syn()?),
461 }))
462 }
463 PureExpr::Struct { path, fields, rest } => {
464 let syn_fields = fields
473 .iter()
474 .map(|(name, expr)| {
475 let is_shorthand = matches!(
476 expr,
477 PureExpr::Path(p) if p == name
478 );
479 Ok(syn::FieldValue {
480 attrs: vec![],
481 member: syn::Member::Named(ident(name)),
482 colon_token: if is_shorthand {
483 None
484 } else {
485 Some(token::Colon::default())
486 },
487 expr: expr.to_syn()?,
488 })
489 })
490 .collect::<Result<_, ToSynError>>()?;
491 let (dot2_token, syn_rest) = match rest {
492 Some(r) => (Some(token::DotDot::default()), Some(Box::new(r.to_syn()?))),
493 None => (None, None),
494 };
495 Ok(syn::Expr::Struct(syn::ExprStruct {
496 attrs: vec![],
497 qself: None,
498 path: try_parse_path(path)?,
499 brace_token: token::Brace::default(),
500 fields: syn_fields,
501 dot2_token,
502 rest: syn_rest,
503 }))
504 }
505 PureExpr::Tuple(elems) => Ok(syn::Expr::Tuple(syn::ExprTuple {
506 attrs: vec![],
507 paren_token: token::Paren::default(),
508 elems: elems.iter().map(|e| e.to_syn()).collect::<Result<_, _>>()?,
509 })),
510 PureExpr::Array(elems) => Ok(syn::Expr::Array(syn::ExprArray {
511 attrs: vec![],
512 bracket_token: token::Bracket::default(),
513 elems: elems.iter().map(|e| e.to_syn()).collect::<Result<_, _>>()?,
514 })),
515 PureExpr::Ref { is_mut, expr } => Ok(syn::Expr::Reference(syn::ExprReference {
516 attrs: vec![],
517 and_token: token::And::default(),
518 mutability: if *is_mut {
519 Some(token::Mut::default())
520 } else {
521 None
522 },
523 expr: Box::new(expr.to_syn()?),
524 })),
525 PureExpr::Macro {
526 name,
527 delimiter,
528 tokens,
529 } => {
530 let parsed_tokens: proc_macro2::TokenStream =
531 tokens
532 .parse()
533 .map_err(|e: proc_macro2::LexError| ToSynError::Other {
534 message: format!("Failed to parse macro tokens '{}': {}", tokens, e),
535 })?;
536 Ok(syn::Expr::Macro(syn::ExprMacro {
537 attrs: vec![],
538 mac: syn::Macro {
539 path: try_parse_path(name)?,
540 bang_token: token::Not::default(),
541 delimiter: match delimiter {
542 MacroDelimiter::Paren => {
543 syn::MacroDelimiter::Paren(token::Paren::default())
544 }
545 MacroDelimiter::Brace => {
546 syn::MacroDelimiter::Brace(token::Brace::default())
547 }
548 MacroDelimiter::Bracket => {
549 syn::MacroDelimiter::Bracket(token::Bracket::default())
550 }
551 },
552 tokens: parsed_tokens,
553 },
554 }))
555 }
556 PureExpr::Await(expr) => Ok(syn::Expr::Await(syn::ExprAwait {
557 attrs: vec![],
558 base: Box::new(expr.to_syn()?),
559 dot_token: token::Dot::default(),
560 await_token: token::Await::default(),
561 })),
562 PureExpr::Try(expr) => Ok(syn::Expr::Try(syn::ExprTry {
563 attrs: vec![],
564 expr: Box::new(expr.to_syn()?),
565 question_token: token::Question::default(),
566 })),
567 PureExpr::Range {
568 start,
569 end,
570 inclusive,
571 } => {
572 let start_expr = start
573 .as_ref()
574 .map(|e| Ok(Box::new(e.to_syn()?)))
575 .transpose()?;
576 let end_expr = end
577 .as_ref()
578 .map(|e| Ok(Box::new(e.to_syn()?)))
579 .transpose()?;
580 Ok(syn::Expr::Range(syn::ExprRange {
581 attrs: vec![],
582 start: start_expr,
583 limits: if *inclusive {
584 syn::RangeLimits::Closed(token::DotDotEq::default())
585 } else {
586 syn::RangeLimits::HalfOpen(token::DotDot::default())
587 },
588 end: end_expr,
589 }))
590 }
591 PureExpr::Cast { expr, ty } => Ok(syn::Expr::Cast(syn::ExprCast {
592 attrs: vec![],
593 expr: Box::new(expr.to_syn()?),
594 as_token: token::As::default(),
595 ty: Box::new(ty.to_syn()?),
596 })),
597 PureExpr::Let { pattern, expr } => Ok(syn::Expr::Let(syn::ExprLet {
598 attrs: vec![],
599 let_token: token::Let::default(),
600 pat: Box::new(pattern.to_syn()?),
601 eq_token: token::Eq::default(),
602 expr: Box::new(expr.to_syn()?),
603 })),
604 PureExpr::Async { is_move, body } => Ok(syn::Expr::Async(syn::ExprAsync {
605 attrs: vec![],
606 async_token: token::Async::default(),
607 capture: if *is_move {
608 Some(token::Move::default())
609 } else {
610 None
611 },
612 block: body.to_syn()?,
613 })),
614 PureExpr::Unsafe(body) => Ok(syn::Expr::Unsafe(syn::ExprUnsafe {
615 attrs: vec![],
616 unsafe_token: token::Unsafe::default(),
617 block: body.to_syn()?,
618 })),
619 PureExpr::Repeat { expr, len } => Ok(syn::Expr::Repeat(syn::ExprRepeat {
620 attrs: vec![],
621 bracket_token: token::Bracket::default(),
622 expr: Box::new(expr.to_syn()?),
623 semi_token: token::Semi::default(),
624 len: Box::new(len.to_syn()?),
625 })),
626 PureExpr::Other(s) => syn::parse_str(s).map_err(|e| ToSynError::ParseExpr {
627 input: s.clone(),
628 message: e.to_string(),
629 }),
630 PureExpr::Verbatim(raw) => {
631 let idx = super::register_verbatim_expr(raw.clone());
636 let stub = format!("__ryo_verbatim_expr_{}", idx);
637 syn::parse_str::<syn::Expr>(&stub).map_err(|e| ToSynError::ParseExpr {
638 input: stub,
639 message: e.to_string(),
640 })
641 }
642 }
643 }
644}
645
646impl ToSyn for PureMatchArm {
647 type Output = syn::Arm;
648
649 fn to_syn(&self) -> Result<syn::Arm, ToSynError> {
650 let guard = self
651 .guard
652 .as_ref()
653 .map(|g| Ok((token::If::default(), Box::new(g.to_syn()?))))
654 .transpose()?;
655 Ok(syn::Arm {
656 attrs: vec![],
657 pat: self.pattern.to_syn()?,
658 guard,
659 fat_arrow_token: token::FatArrow::default(),
660 body: Box::new(self.body.to_syn()?),
661 comma: Some(token::Comma::default()),
662 })
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use crate::pure::ast::{PureBlock, PureClosureParam, PureStmt};
670 use crate::pure::convert::ToPure;
671 use quote::ToTokens;
672
673 #[test]
674 fn test_pure_pattern_ident() {
675 let pat = PurePattern::Ident {
676 name: "x".to_string(),
677 is_mut: false,
678 by_ref: false,
679 };
680 let syn_pat = pat.to_syn().unwrap();
681 let output = syn_pat.to_token_stream().to_string();
682 assert_eq!(output.trim(), "x");
683 }
684
685 #[test]
686 fn test_pure_pattern_mut_ident() {
687 let pat = PurePattern::Ident {
688 name: "x".to_string(),
689 is_mut: true,
690 by_ref: false,
691 };
692 let syn_pat = pat.to_syn().unwrap();
693 let output = syn_pat.to_token_stream().to_string();
694 assert!(output.contains("mut"), "Output: {}", output);
695 }
696
697 #[test]
698 fn test_pure_pattern_ref_ident_roundtrip() {
699 use crate::pure::PureBlock;
707 use crate::pure::PureStmt;
708
709 let src = "{ if let Some(ref query_file) = args.query_file { let _ = query_file; } }";
712 let syn_block: syn::Block = syn::parse_str(src).unwrap();
713 let pure_block: PureBlock = syn_block.to_pure();
714
715 let if_expr = match pure_block.stmts.first().unwrap() {
717 PureStmt::Expr(e) => e,
718 PureStmt::Semi(e) => e,
719 other => panic!("expected expr stmt, got {:?}", other),
720 };
721 let cond = match if_expr {
722 PureExpr::If { cond, .. } => cond.as_ref(),
723 other => panic!("expected If, got {:?}", other),
724 };
725 let let_pat = match cond {
726 PureExpr::Let { pattern, .. } => pattern,
727 other => panic!("expected Let, got {:?}", other),
728 };
729 let fields = match let_pat {
730 PurePattern::Struct { fields, path, .. } => {
731 assert_eq!(path, "Some");
732 fields
733 }
734 other => panic!("expected Struct pattern, got {:?}", other),
735 };
736 let inner = &fields[0].1;
737 match inner {
738 PurePattern::Ident {
739 name,
740 by_ref,
741 is_mut,
742 } => {
743 assert_eq!(name, "query_file");
744 assert!(*by_ref, "by_ref must survive syn → Pure conversion");
745 assert!(!*is_mut);
746 }
747 other => panic!("expected Ident, got {:?}", other),
748 }
749
750 let regenerated = pure_block.to_syn().unwrap();
753 let out = quote::quote!(#regenerated).to_string();
754 assert!(
755 out.contains("ref query_file") || out.contains("ref query_file"),
756 "regenerated source must contain `ref query_file`, got: {}",
757 out
758 );
759 }
760
761 #[test]
762 fn test_pure_pattern_ref_mut_ident_roundtrip() {
763 use crate::pure::PureBlock;
767
768 let src = "{ if let Some(ref mut buf) = sink { let _ = buf; } }";
769 let syn_block: syn::Block = syn::parse_str(src).unwrap();
770 let pure_block: PureBlock = syn_block.to_pure();
771 let regenerated = pure_block.to_syn().unwrap();
772 let out = quote::quote!(#regenerated).to_string();
773 assert!(
774 out.contains("ref mut buf"),
775 "regenerated source must contain `ref mut buf`, got: {}",
776 out
777 );
778 }
779
780 #[test]
781 fn test_pure_expr_binary() {
782 let expr = PureExpr::Binary {
783 op: "+".to_string(),
784 left: Box::new(PureExpr::Path("a".to_string())),
785 right: Box::new(PureExpr::Path("b".to_string())),
786 };
787 let syn_expr = expr.to_syn().unwrap();
788 let output = syn_expr.to_token_stream().to_string();
789 assert!(output.contains("+"), "Output: {}", output);
790 }
791
792 #[test]
793 fn test_pure_expr_closure() {
794 let expr = PureExpr::Closure {
795 is_async: false,
796 is_move: true,
797 params: vec![PureClosureParam::untyped(PurePattern::Ident {
798 name: "x".to_string(),
799 is_mut: false,
800 by_ref: false,
801 })],
802 ret: None,
803 body: Box::new(PureExpr::Path("x".to_string())),
804 };
805 let syn_expr = expr.to_syn().unwrap();
806 let output = syn_expr.to_token_stream().to_string();
807 assert!(output.contains("move"), "Output: {}", output);
808 }
809
810 #[test]
811 fn test_pure_match_arm() {
812 let arm = PureMatchArm {
813 pattern: PurePattern::Ident {
814 name: "x".to_string(),
815 is_mut: false,
816 by_ref: false,
817 },
818 guard: None,
819 body: PureExpr::Path("x".to_string()),
820 };
821 let syn_arm = arm.to_syn().unwrap();
822 let output = syn_arm.to_token_stream().to_string();
823 assert!(output.contains("=>"), "Output: {}", output);
824 }
825
826 #[test]
827 fn test_pure_expr_labeled_loop() {
828 let expr = PureExpr::Loop {
829 label: Some("outer".to_string()),
830 body: PureBlock { stmts: vec![] },
831 };
832 let syn_expr = expr.to_syn().unwrap();
833 let output = syn_expr.to_token_stream().to_string();
834 assert!(
835 output.contains("'outer") || output.contains("' outer"),
836 "Output should contain label: {}",
837 output
838 );
839 assert!(
840 output.contains("loop"),
841 "Output should contain 'loop': {}",
842 output
843 );
844 }
845
846 #[test]
847 fn test_pure_expr_labeled_while() {
848 let expr = PureExpr::While {
849 label: Some("my_loop".to_string()),
850 cond: Box::new(PureExpr::Lit("true".to_string())),
851 body: PureBlock { stmts: vec![] },
852 };
853 let syn_expr = expr.to_syn().unwrap();
854 let output = syn_expr.to_token_stream().to_string();
855 assert!(
856 output.contains("'my_loop") || output.contains("' my_loop"),
857 "Output should contain label: {}",
858 output
859 );
860 assert!(
861 output.contains("while"),
862 "Output should contain 'while': {}",
863 output
864 );
865 }
866
867 #[test]
868 fn test_pure_expr_labeled_for() {
869 let expr = PureExpr::For {
870 label: Some("iter_loop".to_string()),
871 pat: PurePattern::Ident {
872 name: "i".to_string(),
873 is_mut: false,
874 by_ref: false,
875 },
876 expr: Box::new(PureExpr::Path("items".to_string())),
877 body: PureBlock { stmts: vec![] },
878 };
879 let syn_expr = expr.to_syn().unwrap();
880 let output = syn_expr.to_token_stream().to_string();
881 assert!(
882 output.contains("'iter_loop") || output.contains("' iter_loop"),
883 "Output should contain label: {}",
884 output
885 );
886 assert!(
887 output.contains("for"),
888 "Output should contain 'for': {}",
889 output
890 );
891 }
892
893 #[test]
894 fn test_pure_expr_labeled_block() {
895 let expr = PureExpr::Block {
896 label: Some("my_block".to_string()),
897 block: PureBlock {
898 stmts: vec![PureStmt::Expr(PureExpr::Lit("42".to_string()))],
899 },
900 };
901 let syn_expr = expr.to_syn().unwrap();
902 let output = syn_expr.to_token_stream().to_string();
903 assert!(
904 output.contains("'my_block") || output.contains("' my_block"),
905 "Output should contain label: {}",
906 output
907 );
908 }
909
910 #[test]
911 fn test_pure_expr_break_with_label() {
912 let expr = PureExpr::Break {
913 label: Some("outer".to_string()),
914 expr: None,
915 };
916 let syn_expr = expr.to_syn().unwrap();
917 let output = syn_expr.to_token_stream().to_string();
918 assert!(
919 output.contains("break 'outer") || output.contains("break ' outer"),
920 "Output should contain 'break 'outer': {}",
921 output
922 );
923 }
924
925 #[test]
926 fn test_pure_expr_break_with_label_and_value() {
927 let expr = PureExpr::Break {
928 label: Some("block".to_string()),
929 expr: Some(Box::new(PureExpr::Lit("42".to_string()))),
930 };
931 let syn_expr = expr.to_syn().unwrap();
932 let output = syn_expr.to_token_stream().to_string();
933 assert!(
934 output.contains("break 'block 42") || output.contains("break ' block 42"),
935 "Output should contain 'break 'block 42': {}",
936 output
937 );
938 }
939
940 #[test]
941 fn test_pure_expr_continue_with_label() {
942 let expr = PureExpr::Continue {
943 label: Some("loop_label".to_string()),
944 };
945 let syn_expr = expr.to_syn().unwrap();
946 let output = syn_expr.to_token_stream().to_string();
947 assert!(
948 output.contains("continue 'loop_label") || output.contains("continue ' loop_label"),
949 "Output should contain 'continue 'loop_label': {}",
950 output
951 );
952 }
953
954 #[test]
955 fn test_pure_expr_continue_without_label() {
956 let expr = PureExpr::Continue { label: None };
957 let syn_expr = expr.to_syn().unwrap();
958 let output = syn_expr.to_token_stream().to_string();
959 assert_eq!(output.trim(), "continue");
960 }
961
962 #[test]
963 fn test_pure_expr_break_without_label() {
964 let expr = PureExpr::Break {
965 label: None,
966 expr: None,
967 };
968 let syn_expr = expr.to_syn().unwrap();
969 let output = syn_expr.to_token_stream().to_string();
970 assert_eq!(output.trim(), "break");
971 }
972
973 #[test]
974 fn test_pure_expr_loop_without_label() {
975 let expr = PureExpr::Loop {
976 label: None,
977 body: PureBlock { stmts: vec![] },
978 };
979 let syn_expr = expr.to_syn().unwrap();
980 let output = syn_expr.to_token_stream().to_string();
981 assert!(output.contains("loop"), "Output: {}", output);
982 assert!(
983 !output.contains("'"),
984 "Output should not contain label quote: {}",
985 output
986 );
987 }
988}