Skip to main content

assura_codegen/
lib.rs

1//! Rust code generation from type-checked Assura contracts.
2//!
3//! Takes a `TypedFile` from `assura-types` and generates a Rust project
4//! consisting of a `Cargo.toml` and one or more `.rs` source files.
5//! Generated Rust is formatted via `prettyplease`.
6//!
7//! This is T019: the initial scaffolding. Type mapping (T020), contract
8//! codegen (T021), project generation (T022), and struct/enum codegen
9//! (T023) extend this foundation.
10
11use std::fmt::Write;
12
13/// Typed Rust HIR for structured code generation.
14pub mod hir;
15/// Assura-to-Rust type mapping (Int -> i64, Nat -> u64, etc.).
16pub mod type_map;
17
18mod block;
19mod contract;
20mod decl;
21mod expr;
22/// Feature-specific code generation for all 50 verification features.
23pub mod features;
24/// Structured contract metadata for AI agent consumption.
25pub mod metadata;
26mod service;
27mod types_gen;
28
29pub use expr::expr_to_rust_static;
30
31use block::*;
32use contract::*;
33use decl::*;
34use expr::*;
35use service::*;
36use types_gen::*;
37
38use assura_ast::{
39    BinOp, BindDecl, BlockKind, Clause, ClauseKind, CodecRegistryDecl, ContractDecl, Decl,
40    DeclVisitor, EnumDef, Expr, ExternDecl, FnDef, Literal, MagicPattern, ServiceDecl, ServiceItem,
41    SpExpr, Spanned, TypeBody, TypeDef, UnaryOp,
42};
43use assura_types::TypedFile;
44
45/// Lint suppression header prepended to every generated `.rs` file.
46const GENERATED_ALLOW_ATTRS: &str = "#![allow(dead_code, unused_variables, unused_parens, non_camel_case_types, unreachable_code)]\n\n";
47
48/// Comment header prepended to every generated `.rs` file.
49const GENERATED_HEADER: &str = "// Generated by the Assura compiler.\n// Do not edit manually.\n\n";
50
51#[cfg(test)]
52mod codegen_tests;
53
54// ---------------------------------------------------------------------------
55// Public output types
56// ---------------------------------------------------------------------------
57
58/// The result of code generation: a complete Rust project.
59#[derive(Debug, Clone)]
60pub struct GeneratedProject {
61    /// Content of the generated `Cargo.toml`.
62    pub cargo_toml: String,
63    /// Generated source files as `(relative_path, rust_source)` pairs.
64    /// Typically `[("src/lib.rs", "...")]`.
65    pub files: Vec<(String, String)>,
66    /// Structured contract metadata for AI agent consumption.
67    /// Serializable to `assura-contracts.json`.
68    pub metadata: Option<metadata::ProjectMetadata>,
69}
70
71// ---------------------------------------------------------------------------
72// T119: Backend selection (Rustc vs Cranelift for fast dev builds)
73// ---------------------------------------------------------------------------
74
75/// Code generation backend.
76#[derive(Debug, Clone, PartialEq, Default)]
77pub enum CodegenBackend {
78    /// Standard rustc backend (optimized, production).
79    #[default]
80    Rustc,
81    /// Cranelift backend (fast compilation, dev builds).
82    Cranelift,
83}
84
85/// Compilation target for the generated Rust project.
86#[derive(Debug, Clone, PartialEq, Default)]
87pub enum CompileTarget {
88    /// Native host target (default).
89    #[default]
90    Native,
91    /// WebAssembly via wasm32-wasip1 (formerly wasm32-wasi).
92    Wasm,
93}
94
95impl CompileTarget {
96    /// Parse a target string from CLI/config (e.g. "native", "wasm32-wasi").
97    pub fn from_str_loose(s: &str) -> Option<Self> {
98        match s.to_lowercase().as_str() {
99            "native" => Some(Self::Native),
100            "wasm" | "wasm32-wasi" | "wasm32-wasip1" => Some(Self::Wasm),
101            _ => None,
102        }
103    }
104
105    /// The Rust target triple for cargo commands.
106    pub fn rust_target(&self) -> Option<&'static str> {
107        match self {
108            Self::Native => None, // use host default
109            Self::Wasm => Some("wasm32-wasip1"),
110        }
111    }
112}
113
114/// Configuration for code generation.
115#[derive(Debug, Clone)]
116pub struct BackendConfig {
117    pub backend: CodegenBackend,
118    pub opt_level: u8,
119    pub debug_info: bool,
120    pub target: CompileTarget,
121    /// Optional IR-generated Rust function bodies, keyed by contract/function name.
122    ///
123    /// When present, codegen replaces `todo!()` placeholders with real
124    /// implementations from IR sidecar files.
125    pub ir_bodies: std::collections::HashMap<String, String>,
126    /// When true, contract assertions use `assura_runtime::contract_violation`
127    /// instead of `debug_assert!`, persisting checks in release builds.
128    pub runtime_checks: bool,
129}
130
131impl Default for BackendConfig {
132    fn default() -> Self {
133        Self {
134            backend: CodegenBackend::Rustc,
135            opt_level: 2,
136            debug_info: false,
137            target: CompileTarget::Native,
138            ir_bodies: std::collections::HashMap::new(),
139            runtime_checks: false,
140        }
141    }
142}
143
144impl BackendConfig {
145    /// Create a default config with IR bodies from loaded sidecars.
146    pub fn with_ir_bodies(ir_bodies: std::collections::HashMap<String, String>) -> Self {
147        Self {
148            ir_bodies,
149            ..Self::default()
150        }
151    }
152}
153
154// ---------------------------------------------------------------------------
155// Phase helpers (extracted from codegen_with_config)
156// ---------------------------------------------------------------------------
157
158/// Visitor that collects user-defined type names, feature_max constants,
159/// and referenced types from all declarations.
160struct TypeCollectVisitor<'a> {
161    defined_types: &'a mut std::collections::HashSet<String>,
162    feature_max_consts: &'a mut Vec<(String, String)>,
163    referenced_types: &'a mut std::collections::HashSet<String>,
164}
165
166impl DeclVisitor for TypeCollectVisitor<'_> {
167    fn visit_type_def(&mut self, t: &TypeDef) {
168        self.defined_types.insert(t.name.clone());
169        if let TypeBody::Struct(fields) = &t.body {
170            for f in fields {
171                collect_type_refs_from_type_expr(f.ty.as_ref(), self.referenced_types);
172            }
173        }
174    }
175    fn visit_enum_def(&mut self, e: &EnumDef) {
176        self.defined_types.insert(e.name.clone());
177    }
178    fn visit_block(
179        &mut self,
180        kind: &BlockKind,
181        name: &str,
182        value: &Option<Vec<String>>,
183        _body: &[Clause],
184    ) {
185        if *kind == BlockKind::FeatureMax {
186            // Extract type from inline value tokens (e.g., ["Nat", "=", "280"] -> "Nat")
187            let ty = value
188                .as_ref()
189                .and_then(|v| {
190                    v.iter()
191                        .take_while(|t| t.as_str() != "=")
192                        .find(|t| t.chars().next().is_some_and(|c| c.is_uppercase()))
193                })
194                .map(|t| map_type_token(t).to_string())
195                .unwrap_or_else(|| "u64".to_string());
196            self.feature_max_consts.push((name.to_string(), ty));
197        }
198    }
199    fn visit_fn_def(&mut self, f: &FnDef) {
200        collect_type_refs_from_type_expr(f.return_ty.as_ref(), self.referenced_types);
201        for p in &f.params {
202            collect_type_refs_from_type_expr(p.ty.as_ref(), self.referenced_types);
203        }
204    }
205    fn visit_extern(&mut self, ex: &ExternDecl) {
206        collect_type_refs_from_type_expr(ex.return_ty.as_ref(), self.referenced_types);
207        for p in &ex.params {
208            collect_type_refs_from_type_expr(p.ty.as_ref(), self.referenced_types);
209        }
210    }
211    fn visit_contract(&mut self, c: &ContractDecl) {
212        for clause in &c.clauses {
213            collect_type_refs_from_expr(&clause.body, self.referenced_types);
214        }
215    }
216    fn visit_service(&mut self, s: &ServiceDecl) {
217        for item in &s.items {
218            match item {
219                ServiceItem::TypeDef(t) => {
220                    self.defined_types.insert(t.name.clone());
221                }
222                ServiceItem::EnumDef(e) => {
223                    self.defined_types.insert(e.name.clone());
224                }
225                ServiceItem::Operation { clauses, .. } | ServiceItem::Query { clauses, .. } => {
226                    for clause in clauses {
227                        collect_type_refs_from_expr(&clause.body, self.referenced_types);
228                    }
229                }
230                ServiceItem::States(_) | ServiceItem::Invariant(_) | ServiceItem::Other { .. } => {}
231            }
232        }
233    }
234    fn visit_bind(&mut self, b: &BindDecl) {
235        collect_type_refs_from_type_expr(b.return_ty.as_ref(), self.referenced_types);
236        for p in &b.params {
237            collect_type_refs_from_type_expr(p.ty.as_ref(), self.referenced_types);
238        }
239    }
240    // Prophecy / CodecRegistry: default no-op
241}
242
243/// Phase 1+2: Walk decls to collect defined type names, feature_max constants,
244/// and referenced types.
245///
246/// Returns `(defined_types, feature_max_consts, referenced_types)`.
247fn collect_type_names(
248    decls: &[Spanned<Decl>],
249) -> (
250    std::collections::HashSet<String>,
251    Vec<(String, String)>,
252    std::collections::HashSet<String>,
253) {
254    let mut defined_types = std::collections::HashSet::new();
255    let mut feature_max_consts: Vec<(String, String)> = Vec::new();
256    let mut referenced_types = std::collections::HashSet::new();
257
258    let mut visitor = TypeCollectVisitor {
259        defined_types: &mut defined_types,
260        feature_max_consts: &mut feature_max_consts,
261        referenced_types: &mut referenced_types,
262    };
263    assura_ast::walk_decls(&mut visitor, decls);
264
265    // Add built-in type names that should never generate stubs
266    for builtin in &[
267        "Int", "Nat", "Float", "Bool", "String", "Bytes", "Unit", "Never", "U8", "U16", "U32",
268        "U64", "I8", "I16", "I32", "I64", "F32", "F64", "List", "Vec", "Map", "Set", "Option",
269        "Result", "Sequence", "i64", "u64", "f64", "bool", "u8", "u16", "u32", "i8", "i16", "i32",
270        "f32", "f64",
271    ] {
272        defined_types.insert(builtin.to_string());
273    }
274
275    (defined_types, feature_max_consts, referenced_types)
276}
277
278/// Visitor that walks all type expressions in FnDef/Extern/Bind params and
279/// return types, and optionally TypeDef struct fields, applying a callback
280/// to each `TypeExpr`. Used for generic arity detection and feature_max-as-type
281/// detection without token roundtrip.
282struct TypeExprWalkVisitor<'a, F> {
283    visit: F,
284    include_typedef_fields: bool,
285    _marker: std::marker::PhantomData<&'a ()>,
286}
287
288impl<F: FnMut(Option<&assura_ast::TypeExpr>)> DeclVisitor for TypeExprWalkVisitor<'_, F> {
289    fn visit_fn_def(&mut self, f: &FnDef) {
290        (self.visit)(f.return_ty.as_ref());
291        for p in &f.params {
292            (self.visit)(p.ty.as_ref());
293        }
294    }
295    fn visit_extern(&mut self, ex: &ExternDecl) {
296        (self.visit)(ex.return_ty.as_ref());
297        for p in &ex.params {
298            (self.visit)(p.ty.as_ref());
299        }
300    }
301    fn visit_bind(&mut self, b: &BindDecl) {
302        (self.visit)(b.return_ty.as_ref());
303        for p in &b.params {
304            (self.visit)(p.ty.as_ref());
305        }
306    }
307    fn visit_type_def(&mut self, t: &TypeDef) {
308        if self.include_typedef_fields
309            && let TypeBody::Struct(fields) = &t.body
310        {
311            for f in fields {
312                (self.visit)(f.ty.as_ref());
313            }
314        }
315    }
316    // Contract, Service, EnumDef, Prophecy, CodecRegistry, Block: default no-op
317}
318
319/// Phase 3: Detect feature_max names used as type arguments, working directly
320/// on `TypeExpr` instead of token vectors.
321fn detect_feature_max_as_type_direct(
322    decls: &[Spanned<Decl>],
323    feature_max_consts: &[(String, String)],
324) -> std::collections::HashSet<String> {
325    let fm_set: std::collections::HashSet<&str> =
326        feature_max_consts.iter().map(|(n, _)| n.as_str()).collect();
327    let mut result = std::collections::HashSet::new();
328    let mut visitor = TypeExprWalkVisitor {
329        visit: |te: Option<&assura_ast::TypeExpr>| {
330            detect_feature_max_as_type_from_type_expr(te, &fm_set, &mut result);
331        },
332        include_typedef_fields: false,
333        _marker: std::marker::PhantomData,
334    };
335    assura_ast::walk_decls(&mut visitor, decls);
336    result
337}
338
339/// Phase 4: Detect generic arity for type references and collect const-generic
340/// names, working directly on `TypeExpr`.
341///
342/// Returns `(type_generic_params, const_generic_names)`.
343fn detect_generic_arities_direct(
344    decls: &[Spanned<Decl>],
345) -> (
346    std::collections::HashMap<String, Vec<GenericParamKind>>,
347    std::collections::HashSet<String>,
348) {
349    let mut type_generic_params: std::collections::HashMap<String, Vec<GenericParamKind>> =
350        std::collections::HashMap::new();
351    let mut const_generic_names = std::collections::HashSet::new();
352    let mut visitor = TypeExprWalkVisitor {
353        visit: |te: Option<&assura_ast::TypeExpr>| {
354            detect_generic_arity_from_type_expr(
355                te,
356                &mut type_generic_params,
357                &mut const_generic_names,
358            );
359        },
360        include_typedef_fields: true,
361        _marker: std::marker::PhantomData,
362    };
363    assura_ast::walk_decls(&mut visitor, decls);
364    (type_generic_params, const_generic_names)
365}
366
367/// Phase 4b: Collect SCREAMING_SNAKE_CASE names used as type arguments,
368/// combining const_generic_names from Phase 4 with feature_max names in type positions.
369fn detect_const_type_stubs_direct(
370    decls: &[Spanned<Decl>],
371    const_generic_names: &std::collections::HashSet<String>,
372    feature_max_consts: &[(String, String)],
373    defined_types: &std::collections::HashSet<String>,
374) -> Vec<String> {
375    let mut all_const_as_types = const_generic_names.clone();
376    let fm_set: std::collections::HashSet<&str> =
377        feature_max_consts.iter().map(|(n, _)| n.as_str()).collect();
378    let mut extra = std::collections::HashSet::new();
379    let mut visitor = TypeExprWalkVisitor {
380        visit: |te: Option<&assura_ast::TypeExpr>| {
381            detect_feature_max_as_type_from_type_expr(te, &fm_set, &mut extra);
382        },
383        include_typedef_fields: false,
384        _marker: std::marker::PhantomData,
385    };
386    assura_ast::walk_decls(&mut visitor, decls);
387    all_const_as_types.extend(extra);
388
389    let mut const_as_types: Vec<String> = all_const_as_types
390        .iter()
391        .filter(|n| !defined_types.contains(*n))
392        .cloned()
393        .collect();
394    const_as_types.sort();
395    const_as_types
396}
397
398/// Phase 5: Generate stub structs for undefined types.
399fn generate_undefined_type_stubs(
400    referenced_types: &std::collections::HashSet<String>,
401    defined_types: &std::collections::HashSet<String>,
402    feature_max_consts: &[(String, String)],
403    const_as_types: &[String],
404    type_generic_params: &std::collections::HashMap<String, Vec<GenericParamKind>>,
405    code: &mut String,
406) {
407    let feature_max_set: std::collections::HashSet<String> =
408        feature_max_consts.iter().map(|(n, _)| n.clone()).collect();
409    let mut undefined: Vec<String> = referenced_types
410        .difference(defined_types)
411        .filter(|t| {
412            // Skip things that aren't type names (operators, keywords, etc.)
413            !t.is_empty()
414                && t.chars().next().is_some_and(|c| c.is_uppercase())
415                && t.chars().all(|c| c.is_alphanumeric() || c == '_')
416                // Skip feature_max constants (already generated as const + type stub)
417                && !feature_max_consts.iter().any(|(n, _)| n == *t)
418                // Skip SCREAMING_SNAKE_CASE names already handled as const-as-type stubs
419                && !const_as_types.iter().any(|s| s == *t)
420                // Also skip feature_max names that are in const_as_types
421                && !feature_max_set.contains(*t)
422        })
423        .cloned()
424        .collect();
425    undefined.sort();
426    if !undefined.is_empty() {
427        code.push_str("// Placeholder types for types used but not defined in this file.\n");
428        code.push_str("// Replace with real definitions when implementations are provided.\n");
429        for name in &undefined {
430            let arity = type_generic_params.get(name).map_or(0, |v| v.len());
431            if arity > 0 {
432                let params: Vec<String> = (0..arity).map(|i| format!("T{i}")).collect();
433                let phantoms: Vec<String> = params
434                    .iter()
435                    .map(|p| format!("std::marker::PhantomData<{p}>"))
436                    .collect();
437                let _ = writeln!(
438                    code,
439                    "#[derive(Debug, Clone, PartialEq)]\npub struct {name}<{}>({});",
440                    params.join(", "),
441                    phantoms.join(", ")
442                );
443            } else {
444                let _ = writeln!(
445                    code,
446                    "#[derive(Debug, Clone, PartialEq)]\npub struct {name};"
447                );
448            }
449        }
450        code.push('\n');
451    }
452}
453
454/// Visitor that collects contract and service names for module structure decisions.
455struct CollectModuleNames {
456    contract_names: Vec<String>,
457    service_names: Vec<String>,
458}
459
460impl DeclVisitor for CollectModuleNames {
461    fn visit_contract(&mut self, c: &ContractDecl) {
462        self.contract_names.push(c.name.clone());
463    }
464    fn visit_service(&mut self, s: &ServiceDecl) {
465        self.service_names.push(s.name.clone());
466    }
467}
468
469/// Visitor that generates Rust code for each declaration.
470///
471/// In multi-file mode (`include_contracts_services = false`), contracts and
472/// services are skipped (they get their own files). In single-file mode
473/// (`include_contracts_services = true`), they are generated inline.
474struct CodeGenVisitor<'a> {
475    code: &'a mut String,
476    include_contracts_services: bool,
477    ir_bodies: Option<&'a std::collections::HashMap<String, String>>,
478    runtime_checks: bool,
479}
480
481impl DeclVisitor for CodeGenVisitor<'_> {
482    fn visit_type_def(&mut self, t: &TypeDef) {
483        generate_type_def(t, self.code);
484    }
485    fn visit_enum_def(&mut self, e: &EnumDef) {
486        generate_enum_def(e, self.code);
487    }
488    fn visit_extern(&mut self, ex: &ExternDecl) {
489        generate_extern(ex, self.code);
490    }
491    fn visit_bind(&mut self, b: &BindDecl) {
492        generate_bind(b, self.code);
493    }
494    fn visit_fn_def(&mut self, f: &FnDef) {
495        if !f.is_ghost && !f.is_lemma {
496            generate_fn_def(f, self.code, self.ir_bodies);
497        }
498    }
499    fn visit_block(
500        &mut self,
501        kind: &BlockKind,
502        name: &str,
503        _value: &Option<Vec<String>>,
504        body: &[Clause],
505    ) {
506        if *kind != BlockKind::FeatureMax {
507            generate_block(kind, name, body, self.code);
508        }
509    }
510    fn visit_codec_registry(&mut self, cr: &CodecRegistryDecl) {
511        generate_codec_registry(cr, self.code);
512    }
513    fn visit_contract(&mut self, c: &ContractDecl) {
514        if self.include_contracts_services {
515            if self.runtime_checks {
516                generate_contract_runtime(c, self.code, self.ir_bodies);
517            } else {
518                generate_contract(c, self.code, self.ir_bodies);
519            }
520        }
521    }
522    fn visit_service(&mut self, s: &ServiceDecl) {
523        if self.include_contracts_services {
524            generate_service(s, self.code, self.ir_bodies);
525        }
526    }
527    // Prophecy: default no-op (ghost, erased in codegen)
528}
529
530/// Visitor that emits `pub mod` declarations for contracts and services
531/// in multi-file mode.
532struct ModDeclVisitor<'a> {
533    code: &'a mut String,
534}
535
536impl DeclVisitor for ModDeclVisitor<'_> {
537    fn visit_contract(&mut self, c: &ContractDecl) {
538        let mod_name = c.name.to_lowercase();
539        self.code.push_str("pub mod contract_");
540        self.code.push_str(&mod_name);
541        self.code.push_str(";\n");
542    }
543    fn visit_service(&mut self, s: &ServiceDecl) {
544        let mod_name = s.name.to_lowercase();
545        self.code.push_str("pub mod ");
546        self.code.push_str(&mod_name);
547        self.code.push_str(";\n");
548    }
549    // All others: default no-op
550}
551
552// ---------------------------------------------------------------------------
553// Entry point
554// ---------------------------------------------------------------------------
555
556/// Generate a Rust project from a type-checked Assura file.
557///
558/// Walks the AST, maps Assura declarations to Rust source code, and
559/// formats the result with `prettyplease`. Returns a `GeneratedProject`
560/// with a `Cargo.toml` and generated `.rs` files.
561pub fn codegen_with_config(typed: &TypedFile, config: &BackendConfig) -> GeneratedProject {
562    let source = &typed.resolved.source;
563
564    let project_name = source
565        .project
566        .as_ref()
567        .map(|p| p.name.clone())
568        .unwrap_or_else(|| "generated".to_string());
569
570    let crate_name: String = project_name
571        .chars()
572        .map(|c| {
573            if c.is_alphanumeric() || c == '_' || c == '-' {
574                c
575            } else {
576                '_'
577            }
578        })
579        .collect();
580
581    let has_proptest = source_has_testable_contracts(source);
582    let has_errors = source_has_error_types(source);
583    let cargo_toml = generate_cargo_toml_impl(&crate_name, config, has_proptest, has_errors);
584
585    let mut code = String::new();
586
587    // Phase 1+2: Collect defined types, feature_max consts, and referenced types.
588    let (defined_types, feature_max_consts, referenced_types) = collect_type_names(&source.decls);
589
590    // Phase 3: Detect feature_max names used as type arguments (direct TypeExpr walk).
591    let feature_max_used_as_type =
592        detect_feature_max_as_type_direct(&source.decls, &feature_max_consts);
593    for (name, ty) in &feature_max_consts {
594        if feature_max_used_as_type.contains(name) {
595            // Will be emitted as a struct stub in Phase 4b.
596            // Also emit a const with _VALUE suffix for any value-position uses.
597            let value = find_feature_max_value(source, name);
598            let _ = writeln!(code, "pub const {name}_VALUE: {ty} = {value};");
599        } else {
600            let value = find_feature_max_value(source, name);
601            let _ = writeln!(code, "pub const {name}: {ty} = {value};");
602        }
603    }
604    if !feature_max_consts.is_empty() {
605        code.push('\n');
606    }
607
608    // Phase 4: Detect generic arity for type references and collect const-generic names.
609    let (type_generic_params, const_generic_names) = detect_generic_arities_direct(&source.decls);
610
611    // Phase 4b: Generate marker type stubs for SCREAMING_SNAKE_CASE names
612    // used as generic arguments.
613    let const_as_types = detect_const_type_stubs_direct(
614        &source.decls,
615        &const_generic_names,
616        &feature_max_consts,
617        &defined_types,
618    );
619    for name in &const_as_types {
620        // Emit as a marker type (unit struct) rather than a const,
621        // since the generic parameter positions use type params.
622        let _ = writeln!(
623            code,
624            "#[derive(Debug, Clone, PartialEq)]\npub struct {name}; // size param from another module"
625        );
626    }
627    if !const_as_types.is_empty() {
628        code.push('\n');
629    }
630
631    // Phase 5: Generate stub structs for undefined types.
632    generate_undefined_type_stubs(
633        &referenced_types,
634        &defined_types,
635        &feature_max_consts,
636        &const_as_types,
637        &type_generic_params,
638        &mut code,
639    );
640
641    // Count contracts and services to decide on module structure.
642    let mut collector = CollectModuleNames {
643        contract_names: Vec::new(),
644        service_names: Vec::new(),
645    };
646    assura_ast::walk_decls(&mut collector, &source.decls);
647    let contract_names = collector.contract_names;
648    let service_names = collector.service_names;
649    let total_modules = contract_names.len() + service_names.len();
650    let use_multi_file = total_modules >= 2;
651
652    let mut project = if use_multi_file {
653        // ------------------------------------------------------------------
654        // Multi-file mode: separate .rs files for each contract/service,
655        // shared types/functions/externs in lib.rs.
656        // ------------------------------------------------------------------
657        let mut files: Vec<(String, String)> = Vec::new();
658
659        // Build shared code: types, enums, externs, functions, blocks
660        let mut shared = String::new();
661        shared.push_str(GENERATED_ALLOW_ATTRS);
662        // Emit the pre-built preamble (feature_max, const-as-type stubs,
663        // placeholder structs)
664        shared.push_str(&code);
665
666        // Generate shared code (types, enums, externs, functions, blocks).
667        // Contracts and services go into their own files in multi-file mode.
668        let ir_ref = if config.ir_bodies.is_empty() {
669            None
670        } else {
671            Some(&config.ir_bodies)
672        };
673        let mut codegen_visitor = CodeGenVisitor {
674            code: &mut shared,
675            include_contracts_services: false,
676            ir_bodies: ir_ref,
677            runtime_checks: config.runtime_checks,
678        };
679        assura_ast::walk_decls(&mut codegen_visitor, &source.decls);
680
681        // Add pub mod declarations for each contract/service module
682        let mut mod_visitor = ModDeclVisitor { code: &mut shared };
683        assura_ast::walk_decls(&mut mod_visitor, &source.decls);
684
685        let formatted_shared = format_rust(&shared);
686        let lib_rs = format!("{GENERATED_HEADER}{formatted_shared}");
687        files.push(("src/lib.rs".to_string(), lib_rs));
688
689        // Generate per-contract files
690        for decl in &source.decls {
691            if let Decl::Contract(c) = &decl.node {
692                let mod_name = c.name.to_lowercase();
693                let mut mod_code = String::new();
694                mod_code.push_str(GENERATED_ALLOW_ATTRS);
695                mod_code.push_str("use super::*;\n\n");
696                // Generate the contract body without wrapping it in
697                // `pub mod contract_xxx { ... }` since it IS the module file.
698                generate_contract_contents_opts(c, &mut mod_code, ir_ref, config.runtime_checks);
699                // S009: proptest for multi-file contracts
700                generate_proptest_for_contract_contents(c, &mut mod_code);
701                let formatted = format_rust(&mod_code);
702                let content = format!("{GENERATED_HEADER}{formatted}");
703                files.push((format!("src/contract_{mod_name}.rs"), content));
704            }
705        }
706
707        // Generate per-service files
708        for decl in &source.decls {
709            if let Decl::Service(s) = &decl.node {
710                let mod_name = s.name.to_lowercase();
711                let mut mod_code = String::new();
712                mod_code.push_str(GENERATED_ALLOW_ATTRS);
713                mod_code.push_str("use super::*;\n\n");
714                generate_service_contents(s, &mut mod_code, ir_ref);
715                let formatted = format_rust(&mod_code);
716                let content = format!("{GENERATED_HEADER}{formatted}");
717                files.push((format!("src/{mod_name}.rs"), content));
718            }
719        }
720
721        GeneratedProject {
722            cargo_toml: cargo_toml.clone(),
723            files,
724            metadata: None,
725        }
726    } else {
727        // ------------------------------------------------------------------
728        // Single-file mode: everything in lib.rs (current behavior).
729        // ------------------------------------------------------------------
730        let mut all_code = String::new();
731        all_code.push_str(GENERATED_ALLOW_ATTRS);
732        all_code.push_str(&code);
733
734        // Generate code for all declarations (single-file: contracts and services inline).
735        let ir_ref = if config.ir_bodies.is_empty() {
736            None
737        } else {
738            Some(&config.ir_bodies)
739        };
740        let mut codegen_visitor = CodeGenVisitor {
741            code: &mut all_code,
742            include_contracts_services: true,
743            ir_bodies: ir_ref,
744            runtime_checks: config.runtime_checks,
745        };
746        assura_ast::walk_decls(&mut codegen_visitor, &source.decls);
747
748        // S009: Generate proptest tests for testable contracts
749        // Skip proptest for Cranelift (dev builds prioritize fast compilation)
750        if !matches!(config.backend, CodegenBackend::Cranelift) {
751            for decl in &source.decls {
752                if let Decl::Contract(c) = &decl.node {
753                    generate_proptest_for_contract(c, &mut all_code);
754                }
755            }
756        }
757
758        let formatted_body = format_rust(&all_code);
759        let formatted = format!("{GENERATED_HEADER}{formatted_body}");
760
761        GeneratedProject {
762            cargo_toml,
763            files: vec![("src/lib.rs".to_string(), formatted)],
764            metadata: None,
765        }
766    };
767
768    // Generate structured contract metadata
769    let source_name = project_name.clone() + ".assura";
770    project.metadata = Some(metadata::extract_metadata(typed, &source_name));
771
772    // Add .cargo/config.toml for Cranelift backend
773    if matches!(config.backend, CodegenBackend::Cranelift) {
774        project.files.push((
775            ".cargo/config.toml".to_string(),
776            "[unstable]\ncodegen-backend = true\n\n[profile.dev]\ncodegen-backend = \"cranelift\"\n"
777                .to_string(),
778        ));
779
780        // Transform generated Rust code for C ABI compatibility:
781        // - Add #[repr(C)] to structs/enums (deterministic layout for JIT)
782        // - Add #[no_mangle] extern "C" to public functions (callable via JIT)
783        for (path, content) in &mut project.files {
784            if path.ends_with(".rs") {
785                *content = cranelift_transform(content);
786            }
787        }
788    }
789
790    project
791}
792
793/// Transform generated Rust code for Cranelift JIT compatibility.
794///
795/// Cranelift is used for fast dev-cycle builds. To make generated functions
796/// callable from JIT-compiled code, we:
797/// - Add `#[repr(C)]` before `pub struct` and `pub enum` (C ABI layout)
798/// - Replace `pub fn` with `#[no_mangle] pub extern "C" fn` (C ABI calling convention)
799///
800/// This does NOT affect `fn` (private functions) or impl blocks.
801fn cranelift_transform(code: &str) -> String {
802    let mut out = String::with_capacity(code.len() + 512);
803    for line in code.lines() {
804        let trimmed = line.trim_start();
805        if (trimmed.starts_with("pub struct ") || trimmed.starts_with("pub enum "))
806            && !trimmed.contains("pub struct PhantomData")
807        {
808            // Insert #[repr(C)] with matching indentation
809            let indent = &line[..line.len() - trimmed.len()];
810            out.push_str(indent);
811            out.push_str("#[repr(C)]\n");
812            out.push_str(line);
813            out.push('\n');
814        } else if trimmed.starts_with("pub fn ") && !trimmed.contains("fmt(") {
815            // Add #[no_mangle] and extern "C" for top-level pub fn
816            let indent = &line[..line.len() - trimmed.len()];
817            out.push_str(indent);
818            out.push_str("#[no_mangle]\n");
819            let transformed = line.replacen("pub fn ", "pub extern \"C\" fn ", 1);
820            out.push_str(&transformed);
821            out.push('\n');
822        } else {
823            out.push_str(line);
824            out.push('\n');
825        }
826    }
827    out
828}
829
830#[cfg(test)]
831mod cranelift_tests {
832    use super::cranelift_transform;
833
834    #[test]
835    fn transforms_pub_struct() {
836        let input = "pub struct Foo {\n    x: i64,\n}\n";
837        let out = cranelift_transform(input);
838        assert!(out.contains("#[repr(C)]\npub struct Foo"));
839    }
840
841    #[test]
842    fn transforms_pub_enum() {
843        let input = "pub enum Color {\n    Red,\n    Blue,\n}\n";
844        let out = cranelift_transform(input);
845        assert!(out.contains("#[repr(C)]\npub enum Color"));
846    }
847
848    #[test]
849    fn transforms_pub_fn() {
850        let input = "pub fn add(a: i64, b: i64) -> i64 {\n    a + b\n}\n";
851        let out = cranelift_transform(input);
852        assert!(out.contains("#[no_mangle]"));
853        assert!(out.contains("pub extern \"C\" fn add"));
854    }
855
856    #[test]
857    fn skips_private_fn() {
858        let input = "fn helper(x: i64) -> i64 { x }\n";
859        let out = cranelift_transform(input);
860        assert!(!out.contains("#[no_mangle]"));
861        assert!(!out.contains("extern \"C\""));
862    }
863
864    #[test]
865    fn skips_fmt_method() {
866        let input =
867            "    pub fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n";
868        let out = cranelift_transform(input);
869        assert!(!out.contains("#[no_mangle]"));
870        assert!(!out.contains("extern \"C\""));
871    }
872
873    #[test]
874    fn empty_input() {
875        let out = cranelift_transform("");
876        // Empty string produces a single trailing newline from the lines iterator
877        assert!(!out.contains("#[repr(C)]"));
878        assert!(!out.contains("#[no_mangle]"));
879    }
880
881    #[test]
882    fn no_pub_items() {
883        let input = "fn private() {}\nstruct Hidden;\n";
884        let out = cranelift_transform(input);
885        assert!(!out.contains("#[repr(C)]"));
886        assert!(!out.contains("#[no_mangle]"));
887    }
888
889    #[test]
890    fn preserves_indentation() {
891        let input = "    pub struct Inner {\n        val: i32,\n    }\n";
892        let out = cranelift_transform(input);
893        assert!(out.contains("    #[repr(C)]\n    pub struct Inner"));
894    }
895}
896
897/// Generate a Rust project from a type-checked Assura file.
898///
899/// Uses default backend configuration (`Rustc`, opt-level 2, no debug info).
900/// For custom configuration, use [`codegen_with_config`].
901pub fn codegen(typed: &TypedFile) -> GeneratedProject {
902    codegen_with_config(typed, &BackendConfig::default())
903}