1use std::fmt::Write as _;
13
14#[derive(Debug, Clone, PartialEq)]
20pub enum RustItem {
21 Fn(RustFn),
23 Struct(RustStruct),
25 Enum(RustEnum),
27 Trait(RustTrait),
29 Impl(RustImpl),
31 Mod(RustMod),
33 Const(RustConst),
35 Use(String),
37 Comment(String),
39 InnerAttr(String),
41 Raw(String),
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct RustFn {
51 pub name: String,
53 pub type_params: Vec<String>,
55 pub params: Vec<RustParam>,
57 pub ret: Option<RustType>,
59 pub body: Vec<RustStmt>,
61 pub is_pub: bool,
63 pub is_unsafe: bool,
65 pub doc: Vec<String>,
67 pub attrs: Vec<String>,
69 pub is_abstract: bool,
71}
72
73impl Default for RustFn {
74 fn default() -> Self {
75 Self {
76 name: String::new(),
77 type_params: Vec::new(),
78 params: Vec::new(),
79 ret: None,
80 body: Vec::new(),
81 is_pub: true,
82 is_unsafe: false,
83 doc: Vec::new(),
84 attrs: Vec::new(),
85 is_abstract: false,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub struct RustParam {
93 pub name: String,
94 pub ty: RustType,
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub enum RustType {
100 Named(String),
102 Generic(String, Vec<RustType>),
104 Ref(Box<RustType>),
106 RefMut(Box<RustType>),
108 Tuple(Vec<RustType>),
110 Unit,
112 Never,
114 Raw(String),
116}
117
118impl RustType {
119 pub fn i64() -> Self {
121 Self::Named("i64".into())
122 }
123 pub fn u64() -> Self {
124 Self::Named("u64".into())
125 }
126 pub fn f64() -> Self {
127 Self::Named("f64".into())
128 }
129 pub fn bool() -> Self {
130 Self::Named("bool".into())
131 }
132 pub fn string() -> Self {
133 Self::Named("String".into())
134 }
135 pub fn bytes() -> Self {
136 Self::Generic("Vec".into(), vec![Self::Named("u8".into())])
137 }
138 pub fn result(ok: RustType, err: RustType) -> Self {
139 Self::Generic("Result".into(), vec![ok, err])
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
145pub enum RustStmt {
146 Let {
148 name: String,
149 ty: Option<RustType>,
150 init: RustExpr,
151 },
152 Assert { cond: String, label: String },
154 Comment(String),
156 Expr(RustExpr),
158 Raw(String),
160}
161
162#[derive(Debug, Clone, PartialEq)]
164pub enum RustExpr {
165 Ident(String),
167 Literal(String),
169 Call(String, Vec<RustExpr>),
171 MethodCall(Box<RustExpr>, String, Vec<RustExpr>),
173 Todo(String),
175 Ok(Box<RustExpr>),
177 Clone(Box<RustExpr>),
179 Raw(String),
181}
182
183#[derive(Debug, Clone, PartialEq)]
185pub struct RustStruct {
186 pub name: String,
187 pub type_params: Vec<String>,
188 pub fields: Vec<RustField>,
189 pub derives: Vec<String>,
191 pub is_pub: bool,
192 pub doc: Vec<String>,
194}
195
196impl Default for RustStruct {
197 fn default() -> Self {
198 Self {
199 name: String::new(),
200 type_params: Vec::new(),
201 fields: Vec::new(),
202 derives: vec!["Debug".into(), "Clone".into(), "PartialEq".into()],
203 is_pub: true,
204 doc: Vec::new(),
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct RustField {
212 pub name: String,
213 pub ty: RustType,
214 pub is_pub: bool,
215}
216
217#[derive(Debug, Clone, PartialEq)]
219pub struct RustEnum {
220 pub name: String,
221 pub type_params: Vec<String>,
222 pub variants: Vec<RustVariant>,
223 pub derives: Vec<String>,
224 pub is_pub: bool,
225 pub doc: Vec<String>,
226 pub attrs: Vec<String>,
228}
229
230impl Default for RustEnum {
231 fn default() -> Self {
232 Self {
233 name: String::new(),
234 type_params: Vec::new(),
235 variants: Vec::new(),
236 derives: vec!["Debug".into(), "Clone".into(), "PartialEq".into()],
237 is_pub: true,
238 doc: Vec::new(),
239 attrs: Vec::new(),
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
246pub struct RustVariant {
247 pub name: String,
248 pub fields: Vec<RustType>,
250 pub attrs: Vec<String>,
252}
253
254#[derive(Debug, Clone, PartialEq)]
256pub struct RustTrait {
257 pub name: String,
258 pub type_params: Vec<String>,
259 pub supertraits: Vec<String>,
260 pub methods: Vec<RustFn>,
261 pub is_pub: bool,
262 pub doc: Vec<String>,
263}
264
265#[derive(Debug, Clone, PartialEq)]
267pub struct RustImpl {
268 pub trait_name: Option<String>,
270 pub target: String,
272 pub type_params: Vec<String>,
273 pub methods: Vec<RustFn>,
274}
275
276#[derive(Debug, Clone, PartialEq)]
278pub struct RustMod {
279 pub name: String,
280 pub items: Vec<RustItem>,
281 pub is_pub: bool,
282 pub doc: Vec<String>,
283}
284
285#[derive(Debug, Clone, PartialEq)]
287pub struct RustConst {
288 pub name: String,
289 pub ty: RustType,
290 pub value: String,
291 pub is_pub: bool,
292 pub doc: Vec<String>,
293}
294
295pub fn render_items(items: &[RustItem]) -> String {
304 let mut code = String::new();
305 for item in items {
306 render_item(item, &mut code);
307 }
308 match syn::parse_file(&code) {
310 Ok(syntax_tree) => prettyplease::unparse(&syntax_tree),
311 Err(e) => {
312 eprintln!("warning: HIR-generated Rust has syntax errors, skipping formatting: {e}");
313 format!("// WARNING: prettyplease formatting skipped (parse error: {e})\n\n{code}")
314 }
315 }
316}
317
318fn render_item(item: &RustItem, out: &mut String) {
320 match item {
321 RustItem::Fn(f) => render_fn(f, out),
322 RustItem::Struct(s) => render_struct(s, out),
323 RustItem::Enum(e) => render_enum(e, out),
324 RustItem::Trait(t) => render_trait(t, out),
325 RustItem::Impl(i) => render_impl(i, out),
326 RustItem::Mod(m) => render_mod(m, out),
327 RustItem::Const(c) => render_const(c, out),
328 RustItem::Use(path) => {
329 let _ = writeln!(out, "use {path};");
330 }
331 RustItem::Comment(text) => {
332 let _ = writeln!(out, "// {text}");
333 }
334 RustItem::InnerAttr(attr) => {
335 let _ = writeln!(out, "#![{attr}]");
336 }
337 RustItem::Raw(code) => {
338 out.push_str(code);
339 if !code.ends_with('\n') {
340 out.push('\n');
341 }
342 }
343 }
344}
345
346fn render_fn(f: &RustFn, out: &mut String) {
347 render_fn_opts(f, out, &RenderOpts::default());
348}
349
350fn render_fn_opts(f: &RustFn, out: &mut String, opts: &RenderOpts) {
351 for doc in &f.doc {
352 let _ = writeln!(out, "/// {doc}");
353 }
354 for attr in &f.attrs {
355 let _ = writeln!(out, "#[{attr}]");
356 }
357
358 let vis = if f.is_pub { "pub " } else { "" };
359 let unsafe_kw = if f.is_unsafe { "unsafe " } else { "" };
360 let tps = if f.type_params.is_empty() {
361 String::new()
362 } else {
363 format!("<{}>", f.type_params.join(", "))
364 };
365
366 let params: Vec<String> = f
367 .params
368 .iter()
369 .map(|p| {
370 if p.name == "&self" || p.name == "self" || p.name == "&mut self" {
372 p.name.clone()
373 } else {
374 format!("{}: {}", p.name, render_type(&p.ty))
375 }
376 })
377 .collect();
378 let params_s = params.join(", ");
379
380 let ret = match &f.ret {
381 Some(ty) => format!(" -> {}", render_type(ty)),
382 None => String::new(),
383 };
384
385 if f.is_abstract {
386 let _ = writeln!(
387 out,
388 "{vis}{unsafe_kw}fn {}{tps}({params_s}){ret};\n",
389 f.name
390 );
391 } else {
392 let _ = writeln!(
393 out,
394 "{vis}{unsafe_kw}fn {}{tps}({params_s}){ret} {{",
395 f.name
396 );
397
398 for stmt in &f.body {
399 render_stmt_opts(stmt, out, 1, opts);
400 }
401
402 out.push_str("}\n\n");
403 }
404}
405
406fn render_struct(s: &RustStruct, out: &mut String) {
407 for doc in &s.doc {
408 let _ = writeln!(out, "/// {doc}");
409 }
410 if !s.derives.is_empty() {
411 let _ = writeln!(out, "#[derive({})]", s.derives.join(", "));
412 }
413
414 let vis = if s.is_pub { "pub " } else { "" };
415 let tps = if s.type_params.is_empty() {
416 String::new()
417 } else {
418 format!("<{}>", s.type_params.join(", "))
419 };
420
421 if s.fields.is_empty() {
422 let _ = writeln!(out, "{vis}struct {}{tps};\n", s.name);
423 } else {
424 let _ = writeln!(out, "{vis}struct {}{tps} {{", s.name);
425 for field in &s.fields {
426 let fvis = if field.is_pub { "pub " } else { "" };
427 let _ = writeln!(out, " {fvis}{}: {},", field.name, render_type(&field.ty));
428 }
429 out.push_str("}\n\n");
430 }
431}
432
433fn render_enum(e: &RustEnum, out: &mut String) {
434 for doc in &e.doc {
435 let _ = writeln!(out, "/// {doc}");
436 }
437 if !e.derives.is_empty() {
438 let _ = writeln!(out, "#[derive({})]", e.derives.join(", "));
439 }
440 for attr in &e.attrs {
441 let _ = writeln!(out, "#[{attr}]");
442 }
443
444 let vis = if e.is_pub { "pub " } else { "" };
445 let tps = if e.type_params.is_empty() {
446 String::new()
447 } else {
448 format!("<{}>", e.type_params.join(", "))
449 };
450
451 let _ = writeln!(out, "{vis}enum {}{tps} {{", e.name);
452 for variant in &e.variants {
453 for attr in &variant.attrs {
454 let _ = writeln!(out, " #[{attr}]");
455 }
456 if variant.fields.is_empty() {
457 let _ = writeln!(out, " {},", variant.name);
458 } else {
459 let fields: Vec<String> = variant.fields.iter().map(render_type).collect();
460 let _ = writeln!(out, " {}({}),", variant.name, fields.join(", "));
461 }
462 }
463 out.push_str("}\n\n");
464}
465
466fn render_trait(t: &RustTrait, out: &mut String) {
467 for doc in &t.doc {
468 let _ = writeln!(out, "/// {doc}");
469 }
470
471 let vis = if t.is_pub { "pub " } else { "" };
472 let tps = if t.type_params.is_empty() {
473 String::new()
474 } else {
475 format!("<{}>", t.type_params.join(", "))
476 };
477 let bounds = if t.supertraits.is_empty() {
478 String::new()
479 } else {
480 format!(": {}", t.supertraits.join(" + "))
481 };
482
483 let _ = writeln!(out, "{vis}trait {}{tps}{bounds} {{", t.name);
484 for method in &t.methods {
485 render_fn(method, out);
487 }
488 out.push_str("}\n\n");
489}
490
491fn render_impl(i: &RustImpl, out: &mut String) {
492 let tps = if i.type_params.is_empty() {
493 String::new()
494 } else {
495 format!("<{}>", i.type_params.join(", "))
496 };
497
498 match &i.trait_name {
499 Some(trait_name) => {
500 let _ = writeln!(out, "impl{tps} {trait_name} for {}{tps} {{", i.target);
501 }
502 None => {
503 let _ = writeln!(out, "impl{tps} {}{tps} {{", i.target);
504 }
505 }
506 for method in &i.methods {
507 render_fn(method, out);
508 }
509 out.push_str("}\n\n");
510}
511
512fn render_mod(m: &RustMod, out: &mut String) {
513 for doc in &m.doc {
514 let _ = writeln!(out, "/// {doc}");
515 }
516 let vis = if m.is_pub { "pub " } else { "" };
517 let _ = writeln!(out, "{vis}mod {} {{", m.name);
518 for item in &m.items {
519 render_item(item, out);
520 }
521 out.push_str("}\n\n");
522}
523
524fn render_const(c: &RustConst, out: &mut String) {
525 for doc in &c.doc {
526 let _ = writeln!(out, "/// {doc}");
527 }
528 let vis = if c.is_pub { "pub " } else { "" };
529 let _ = writeln!(
530 out,
531 "{vis}const {}: {} = {};",
532 c.name,
533 render_type(&c.ty),
534 c.value
535 );
536}
537
538fn render_type(ty: &RustType) -> String {
539 match ty {
540 RustType::Named(name) => name.clone(),
541 RustType::Generic(name, args) => {
542 let args_s: Vec<String> = args.iter().map(render_type).collect();
543 format!("{name}<{}>", args_s.join(", "))
544 }
545 RustType::Ref(inner) => format!("&{}", render_type(inner)),
546 RustType::RefMut(inner) => format!("&mut {}", render_type(inner)),
547 RustType::Tuple(items) => {
548 let items_s: Vec<String> = items.iter().map(render_type).collect();
549 format!("({})", items_s.join(", "))
550 }
551 RustType::Unit => "()".into(),
552 RustType::Never => "!".into(),
553 RustType::Raw(s) => s.clone(),
554 }
555}
556
557#[cfg(test)]
558pub(crate) fn render_stmt(stmt: &RustStmt, out: &mut String, indent: usize) {
559 render_stmt_opts(stmt, out, indent, &RenderOpts::default());
560}
561
562#[derive(Debug, Clone, Default)]
564pub(crate) struct RenderOpts {
565 pub runtime_checks: bool,
568 pub contract_name: String,
570}
571
572pub(crate) fn render_stmt_opts(
573 stmt: &RustStmt,
574 out: &mut String,
575 indent: usize,
576 opts: &RenderOpts,
577) {
578 let pad = " ".repeat(indent);
579 match stmt {
580 RustStmt::Let { name, ty, init } => {
581 let ty_s = match ty {
582 Some(t) => format!(": {}", render_type(t)),
583 None => String::new(),
584 };
585 let _ = writeln!(out, "{pad}let {name}{ty_s} = {};", render_expr(init));
586 }
587 RustStmt::Assert { cond, label } => {
588 if crate::has_deep_field_access(cond) {
591 let _ = writeln!(out, "{pad}// {label}: {}", cond.replace('"', "\\\""));
592 } else if opts.runtime_checks {
593 let escaped_cond = cond.replace('"', "\\\"");
595 let contract = opts.contract_name.replace('"', "\\\"");
596 if cond.contains('\n') {
597 let flat_cond = cond.replace('\n', " ");
598 let _ = writeln!(
599 out,
600 "{pad}if !({{ {cond} }}) {{ assura_runtime::contract_violation(\"{contract}\", \"{label}\", \"{escaped_cond}\", file!(), line!()); }}"
601 );
602 let _ = flat_cond.len(); } else {
604 let _ = writeln!(
605 out,
606 "{pad}if !({cond}) {{ assura_runtime::contract_violation(\"{contract}\", \"{label}\", \"{escaped_cond}\", file!(), line!()); }}"
607 );
608 }
609 } else if cond.contains('\n') {
610 let msg = cond.replace('\n', " ").replace('"', "\\\"");
611 let _ = writeln!(out, "{pad}debug_assert!({{ {cond} }}, \"{label}: {msg}\");");
612 } else {
613 let escaped_cond = cond.replace('"', "\\\"");
614 let _ = writeln!(
615 out,
616 "{pad}debug_assert!({cond}, \"{label}: {escaped_cond}\");"
617 );
618 }
619 }
620 RustStmt::Comment(text) => {
621 let _ = writeln!(out, "{pad}// {text}");
622 }
623 RustStmt::Expr(expr) => {
624 let _ = writeln!(out, "{pad}{}", render_expr(expr));
625 }
626 RustStmt::Raw(code) => {
627 for line in code.lines() {
628 let _ = writeln!(out, "{pad}{line}");
629 }
630 }
631 }
632}
633
634fn render_expr(expr: &RustExpr) -> String {
635 match expr {
636 RustExpr::Ident(name) => name.clone(),
637 RustExpr::Literal(lit) => lit.clone(),
638 RustExpr::Call(func, args) => {
639 let args_s: Vec<String> = args.iter().map(render_expr).collect();
640 format!("{func}({})", args_s.join(", "))
641 }
642 RustExpr::MethodCall(receiver, method, args) => {
643 let args_s: Vec<String> = args.iter().map(render_expr).collect();
644 format!("{}.{method}({})", render_expr(receiver), args_s.join(", "))
645 }
646 RustExpr::Todo(msg) => format!("todo!(\"{msg}\")"),
647 RustExpr::Ok(inner) => format!("Ok({})", render_expr(inner)),
648 RustExpr::Clone(inner) => format!("{}.clone()", render_expr(inner)),
649 RustExpr::Raw(code) => code.clone(),
650 }
651}
652
653pub fn build_error_enum(contract_name: &str, variants: &[String]) -> RustItem {
661 let enum_name = format!("{contract_name}Error");
662 RustItem::Enum(RustEnum {
663 name: enum_name,
664 variants: variants
665 .iter()
666 .map(|v| RustVariant {
667 name: v.clone(),
668 fields: vec![],
669 attrs: vec![format!("error(\"{v}\")")],
670 })
671 .collect(),
672 derives: vec!["Debug".into(), "thiserror::Error".into()],
673 ..RustEnum::default()
674 })
675}
676
677pub fn build_enum_def(e: &assura_ast::EnumDef) -> Vec<RustItem> {
683 let mut items = Vec::new();
684
685 let tps: Vec<String> = e.type_params.clone();
686
687 let rust_enum = RustEnum {
689 name: e.name.clone(),
690 type_params: tps.clone(),
691 variants: e
692 .variants
693 .iter()
694 .map(|v| RustVariant {
695 name: v.name.clone(),
696 fields: v
697 .fields
698 .iter()
699 .map(|f| RustType::Raw(super::map_type_token(f).to_string()))
700 .collect(),
701 attrs: vec![],
702 })
703 .collect(),
704 ..RustEnum::default()
705 };
706 items.push(RustItem::Enum(rust_enum));
707
708 if tps.is_empty() {
710 let arms: Vec<String> = e
711 .variants
712 .iter()
713 .map(|v| {
714 if v.fields.is_empty() {
715 format!("{}::{} => write!(f, \"{}\")", e.name, v.name, v.name)
716 } else {
717 let underscores: Vec<&str> = (0..v.fields.len()).map(|_| "_").collect();
718 format!(
719 "{}::{}({}) => write!(f, \"{}(...)\")",
720 e.name,
721 v.name,
722 underscores.join(", "),
723 v.name
724 )
725 }
726 })
727 .collect();
728
729 let match_body = format!("match self {{ {} }}", arms.join(", "));
730 items.push(RustItem::Impl(RustImpl {
731 trait_name: Some("std::fmt::Display".into()),
732 target: e.name.clone(),
733 type_params: vec![],
734 methods: vec![RustFn {
735 name: "fmt".into(),
736 params: vec![
737 RustParam {
738 name: "&self".into(),
739 ty: RustType::Raw("&Self".into()),
740 },
741 RustParam {
742 name: "f".into(),
743 ty: RustType::Raw("&mut std::fmt::Formatter<'_>".into()),
744 },
745 ],
746 ret: Some(RustType::Raw("std::fmt::Result".into())),
747 body: vec![RustStmt::Raw(match_body)],
748 is_pub: false,
749 ..RustFn::default()
750 }],
751 }));
752 }
753
754 if !e.variants.is_empty() && tps.is_empty() {
756 let arms: Vec<String> = e
757 .variants
758 .iter()
759 .map(|v| {
760 if v.fields.is_empty() {
761 format!("{}::{} => \"{}\"", e.name, v.name, v.name)
762 } else {
763 let underscores: Vec<&str> = (0..v.fields.len()).map(|_| "_").collect();
764 format!(
765 "{}::{}({}) => \"{}\"",
766 e.name,
767 v.name,
768 underscores.join(", "),
769 v.name
770 )
771 }
772 })
773 .collect();
774
775 let match_body = format!("match v {{ {} }}", arms.join(", "));
776 items.push(RustItem::Fn(RustFn {
777 name: format!("__exhaustive_check_{}", e.name.to_lowercase()),
778 params: vec![RustParam {
779 name: "v".into(),
780 ty: RustType::Ref(Box::new(RustType::Named(e.name.clone()))),
781 }],
782 ret: Some(RustType::Raw("&'static str".into())),
783 body: vec![RustStmt::Raw(match_body)],
784 is_pub: false,
785 attrs: vec!["allow(dead_code)".into()],
786 doc: vec![
787 format!("Compile-time exhaustiveness check for `{}`.", e.name),
788 "Adding a variant without updating all match sites causes a build error.".into(),
789 ],
790 ..RustFn::default()
791 }));
792 }
793
794 items
795}
796
797pub fn render_item_raw(item: &RustItem) -> String {
801 let mut out = String::new();
802 render_item(item, &mut out);
803 out
804}
805
806pub(crate) fn render_item_raw_with_opts(item: &RustItem, opts: &RenderOpts) -> String {
808 let mut out = String::new();
809 render_item_with_opts(item, &mut out, opts);
810 out
811}
812
813fn render_item_with_opts(item: &RustItem, out: &mut String, opts: &RenderOpts) {
814 match item {
815 RustItem::Fn(f) => render_fn_opts(f, out, opts),
816 other => render_item(other, out),
819 }
820}
821
822pub fn build_type_def(t: &assura_ast::TypeDef) -> Vec<RustItem> {
831 use assura_ast::TypeBody;
832 let tps = t.type_params.clone();
833
834 match &t.body {
835 TypeBody::Struct(fields) => {
836 let rust_fields: Vec<RustField> = fields
837 .iter()
838 .map(|f| {
839 let ty_tokens = f.ty.as_ref().map(|t| t.to_tokens()).unwrap_or_default();
840 RustField {
841 name: f.name.clone(),
842 ty: RustType::Raw(crate::map_type_tokens(&ty_tokens)),
843 is_pub: f.is_pub,
844 }
845 })
846 .collect();
847 vec![RustItem::Struct(RustStruct {
848 name: t.name.clone(),
849 type_params: tps,
850 fields: rust_fields,
851 derives: vec!["Debug".into(), "Clone".into(), "PartialEq".into()],
852 ..RustStruct::default()
853 })]
854 }
855 TypeBody::Alias(tokens) => {
856 let rust_ty = crate::map_type_tokens(tokens);
857 vec![RustItem::Raw(format!("pub type {} = {rust_ty};\n", t.name))]
858 }
859 TypeBody::Refined(tokens) => {
860 let base = crate::extract_base_type_from_refined(tokens);
861 let tps_str = if tps.is_empty() {
862 String::new()
863 } else {
864 format!("<{}>", tps.join(", "))
865 };
866 vec![RustItem::Raw(format!(
867 "#[derive(Debug, Clone, PartialEq)]\npub struct {}{tps_str}(pub {base});\n",
868 t.name
869 ))]
870 }
871 TypeBody::Empty => {
872 let tps_str = if tps.is_empty() {
873 String::new()
874 } else {
875 format!("<{}>", tps.join(", "))
876 };
877 if tps.is_empty() {
878 vec![RustItem::Raw(format!(
879 "#[derive(Debug, Clone, PartialEq)]\npub struct {}{};\n",
880 t.name, tps_str
881 ))]
882 } else {
883 let phantoms: Vec<String> = tps
884 .iter()
885 .map(|p| format!("std::marker::PhantomData<{p}>"))
886 .collect();
887 vec![RustItem::Raw(format!(
888 "#[derive(Debug, Clone, PartialEq)]\npub struct {}{tps_str}({});\n",
889 t.name,
890 phantoms.join(", ")
891 ))]
892 }
893 }
894 }
895}
896
897#[cfg(test)]
902mod tests {
903 use super::*;
904
905 #[test]
906 fn render_simple_fn() {
907 let f = RustFn {
908 name: "add".into(),
909 params: vec![
910 RustParam {
911 name: "a".into(),
912 ty: RustType::i64(),
913 },
914 RustParam {
915 name: "b".into(),
916 ty: RustType::i64(),
917 },
918 ],
919 ret: Some(RustType::i64()),
920 body: vec![RustStmt::Expr(RustExpr::Raw("a + b".into()))],
921 ..RustFn::default()
922 };
923 let code = render_items(&[RustItem::Fn(f)]);
924 assert!(code.contains("pub fn add(a: i64, b: i64) -> i64"));
925 assert!(code.contains("a + b"));
926 }
927
928 #[test]
929 fn render_unit_struct() {
930 let s = RustStruct {
931 name: "Marker".into(),
932 ..RustStruct::default()
933 };
934 let code = render_items(&[RustItem::Struct(s)]);
935 assert!(code.contains("#[derive(Debug, Clone, PartialEq)]"));
936 assert!(code.contains("pub struct Marker;"));
937 }
938
939 #[test]
940 fn render_struct_with_fields() {
941 let s = RustStruct {
942 name: "Point".into(),
943 fields: vec![
944 RustField {
945 name: "x".into(),
946 ty: RustType::f64(),
947 is_pub: true,
948 },
949 RustField {
950 name: "y".into(),
951 ty: RustType::f64(),
952 is_pub: true,
953 },
954 ],
955 ..RustStruct::default()
956 };
957 let code = render_items(&[RustItem::Struct(s)]);
958 assert!(code.contains("pub struct Point"));
959 assert!(code.contains("pub x: f64"));
960 assert!(code.contains("pub y: f64"));
961 }
962
963 #[test]
964 fn render_error_enum() {
965 let e = RustEnum {
966 name: "SafeDivisionError".into(),
967 variants: vec![
968 RustVariant {
969 name: "DivByZero".into(),
970 fields: vec![],
971 attrs: vec!["error(\"DivByZero\")".into()],
972 },
973 RustVariant {
974 name: "Overflow".into(),
975 fields: vec![],
976 attrs: vec!["error(\"Overflow\")".into()],
977 },
978 ],
979 derives: vec!["Debug".into(), "thiserror::Error".into()],
980 ..RustEnum::default()
981 };
982 let code = render_items(&[RustItem::Enum(e)]);
983 assert!(code.contains("#[derive(Debug, thiserror::Error)]"));
984 assert!(code.contains("pub enum SafeDivisionError"));
985 assert!(code.contains("#[error(\"DivByZero\")]"));
986 assert!(code.contains("DivByZero,"));
987 assert!(code.contains("#[error(\"Overflow\")]"));
988 assert!(code.contains("Overflow,"));
989 }
990
991 #[test]
992 fn render_fn_with_assert() {
993 let f = RustFn {
994 name: "check".into(),
995 params: vec![RustParam {
996 name: "x".into(),
997 ty: RustType::i64(),
998 }],
999 ret: Some(RustType::i64()),
1000 body: vec![
1001 RustStmt::Assert {
1002 cond: "(x > 0)".into(),
1003 label: "requires".into(),
1004 },
1005 RustStmt::Let {
1006 name: "__assura_result".into(),
1007 ty: Some(RustType::i64()),
1008 init: RustExpr::Todo("implementation provided by AI agent".into()),
1009 },
1010 RustStmt::Expr(RustExpr::Ident("__assura_result".into())),
1011 ],
1012 ..RustFn::default()
1013 };
1014 let code = render_items(&[RustItem::Fn(f)]);
1015 assert!(code.contains("debug_assert!"));
1016 assert!(code.contains("requires"));
1017 assert!(code.contains("todo!"));
1018 }
1019
1020 #[test]
1021 fn render_const() {
1022 let c = RustConst {
1023 name: "MAX_SIZE".into(),
1024 ty: RustType::u64(),
1025 value: "1024".into(),
1026 is_pub: true,
1027 doc: vec![],
1028 };
1029 let code = render_items(&[RustItem::Const(c)]);
1030 assert!(code.contains("pub const MAX_SIZE: u64 = 1024;"));
1031 }
1032
1033 #[test]
1034 fn render_generic_type() {
1035 let ty = RustType::result(RustType::i64(), RustType::Named("MyError".into()));
1036 assert_eq!(render_type(&ty), "Result<i64, MyError>");
1037 }
1038
1039 #[test]
1040 fn render_module() {
1041 let m = RustMod {
1042 name: "contract_foo".into(),
1043 items: vec![RustItem::Comment("inner".into())],
1044 is_pub: true,
1045 doc: vec!["Contract: Foo".into()],
1046 };
1047 let code = render_items(&[RustItem::Mod(m)]);
1048 assert!(code.contains("/// Contract: Foo"));
1049 assert!(code.contains("pub mod contract_foo"));
1050 }
1051
1052 #[test]
1053 fn render_impl_block() {
1054 let i = RustImpl {
1055 trait_name: Some("Display".into()),
1056 target: "Color".into(),
1057 type_params: vec![],
1058 methods: vec![RustFn {
1059 name: "fmt".into(),
1060 params: vec![
1061 RustParam {
1062 name: "&self".into(),
1063 ty: RustType::Raw("&Self".into()),
1064 },
1065 RustParam {
1066 name: "f".into(),
1067 ty: RustType::Raw("&mut std::fmt::Formatter<'_>".into()),
1068 },
1069 ],
1070 ret: Some(RustType::Raw("std::fmt::Result".into())),
1071 body: vec![RustStmt::Raw(
1072 "match self { _ => write!(f, \"Color\") }".into(),
1073 )],
1074 is_pub: false,
1075 ..RustFn::default()
1076 }],
1077 };
1078 let code = render_items(&[RustItem::Impl(i)]);
1079 assert!(code.contains("impl Display for Color"));
1080 }
1081
1082 #[test]
1083 fn type_shorthand_helpers() {
1084 assert_eq!(render_type(&RustType::i64()), "i64");
1085 assert_eq!(render_type(&RustType::u64()), "u64");
1086 assert_eq!(render_type(&RustType::f64()), "f64");
1087 assert_eq!(render_type(&RustType::bool()), "bool");
1088 assert_eq!(render_type(&RustType::string()), "String");
1089 assert_eq!(render_type(&RustType::bytes()), "Vec<u8>");
1090 assert_eq!(render_type(&RustType::Unit), "()");
1091 assert_eq!(render_type(&RustType::Never), "!");
1092 }
1093
1094 #[test]
1095 fn render_trait_def() {
1096 let t = RustTrait {
1097 name: "Serializable".into(),
1098 type_params: vec![],
1099 supertraits: vec!["Clone".into()],
1100 methods: vec![RustFn {
1101 name: "serialize".into(),
1102 params: vec![RustParam {
1103 name: "&self".into(),
1104 ty: RustType::Raw("&Self".into()),
1105 }],
1106 ret: Some(RustType::bytes()),
1107 body: vec![],
1108 is_pub: false,
1109 ..RustFn::default()
1110 }],
1111 is_pub: true,
1112 doc: vec![],
1113 };
1114 let code = render_items(&[RustItem::Trait(t)]);
1115 assert!(code.contains("pub trait Serializable: Clone"));
1116 }
1117
1118 #[test]
1121 fn count_asserts_in_fn() {
1122 let f = RustFn {
1123 name: "check".into(),
1124 params: vec![RustParam {
1125 name: "x".into(),
1126 ty: RustType::i64(),
1127 }],
1128 ret: Some(RustType::i64()),
1129 body: vec![
1130 RustStmt::Assert {
1131 cond: "(x > 0)".into(),
1132 label: "requires".into(),
1133 },
1134 RustStmt::Assert {
1135 cond: "(x < 100)".into(),
1136 label: "requires".into(),
1137 },
1138 RustStmt::Expr(RustExpr::Ident("x".into())),
1139 ],
1140 ..RustFn::default()
1141 };
1142 let assert_count = f
1143 .body
1144 .iter()
1145 .filter(|s| matches!(s, RustStmt::Assert { .. }))
1146 .count();
1147 assert_eq!(assert_count, 2, "should have exactly 2 assert statements");
1148 }
1149
1150 #[test]
1153 fn build_error_enum_structure() {
1154 let item = build_error_enum("SafeDivision", &["DivByZero".into(), "Overflow".into()]);
1155 if let RustItem::Enum(e) = &item {
1156 assert_eq!(e.name, "SafeDivisionError");
1157 assert_eq!(e.variants.len(), 2);
1158 assert_eq!(e.variants[0].name, "DivByZero");
1159 assert_eq!(e.variants[1].name, "Overflow");
1160 assert!(e.derives.contains(&"thiserror::Error".to_string()));
1161 for v in &e.variants {
1163 assert_eq!(v.attrs.len(), 1);
1164 assert!(v.attrs[0].starts_with("error(\""));
1165 }
1166 } else {
1167 panic!("Expected RustItem::Enum");
1168 }
1169 }
1170
1171 #[test]
1172 fn build_enum_def_structure() {
1173 let enum_def = assura_ast::EnumDef {
1174 name: "Color".into(),
1175 type_params: vec![],
1176 variants: vec![
1177 assura_ast::EnumVariant {
1178 name: "Red".into(),
1179 fields: vec![],
1180 },
1181 assura_ast::EnumVariant {
1182 name: "Blue".into(),
1183 fields: vec![],
1184 },
1185 assura_ast::EnumVariant {
1186 name: "Custom".into(),
1187 fields: vec!["Int".into()],
1188 },
1189 ],
1190 };
1191 let items = build_enum_def(&enum_def);
1192 assert_eq!(items.len(), 3, "enum + Display + exhaustive");
1194
1195 if let RustItem::Enum(e) = &items[0] {
1197 assert_eq!(e.name, "Color");
1198 assert_eq!(e.variants.len(), 3);
1199 assert_eq!(e.variants[2].name, "Custom");
1200 assert_eq!(e.variants[2].fields.len(), 1);
1201 } else {
1202 panic!("Expected RustItem::Enum");
1203 }
1204
1205 assert!(
1207 matches!(&items[1], RustItem::Impl(i) if i.trait_name.as_deref() == Some("std::fmt::Display"))
1208 );
1209
1210 assert!(matches!(&items[2], RustItem::Fn(f) if f.name == "__exhaustive_check_color"));
1212 }
1213
1214 #[test]
1215 fn build_enum_def_generic_no_display() {
1216 let enum_def = assura_ast::EnumDef {
1217 name: "Option".into(),
1218 type_params: vec!["T".into()],
1219 variants: vec![
1220 assura_ast::EnumVariant {
1221 name: "Some".into(),
1222 fields: vec!["T".into()],
1223 },
1224 assura_ast::EnumVariant {
1225 name: "None".into(),
1226 fields: vec![],
1227 },
1228 ],
1229 };
1230 let items = build_enum_def(&enum_def);
1231 assert_eq!(
1233 items.len(),
1234 1,
1235 "only enum, no Display or exhaustive for generic"
1236 );
1237 assert!(matches!(&items[0], RustItem::Enum(e) if e.type_params == vec!["T"]));
1238 }
1239
1240 #[test]
1243 fn round_trip_parse_format() {
1244 let items = vec![
1246 RustItem::Fn(RustFn {
1247 name: "add".into(),
1248 params: vec![
1249 RustParam {
1250 name: "a".into(),
1251 ty: RustType::i64(),
1252 },
1253 RustParam {
1254 name: "b".into(),
1255 ty: RustType::i64(),
1256 },
1257 ],
1258 ret: Some(RustType::i64()),
1259 body: vec![RustStmt::Expr(RustExpr::Raw("a + b".into()))],
1260 ..RustFn::default()
1261 }),
1262 RustItem::Struct(RustStruct {
1263 name: "Point".into(),
1264 fields: vec![
1265 RustField {
1266 name: "x".into(),
1267 ty: RustType::f64(),
1268 is_pub: true,
1269 },
1270 RustField {
1271 name: "y".into(),
1272 ty: RustType::f64(),
1273 is_pub: true,
1274 },
1275 ],
1276 ..RustStruct::default()
1277 }),
1278 ];
1279
1280 let rendered = render_items(&items);
1281 let parsed = syn::parse_file(&rendered);
1283 assert!(
1284 parsed.is_ok(),
1285 "Rendered HIR should parse as valid Rust: {:?}",
1286 parsed.err()
1287 );
1288
1289 let reparsed = prettyplease::unparse(&parsed.unwrap());
1291 assert_eq!(rendered, reparsed, "Formatting should be idempotent");
1293 }
1294
1295 #[test]
1298 fn type_rendering_coverage() {
1299 let cases = vec![
1301 (RustType::Named("MyStruct".into()), "MyStruct"),
1302 (
1303 RustType::Generic("Vec".into(), vec![RustType::Named("u8".into())]),
1304 "Vec<u8>",
1305 ),
1306 (
1307 RustType::Ref(Box::new(RustType::Named("str".into()))),
1308 "&str",
1309 ),
1310 (RustType::RefMut(Box::new(RustType::i64())), "&mut i64"),
1311 (
1312 RustType::Tuple(vec![RustType::i64(), RustType::bool()]),
1313 "(i64, bool)",
1314 ),
1315 (RustType::Unit, "()"),
1316 (RustType::Never, "!"),
1317 (RustType::Raw("Box<dyn Fn()>".into()), "Box<dyn Fn()>"),
1318 ];
1319 for (ty, expected) in cases {
1320 assert_eq!(
1321 render_type(&ty),
1322 expected,
1323 "type rendering mismatch for {:?}",
1324 ty
1325 );
1326 }
1327 }
1328
1329 #[test]
1332 fn malformed_hir_produces_warning() {
1333 let items = vec![RustItem::Fn(RustFn {
1335 name: "bad".into(),
1336 body: vec![RustStmt::Raw("this is {{ not valid {{ rust".into())],
1337 ..RustFn::default()
1338 })];
1339 let rendered = render_items(&items);
1340 assert!(
1341 rendered.contains("WARNING"),
1342 "Invalid HIR should produce a warning in output"
1343 );
1344 }
1345}