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
585 use crate::types::Type;
586
587 let program = Program {
588 includes: vec![],
589 unions: vec![],
590 words: vec![
591 WordDef {
592 name: "test_dup".to_string(),
593 effect: None,
594 body: vec![
595 Statement::IntLiteral(42), Statement::WordCall {
597 name: "dup".to_string(),
599 span: None,
600 },
601 Statement::WordCall {
602 name: "drop".to_string(),
603 span: None,
604 },
605 Statement::WordCall {
606 name: "drop".to_string(),
607 span: None,
608 },
609 ],
610 source: None,
611 allowed_lints: vec![],
612 },
613 WordDef {
614 name: "main".to_string(),
615 effect: None,
616 body: vec![Statement::WordCall {
617 name: "test_dup".to_string(),
618 span: None,
619 }],
620 source: None,
621 allowed_lints: vec![],
622 },
623 ],
624 };
625
626 let mut statement_types = HashMap::new();
628 statement_types.insert(("test_dup".to_string(), 1), Type::Int);
629
630 let ir = codegen
631 .codegen_program(&program, HashMap::new(), statement_types)
632 .unwrap();
633
634 let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
636 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
637 let test_dup_fn = &ir[func_start..func_end];
638
639 assert!(
641 test_dup_fn.contains("load i64"),
642 "Optimized dup should use 'load i64', got:\n{}",
643 test_dup_fn
644 );
645 assert!(
646 test_dup_fn.contains("store i64"),
647 "Optimized dup should use 'store i64', got:\n{}",
648 test_dup_fn
649 );
650
651 assert!(
653 !test_dup_fn.contains("@patch_seq_clone_value"),
654 "Optimized dup should NOT call clone_value for Int, got:\n{}",
655 test_dup_fn
656 );
657 }
658
659 #[test]
660 fn test_dup_optimization_after_literal() {
661 let mut codegen = CodeGen::new();
664
665 let program = Program {
666 includes: vec![],
667 unions: vec![],
668 words: vec![
669 WordDef {
670 name: "test_dup".to_string(),
671 effect: None,
672 body: vec![
673 Statement::IntLiteral(42), Statement::WordCall {
675 name: "dup".to_string(),
677 span: None,
678 },
679 Statement::WordCall {
680 name: "drop".to_string(),
681 span: None,
682 },
683 Statement::WordCall {
684 name: "drop".to_string(),
685 span: None,
686 },
687 ],
688 source: None,
689 allowed_lints: vec![],
690 },
691 WordDef {
692 name: "main".to_string(),
693 effect: None,
694 body: vec![Statement::WordCall {
695 name: "test_dup".to_string(),
696 span: None,
697 }],
698 source: None,
699 allowed_lints: vec![],
700 },
701 ],
702 };
703
704 let ir = codegen
706 .codegen_program(&program, HashMap::new(), HashMap::new())
707 .unwrap();
708
709 let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
711 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
712 let test_dup_fn = &ir[func_start..func_end];
713
714 assert!(
716 test_dup_fn.contains("load i64"),
717 "Dup after int literal should use optimized load, got:\n{}",
718 test_dup_fn
719 );
720 assert!(
721 test_dup_fn.contains("store i64"),
722 "Dup after int literal should use optimized store, got:\n{}",
723 test_dup_fn
724 );
725 assert!(
726 !test_dup_fn.contains("@patch_seq_clone_value"),
727 "Dup after int literal should NOT call clone_value, got:\n{}",
728 test_dup_fn
729 );
730 }
731
732 #[test]
733 fn test_dup_no_optimization_after_word_call() {
734 let mut codegen = CodeGen::new();
736
737 let program = Program {
738 includes: vec![],
739 unions: vec![],
740 words: vec![
741 WordDef {
742 name: "get_value".to_string(),
743 effect: None,
744 body: vec![Statement::IntLiteral(42)],
745 source: None,
746 allowed_lints: vec![],
747 },
748 WordDef {
749 name: "test_dup".to_string(),
750 effect: None,
751 body: vec![
752 Statement::WordCall {
753 name: "get_value".to_string(),
755 span: None,
756 },
757 Statement::WordCall {
758 name: "dup".to_string(),
760 span: None,
761 },
762 Statement::WordCall {
763 name: "drop".to_string(),
764 span: None,
765 },
766 Statement::WordCall {
767 name: "drop".to_string(),
768 span: None,
769 },
770 ],
771 source: None,
772 allowed_lints: vec![],
773 },
774 WordDef {
775 name: "main".to_string(),
776 effect: None,
777 body: vec![Statement::WordCall {
778 name: "test_dup".to_string(),
779 span: None,
780 }],
781 source: None,
782 allowed_lints: vec![],
783 },
784 ],
785 };
786
787 let ir = codegen
789 .codegen_program(&program, HashMap::new(), HashMap::new())
790 .unwrap();
791
792 let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
794 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
795 let test_dup_fn = &ir[func_start..func_end];
796
797 assert!(
799 test_dup_fn.contains("@patch_seq_clone_value"),
800 "Dup after word call should call clone_value, got:\n{}",
801 test_dup_fn
802 );
803 }
804
805 #[test]
806 fn test_roll_constant_optimization() {
807 let mut codegen = CodeGen::new();
810
811 let program = Program {
812 includes: vec![],
813 unions: vec![],
814 words: vec![
815 WordDef {
816 name: "test_roll".to_string(),
817 effect: None,
818 body: vec![
819 Statement::IntLiteral(1),
820 Statement::IntLiteral(2),
821 Statement::IntLiteral(3),
822 Statement::IntLiteral(2), Statement::WordCall {
824 name: "roll".to_string(),
826 span: None,
827 },
828 Statement::WordCall {
829 name: "drop".to_string(),
830 span: None,
831 },
832 Statement::WordCall {
833 name: "drop".to_string(),
834 span: None,
835 },
836 Statement::WordCall {
837 name: "drop".to_string(),
838 span: None,
839 },
840 ],
841 source: None,
842 allowed_lints: vec![],
843 },
844 WordDef {
845 name: "main".to_string(),
846 effect: None,
847 body: vec![Statement::WordCall {
848 name: "test_roll".to_string(),
849 span: None,
850 }],
851 source: None,
852 allowed_lints: vec![],
853 },
854 ],
855 };
856
857 let ir = codegen
858 .codegen_program(&program, HashMap::new(), HashMap::new())
859 .unwrap();
860
861 let func_start = ir.find("define tailcc ptr @seq_test_roll").unwrap();
863 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
864 let test_roll_fn = &ir[func_start..func_end];
865
866 assert!(
869 !test_roll_fn.contains("= add i64 %"),
870 "Constant roll should use constant offset, not dynamic add, got:\n{}",
871 test_roll_fn
872 );
873
874 assert!(
876 !test_roll_fn.contains("@llvm.memmove"),
877 "2 roll should not use memmove, got:\n{}",
878 test_roll_fn
879 );
880 }
881
882 #[test]
883 fn test_pick_constant_optimization() {
884 let mut codegen = CodeGen::new();
887
888 let program = Program {
889 includes: vec![],
890 unions: vec![],
891 words: vec![
892 WordDef {
893 name: "test_pick".to_string(),
894 effect: None,
895 body: vec![
896 Statement::IntLiteral(10),
897 Statement::IntLiteral(20),
898 Statement::IntLiteral(1), Statement::WordCall {
900 name: "pick".to_string(),
902 span: None,
903 },
904 Statement::WordCall {
905 name: "drop".to_string(),
906 span: None,
907 },
908 Statement::WordCall {
909 name: "drop".to_string(),
910 span: None,
911 },
912 Statement::WordCall {
913 name: "drop".to_string(),
914 span: None,
915 },
916 ],
917 source: None,
918 allowed_lints: vec![],
919 },
920 WordDef {
921 name: "main".to_string(),
922 effect: None,
923 body: vec![Statement::WordCall {
924 name: "test_pick".to_string(),
925 span: None,
926 }],
927 source: None,
928 allowed_lints: vec![],
929 },
930 ],
931 };
932
933 let ir = codegen
934 .codegen_program(&program, HashMap::new(), HashMap::new())
935 .unwrap();
936
937 let func_start = ir.find("define tailcc ptr @seq_test_pick").unwrap();
939 let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
940 let test_pick_fn = &ir[func_start..func_end];
941
942 assert!(
945 !test_pick_fn.contains("= add i64 %"),
946 "Constant pick should use constant offset, not dynamic add, got:\n{}",
947 test_pick_fn
948 );
949
950 assert!(
952 test_pick_fn.contains("i64 -3"),
953 "1 pick should use offset -3 (-(1+2)), got:\n{}",
954 test_pick_fn
955 );
956 }
957
958 #[test]
959 fn test_small_word_marked_alwaysinline() {
960 let mut codegen = CodeGen::new();
962
963 let program = Program {
964 includes: vec![],
965 unions: vec![],
966 words: vec![
967 WordDef {
968 name: "double".to_string(), effect: None,
970 body: vec![
971 Statement::WordCall {
972 name: "dup".to_string(),
973 span: None,
974 },
975 Statement::WordCall {
976 name: "i.+".to_string(),
977 span: None,
978 },
979 ],
980 source: None,
981 allowed_lints: vec![],
982 },
983 WordDef {
984 name: "main".to_string(),
985 effect: None,
986 body: vec![
987 Statement::IntLiteral(21),
988 Statement::WordCall {
989 name: "double".to_string(),
990 span: None,
991 },
992 ],
993 source: None,
994 allowed_lints: vec![],
995 },
996 ],
997 };
998
999 let ir = codegen
1000 .codegen_program(&program, HashMap::new(), HashMap::new())
1001 .unwrap();
1002
1003 assert!(
1005 ir.contains("define tailcc ptr @seq_double(ptr %stack) alwaysinline"),
1006 "Small word should have alwaysinline attribute, got:\n{}",
1007 ir.lines()
1008 .filter(|l| l.contains("define"))
1009 .collect::<Vec<_>>()
1010 .join("\n")
1011 );
1012
1013 assert!(
1015 ir.contains("define ptr @seq_main(ptr %stack) {"),
1016 "main should not have alwaysinline, got:\n{}",
1017 ir.lines()
1018 .filter(|l| l.contains("define"))
1019 .collect::<Vec<_>>()
1020 .join("\n")
1021 );
1022 }
1023
1024 #[test]
1025 fn test_recursive_word_not_inlined() {
1026 let mut codegen = CodeGen::new();
1028
1029 let program = Program {
1030 includes: vec![],
1031 unions: vec![],
1032 words: vec![
1033 WordDef {
1034 name: "countdown".to_string(), effect: None,
1036 body: vec![
1037 Statement::WordCall {
1038 name: "dup".to_string(),
1039 span: None,
1040 },
1041 Statement::If {
1042 then_branch: vec![
1043 Statement::IntLiteral(1),
1044 Statement::WordCall {
1045 name: "i.-".to_string(),
1046 span: None,
1047 },
1048 Statement::WordCall {
1049 name: "countdown".to_string(), span: None,
1051 },
1052 ],
1053 else_branch: Some(vec![]),
1054 span: None,
1055 },
1056 ],
1057 source: None,
1058 allowed_lints: vec![],
1059 },
1060 WordDef {
1061 name: "main".to_string(),
1062 effect: None,
1063 body: vec![
1064 Statement::IntLiteral(5),
1065 Statement::WordCall {
1066 name: "countdown".to_string(),
1067 span: None,
1068 },
1069 ],
1070 source: None,
1071 allowed_lints: vec![],
1072 },
1073 ],
1074 };
1075
1076 let ir = codegen
1077 .codegen_program(&program, HashMap::new(), HashMap::new())
1078 .unwrap();
1079
1080 assert!(
1082 ir.contains("define tailcc ptr @seq_countdown(ptr %stack) {"),
1083 "Recursive word should NOT have alwaysinline, got:\n{}",
1084 ir.lines()
1085 .filter(|l| l.contains("define"))
1086 .collect::<Vec<_>>()
1087 .join("\n")
1088 );
1089 }
1090
1091 #[test]
1092 fn test_recursive_word_in_match_not_inlined() {
1093 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
1095
1096 let mut codegen = CodeGen::new();
1097
1098 let program = Program {
1099 includes: vec![],
1100 unions: vec![UnionDef {
1101 name: "Option".to_string(),
1102 variants: vec![
1103 UnionVariant {
1104 name: "Some".to_string(),
1105 fields: vec![],
1106 source: None,
1107 },
1108 UnionVariant {
1109 name: "None".to_string(),
1110 fields: vec![],
1111 source: None,
1112 },
1113 ],
1114 source: None,
1115 }],
1116 words: vec![
1117 WordDef {
1118 name: "process".to_string(), effect: None,
1120 body: vec![Statement::Match {
1121 arms: vec![
1122 MatchArm {
1123 pattern: Pattern::Variant("Some".to_string()),
1124 body: vec![Statement::WordCall {
1125 name: "process".to_string(), span: None,
1127 }],
1128 span: None,
1129 },
1130 MatchArm {
1131 pattern: Pattern::Variant("None".to_string()),
1132 body: vec![],
1133 span: None,
1134 },
1135 ],
1136 span: None,
1137 }],
1138 source: None,
1139 allowed_lints: vec![],
1140 },
1141 WordDef {
1142 name: "main".to_string(),
1143 effect: None,
1144 body: vec![Statement::WordCall {
1145 name: "process".to_string(),
1146 span: None,
1147 }],
1148 source: None,
1149 allowed_lints: vec![],
1150 },
1151 ],
1152 };
1153
1154 let ir = codegen
1155 .codegen_program(&program, HashMap::new(), HashMap::new())
1156 .unwrap();
1157
1158 assert!(
1160 ir.contains("define tailcc ptr @seq_process(ptr %stack) {"),
1161 "Recursive word in match should NOT have alwaysinline, got:\n{}",
1162 ir.lines()
1163 .filter(|l| l.contains("define"))
1164 .collect::<Vec<_>>()
1165 .join("\n")
1166 );
1167 }
1168
1169 #[test]
1170 fn test_issue_338_specialized_call_in_if_branch_has_terminator() {
1171 use crate::types::{Effect, StackType, Type};
1179
1180 let mut codegen = CodeGen::new();
1181
1182 let get_value_effect = Effect {
1185 inputs: StackType::Cons {
1186 rest: Box::new(StackType::RowVar("S".to_string())),
1187 top: Type::Int,
1188 },
1189 outputs: StackType::Cons {
1190 rest: Box::new(StackType::RowVar("S".to_string())),
1191 top: Type::Int,
1192 },
1193 effects: vec![],
1194 };
1195
1196 let program = Program {
1199 includes: vec![],
1200 unions: vec![],
1201 words: vec![
1202 WordDef {
1204 name: "get-value".to_string(),
1205 effect: Some(get_value_effect),
1206 body: vec![Statement::WordCall {
1207 name: "dup".to_string(),
1208 span: None,
1209 }],
1210 source: None,
1211 allowed_lints: vec![],
1212 },
1213 WordDef {
1216 name: "test-caller".to_string(),
1217 effect: None,
1218 body: vec![Statement::If {
1219 then_branch: vec![Statement::WordCall {
1220 name: "get-value".to_string(),
1221 span: None,
1222 }],
1223 else_branch: Some(vec![
1224 Statement::WordCall {
1225 name: "drop".to_string(),
1226 span: None,
1227 },
1228 Statement::IntLiteral(0),
1229 ]),
1230 span: None,
1231 }],
1232 source: None,
1233 allowed_lints: vec![],
1234 },
1235 WordDef {
1237 name: "main".to_string(),
1238 effect: None,
1239 body: vec![
1240 Statement::BoolLiteral(true),
1241 Statement::IntLiteral(42),
1242 Statement::WordCall {
1243 name: "test-caller".to_string(),
1244 span: None,
1245 },
1246 Statement::WordCall {
1247 name: "drop".to_string(),
1248 span: None,
1249 },
1250 ],
1251 source: None,
1252 allowed_lints: vec![],
1253 },
1254 ],
1255 };
1256
1257 let ir = codegen
1259 .codegen_program(&program, HashMap::new(), HashMap::new())
1260 .expect("Issue #338: codegen should succeed for specialized call in if branch");
1261
1262 assert!(
1264 ir.contains("@seq_get_value_i64"),
1265 "Should generate specialized version of get-value"
1266 );
1267
1268 assert!(
1271 ir.contains("define tailcc ptr @seq_test_caller"),
1272 "Should generate test-caller function"
1273 );
1274
1275 assert!(
1278 ir.contains("musttail call tailcc ptr @seq_get_value"),
1279 "Then branch should use tail call to stack-based version, not specialized dispatch"
1280 );
1281 }
1282
1283 #[test]
1284 fn test_report_call_in_normal_mode() {
1285 let mut codegen = CodeGen::new();
1286 let program = Program {
1287 includes: vec![],
1288 unions: vec![],
1289 words: vec![WordDef {
1290 name: "main".to_string(),
1291 effect: None,
1292 body: vec![
1293 Statement::IntLiteral(42),
1294 Statement::WordCall {
1295 name: "io.write-line".to_string(),
1296 span: None,
1297 },
1298 ],
1299 source: None,
1300 allowed_lints: vec![],
1301 }],
1302 };
1303
1304 let ir = codegen
1305 .codegen_program(&program, HashMap::new(), HashMap::new())
1306 .unwrap();
1307
1308 assert!(
1310 ir.contains("call void @patch_seq_report()"),
1311 "Normal mode should emit report call"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_report_call_absent_in_pure_inline() {
1317 let mut codegen = CodeGen::new_pure_inline_test();
1318 let program = Program {
1319 includes: vec![],
1320 unions: vec![],
1321 words: vec![WordDef {
1322 name: "main".to_string(),
1323 effect: None,
1324 body: vec![Statement::IntLiteral(42)],
1325 source: None,
1326 allowed_lints: vec![],
1327 }],
1328 };
1329
1330 let ir = codegen
1331 .codegen_program(&program, HashMap::new(), HashMap::new())
1332 .unwrap();
1333
1334 assert!(
1336 !ir.contains("call void @patch_seq_report()"),
1337 "Pure inline mode should not emit report call"
1338 );
1339 }
1340
1341 #[test]
1342 fn test_instrument_emits_counters_and_atomicrmw() {
1343 let mut codegen = CodeGen::new();
1344 let program = Program {
1345 includes: vec![],
1346 unions: vec![],
1347 words: vec![
1348 WordDef {
1349 name: "helper".to_string(),
1350 effect: None,
1351 body: vec![Statement::IntLiteral(1)],
1352 source: None,
1353 allowed_lints: vec![],
1354 },
1355 WordDef {
1356 name: "main".to_string(),
1357 effect: None,
1358 body: vec![Statement::WordCall {
1359 name: "helper".to_string(),
1360 span: None,
1361 }],
1362 source: None,
1363 allowed_lints: vec![],
1364 },
1365 ],
1366 };
1367
1368 let config = CompilerConfig {
1369 instrument: true,
1370 ..CompilerConfig::default()
1371 };
1372
1373 let ir = codegen
1374 .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
1375 .unwrap();
1376
1377 assert!(
1379 ir.contains("@seq_word_counters = global [2 x i64] zeroinitializer"),
1380 "Should emit counter array for 2 words"
1381 );
1382
1383 assert!(
1385 ir.contains("@seq_word_name_"),
1386 "Should emit word name constants"
1387 );
1388
1389 assert!(
1391 ir.contains("@seq_word_names = private constant [2 x ptr]"),
1392 "Should emit name pointer table"
1393 );
1394
1395 assert!(
1397 ir.contains("atomicrmw add ptr %instr_ptr_"),
1398 "Should emit atomicrmw add for word counters"
1399 );
1400
1401 assert!(
1403 ir.contains("call void @patch_seq_report_init(ptr @seq_word_counters, ptr @seq_word_names, i64 2)"),
1404 "Should emit report_init call with correct count"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_no_instrument_no_counters() {
1410 let mut codegen = CodeGen::new();
1411 let program = Program {
1412 includes: vec![],
1413 unions: vec![],
1414 words: vec![WordDef {
1415 name: "main".to_string(),
1416 effect: None,
1417 body: vec![Statement::IntLiteral(42)],
1418 source: None,
1419 allowed_lints: vec![],
1420 }],
1421 };
1422
1423 let config = CompilerConfig::default();
1424 assert!(!config.instrument);
1425
1426 let ir = codegen
1427 .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
1428 .unwrap();
1429
1430 assert!(
1432 !ir.contains("@seq_word_counters"),
1433 "Should not emit counters when instrument=false"
1434 );
1435
1436 assert!(
1438 !ir.contains("atomicrmw"),
1439 "Should not emit atomicrmw when instrument=false"
1440 );
1441
1442 assert!(
1444 !ir.contains("call void @patch_seq_report_init"),
1445 "Should not emit report_init when instrument=false"
1446 );
1447 }
1448}