Skip to main content

assura_codegen/
hir.rs

1//! Typed Rust HIR (High-level Intermediate Representation) for codegen.
2//!
3//! Instead of building Rust source code via string concatenation,
4//! codegen builds structured `RustItem` / `RustStmt` / `RustExpr` trees.
5//! These are converted to `syn` AST nodes, then formatted by `prettyplease`.
6//!
7//! Benefits:
8//! - Structural validation: malformed HIR caught at construction, not formatting.
9//! - Testability: assert on HIR structure (e.g., "2 Assert nodes"), not strings.
10//! - No string-escaping bugs: identifiers and types are typed, not interpolated.
11
12use std::fmt::Write as _;
13
14// ---------------------------------------------------------------------------
15// Core HIR types
16// ---------------------------------------------------------------------------
17
18/// A top-level Rust item (function, struct, enum, trait, const, mod, etc.).
19#[derive(Debug, Clone, PartialEq)]
20pub enum RustItem {
21    /// `pub fn name(params) -> ret { body }`
22    Fn(RustFn),
23    /// `pub struct Name { fields }` or `pub struct Name;`
24    Struct(RustStruct),
25    /// `pub enum Name { variants }`
26    Enum(RustEnum),
27    /// `pub trait Name { methods }`
28    Trait(RustTrait),
29    /// `impl Trait for Type { methods }`
30    Impl(RustImpl),
31    /// `pub mod name { items }`
32    Mod(RustMod),
33    /// `pub const NAME: Type = value;`
34    Const(RustConst),
35    /// `use path;`
36    Use(String),
37    /// `// comment` or `/// doc comment`
38    Comment(String),
39    /// An attribute like `#[allow(...)]` or `#![allow(...)]` applied to the crate/file.
40    InnerAttr(String),
41    /// Raw Rust code string (escape hatch for migration).
42    ///
43    /// Used during incremental migration: parts of codegen not yet converted
44    /// to HIR can emit raw strings that are spliced into the output.
45    Raw(String),
46}
47
48/// A Rust function definition.
49#[derive(Debug, Clone, PartialEq)]
50pub struct RustFn {
51    /// Function name.
52    pub name: String,
53    /// Generic type parameters (e.g., `["T", "U"]`).
54    pub type_params: Vec<String>,
55    /// Function parameters.
56    pub params: Vec<RustParam>,
57    /// Return type. `None` means no explicit return type (unit).
58    pub ret: Option<RustType>,
59    /// Function body statements.
60    pub body: Vec<RustStmt>,
61    /// Whether the function is `pub`.
62    pub is_pub: bool,
63    /// Whether the function is `unsafe`.
64    pub is_unsafe: bool,
65    /// Doc comments (each string is one `///` line).
66    pub doc: Vec<String>,
67    /// Attributes (e.g., `#[allow(dead_code)]`).
68    pub attrs: Vec<String>,
69    /// Whether this is an abstract method declaration (no body, e.g., trait methods).
70    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/// A function parameter.
91#[derive(Debug, Clone, PartialEq)]
92pub struct RustParam {
93    pub name: String,
94    pub ty: RustType,
95}
96
97/// A Rust type.
98#[derive(Debug, Clone, PartialEq)]
99pub enum RustType {
100    /// A simple named type: `i64`, `String`, `MyStruct`.
101    Named(String),
102    /// A generic type: `Vec<u8>`, `Result<T, E>`.
103    Generic(String, Vec<RustType>),
104    /// A reference: `&T` or `&[u8]`.
105    Ref(Box<RustType>),
106    /// A mutable reference: `&mut T`.
107    RefMut(Box<RustType>),
108    /// A tuple type: `(A, B, C)`.
109    Tuple(Vec<RustType>),
110    /// Unit type: `()`.
111    Unit,
112    /// Never type: `!`.
113    Never,
114    /// A raw type string (escape hatch for complex types not yet modeled).
115    Raw(String),
116}
117
118impl RustType {
119    /// Shorthand for common types.
120    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/// A Rust statement inside a function body.
144#[derive(Debug, Clone, PartialEq)]
145pub enum RustStmt {
146    /// `let name: ty = init;` or `let name = init;`
147    Let {
148        name: String,
149        ty: Option<RustType>,
150        init: RustExpr,
151    },
152    /// `debug_assert!(cond, "label: msg");`
153    Assert { cond: String, label: String },
154    /// A `// comment` line.
155    Comment(String),
156    /// An expression used as a statement (e.g., `x;` or `x` as trailing expr).
157    Expr(RustExpr),
158    /// Raw Rust code (escape hatch during migration).
159    Raw(String),
160}
161
162/// A Rust expression.
163#[derive(Debug, Clone, PartialEq)]
164pub enum RustExpr {
165    /// An identifier: `x`, `RESULT_VAR`.
166    Ident(String),
167    /// A literal: `42`, `"hello"`, `true`.
168    Literal(String),
169    /// A function/method call: `func(args)`.
170    Call(String, Vec<RustExpr>),
171    /// A method call: `receiver.method(args)`.
172    MethodCall(Box<RustExpr>, String, Vec<RustExpr>),
173    /// `todo!("msg")`.
174    Todo(String),
175    /// `Ok(expr)`.
176    Ok(Box<RustExpr>),
177    /// `.clone()` on an expression.
178    Clone(Box<RustExpr>),
179    /// Raw Rust expression string (escape hatch).
180    Raw(String),
181}
182
183/// A Rust struct definition.
184#[derive(Debug, Clone, PartialEq)]
185pub struct RustStruct {
186    pub name: String,
187    pub type_params: Vec<String>,
188    pub fields: Vec<RustField>,
189    /// Derive attributes (e.g., `["Debug", "Clone", "PartialEq"]`).
190    pub derives: Vec<String>,
191    pub is_pub: bool,
192    /// Doc comments.
193    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/// A struct field.
210#[derive(Debug, Clone, PartialEq)]
211pub struct RustField {
212    pub name: String,
213    pub ty: RustType,
214    pub is_pub: bool,
215}
216
217/// A Rust enum definition.
218#[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    /// Extra attributes beyond derives (e.g., `#[non_exhaustive]`).
227    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/// An enum variant.
245#[derive(Debug, Clone, PartialEq)]
246pub struct RustVariant {
247    pub name: String,
248    /// Tuple fields (empty for unit variants).
249    pub fields: Vec<RustType>,
250    /// Extra attributes (e.g., `#[error("...")]` for thiserror).
251    pub attrs: Vec<String>,
252}
253
254/// A Rust trait definition.
255#[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/// An `impl` block.
266#[derive(Debug, Clone, PartialEq)]
267pub struct RustImpl {
268    /// The trait being implemented (e.g., `"Display"`). `None` for inherent impls.
269    pub trait_name: Option<String>,
270    /// The type the impl is for (e.g., `"MyStruct"`).
271    pub target: String,
272    pub type_params: Vec<String>,
273    pub methods: Vec<RustFn>,
274}
275
276/// A module definition.
277#[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/// A constant definition.
286#[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
295// ---------------------------------------------------------------------------
296// HIR -> String rendering (via syn + prettyplease)
297// ---------------------------------------------------------------------------
298
299/// Render a list of `RustItem`s to formatted Rust source code.
300///
301/// Converts each item to its string representation, joins them,
302/// then formats the result via `syn::parse_file` + `prettyplease::unparse`.
303pub 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    // Format via prettyplease (same path as the existing format_rust)
309    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
318/// Render a single `RustItem` to a string, appending to `out`.
319fn 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            // Self receiver params are rendered without an explicit type
371            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 as a trait method (with or without default body)
486        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/// Options that control how statements are rendered.
563#[derive(Debug, Clone, Default)]
564pub(crate) struct RenderOpts {
565    /// When true, assertions emit `assura_runtime::contract_violation` calls
566    /// that persist in release builds instead of `debug_assert!`.
567    pub runtime_checks: bool,
568    /// The contract name for runtime violation reports.
569    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 expression references deep field chains (e.g., state.head.extra),
589            // emit as a comment since stub types don't have these fields.
590            if crate::has_deep_field_access(cond) {
591                let _ = writeln!(out, "{pad}// {label}: {}", cond.replace('"', "\\\""));
592            } else if opts.runtime_checks {
593                // Runtime checks: persist in release builds via assura_runtime
594                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(); // suppress unused warning
603                } 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
653// ---------------------------------------------------------------------------
654// Builder helpers for common codegen patterns
655// ---------------------------------------------------------------------------
656
657/// Build a `#[derive(Debug, thiserror::Error)]` enum for contract error types.
658///
659/// Each variant gets a `#[error("VariantName")]` attribute for Display impl.
660pub 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
677/// Build a `#[derive(Debug, Clone, PartialEq)]` enum with optional Display impl
678/// and exhaustiveness check function.
679///
680/// Returns a list of items: the enum definition, optionally a Display impl,
681/// and optionally an exhaustiveness check function.
682pub 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    // Build enum
688    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    // Display impl (only for non-generic enums)
709    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    // Exhaustiveness check (only for non-generic, non-empty enums)
755    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
797/// Render a single HIR item to a code string (without prettyplease formatting).
798///
799/// Used by migration code that appends HIR items to an existing string buffer.
800pub fn render_item_raw(item: &RustItem) -> String {
801    let mut out = String::new();
802    render_item(item, &mut out);
803    out
804}
805
806/// Like `render_item_raw` but passes `RenderOpts` to control assertion style.
807pub(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        // Only functions contain assertions; other items delegate to the
817        // regular renderer.
818        other => render_item(other, out),
819    }
820}
821
822// ---------------------------------------------------------------------------
823// Builders: AST -> HIR
824// ---------------------------------------------------------------------------
825
826/// Build HIR items for an Assura `TypeDef`.
827///
828/// Returns a `Vec<RustItem>` because some type bodies produce multiple items
829/// (e.g., a refined type produces a newtype struct).
830pub 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// ---------------------------------------------------------------------------
898// Tests
899// ---------------------------------------------------------------------------
900
901#[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    // --- Structural tests ---
1119
1120    #[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    // --- HIR builder structural tests ---
1151
1152    #[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            // Each variant has an #[error("...")] attribute
1162            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        // Should produce: enum, Display impl, exhaustiveness check
1193        assert_eq!(items.len(), 3, "enum + Display + exhaustive");
1194
1195        // First item: the enum
1196        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        // Second item: Display impl
1206        assert!(
1207            matches!(&items[1], RustItem::Impl(i) if i.trait_name.as_deref() == Some("std::fmt::Display"))
1208        );
1209
1210        // Third item: exhaustiveness check fn
1211        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        // Generic enums: no Display impl, no exhaustiveness check
1232        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    // --- Round-trip test ---
1241
1242    #[test]
1243    fn round_trip_parse_format() {
1244        // Build HIR -> render -> parse via syn -> re-format via prettyplease
1245        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        // Parse the rendered output back with syn
1282        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        // Re-format the parsed result
1290        let reparsed = prettyplease::unparse(&parsed.unwrap());
1291        // The double-formatted result should be identical (idempotent)
1292        assert_eq!(rendered, reparsed, "Formatting should be idempotent");
1293    }
1294
1295    // --- Type mapping coverage test ---
1296
1297    #[test]
1298    fn type_rendering_coverage() {
1299        // Test all RustType variants render correctly
1300        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    // --- Malformed HIR rejection test ---
1330
1331    #[test]
1332    fn malformed_hir_produces_warning() {
1333        // A function with syntax errors in its body should produce a warning comment
1334        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}