Skip to main content

assura_codegen/
types_gen.rs

1//! Type mapping, Cargo.toml generation, and type definition codegen.
2
3use super::*;
4
5pub(crate) fn generate_cargo_toml_impl(
6    crate_name: &str,
7    config: &BackendConfig,
8    include_proptest: bool,
9    include_thiserror: bool,
10) -> String {
11    let mut toml = format!(
12        r#"[package]
13name = "{crate_name}"
14version = "0.1.0"
15edition = "2024"
16
17# Generated by the Assura compiler.
18# Do not edit manually.
19
20[dependencies]
21"#
22    );
23
24    if include_thiserror {
25        toml.push_str("thiserror = \"2\"\n");
26    }
27
28    if config.runtime_checks {
29        // Use crates.io version when published; until then, users must add
30        // a [patch] or path override pointing to their local assura-runtime.
31        toml.push_str("assura-runtime = \"0.1\"\n");
32    }
33
34    if include_proptest {
35        toml.push_str("\n[dev-dependencies]\nproptest = \"1\"\n");
36    }
37
38    // Add profile configuration based on BackendConfig
39    if config.opt_level != 2 || config.debug_info {
40        toml.push_str("\n[profile.release]\n");
41        toml.push_str("opt-level = ");
42        toml.push_str(&config.opt_level.to_string());
43        toml.push('\n');
44        if config.debug_info {
45            toml.push_str("debug = true\n");
46        }
47    }
48
49    // Add backend-specific configuration
50    if matches!(config.backend, CodegenBackend::Cranelift) {
51        toml.push_str(
52            "\n# Cranelift backend for fast dev builds\n\
53             # Install: rustup component add rustc-codegen-cranelift\n\
54             # Note: Cranelift requires nightly Rust\n",
55        );
56        // Force dev-friendly profile settings for Cranelift
57        if config.opt_level == 2 && !config.debug_info {
58            toml.push_str("\n[profile.dev]\nopt-level = 0\ndebug = true\n");
59        }
60    }
61
62    // WASM target: add cdylib crate type so cargo produces a .wasm file
63    if matches!(config.target, CompileTarget::Wasm) {
64        toml.push_str("\n[lib]\ncrate-type = [\"cdylib\"]\n");
65        toml.push_str(
66            "\n# WASM target: build with `cargo build --target wasm32-wasip1`\n\
67             # Install target: `rustup target add wasm32-wasip1`\n",
68        );
69    }
70
71    toml
72}
73
74// ---------------------------------------------------------------------------
75// Type mapping: Assura types -> Rust types
76// ---------------------------------------------------------------------------
77
78/// Map a single Assura type token to its Rust equivalent.
79pub fn map_type_token(tok: &str) -> &str {
80    match tok {
81        "Int" => "i64",
82        "Nat" => "u64",
83        "Float" => "f64",
84        "Bool" => "bool",
85        "String" => "String",
86        "Bytes" => "Vec<u8>",
87        "Unit" => "()",
88        "Never" => "!",
89        "U8" => "u8",
90        "U16" => "u16",
91        "U32" => "u32",
92        "U64" => "u64",
93        "I8" => "i8",
94        "I16" => "i16",
95        "I32" => "i32",
96        "I64" => "i64",
97        "F32" => "f32",
98        "F64" => "f64",
99        "List" => "Vec",
100        "Map" => "std::collections::BTreeMap",
101        "Set" => "std::collections::BTreeSet",
102        "Sequence" => "Vec",
103        // Option, Result map to themselves
104        _ => tok,
105    }
106}
107
108/// Convert an Assura type token sequence (e.g., `["List", "<", "Int", ">"]`)
109/// to a valid Rust type string.
110///
111/// Handles Assura-specific syntax that is not valid Rust:
112/// - Refinement types: `{ x : T | P }` -> base type `T`
113/// - Taint annotations: `T @ taint : label` -> just `T`
114/// - Union error types: `T | E` -> `Result<T, E>`
115/// - Smart joining: `Vec < i64 >` -> `Vec<i64>`
116pub fn map_type_tokens(tokens: &[String]) -> String {
117    if tokens.is_empty() {
118        return "()".to_string();
119    }
120
121    // Phase 1: Strip annotations, qualifiers, and clause keywords
122    // Stops at: "@" (taint), "#" (attribute), "decreases", "where"
123    // Filters out type qualifiers with no Rust equivalent
124    let clean: Vec<&str> = tokens
125        .iter()
126        .map(|s| s.as_str())
127        .take_while(|t| !matches!(*t, "@" | "#" | "decreases" | "where"))
128        .filter(|t| {
129            !matches!(
130                *t,
131                "linear" | "secret" | "tainted" | "taint" | "untrusted" | "validated"
132            )
133        })
134        .collect();
135    if clean.is_empty() {
136        return "()".to_string();
137    }
138
139    // Phase 2: Strip refinement predicates: { x : T | P } -> just the base type
140    if clean.first() == Some(&"{") {
141        return extract_base_type_from_refined(tokens);
142    }
143
144    // Phase 3: Handle union error types: T | E -> Result<T, E>
145    // Find a "|" not inside angle brackets
146    let mut depth = 0i32;
147    let mut pipe_pos = None;
148    for (i, tok) in clean.iter().enumerate() {
149        match *tok {
150            "<" => depth += 1,
151            ">" => {
152                if depth > 0 {
153                    depth -= 1;
154                }
155            }
156            "|" if depth == 0 => {
157                pipe_pos = Some(i);
158                break;
159            }
160            _ => {}
161        }
162    }
163
164    if let Some(pos) = pipe_pos {
165        let ok_tokens: Vec<String> = clean[..pos].iter().map(|s| s.to_string()).collect();
166        let err_tokens: Vec<String> = clean[pos + 1..].iter().map(|s| s.to_string()).collect();
167        let ok_type = map_type_tokens(&ok_tokens);
168        let err_type = map_type_tokens(&err_tokens);
169        return format!("Result<{ok_type}, {err_type}>");
170    }
171
172    // Phase 4: Map each token and join smartly (no extra spaces around < > & etc.)
173    // Convert SCREAMING_SNAKE_CASE names inside angle brackets to marker types,
174    // since Assura const-generics don't translate directly to Rust generics.
175    let mut in_angle = 0i32;
176    let mapped: Vec<String> = clean
177        .iter()
178        .map(|t| {
179            match *t {
180                "<" => in_angle += 1,
181                ">" if in_angle > 0 => {
182                    in_angle -= 1;
183                }
184                _ => {}
185            }
186            if in_angle > 0 && is_const_name(t) {
187                // Const-generic name used as type param: wrap as marker type
188                t.to_string()
189            } else {
190                map_type_token(t).to_string()
191            }
192        })
193        .collect();
194    let refs: Vec<&str> = mapped.iter().map(|s| s.as_str()).collect();
195    smart_join_type_tokens(&refs)
196}
197
198/// Join type tokens without spurious spaces around angle brackets, ampersands, etc.
199pub(crate) fn smart_join_type_tokens(tokens: &[&str]) -> String {
200    let mut result = String::new();
201    for (i, tok) in tokens.iter().enumerate() {
202        if i > 0 {
203            let prev = tokens[i - 1];
204            let no_space = matches!(*tok, ">" | "<" | "," | ")" | ".")
205                || matches!(prev, "<" | "(" | "&" | ".")
206                || (*tok == "mut" && prev == "&");
207            if !no_space {
208                result.push(' ');
209            }
210        }
211        result.push_str(tok);
212    }
213    result
214}
215
216// ---------------------------------------------------------------------------
217
218// Type definitions
219// ---------------------------------------------------------------------------
220
221pub(crate) fn generate_type_def(t: &TypeDef, code: &mut String) {
222    let items = crate::hir::build_type_def(t);
223    for item in &items {
224        code.push_str(&crate::hir::render_item_raw(item));
225    }
226}
227
228// ---------------------------------------------------------------------------
229// Type reference collection (for generating stub types)
230// ---------------------------------------------------------------------------
231
232/// Check if a name looks like a constant (SCREAMING_SNAKE_CASE).
233/// E.g., `TOTAL_TABLE_SIZE`, `MAX_ALPHABET_SIZE`.
234pub(crate) fn is_const_name(name: &str) -> bool {
235    !name.is_empty()
236        && name
237            .chars()
238            .all(|c| c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit())
239        && name.contains('_')
240}
241
242/// Info about a generic parameter: either a type param or a const param.
243#[derive(Debug, Clone)]
244pub(crate) enum GenericParamKind {
245    Type,
246    Const,
247}
248
249/// Scan a type token sequence for `TypeName<Arg1, Arg2, ...>` patterns.
250/// Records the generic params (type vs const) for each type name, and
251/// collects names that appear as const-generic arguments.
252///
253/// Superseded in production by `detect_generic_arity_from_type_expr` which
254/// operates directly on `TypeExpr`. Retained for unit tests.
255#[cfg(test)]
256pub(crate) fn detect_generic_arity(
257    tokens: &[String],
258    param_map: &mut std::collections::HashMap<String, Vec<GenericParamKind>>,
259    const_names: &mut std::collections::HashSet<String>,
260) {
261    let mut i = 0;
262    while i < tokens.len() {
263        if i + 1 < tokens.len() && tokens[i + 1] == "<" && is_user_type_name(&tokens[i]) {
264            let type_name = tokens[i].clone();
265            let mut depth = 0;
266            let mut params = Vec::new();
267            let mut current_is_const = false;
268            let mut j = i + 1;
269            while j < tokens.len() {
270                match tokens[j].as_str() {
271                    "<" => {
272                        depth += 1;
273                        if depth == 1 {
274                            current_is_const = false;
275                        }
276                    }
277                    ">" if depth > 0 => {
278                        depth -= 1;
279                        if depth == 0 {
280                            // Record the last parameter
281                            params.push(if current_is_const {
282                                GenericParamKind::Const
283                            } else {
284                                GenericParamKind::Type
285                            });
286                            break;
287                        }
288                    }
289                    "," if depth == 1 => {
290                        params.push(if current_is_const {
291                            GenericParamKind::Const
292                        } else {
293                            GenericParamKind::Type
294                        });
295                        current_is_const = false;
296                    }
297                    tok if depth == 1 && is_const_name(tok) => {
298                        const_names.insert(tok.to_string());
299                        current_is_const = true;
300                    }
301                    tok if depth == 1
302                        && !tok.is_empty()
303                        && tok.chars().all(|c| c.is_ascii_digit()) =>
304                    {
305                        // Numeric literal as const generic
306                        current_is_const = true;
307                    }
308                    _ => {}
309                }
310                j += 1;
311            }
312            let existing_len = param_map.get(&type_name).map_or(0, |v| v.len());
313            if params.len() > existing_len {
314                param_map.insert(type_name, params);
315            }
316            i = j + 1;
317        } else {
318            i += 1;
319        }
320    }
321}
322
323/// Check if a token looks like a user-defined type name (starts with uppercase,
324/// alphanumeric, not a Rust keyword or built-in).
325pub(crate) fn is_user_type_name(tok: &str) -> bool {
326    !tok.is_empty()
327        && tok.chars().next().is_some_and(|c| c.is_uppercase())
328        && tok.chars().all(|c| c.is_alphanumeric() || c == '_')
329        && !matches!(
330            tok,
331            "Int"
332                | "Nat"
333                | "Float"
334                | "Bool"
335                | "String"
336                | "Bytes"
337                | "Unit"
338                | "Never"
339                | "U8"
340                | "U16"
341                | "U32"
342                | "U64"
343                | "I8"
344                | "I16"
345                | "I32"
346                | "I64"
347                | "F32"
348                | "F64"
349                | "List"
350                | "Vec"
351                | "Map"
352                | "Set"
353                | "Option"
354                | "Result"
355                | "Sequence"
356                | "Self"
357                | "Box"
358                | "Fn"
359                | "FnOnce"
360                | "FnMut"
361        )
362}
363
364/// Collect user-defined type names from a type token sequence.
365///
366/// Superseded in production by `collect_type_refs_from_type_expr`. Retained
367/// for unit tests.
368#[cfg(test)]
369pub(crate) fn collect_type_refs_from_tokens(
370    tokens: &[String],
371    out: &mut std::collections::HashSet<String>,
372) {
373    for tok in tokens {
374        // Skip taint annotations, attributes, and keywords
375        if matches!(
376            tok.as_str(),
377            "@" | "#"
378                | "taint"
379                | "untrusted"
380                | "validated"
381                | "secret"
382                | "pub"
383                | "mut"
384                | ":"
385                | "|"
386                | "&"
387                | ">"
388                | "<"
389                | ","
390                | "("
391                | ")"
392                | "{"
393                | "}"
394                | "["
395                | "]"
396                | "decreases"
397                | "where"
398        ) {
399            continue;
400        }
401        if is_user_type_name(tok) {
402            out.insert(tok.clone());
403        }
404    }
405}
406
407/// Collect user-defined type names directly from a `TypeExpr`, avoiding the
408/// token roundtrip through `type_expr_to_token_vec`.
409pub(crate) fn collect_type_refs_from_type_expr(
410    te: Option<&assura_ast::TypeExpr>,
411    out: &mut std::collections::HashSet<String>,
412) {
413    let Some(te) = te else { return };
414    match te {
415        assura_ast::TypeExpr::Named(name) => {
416            // Named may contain spaces from fallback join; check each word.
417            for word in name.split_whitespace() {
418                if is_user_type_name(word) {
419                    out.insert(word.to_string());
420                }
421            }
422        }
423        assura_ast::TypeExpr::Generic(name, args) => {
424            if is_user_type_name(name) {
425                out.insert(name.clone());
426            }
427            for arg in args {
428                collect_type_refs_from_type_expr(Some(arg), out);
429            }
430        }
431        assura_ast::TypeExpr::Tuple(items) => {
432            for item in items {
433                collect_type_refs_from_type_expr(Some(item), out);
434            }
435        }
436        assura_ast::TypeExpr::Fn { params, ret } => {
437            for p in params {
438                collect_type_refs_from_type_expr(Some(p), out);
439            }
440            collect_type_refs_from_type_expr(Some(ret), out);
441        }
442        assura_ast::TypeExpr::Refined { base, .. } => {
443            collect_type_refs_from_type_expr(Some(base), out);
444        }
445        assura_ast::TypeExpr::Unit => {}
446    }
447}
448
449/// Detect generic arity directly from `TypeExpr`, replacing the token-based
450/// `detect_generic_arity` for codegen purposes.
451///
452/// Walks the type expression tree looking for `Generic(name, args)` nodes.
453/// Each arg is classified as `Const` (SCREAMING_SNAKE_CASE or numeric literal)
454/// or `Type` (otherwise). The widest arity seen for each type name wins.
455pub(crate) fn detect_generic_arity_from_type_expr(
456    te: Option<&assura_ast::TypeExpr>,
457    param_map: &mut std::collections::HashMap<String, Vec<GenericParamKind>>,
458    const_names: &mut std::collections::HashSet<String>,
459) {
460    let Some(te) = te else { return };
461    match te {
462        assura_ast::TypeExpr::Generic(name, args) => {
463            if is_user_type_name(name) {
464                let params: Vec<GenericParamKind> = args
465                    .iter()
466                    .map(|arg| {
467                        if is_const_type_expr_arg(arg) {
468                            // Collect the const name if it's a named constant
469                            if let assura_ast::TypeExpr::Named(n) = arg
470                                && is_const_name(n)
471                            {
472                                const_names.insert(n.clone());
473                            }
474                            GenericParamKind::Const
475                        } else {
476                            GenericParamKind::Type
477                        }
478                    })
479                    .collect();
480                let existing_len = param_map.get(name).map_or(0, |v| v.len());
481                if params.len() > existing_len {
482                    param_map.insert(name.clone(), params);
483                }
484            }
485            // Recurse into args
486            for arg in args {
487                detect_generic_arity_from_type_expr(Some(arg), param_map, const_names);
488            }
489        }
490        assura_ast::TypeExpr::Tuple(items) => {
491            for item in items {
492                detect_generic_arity_from_type_expr(Some(item), param_map, const_names);
493            }
494        }
495        assura_ast::TypeExpr::Fn { params, ret } => {
496            for p in params {
497                detect_generic_arity_from_type_expr(Some(p), param_map, const_names);
498            }
499            detect_generic_arity_from_type_expr(Some(ret), param_map, const_names);
500        }
501        assura_ast::TypeExpr::Refined { base, .. } => {
502            detect_generic_arity_from_type_expr(Some(base), param_map, const_names);
503        }
504        assura_ast::TypeExpr::Named(_) | assura_ast::TypeExpr::Unit => {}
505    }
506}
507
508/// Check if a `TypeExpr` argument to a generic looks like a const (SCREAMING_SNAKE_CASE
509/// or numeric literal embedded in a Named).
510fn is_const_type_expr_arg(te: &assura_ast::TypeExpr) -> bool {
511    match te {
512        assura_ast::TypeExpr::Named(n) => {
513            is_const_name(n) || (!n.is_empty() && n.chars().all(|c| c.is_ascii_digit()))
514        }
515        _ => false,
516    }
517}
518
519/// Detect feature_max names used as type arguments directly from `TypeExpr`.
520pub(crate) fn detect_feature_max_as_type_from_type_expr(
521    te: Option<&assura_ast::TypeExpr>,
522    feature_max_set: &std::collections::HashSet<&str>,
523    out: &mut std::collections::HashSet<String>,
524) {
525    let Some(te) = te else { return };
526    match te {
527        assura_ast::TypeExpr::Generic(_, args) => {
528            for arg in args {
529                // Each arg is in a type-argument position (inside `<>`)
530                if let assura_ast::TypeExpr::Named(n) = arg
531                    && feature_max_set.contains(n.as_str())
532                {
533                    out.insert(n.clone());
534                }
535                // Recurse for nested generics
536                detect_feature_max_as_type_from_type_expr(Some(arg), feature_max_set, out);
537            }
538        }
539        assura_ast::TypeExpr::Tuple(items) => {
540            for item in items {
541                detect_feature_max_as_type_from_type_expr(Some(item), feature_max_set, out);
542            }
543        }
544        assura_ast::TypeExpr::Fn { params, ret } => {
545            for p in params {
546                detect_feature_max_as_type_from_type_expr(Some(p), feature_max_set, out);
547            }
548            detect_feature_max_as_type_from_type_expr(Some(ret), feature_max_set, out);
549        }
550        assura_ast::TypeExpr::Refined { base, .. } => {
551            detect_feature_max_as_type_from_type_expr(Some(base), feature_max_set, out);
552        }
553        assura_ast::TypeExpr::Named(_) | assura_ast::TypeExpr::Unit => {}
554    }
555}
556
557/// Collect type names referenced in expressions (e.g., constructor calls).
558pub(crate) fn collect_type_refs_from_expr(
559    expr: &SpExpr,
560    out: &mut std::collections::HashSet<String>,
561) {
562    match &expr.node {
563        Expr::Ident(name) => {
564            if is_user_type_name(name) {
565                out.insert(name.clone());
566            }
567        }
568        Expr::Call { func, args } => {
569            collect_type_refs_from_expr(func, out);
570            for a in args {
571                collect_type_refs_from_expr(a, out);
572            }
573        }
574        Expr::MethodCall {
575            receiver,
576            method: _,
577            args,
578        } => {
579            collect_type_refs_from_expr(receiver, out);
580            for a in args {
581                collect_type_refs_from_expr(a, out);
582            }
583        }
584        Expr::Field(recv, _) => collect_type_refs_from_expr(recv, out),
585        Expr::Index { expr: e, index } => {
586            collect_type_refs_from_expr(e, out);
587            collect_type_refs_from_expr(index, out);
588        }
589        Expr::BinOp { lhs, rhs, .. } => {
590            collect_type_refs_from_expr(lhs, out);
591            collect_type_refs_from_expr(rhs, out);
592        }
593        Expr::UnaryOp { expr: e, .. }
594        | Expr::Old(e)
595        | Expr::Cast { expr: e, .. }
596        | Expr::Ghost(e) => {
597            collect_type_refs_from_expr(e, out);
598        }
599        Expr::If {
600            cond,
601            then_branch,
602            else_branch,
603        } => {
604            collect_type_refs_from_expr(cond, out);
605            collect_type_refs_from_expr(then_branch, out);
606            if let Some(eb) = else_branch {
607                collect_type_refs_from_expr(eb, out);
608            }
609        }
610        Expr::Forall { domain, body, .. } | Expr::Exists { domain, body, .. } => {
611            collect_type_refs_from_expr(domain, out);
612            collect_type_refs_from_expr(body, out);
613        }
614        Expr::Let { value, body, .. } => {
615            collect_type_refs_from_expr(value, out);
616            collect_type_refs_from_expr(body, out);
617        }
618        Expr::Match { scrutinee, arms } => {
619            collect_type_refs_from_expr(scrutinee, out);
620            for arm in arms {
621                collect_type_refs_from_expr(&arm.body, out);
622            }
623        }
624        Expr::Apply { args, .. } => {
625            for a in args {
626                collect_type_refs_from_expr(a, out);
627            }
628        }
629        Expr::List(items) | Expr::Tuple(items) | Expr::Block(items) => {
630            for item in items {
631                collect_type_refs_from_expr(item, out);
632            }
633        }
634        Expr::Raw(tokens) => {
635            for tok in tokens {
636                if is_user_type_name(tok) {
637                    out.insert(tok.clone());
638                }
639            }
640        }
641        // Literals never contain type references
642        Expr::Literal(_) => {}
643    }
644}
645
646/// Find the value for a feature_max constant from the AST.
647pub(crate) fn find_feature_max_value(source: &assura_ast::SourceFile, name: &str) -> String {
648    for decl in &source.decls {
649        if let Decl::Block {
650            kind,
651            name: n,
652            value,
653            body,
654        } = &decl.node
655            && *kind == BlockKind::FeatureMax
656            && n == name
657        {
658            // First, try the inline value field.
659            // The value tokens may include a type annotation: `["Nat", "=", "280"]`
660            // from `feature_max X: Nat = 280`. Extract after the `=`.
661            if let Some(val_tokens) = value {
662                if let Some(eq_pos) = val_tokens.iter().position(|t| t == "=") {
663                    let after_eq = &val_tokens[eq_pos + 1..];
664                    if !after_eq.is_empty() {
665                        return after_eq.join(" ");
666                    }
667                } else if val_tokens.len() == 1 {
668                    // Single-token value without `=` (e.g., just a number)
669                    return val_tokens[0].clone();
670                }
671            }
672            // Fallback: try to extract from body clauses
673            for clause in body {
674                let val = expr_to_rust_static(&clause.body);
675                if !val.is_empty() && val != "()" {
676                    return val;
677                }
678            }
679        }
680    }
681    // No value found: emit a compile_error! so generated code fails to
682    // build rather than silently using 0, which hides missing definitions.
683    format!("compile_error!(\"feature_max `{name}` has no value\")")
684}
685
686// expr_to_rust_static is now defined in expr.rs (unified RustCodegenFolder)
687
688/// Extract the base type from a refined type token sequence like
689/// `["n", ":", "Int", "|", "n", ">", "0"]` -> "i64".
690pub(crate) fn extract_base_type_from_refined(tokens: &[String]) -> String {
691    // Look for a token after ':' that starts with uppercase
692    let mut after_colon = false;
693    for tok in tokens {
694        if tok == ":" {
695            after_colon = true;
696            continue;
697        }
698        if after_colon {
699            if tok.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
700                return map_type_token(tok).to_string();
701            }
702            // Not a type, stop looking
703            after_colon = false;
704        }
705    }
706    // Fallback: just use the first type-looking token
707    for tok in tokens {
708        if tok.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
709            return map_type_token(tok).to_string();
710        }
711    }
712    "i64".to_string()
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use assura_ast::Spanned;
719
720    // ---- map_type_token ----
721
722    #[test]
723    fn map_basic_types() {
724        assert_eq!(map_type_token("Int"), "i64");
725        assert_eq!(map_type_token("Nat"), "u64");
726        assert_eq!(map_type_token("Float"), "f64");
727        assert_eq!(map_type_token("Bool"), "bool");
728        assert_eq!(map_type_token("String"), "String");
729        assert_eq!(map_type_token("Bytes"), "Vec<u8>");
730        assert_eq!(map_type_token("Unit"), "()");
731        assert_eq!(map_type_token("Never"), "!");
732    }
733
734    #[test]
735    fn map_fixed_width_types() {
736        assert_eq!(map_type_token("U8"), "u8");
737        assert_eq!(map_type_token("U16"), "u16");
738        assert_eq!(map_type_token("U32"), "u32");
739        assert_eq!(map_type_token("U64"), "u64");
740        assert_eq!(map_type_token("I8"), "i8");
741        assert_eq!(map_type_token("I16"), "i16");
742        assert_eq!(map_type_token("I32"), "i32");
743        assert_eq!(map_type_token("I64"), "i64");
744        assert_eq!(map_type_token("F32"), "f32");
745        assert_eq!(map_type_token("F64"), "f64");
746    }
747
748    #[test]
749    fn map_collection_types() {
750        assert_eq!(map_type_token("List"), "Vec");
751        assert_eq!(map_type_token("Map"), "std::collections::BTreeMap");
752        assert_eq!(map_type_token("Set"), "std::collections::BTreeSet");
753        assert_eq!(map_type_token("Sequence"), "Vec");
754    }
755
756    #[test]
757    fn map_passthrough() {
758        assert_eq!(map_type_token("Option"), "Option");
759        assert_eq!(map_type_token("Result"), "Result");
760        assert_eq!(map_type_token("MyCustomType"), "MyCustomType");
761    }
762
763    // ---- map_type_tokens ----
764
765    #[test]
766    fn map_tokens_empty() {
767        assert_eq!(map_type_tokens(&[]), "()");
768    }
769
770    #[test]
771    fn map_tokens_single() {
772        let tokens = vec!["Int".to_string()];
773        assert_eq!(map_type_tokens(&tokens), "i64");
774    }
775
776    #[test]
777    fn map_tokens_generic() {
778        let tokens: Vec<String> = vec!["List", "<", "Int", ">"]
779            .into_iter()
780            .map(String::from)
781            .collect();
782        assert_eq!(map_type_tokens(&tokens), "Vec<i64>");
783    }
784
785    #[test]
786    fn map_tokens_strips_taint() {
787        let tokens: Vec<String> = vec!["Int", "@", "taint", ":", "untrusted"]
788            .into_iter()
789            .map(String::from)
790            .collect();
791        assert_eq!(map_type_tokens(&tokens), "i64");
792    }
793
794    #[test]
795    fn map_tokens_union_error() {
796        let tokens: Vec<String> = vec!["Int", "|", "MyError"]
797            .into_iter()
798            .map(String::from)
799            .collect();
800        assert_eq!(map_type_tokens(&tokens), "Result<i64, MyError>");
801    }
802
803    #[test]
804    fn map_tokens_refinement() {
805        let tokens: Vec<String> = vec!["{", "x", ":", "Int", "|", "x", ">", "0", "}"]
806            .into_iter()
807            .map(String::from)
808            .collect();
809        assert_eq!(map_type_tokens(&tokens), "i64");
810    }
811
812    // ---- smart_join_type_tokens ----
813
814    #[test]
815    fn smart_join_no_space_after_open_angle() {
816        let tokens = vec!["Vec", "<", "i64", ">"];
817        // No space before `<`, after `<`, or before `>`
818        assert_eq!(smart_join_type_tokens(&tokens), "Vec<i64>");
819    }
820
821    #[test]
822    fn smart_join_no_space_after_ampersand() {
823        let tokens = vec!["&", "str"];
824        assert_eq!(smart_join_type_tokens(&tokens), "&str");
825    }
826
827    #[test]
828    fn smart_join_mut_ref() {
829        let tokens = vec!["&", "mut", "i64"];
830        assert_eq!(smart_join_type_tokens(&tokens), "&mut i64");
831    }
832
833    // ---- is_const_name ----
834
835    #[test]
836    fn const_name_true() {
837        assert!(is_const_name("MAX_SIZE"));
838        assert!(is_const_name("TOTAL_TABLE_SIZE"));
839    }
840
841    #[test]
842    fn const_name_false() {
843        assert!(!is_const_name("MyType"));
844        assert!(!is_const_name("x"));
845        assert!(!is_const_name(""));
846        assert!(!is_const_name("ALLCAPS")); // no underscore
847    }
848
849    // ---- is_user_type_name ----
850
851    #[test]
852    fn user_type_name_true() {
853        assert!(is_user_type_name("MyStruct"));
854        assert!(is_user_type_name("Point"));
855        assert!(is_user_type_name("HuffmanTree"));
856    }
857
858    #[test]
859    fn user_type_name_false_builtins() {
860        assert!(!is_user_type_name("Int"));
861        assert!(!is_user_type_name("Bool"));
862        assert!(!is_user_type_name("String"));
863        assert!(!is_user_type_name("List"));
864        assert!(!is_user_type_name("Option"));
865        assert!(!is_user_type_name("Result"));
866    }
867
868    #[test]
869    fn user_type_name_false_lowercase() {
870        assert!(!is_user_type_name("myvar"));
871        assert!(!is_user_type_name(""));
872    }
873
874    // ---- collect_type_refs_from_tokens ----
875
876    #[test]
877    fn collect_type_refs_basic() {
878        let tokens: Vec<String> = vec!["List", "<", "Point", ">"]
879            .into_iter()
880            .map(String::from)
881            .collect();
882        let mut out = std::collections::HashSet::new();
883        collect_type_refs_from_tokens(&tokens, &mut out);
884        assert!(out.contains("Point"));
885        assert!(!out.contains("List")); // built-in, excluded
886    }
887
888    #[test]
889    fn collect_type_refs_skips_annotations() {
890        let tokens: Vec<String> = vec!["MyType", "@", "taint"]
891            .into_iter()
892            .map(String::from)
893            .collect();
894        let mut out = std::collections::HashSet::new();
895        collect_type_refs_from_tokens(&tokens, &mut out);
896        assert!(out.contains("MyType"));
897        assert!(!out.contains("@"));
898        assert!(!out.contains("taint"));
899    }
900
901    // ---- collect_type_refs_from_expr ----
902
903    #[test]
904    fn collect_refs_from_ident() {
905        let mut out = std::collections::HashSet::new();
906        collect_type_refs_from_expr(&Spanned::no_span(Expr::Ident("MyType".into())), &mut out);
907        assert!(out.contains("MyType"));
908    }
909
910    #[test]
911    fn collect_refs_from_ident_lowercase() {
912        let mut out = std::collections::HashSet::new();
913        collect_type_refs_from_expr(&Spanned::no_span(Expr::Ident("x".into())), &mut out);
914        assert!(out.is_empty());
915    }
916
917    #[test]
918    fn collect_refs_nested() {
919        let mut out = std::collections::HashSet::new();
920        let e = Spanned::no_span(Expr::Call {
921            func: Box::new(Spanned::no_span(Expr::Ident("Create".into()))),
922            args: vec![Spanned::no_span(Expr::Ident("Config".into()))],
923        });
924        collect_type_refs_from_expr(&e, &mut out);
925        assert!(out.contains("Create"));
926        assert!(out.contains("Config"));
927    }
928
929    // ---- detect_generic_arity ----
930
931    #[test]
932    fn detect_generic_one_type_param() {
933        let tokens: Vec<String> = vec!["Wrapper", "<", "Int", ">"]
934            .into_iter()
935            .map(String::from)
936            .collect();
937        let mut map = std::collections::HashMap::new();
938        let mut consts = std::collections::HashSet::new();
939        detect_generic_arity(&tokens, &mut map, &mut consts);
940        assert_eq!(map.get("Wrapper").map(|v| v.len()), Some(1));
941    }
942
943    #[test]
944    fn detect_generic_const_param() {
945        let tokens: Vec<String> = vec!["Buffer", "<", "MAX_SIZE", ">"]
946            .into_iter()
947            .map(String::from)
948            .collect();
949        let mut map = std::collections::HashMap::new();
950        let mut consts = std::collections::HashSet::new();
951        detect_generic_arity(&tokens, &mut map, &mut consts);
952        assert!(consts.contains("MAX_SIZE"));
953        assert!(matches!(
954            map.get("Buffer").and_then(|v| v.first()),
955            Some(GenericParamKind::Const)
956        ));
957    }
958
959    // ---- generate_cargo_toml_impl ----
960
961    #[test]
962    fn cargo_toml_default_config() {
963        let cfg = BackendConfig::default();
964        let toml = generate_cargo_toml_impl("my_crate", &cfg, false, false);
965        assert!(toml.contains("name = \"my_crate\""));
966        assert!(toml.contains("edition = \"2024\""));
967        assert!(!toml.contains("[profile.release]"));
968    }
969
970    #[test]
971    fn cargo_toml_with_proptest() {
972        let cfg = BackendConfig::default();
973        let toml = generate_cargo_toml_impl("test_crate", &cfg, true, false);
974        assert!(toml.contains("[dev-dependencies]"));
975        assert!(toml.contains("proptest"));
976    }
977
978    #[test]
979    fn cargo_toml_with_thiserror() {
980        let cfg = BackendConfig::default();
981        let toml = generate_cargo_toml_impl("err_crate", &cfg, false, true);
982        assert!(toml.contains("thiserror"));
983    }
984
985    #[test]
986    fn cargo_toml_cranelift() {
987        let cfg = BackendConfig {
988            backend: CodegenBackend::Cranelift,
989            ..Default::default()
990        };
991        let toml = generate_cargo_toml_impl("fast", &cfg, false, false);
992        assert!(toml.contains("Cranelift"));
993    }
994
995    #[test]
996    fn cargo_toml_wasm() {
997        let cfg = BackendConfig {
998            target: CompileTarget::Wasm,
999            ..Default::default()
1000        };
1001        let toml = generate_cargo_toml_impl("wasm_mod", &cfg, false, false);
1002        assert!(toml.contains("cdylib"));
1003        assert!(toml.contains("wasm32-wasip1"));
1004    }
1005
1006    #[test]
1007    fn cargo_toml_runtime_checks() {
1008        let cfg = BackendConfig {
1009            runtime_checks: true,
1010            ..Default::default()
1011        };
1012        let toml = generate_cargo_toml_impl("rt_crate", &cfg, false, false);
1013        assert!(
1014            toml.contains("assura-runtime"),
1015            "runtime checks should add assura-runtime dep: {toml}"
1016        );
1017    }
1018
1019    #[test]
1020    fn cargo_toml_no_runtime_checks() {
1021        let cfg = BackendConfig::default();
1022        let toml = generate_cargo_toml_impl("rt_crate", &cfg, false, false);
1023        assert!(
1024            !toml.contains("assura-runtime"),
1025            "default should not include assura-runtime: {toml}"
1026        );
1027    }
1028
1029    // ---- generate_type_def ----
1030
1031    #[test]
1032    fn type_def_struct() {
1033        let t = TypeDef {
1034            name: "Point".into(),
1035            type_params: vec![],
1036            body: TypeBody::Struct(vec![
1037                assura_ast::FieldDef {
1038                    name: "x".into(),
1039                    ty: Some(assura_ast::TypeExpr::named("Int")),
1040                    is_pub: true,
1041                },
1042                assura_ast::FieldDef {
1043                    name: "y".into(),
1044                    ty: Some(assura_ast::TypeExpr::named("Int")),
1045                    is_pub: true,
1046                },
1047            ]),
1048        };
1049        let mut code = String::new();
1050        generate_type_def(&t, &mut code);
1051        assert!(code.contains("pub struct Point"));
1052        assert!(code.contains("pub x: i64"));
1053        assert!(code.contains("pub y: i64"));
1054    }
1055
1056    #[test]
1057    fn type_def_alias() {
1058        let t = TypeDef {
1059            name: "Identifier".into(),
1060            type_params: vec![],
1061            body: TypeBody::Alias(vec!["String".into()]),
1062        };
1063        let mut code = String::new();
1064        generate_type_def(&t, &mut code);
1065        assert!(code.contains("pub type Identifier = String"));
1066    }
1067
1068    #[test]
1069    fn type_def_generic() {
1070        let t = TypeDef {
1071            name: "Wrapper".into(),
1072            type_params: vec!["T".into()],
1073            body: TypeBody::Empty,
1074        };
1075        let mut code = String::new();
1076        generate_type_def(&t, &mut code);
1077        assert!(code.contains("pub struct Wrapper<T>"));
1078    }
1079
1080    #[test]
1081    fn type_def_refined() {
1082        let t = TypeDef {
1083            name: "PositiveInt".into(),
1084            type_params: vec![],
1085            body: TypeBody::Refined(vec![
1086                "n".into(),
1087                ":".into(),
1088                "Int".into(),
1089                "|".into(),
1090                "n".into(),
1091                ">".into(),
1092                "0".into(),
1093            ]),
1094        };
1095        let mut code = String::new();
1096        generate_type_def(&t, &mut code);
1097        assert!(code.contains("pub struct PositiveInt(pub i64)"));
1098    }
1099
1100    // ---- extract_base_type_from_refined ----
1101
1102    #[test]
1103    fn extract_base_type_after_colon() {
1104        let tokens: Vec<String> = vec!["n", ":", "Int", "|", "n", ">", "0"]
1105            .into_iter()
1106            .map(String::from)
1107            .collect();
1108        assert_eq!(extract_base_type_from_refined(&tokens), "i64");
1109    }
1110
1111    #[test]
1112    fn extract_base_type_fallback() {
1113        let tokens: Vec<String> = vec!["Nat"].into_iter().map(String::from).collect();
1114        assert_eq!(extract_base_type_from_refined(&tokens), "u64");
1115    }
1116
1117    // ---- expr_to_rust_static ----
1118
1119    #[test]
1120    fn static_int_literal() {
1121        assert_eq!(
1122            expr_to_rust_static(&Spanned::no_span(Expr::Literal(Literal::Int("42".into())))),
1123            "42"
1124        );
1125    }
1126
1127    #[test]
1128    fn static_binop() {
1129        let e = Spanned::no_span(Expr::BinOp {
1130            lhs: Box::new(Spanned::no_span(Expr::Ident("a".into()))),
1131            op: BinOp::Add,
1132            rhs: Box::new(Spanned::no_span(Expr::Ident("b".into()))),
1133        });
1134        assert_eq!(expr_to_rust_static(&e), "(a + b)");
1135    }
1136
1137    #[test]
1138    fn static_ghost_erased() {
1139        let e = Spanned::no_span(Expr::Ghost(Box::new(Spanned::no_span(Expr::Ident(
1140            "x".into(),
1141        )))));
1142        let result = expr_to_rust_static(&e);
1143        assert!(result.contains("ghost:"));
1144        assert!(result.ends_with("()"));
1145    }
1146
1147    #[test]
1148    fn static_forall_comment() {
1149        let e = Spanned::no_span(Expr::Forall {
1150            var: "i".into(),
1151            domain: Box::new(Spanned::no_span(Expr::Ident("items".into()))),
1152            body: Box::new(Spanned::no_span(Expr::Literal(Literal::Bool(true)))),
1153        });
1154        let result = expr_to_rust_static(&e);
1155        assert!(result.contains("forall"));
1156        assert!(result.ends_with("true"));
1157    }
1158
1159    #[test]
1160    fn static_raw_single_token() {
1161        let e = Spanned::no_span(Expr::Raw(vec!["42".into()]));
1162        assert_eq!(expr_to_rust_static(&e), "42");
1163    }
1164
1165    #[test]
1166    fn static_raw_eq_value() {
1167        let e = Spanned::no_span(Expr::Raw(vec!["=".into(), "100".into()]));
1168        assert_eq!(expr_to_rust_static(&e), "100");
1169    }
1170
1171    // ---- collect_type_refs_from_type_expr ----
1172
1173    #[test]
1174    fn collect_type_refs_type_expr_named() {
1175        let mut out = std::collections::HashSet::new();
1176        let te = assura_ast::TypeExpr::Named("MyStruct".into());
1177        collect_type_refs_from_type_expr(Some(&te), &mut out);
1178        assert!(out.contains("MyStruct"));
1179    }
1180
1181    #[test]
1182    fn collect_type_refs_type_expr_named_builtin() {
1183        let mut out = std::collections::HashSet::new();
1184        let te = assura_ast::TypeExpr::Named("Int".into());
1185        collect_type_refs_from_type_expr(Some(&te), &mut out);
1186        assert!(out.is_empty());
1187    }
1188
1189    #[test]
1190    fn collect_type_refs_type_expr_generic() {
1191        let mut out = std::collections::HashSet::new();
1192        let te = assura_ast::TypeExpr::Generic(
1193            "List".into(),
1194            vec![assura_ast::TypeExpr::Named("Point".into())],
1195        );
1196        collect_type_refs_from_type_expr(Some(&te), &mut out);
1197        assert!(out.contains("Point"));
1198        assert!(!out.contains("List")); // built-in
1199    }
1200
1201    #[test]
1202    fn collect_type_refs_type_expr_none() {
1203        let mut out = std::collections::HashSet::new();
1204        collect_type_refs_from_type_expr(None, &mut out);
1205        assert!(out.is_empty());
1206    }
1207
1208    // ---- detect_generic_arity_from_type_expr ----
1209
1210    #[test]
1211    fn detect_generic_arity_type_expr_one_type_param() {
1212        let te = assura_ast::TypeExpr::Generic(
1213            "Buffer".into(),
1214            vec![assura_ast::TypeExpr::Named("Byte".into())],
1215        );
1216        let mut map = std::collections::HashMap::new();
1217        let mut consts = std::collections::HashSet::new();
1218        detect_generic_arity_from_type_expr(Some(&te), &mut map, &mut consts);
1219        assert_eq!(map.get("Buffer").map(|v| v.len()), Some(1));
1220        assert!(matches!(
1221            map.get("Buffer").and_then(|v| v.first()),
1222            Some(GenericParamKind::Type)
1223        ));
1224    }
1225
1226    #[test]
1227    fn detect_generic_arity_type_expr_const_param() {
1228        let te = assura_ast::TypeExpr::Generic(
1229            "Buffer".into(),
1230            vec![assura_ast::TypeExpr::Named("MAX_SIZE".into())],
1231        );
1232        let mut map = std::collections::HashMap::new();
1233        let mut consts = std::collections::HashSet::new();
1234        detect_generic_arity_from_type_expr(Some(&te), &mut map, &mut consts);
1235        assert!(consts.contains("MAX_SIZE"));
1236        assert!(matches!(
1237            map.get("Buffer").and_then(|v| v.first()),
1238            Some(GenericParamKind::Const)
1239        ));
1240    }
1241
1242    #[test]
1243    fn detect_generic_arity_type_expr_mixed() {
1244        let te = assura_ast::TypeExpr::Generic(
1245            "Table".into(),
1246            vec![
1247                assura_ast::TypeExpr::Named("Entry".into()),
1248                assura_ast::TypeExpr::Named("MAX_ENTRIES".into()),
1249            ],
1250        );
1251        let mut map = std::collections::HashMap::new();
1252        let mut consts = std::collections::HashSet::new();
1253        detect_generic_arity_from_type_expr(Some(&te), &mut map, &mut consts);
1254        assert_eq!(map.get("Table").map(|v| v.len()), Some(2));
1255        assert!(consts.contains("MAX_ENTRIES"));
1256    }
1257
1258    #[test]
1259    fn detect_generic_arity_type_expr_widest_wins() {
1260        // First usage with 1 param
1261        let te1 = assura_ast::TypeExpr::Generic(
1262            "Container".into(),
1263            vec![assura_ast::TypeExpr::Named("Foo".into())],
1264        );
1265        // Second usage with 2 params
1266        let te2 = assura_ast::TypeExpr::Generic(
1267            "Container".into(),
1268            vec![
1269                assura_ast::TypeExpr::Named("Foo".into()),
1270                assura_ast::TypeExpr::Named("Bar".into()),
1271            ],
1272        );
1273        let mut map = std::collections::HashMap::new();
1274        let mut consts = std::collections::HashSet::new();
1275        detect_generic_arity_from_type_expr(Some(&te1), &mut map, &mut consts);
1276        detect_generic_arity_from_type_expr(Some(&te2), &mut map, &mut consts);
1277        assert_eq!(map.get("Container").map(|v| v.len()), Some(2));
1278    }
1279
1280    // ---- detect_feature_max_as_type_from_type_expr ----
1281
1282    #[test]
1283    fn feature_max_as_type_expr_found() {
1284        let te = assura_ast::TypeExpr::Generic(
1285            "Buffer".into(),
1286            vec![assura_ast::TypeExpr::Named("MAX_BUF_SIZE".into())],
1287        );
1288        let fm_set: std::collections::HashSet<&str> = ["MAX_BUF_SIZE"].iter().copied().collect();
1289        let mut out = std::collections::HashSet::new();
1290        detect_feature_max_as_type_from_type_expr(Some(&te), &fm_set, &mut out);
1291        assert!(out.contains("MAX_BUF_SIZE"));
1292    }
1293
1294    #[test]
1295    fn feature_max_as_type_expr_not_in_generic() {
1296        // Named at top level (not inside Generic args) should NOT be found
1297        let te = assura_ast::TypeExpr::Named("MAX_BUF_SIZE".into());
1298        let fm_set: std::collections::HashSet<&str> = ["MAX_BUF_SIZE"].iter().copied().collect();
1299        let mut out = std::collections::HashSet::new();
1300        detect_feature_max_as_type_from_type_expr(Some(&te), &fm_set, &mut out);
1301        assert!(out.is_empty());
1302    }
1303
1304    #[test]
1305    fn feature_max_as_type_expr_nested() {
1306        // List<Buffer<MAX_SIZE>> should find MAX_SIZE
1307        let te = assura_ast::TypeExpr::Generic(
1308            "List".into(),
1309            vec![assura_ast::TypeExpr::Generic(
1310                "Buffer".into(),
1311                vec![assura_ast::TypeExpr::Named("MAX_SIZE".into())],
1312            )],
1313        );
1314        let fm_set: std::collections::HashSet<&str> = ["MAX_SIZE"].iter().copied().collect();
1315        let mut out = std::collections::HashSet::new();
1316        detect_feature_max_as_type_from_type_expr(Some(&te), &fm_set, &mut out);
1317        assert!(out.contains("MAX_SIZE"));
1318    }
1319
1320    // ---- qualifier stripping in map_type_tokens ----
1321
1322    #[test]
1323    fn map_type_tokens_strips_secret_qualifier() {
1324        let tokens: Vec<String> = vec!["secret".into(), "Int".into()];
1325        assert_eq!(map_type_tokens(&tokens), "i64");
1326    }
1327
1328    #[test]
1329    fn map_type_tokens_strips_tainted_qualifier() {
1330        let tokens: Vec<String> = vec!["tainted".into(), "String".into()];
1331        assert_eq!(map_type_tokens(&tokens), "String");
1332    }
1333
1334    #[test]
1335    fn map_type_tokens_strips_linear_qualifier() {
1336        let tokens: Vec<String> = vec!["linear".into(), "Int".into()];
1337        assert_eq!(map_type_tokens(&tokens), "i64");
1338    }
1339
1340    #[test]
1341    fn map_type_tokens_strips_untrusted_qualifier() {
1342        let tokens: Vec<String> = vec!["untrusted".into(), "Bytes".into()];
1343        assert_eq!(map_type_tokens(&tokens), "Vec<u8>");
1344    }
1345
1346    #[test]
1347    fn map_type_tokens_strips_validated_qualifier() {
1348        let tokens: Vec<String> = vec!["validated".into(), "String".into()];
1349        assert_eq!(map_type_tokens(&tokens), "String");
1350    }
1351}