1use 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 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 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 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 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 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
74pub 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 _ => tok,
105 }
106}
107
108pub fn map_type_tokens(tokens: &[String]) -> String {
117 if tokens.is_empty() {
118 return "()".to_string();
119 }
120
121 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 if clean.first() == Some(&"{") {
141 return extract_base_type_from_refined(tokens);
142 }
143
144 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 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 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
198pub(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
216pub(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
228pub(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#[derive(Debug, Clone)]
244pub(crate) enum GenericParamKind {
245 Type,
246 Const,
247}
248
249#[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 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 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
323pub(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#[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 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
407pub(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 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
449pub(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 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 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
508fn 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
519pub(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 if let assura_ast::TypeExpr::Named(n) = arg
531 && feature_max_set.contains(n.as_str())
532 {
533 out.insert(n.clone());
534 }
535 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
557pub(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 Expr::Literal(_) => {}
643 }
644}
645
646pub(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 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 return val_tokens[0].clone();
670 }
671 }
672 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 format!("compile_error!(\"feature_max `{name}` has no value\")")
684}
685
686pub(crate) fn extract_base_type_from_refined(tokens: &[String]) -> String {
691 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 after_colon = false;
704 }
705 }
706 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 #[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 #[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 #[test]
815 fn smart_join_no_space_after_open_angle() {
816 let tokens = vec!["Vec", "<", "i64", ">"];
817 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 #[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")); }
848
849 #[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 #[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")); }
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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")); }
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 #[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 let te1 = assura_ast::TypeExpr::Generic(
1262 "Container".into(),
1263 vec![assura_ast::TypeExpr::Named("Foo".into())],
1264 );
1265 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 #[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 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 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 #[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}