1mod control_flow;
75mod error;
76mod ffi_wrappers;
77mod globals;
78mod inline;
79mod platform;
80mod program;
81mod runtime;
82mod specialization;
83mod state;
84mod statements;
85mod types;
86mod virtual_stack;
87mod words;
88
89pub use error::CodeGenError;
91pub use platform::{ffi_c_args, ffi_return_type, get_target_triple};
92pub use runtime::{BUILTIN_SYMBOLS, RUNTIME_DECLARATIONS, emit_runtime_decls};
93pub use state::CodeGen;
94
95use state::{
97 BranchResult, MAX_VIRTUAL_STACK, QuotationFunctions, TailPosition, UNREACHABLE_PREDECESSOR,
98 VirtualValue, mangle_name,
99};
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::ast::{Program, Statement, WordDef};
105 use crate::config::CompilerConfig;
106 use std::collections::HashMap;
107
108 #[test]
109 fn test_codegen_hello_world() {
110 let mut codegen = CodeGen::new();
111
112 let program = Program {
113 includes: vec![],
114 unions: vec![],
115 words: vec![WordDef {
116 name: "main".to_string(),
117 effect: None,
118 body: vec![
119 Statement::StringLiteral("Hello, World!".to_string()),
120 Statement::WordCall {
121 name: "io.write-line".to_string(),
122 span: None,
123 },
124 ],
125 source: None,
126 allowed_lints: vec![],
127 }],
128 };
129
130 let ir = codegen
131 .codegen_program(&program, HashMap::new(), HashMap::new())
132 .unwrap();
133
134 assert!(ir.contains("define i32 @main(i32 %argc, ptr %argv)"));
135 assert!(ir.contains("define ptr @seq_main(ptr %stack)"));
137 assert!(ir.contains("call ptr @patch_seq_push_string"));
138 assert!(ir.contains("call ptr @patch_seq_write_line"));
139 assert!(ir.contains("\"Hello, World!\\00\""));
140 }
141
142 #[test]
143 fn test_codegen_io_write() {
144 let mut codegen = CodeGen::new();
146
147 let program = Program {
148 includes: vec![],
149 unions: vec![],
150 words: vec![WordDef {
151 name: "main".to_string(),
152 effect: None,
153 body: vec![
154 Statement::StringLiteral("no newline".to_string()),
155 Statement::WordCall {
156 name: "io.write".to_string(),
157 span: None,
158 },
159 ],
160 source: None,
161 allowed_lints: vec![],
162 }],
163 };
164
165 let ir = codegen
166 .codegen_program(&program, HashMap::new(), HashMap::new())
167 .unwrap();
168
169 assert!(ir.contains("call ptr @patch_seq_push_string"));
170 assert!(ir.contains("call ptr @patch_seq_write"));
171 assert!(ir.contains("\"no newline\\00\""));
172 }
173
174 #[test]
175 fn test_codegen_arithmetic() {
176 let mut codegen = CodeGen::new();
178
179 let program = Program {
180 includes: vec![],
181 unions: vec![],
182 words: vec![WordDef {
183 name: "main".to_string(),
184 effect: None,
185 body: vec![
186 Statement::IntLiteral(2),
187 Statement::IntLiteral(3),
188 Statement::WordCall {
189 name: "i.add".to_string(),
190 span: None,
191 },
192 ],
193 source: None,
194 allowed_lints: vec![],
195 }],
196 };
197
198 let ir = codegen
199 .codegen_program(&program, HashMap::new(), HashMap::new())
200 .unwrap();
201
202 assert!(ir.contains("add i64 0, 2"), "Should create SSA var for 2");
205 assert!(ir.contains("add i64 0, 3"), "Should create SSA var for 3");
206 assert!(ir.contains("add i64 %"), "Should add SSA variables");
208 }
209
210 #[test]
211 fn test_pure_inline_test_mode() {
212 let mut codegen = CodeGen::new_pure_inline_test();
213
214 let program = Program {
216 includes: vec![],
217 unions: vec![],
218 words: vec![WordDef {
219 name: "main".to_string(),
220 effect: None,
221 body: vec![
222 Statement::IntLiteral(5),
223 Statement::IntLiteral(3),
224 Statement::WordCall {
225 name: "i.add".to_string(),
226 span: None,
227 },
228 ],
229 source: None,
230 allowed_lints: vec![],
231 }],
232 };
233
234 let ir = codegen
235 .codegen_program(&program, HashMap::new(), HashMap::new())
236 .unwrap();
237
238 assert!(!ir.contains("call void @patch_seq_scheduler_init"));
241 assert!(!ir.contains("call i64 @patch_seq_strand_spawn"));
242
243 assert!(ir.contains("call ptr @seq_stack_new_default()"));
245 assert!(ir.contains("call ptr @seq_main(ptr %stack_base)"));
246
247 assert!(ir.contains("trunc i64 %result to i32"));
249 assert!(ir.contains("ret i32 %exit_code"));
250
251 assert!(!ir.contains("call ptr @patch_seq_push_int"));
253 assert!(ir.contains("add i64 0, 5"), "Should create SSA var for 5");
255 assert!(ir.contains("add i64 0, 3"), "Should create SSA var for 3");
256
257 assert!(!ir.contains("call ptr @patch_seq_add"));
259 assert!(ir.contains("add i64 %"), "Should add SSA variables");
260 }
261
262 #[test]
263 fn test_escape_llvm_string() {
264 assert_eq!(CodeGen::escape_llvm_string("hello").unwrap(), "hello");
265 assert_eq!(CodeGen::escape_llvm_string("a\nb").unwrap(), r"a\0Ab");
266 assert_eq!(CodeGen::escape_llvm_string("a\tb").unwrap(), r"a\09b");
267 assert_eq!(CodeGen::escape_llvm_string("a\"b").unwrap(), r"a\22b");
268 }
269
270 #[test]
271 #[allow(deprecated)] fn test_external_builtins_declared() {
273 use crate::config::{CompilerConfig, ExternalBuiltin};
274
275 let mut codegen = CodeGen::new();
276
277 let program = Program {
278 includes: vec![],
279 unions: vec![],
280 words: vec![WordDef {
281 name: "main".to_string(),
282 effect: None, body: vec![
284 Statement::IntLiteral(42),
285 Statement::WordCall {
286 name: "my-external-op".to_string(),
287 span: None,
288 },
289 ],
290 source: None,
291 allowed_lints: vec![],
292 }],
293 };
294
295 let config = CompilerConfig::new()
296 .with_builtin(ExternalBuiltin::new("my-external-op", "test_runtime_my_op"));
297
298 let ir = codegen
299 .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
300 .unwrap();
301
302 assert!(
304 ir.contains("declare ptr @test_runtime_my_op(ptr)"),
305 "IR should declare external builtin"
306 );
307
308 assert!(
310 ir.contains("call ptr @test_runtime_my_op"),
311 "IR should call external builtin"
312 );
313 }
314
315 #[test]
316 #[allow(deprecated)] fn test_multiple_external_builtins() {
318 use crate::config::{CompilerConfig, ExternalBuiltin};
319
320 let mut codegen = CodeGen::new();
321
322 let program = Program {
323 includes: vec![],
324 unions: vec![],
325 words: vec![WordDef {
326 name: "main".to_string(),
327 effect: None, body: vec![
329 Statement::WordCall {
330 name: "actor-self".to_string(),
331 span: None,
332 },
333 Statement::WordCall {
334 name: "journal-append".to_string(),
335 span: None,
336 },
337 ],
338 source: None,
339 allowed_lints: vec![],
340 }],
341 };
342
343 let config = CompilerConfig::new()
344 .with_builtin(ExternalBuiltin::new("actor-self", "seq_actors_self"))
345 .with_builtin(ExternalBuiltin::new(
346 "journal-append",
347 "seq_actors_journal_append",
348 ));
349
350 let ir = codegen
351 .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
352 .unwrap();
353
354 assert!(ir.contains("declare ptr @seq_actors_self(ptr)"));
356 assert!(ir.contains("declare ptr @seq_actors_journal_append(ptr)"));
357
358 assert!(ir.contains("call ptr @seq_actors_self"));
360 assert!(ir.contains("call ptr @seq_actors_journal_append"));
361 }
362
363 #[test]
364 #[allow(deprecated)] fn test_external_builtins_with_library_paths() {
366 use crate::config::{CompilerConfig, ExternalBuiltin};
367
368 let config = CompilerConfig::new()
369 .with_builtin(ExternalBuiltin::new("my-op", "runtime_my_op"))
370 .with_library_path("/custom/lib")
371 .with_library("myruntime");
372
373 assert_eq!(config.external_builtins.len(), 1);
374 assert_eq!(config.library_paths, vec!["/custom/lib"]);
375 assert_eq!(config.libraries, vec!["myruntime"]);
376 }
377
378 #[test]
379 fn test_external_builtin_full_pipeline() {
380 use crate::compile_to_ir_with_config;
383 use crate::config::{CompilerConfig, ExternalBuiltin};
384 use crate::types::{Effect, StackType, Type};
385
386 let source = r#"
387 : main ( -- Int )
388 42 my-transform
389 0
390 ;
391 "#;
392
393 let effect = Effect::new(StackType::singleton(Type::Int), StackType::Empty);
395 let config = CompilerConfig::new().with_builtin(ExternalBuiltin::with_effect(
396 "my-transform",
397 "ext_runtime_transform",
398 effect,
399 ));
400
401 let result = compile_to_ir_with_config(source, &config);
403 assert!(
404 result.is_ok(),
405 "Compilation should succeed: {:?}",
406 result.err()
407 );
408
409 let ir = result.unwrap();
410 assert!(ir.contains("declare ptr @ext_runtime_transform(ptr)"));
411 assert!(ir.contains("call ptr @ext_runtime_transform"));
412 }
413
414 #[test]
415 fn test_external_builtin_without_config_fails() {
416 use crate::compile_to_ir;
418
419 let source = r#"
420 : main ( -- Int )
421 42 unknown-builtin
422 0
423 ;
424 "#;
425
426 let result = compile_to_ir(source);
428 assert!(result.is_err());
429 assert!(result.unwrap_err().contains("unknown-builtin"));
430 }
431
432 #[test]
433 fn test_match_exhaustiveness_error() {
434 use crate::compile_to_ir;
435
436 let source = r#"
437 union Result { Ok { value: Int } Err { msg: String } }
438
439 : handle ( Variant -- Int )
440 match
441 Ok -> drop 1
442 # Missing Err arm!
443 end
444 ;
445
446 : main ( -- ) 42 Make-Ok handle drop ;
447 "#;
448
449 let result = compile_to_ir(source);
450 assert!(result.is_err());
451 let err = result.unwrap_err();
452 assert!(err.contains("Non-exhaustive match"));
453 assert!(err.contains("Result"));
454 assert!(err.contains("Err"));
455 }
456
457 #[test]
458 fn test_match_exhaustive_compiles() {
459 use crate::compile_to_ir;
460
461 let source = r#"
462 union Result { Ok { value: Int } Err { msg: String } }
463
464 : handle ( Variant -- Int )
465 match
466 Ok -> drop 1
467 Err -> drop 0
468 end
469 ;
470
471 : main ( -- ) 42 Make-Ok handle drop ;
472 "#;
473
474 let result = compile_to_ir(source);
475 assert!(
476 result.is_ok(),
477 "Exhaustive match should compile: {:?}",
478 result
479 );
480 }
481
482 #[test]
483 fn test_codegen_symbol() {
484 let mut codegen = CodeGen::new();
486
487 let program = Program {
488 includes: vec![],
489 unions: vec![],
490 words: vec![WordDef {
491 name: "main".to_string(),
492 effect: None,
493 body: vec![
494 Statement::Symbol("hello".to_string()),
495 Statement::WordCall {
496 name: "symbol->string".to_string(),
497 span: None,
498 },
499 Statement::WordCall {
500 name: "io.write-line".to_string(),
501 span: None,
502 },
503 ],
504 source: None,
505 allowed_lints: vec![],
506 }],
507 };
508
509 let ir = codegen
510 .codegen_program(&program, HashMap::new(), HashMap::new())
511 .unwrap();
512
513 assert!(ir.contains("call ptr @patch_seq_push_interned_symbol"));
514 assert!(ir.contains("call ptr @patch_seq_symbol_to_string"));
515 assert!(ir.contains("\"hello\\00\""));
516 }
517
518 #[test]
519 fn test_symbol_interning_dedup() {
520 let mut codegen = CodeGen::new();
522
523 let program = Program {
524 includes: vec![],
525 unions: vec![],
526 words: vec![WordDef {
527 name: "main".to_string(),
528 effect: None,
529 body: vec![
530 Statement::Symbol("hello".to_string()),
532 Statement::Symbol("hello".to_string()),
533 Statement::Symbol("world".to_string()), ],
535 source: None,
536 allowed_lints: vec![],
537 }],
538 };
539
540 let ir = codegen
541 .codegen_program(&program, HashMap::new(), HashMap::new())
542 .unwrap();
543
544 let sym_defs: Vec<_> = ir
547 .lines()
548 .filter(|l| l.trim().starts_with("@.sym."))
549 .collect();
550
551 assert_eq!(
553 sym_defs.len(),
554 2,
555 "Expected 2 symbol globals, got: {:?}",
556 sym_defs
557 );
558
559 let hello_uses: usize = ir.matches("@.sym.0").count();
561 assert_eq!(
562 hello_uses, 3,
563 "Expected 3 occurrences of .sym.0 (1 def + 2 uses)"
564 );
565
566 assert!(
568 ir.contains("i64 0, i8 1"),
569 "Symbol global should have capacity=0 and global=1"
570 );
571 }
572
573 #[test]
574 fn test_dup_optimization_for_int() {
575 let mut codegen = CodeGen::new();
578
579 use crate::types::Type;
580
581 let program = Program {
582 includes: vec![],
583 unions: vec![],
584 words: vec![
585 WordDef {
586 name: "test_dup".to_string(),
587 effect: None,
588 body: vec![
589 Statement::IntLiteral(42), Statement::WordCall {
591 name: "dup".to_string(),
593 span: None,
594 },
595 Statement::WordCall {
596 name: "drop".to_string(),
597 span: None,
598 },
599 Statement::WordCall {
600 name: "drop".to_string(),
601 span: None,
602 },
603 ],
604 source: None,
605 allowed_lints: vec![],
606 },
607 WordDef {
608 name: "main".to_string(),
609 effect: None,
610 body: vec![Statement::WordCall {
611 name: "test_dup".to_string(),
612 span: None,
613 }],
614 source: None,
615 allowed_lints: vec![],
616 },
617 ],
618 };
619
620 let mut statement_types = HashMap::new();
622 statement_types.insert(("test_dup".to_string(), 1), Type::Int);
623
624 let ir = codegen
625 .codegen_program(&program, HashMap::new(), statement_types)
626 .unwrap();
627
628 let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
630 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
631 let test_dup_fn = &ir[func_start..func_end];
632
633 assert!(
635 test_dup_fn.contains("load %Value"),
636 "Optimized dup should use 'load %Value', got:\n{}",
637 test_dup_fn
638 );
639 assert!(
640 test_dup_fn.contains("store %Value"),
641 "Optimized dup should use 'store %Value', got:\n{}",
642 test_dup_fn
643 );
644
645 assert!(
647 !test_dup_fn.contains("@patch_seq_clone_value"),
648 "Optimized dup should NOT call clone_value for Int, got:\n{}",
649 test_dup_fn
650 );
651 }
652
653 #[test]
654 fn test_dup_optimization_after_literal() {
655 let mut codegen = CodeGen::new();
658
659 let program = Program {
660 includes: vec![],
661 unions: vec![],
662 words: vec![
663 WordDef {
664 name: "test_dup".to_string(),
665 effect: None,
666 body: vec![
667 Statement::IntLiteral(42), Statement::WordCall {
669 name: "dup".to_string(),
671 span: None,
672 },
673 Statement::WordCall {
674 name: "drop".to_string(),
675 span: None,
676 },
677 Statement::WordCall {
678 name: "drop".to_string(),
679 span: None,
680 },
681 ],
682 source: None,
683 allowed_lints: vec![],
684 },
685 WordDef {
686 name: "main".to_string(),
687 effect: None,
688 body: vec![Statement::WordCall {
689 name: "test_dup".to_string(),
690 span: None,
691 }],
692 source: None,
693 allowed_lints: vec![],
694 },
695 ],
696 };
697
698 let ir = codegen
700 .codegen_program(&program, HashMap::new(), HashMap::new())
701 .unwrap();
702
703 let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
705 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
706 let test_dup_fn = &ir[func_start..func_end];
707
708 assert!(
710 test_dup_fn.contains("load %Value"),
711 "Dup after int literal should use optimized load, got:\n{}",
712 test_dup_fn
713 );
714 assert!(
715 test_dup_fn.contains("store %Value"),
716 "Dup after int literal should use optimized store, got:\n{}",
717 test_dup_fn
718 );
719 assert!(
720 !test_dup_fn.contains("@patch_seq_clone_value"),
721 "Dup after int literal should NOT call clone_value, got:\n{}",
722 test_dup_fn
723 );
724 }
725
726 #[test]
727 fn test_dup_no_optimization_after_word_call() {
728 let mut codegen = CodeGen::new();
730
731 let program = Program {
732 includes: vec![],
733 unions: vec![],
734 words: vec![
735 WordDef {
736 name: "get_value".to_string(),
737 effect: None,
738 body: vec![Statement::IntLiteral(42)],
739 source: None,
740 allowed_lints: vec![],
741 },
742 WordDef {
743 name: "test_dup".to_string(),
744 effect: None,
745 body: vec![
746 Statement::WordCall {
747 name: "get_value".to_string(),
749 span: None,
750 },
751 Statement::WordCall {
752 name: "dup".to_string(),
754 span: None,
755 },
756 Statement::WordCall {
757 name: "drop".to_string(),
758 span: None,
759 },
760 Statement::WordCall {
761 name: "drop".to_string(),
762 span: None,
763 },
764 ],
765 source: None,
766 allowed_lints: vec![],
767 },
768 WordDef {
769 name: "main".to_string(),
770 effect: None,
771 body: vec![Statement::WordCall {
772 name: "test_dup".to_string(),
773 span: None,
774 }],
775 source: None,
776 allowed_lints: vec![],
777 },
778 ],
779 };
780
781 let ir = codegen
783 .codegen_program(&program, HashMap::new(), HashMap::new())
784 .unwrap();
785
786 let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
788 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
789 let test_dup_fn = &ir[func_start..func_end];
790
791 assert!(
793 test_dup_fn.contains("@patch_seq_clone_value"),
794 "Dup after word call should call clone_value, got:\n{}",
795 test_dup_fn
796 );
797 }
798
799 #[test]
800 fn test_roll_constant_optimization() {
801 let mut codegen = CodeGen::new();
804
805 let program = Program {
806 includes: vec![],
807 unions: vec![],
808 words: vec![
809 WordDef {
810 name: "test_roll".to_string(),
811 effect: None,
812 body: vec![
813 Statement::IntLiteral(1),
814 Statement::IntLiteral(2),
815 Statement::IntLiteral(3),
816 Statement::IntLiteral(2), Statement::WordCall {
818 name: "roll".to_string(),
820 span: None,
821 },
822 Statement::WordCall {
823 name: "drop".to_string(),
824 span: None,
825 },
826 Statement::WordCall {
827 name: "drop".to_string(),
828 span: None,
829 },
830 Statement::WordCall {
831 name: "drop".to_string(),
832 span: None,
833 },
834 ],
835 source: None,
836 allowed_lints: vec![],
837 },
838 WordDef {
839 name: "main".to_string(),
840 effect: None,
841 body: vec![Statement::WordCall {
842 name: "test_roll".to_string(),
843 span: None,
844 }],
845 source: None,
846 allowed_lints: vec![],
847 },
848 ],
849 };
850
851 let ir = codegen
852 .codegen_program(&program, HashMap::new(), HashMap::new())
853 .unwrap();
854
855 let func_start = ir.find("define tailcc ptr @seq_test_roll").unwrap();
857 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
858 let test_roll_fn = &ir[func_start..func_end];
859
860 assert!(
863 !test_roll_fn.contains("= add i64 %"),
864 "Constant roll should use constant offset, not dynamic add, got:\n{}",
865 test_roll_fn
866 );
867
868 assert!(
870 !test_roll_fn.contains("@llvm.memmove"),
871 "2 roll should not use memmove, got:\n{}",
872 test_roll_fn
873 );
874 }
875
876 #[test]
877 fn test_pick_constant_optimization() {
878 let mut codegen = CodeGen::new();
881
882 let program = Program {
883 includes: vec![],
884 unions: vec![],
885 words: vec![
886 WordDef {
887 name: "test_pick".to_string(),
888 effect: None,
889 body: vec![
890 Statement::IntLiteral(10),
891 Statement::IntLiteral(20),
892 Statement::IntLiteral(1), Statement::WordCall {
894 name: "pick".to_string(),
896 span: None,
897 },
898 Statement::WordCall {
899 name: "drop".to_string(),
900 span: None,
901 },
902 Statement::WordCall {
903 name: "drop".to_string(),
904 span: None,
905 },
906 Statement::WordCall {
907 name: "drop".to_string(),
908 span: None,
909 },
910 ],
911 source: None,
912 allowed_lints: vec![],
913 },
914 WordDef {
915 name: "main".to_string(),
916 effect: None,
917 body: vec![Statement::WordCall {
918 name: "test_pick".to_string(),
919 span: None,
920 }],
921 source: None,
922 allowed_lints: vec![],
923 },
924 ],
925 };
926
927 let ir = codegen
928 .codegen_program(&program, HashMap::new(), HashMap::new())
929 .unwrap();
930
931 let func_start = ir.find("define tailcc ptr @seq_test_pick").unwrap();
933 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
934 let test_pick_fn = &ir[func_start..func_end];
935
936 assert!(
939 !test_pick_fn.contains("= add i64 %"),
940 "Constant pick should use constant offset, not dynamic add, got:\n{}",
941 test_pick_fn
942 );
943
944 assert!(
946 test_pick_fn.contains("i64 -3"),
947 "1 pick should use offset -3 (-(1+2)), got:\n{}",
948 test_pick_fn
949 );
950 }
951
952 #[test]
953 fn test_small_word_marked_alwaysinline() {
954 let mut codegen = CodeGen::new();
956
957 let program = Program {
958 includes: vec![],
959 unions: vec![],
960 words: vec![
961 WordDef {
962 name: "double".to_string(), effect: None,
964 body: vec![
965 Statement::WordCall {
966 name: "dup".to_string(),
967 span: None,
968 },
969 Statement::WordCall {
970 name: "i.+".to_string(),
971 span: None,
972 },
973 ],
974 source: None,
975 allowed_lints: vec![],
976 },
977 WordDef {
978 name: "main".to_string(),
979 effect: None,
980 body: vec![
981 Statement::IntLiteral(21),
982 Statement::WordCall {
983 name: "double".to_string(),
984 span: None,
985 },
986 ],
987 source: None,
988 allowed_lints: vec![],
989 },
990 ],
991 };
992
993 let ir = codegen
994 .codegen_program(&program, HashMap::new(), HashMap::new())
995 .unwrap();
996
997 assert!(
999 ir.contains("define tailcc ptr @seq_double(ptr %stack) alwaysinline"),
1000 "Small word should have alwaysinline attribute, got:\n{}",
1001 ir.lines()
1002 .filter(|l| l.contains("define"))
1003 .collect::<Vec<_>>()
1004 .join("\n")
1005 );
1006
1007 assert!(
1009 ir.contains("define ptr @seq_main(ptr %stack) {"),
1010 "main should not have alwaysinline, got:\n{}",
1011 ir.lines()
1012 .filter(|l| l.contains("define"))
1013 .collect::<Vec<_>>()
1014 .join("\n")
1015 );
1016 }
1017
1018 #[test]
1019 fn test_recursive_word_not_inlined() {
1020 let mut codegen = CodeGen::new();
1022
1023 let program = Program {
1024 includes: vec![],
1025 unions: vec![],
1026 words: vec![
1027 WordDef {
1028 name: "countdown".to_string(), effect: None,
1030 body: vec![
1031 Statement::WordCall {
1032 name: "dup".to_string(),
1033 span: None,
1034 },
1035 Statement::If {
1036 then_branch: vec![
1037 Statement::IntLiteral(1),
1038 Statement::WordCall {
1039 name: "i.-".to_string(),
1040 span: None,
1041 },
1042 Statement::WordCall {
1043 name: "countdown".to_string(), span: None,
1045 },
1046 ],
1047 else_branch: Some(vec![]),
1048 span: None,
1049 },
1050 ],
1051 source: None,
1052 allowed_lints: vec![],
1053 },
1054 WordDef {
1055 name: "main".to_string(),
1056 effect: None,
1057 body: vec![
1058 Statement::IntLiteral(5),
1059 Statement::WordCall {
1060 name: "countdown".to_string(),
1061 span: None,
1062 },
1063 ],
1064 source: None,
1065 allowed_lints: vec![],
1066 },
1067 ],
1068 };
1069
1070 let ir = codegen
1071 .codegen_program(&program, HashMap::new(), HashMap::new())
1072 .unwrap();
1073
1074 assert!(
1076 ir.contains("define tailcc ptr @seq_countdown(ptr %stack) {"),
1077 "Recursive word should NOT have alwaysinline, got:\n{}",
1078 ir.lines()
1079 .filter(|l| l.contains("define"))
1080 .collect::<Vec<_>>()
1081 .join("\n")
1082 );
1083 }
1084
1085 #[test]
1086 fn test_recursive_word_in_match_not_inlined() {
1087 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
1089
1090 let mut codegen = CodeGen::new();
1091
1092 let program = Program {
1093 includes: vec![],
1094 unions: vec![UnionDef {
1095 name: "Option".to_string(),
1096 variants: vec![
1097 UnionVariant {
1098 name: "Some".to_string(),
1099 fields: vec![],
1100 source: None,
1101 },
1102 UnionVariant {
1103 name: "None".to_string(),
1104 fields: vec![],
1105 source: None,
1106 },
1107 ],
1108 source: None,
1109 }],
1110 words: vec![
1111 WordDef {
1112 name: "process".to_string(), effect: None,
1114 body: vec![Statement::Match {
1115 arms: vec![
1116 MatchArm {
1117 pattern: Pattern::Variant("Some".to_string()),
1118 body: vec![Statement::WordCall {
1119 name: "process".to_string(), span: None,
1121 }],
1122 span: None,
1123 },
1124 MatchArm {
1125 pattern: Pattern::Variant("None".to_string()),
1126 body: vec![],
1127 span: None,
1128 },
1129 ],
1130 span: None,
1131 }],
1132 source: None,
1133 allowed_lints: vec![],
1134 },
1135 WordDef {
1136 name: "main".to_string(),
1137 effect: None,
1138 body: vec![Statement::WordCall {
1139 name: "process".to_string(),
1140 span: None,
1141 }],
1142 source: None,
1143 allowed_lints: vec![],
1144 },
1145 ],
1146 };
1147
1148 let ir = codegen
1149 .codegen_program(&program, HashMap::new(), HashMap::new())
1150 .unwrap();
1151
1152 assert!(
1154 ir.contains("define tailcc ptr @seq_process(ptr %stack) {"),
1155 "Recursive word in match should NOT have alwaysinline, got:\n{}",
1156 ir.lines()
1157 .filter(|l| l.contains("define"))
1158 .collect::<Vec<_>>()
1159 .join("\n")
1160 );
1161 }
1162
1163 #[test]
1164 fn test_issue_338_specialized_call_in_if_branch_has_terminator() {
1165 use crate::types::{Effect, StackType, Type};
1173
1174 let mut codegen = CodeGen::new();
1175
1176 let get_value_effect = Effect {
1179 inputs: StackType::Cons {
1180 rest: Box::new(StackType::RowVar("S".to_string())),
1181 top: Type::Int,
1182 },
1183 outputs: StackType::Cons {
1184 rest: Box::new(StackType::RowVar("S".to_string())),
1185 top: Type::Int,
1186 },
1187 effects: vec![],
1188 };
1189
1190 let program = Program {
1193 includes: vec![],
1194 unions: vec![],
1195 words: vec![
1196 WordDef {
1198 name: "get-value".to_string(),
1199 effect: Some(get_value_effect),
1200 body: vec![Statement::WordCall {
1201 name: "dup".to_string(),
1202 span: None,
1203 }],
1204 source: None,
1205 allowed_lints: vec![],
1206 },
1207 WordDef {
1210 name: "test-caller".to_string(),
1211 effect: None,
1212 body: vec![Statement::If {
1213 then_branch: vec![Statement::WordCall {
1214 name: "get-value".to_string(),
1215 span: None,
1216 }],
1217 else_branch: Some(vec![
1218 Statement::WordCall {
1219 name: "drop".to_string(),
1220 span: None,
1221 },
1222 Statement::IntLiteral(0),
1223 ]),
1224 span: None,
1225 }],
1226 source: None,
1227 allowed_lints: vec![],
1228 },
1229 WordDef {
1231 name: "main".to_string(),
1232 effect: None,
1233 body: vec![
1234 Statement::BoolLiteral(true),
1235 Statement::IntLiteral(42),
1236 Statement::WordCall {
1237 name: "test-caller".to_string(),
1238 span: None,
1239 },
1240 Statement::WordCall {
1241 name: "drop".to_string(),
1242 span: None,
1243 },
1244 ],
1245 source: None,
1246 allowed_lints: vec![],
1247 },
1248 ],
1249 };
1250
1251 let ir = codegen
1253 .codegen_program(&program, HashMap::new(), HashMap::new())
1254 .expect("Issue #338: codegen should succeed for specialized call in if branch");
1255
1256 assert!(
1258 ir.contains("@seq_get_value_i64"),
1259 "Should generate specialized version of get-value"
1260 );
1261
1262 assert!(
1265 ir.contains("define tailcc ptr @seq_test_caller"),
1266 "Should generate test-caller function"
1267 );
1268
1269 assert!(
1272 ir.contains("musttail call tailcc ptr @seq_get_value"),
1273 "Then branch should use tail call to stack-based version, not specialized dispatch"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_report_call_in_normal_mode() {
1279 let mut codegen = CodeGen::new();
1280 let program = Program {
1281 includes: vec![],
1282 unions: vec![],
1283 words: vec![WordDef {
1284 name: "main".to_string(),
1285 effect: None,
1286 body: vec![
1287 Statement::IntLiteral(42),
1288 Statement::WordCall {
1289 name: "io.write-line".to_string(),
1290 span: None,
1291 },
1292 ],
1293 source: None,
1294 allowed_lints: vec![],
1295 }],
1296 };
1297
1298 let ir = codegen
1299 .codegen_program(&program, HashMap::new(), HashMap::new())
1300 .unwrap();
1301
1302 assert!(
1304 ir.contains("call void @patch_seq_report()"),
1305 "Normal mode should emit report call"
1306 );
1307 }
1308
1309 #[test]
1310 fn test_report_call_absent_in_pure_inline() {
1311 let mut codegen = CodeGen::new_pure_inline_test();
1312 let program = Program {
1313 includes: vec![],
1314 unions: vec![],
1315 words: vec![WordDef {
1316 name: "main".to_string(),
1317 effect: None,
1318 body: vec![Statement::IntLiteral(42)],
1319 source: None,
1320 allowed_lints: vec![],
1321 }],
1322 };
1323
1324 let ir = codegen
1325 .codegen_program(&program, HashMap::new(), HashMap::new())
1326 .unwrap();
1327
1328 assert!(
1330 !ir.contains("call void @patch_seq_report()"),
1331 "Pure inline mode should not emit report call"
1332 );
1333 }
1334
1335 #[test]
1336 fn test_instrument_emits_counters_and_atomicrmw() {
1337 let mut codegen = CodeGen::new();
1338 let program = Program {
1339 includes: vec![],
1340 unions: vec![],
1341 words: vec![
1342 WordDef {
1343 name: "helper".to_string(),
1344 effect: None,
1345 body: vec![Statement::IntLiteral(1)],
1346 source: None,
1347 allowed_lints: vec![],
1348 },
1349 WordDef {
1350 name: "main".to_string(),
1351 effect: None,
1352 body: vec![Statement::WordCall {
1353 name: "helper".to_string(),
1354 span: None,
1355 }],
1356 source: None,
1357 allowed_lints: vec![],
1358 },
1359 ],
1360 };
1361
1362 let config = CompilerConfig {
1363 instrument: true,
1364 ..CompilerConfig::default()
1365 };
1366
1367 let ir = codegen
1368 .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
1369 .unwrap();
1370
1371 assert!(
1373 ir.contains("@seq_word_counters = global [2 x i64] zeroinitializer"),
1374 "Should emit counter array for 2 words"
1375 );
1376
1377 assert!(
1379 ir.contains("@seq_word_name_"),
1380 "Should emit word name constants"
1381 );
1382
1383 assert!(
1385 ir.contains("@seq_word_names = private constant [2 x ptr]"),
1386 "Should emit name pointer table"
1387 );
1388
1389 assert!(
1391 ir.contains("atomicrmw add ptr %instr_ptr_"),
1392 "Should emit atomicrmw add for word counters"
1393 );
1394
1395 assert!(
1397 ir.contains("call void @patch_seq_report_init(ptr @seq_word_counters, ptr @seq_word_names, i64 2)"),
1398 "Should emit report_init call with correct count"
1399 );
1400 }
1401
1402 #[test]
1403 fn test_no_instrument_no_counters() {
1404 let mut codegen = CodeGen::new();
1405 let program = Program {
1406 includes: vec![],
1407 unions: vec![],
1408 words: vec![WordDef {
1409 name: "main".to_string(),
1410 effect: None,
1411 body: vec![Statement::IntLiteral(42)],
1412 source: None,
1413 allowed_lints: vec![],
1414 }],
1415 };
1416
1417 let config = CompilerConfig::default();
1418 assert!(!config.instrument);
1419
1420 let ir = codegen
1421 .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
1422 .unwrap();
1423
1424 assert!(
1426 !ir.contains("@seq_word_counters"),
1427 "Should not emit counters when instrument=false"
1428 );
1429
1430 assert!(
1432 !ir.contains("atomicrmw"),
1433 "Should not emit atomicrmw when instrument=false"
1434 );
1435
1436 assert!(
1438 !ir.contains("call void @patch_seq_report_init"),
1439 "Should not emit report_init when instrument=false"
1440 );
1441 }
1442}