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