Skip to main content

seqc/codegen/
mod.rs

1//! LLVM IR Code Generation
2//!
3//! This module generates LLVM IR as text (.ll files) for Seq programs.
4//! The code generation is split into focused submodules for maintainability.
5//!
6//! # Key Concepts
7//!
8//! ## Value Representation
9//!
10//! All Seq values use the `%Value` type, an 8-byte tagged pointer (i64).
11//! Int and Bool are encoded inline; heap types (Float, String, Variant, etc.)
12//! are stored as Arc<Value> pointers.
13//!
14//! ## Calling Conventions
15//!
16//! - **User-defined words**: Use `tailcc` (tail call convention) to enable TCO.
17//!   Each word has two functions: a C-convention wrapper (`seq_word_*`) for
18//!   external calls and a `tailcc` implementation (`seq_word_*_impl`) for
19//!   internal calls that can use `musttail`.
20//!
21//! - **Runtime functions**: Use C convention (`ccc`). Declared in `runtime.rs`.
22//!
23//! - **Quotations**: Use C convention. Quotations are first-class functions that
24//!   capture their environment. They have wrapper/impl pairs but currently don't
25//!   support TCO due to closure complexity.
26//!
27//! ## Virtual Stack Optimization
28//!
29//! The top N values (default 4) are kept in SSA virtual registers instead of
30//! memory. This avoids store/load overhead for common patterns like `2 3 i.+`.
31//! Values are "spilled" to the memory stack at control flow points (if/else,
32//! loops) and function calls. See `virtual_stack.rs` and `VirtualValue` in
33//! `state.rs`.
34//!
35//! ## Tail Call Optimization (TCO)
36//!
37//! Word calls in tail position use LLVM's `musttail` for guaranteed TCO.
38//! A call is in tail position when it's the last operation before return.
39//! TCO is disabled in these contexts:
40//! - Inside `main` (uses C convention for entry point)
41//! - Inside quotations (closure semantics require stack frames)
42//! - Inside closures that capture variables
43//!
44//! ## Quotations and Closures
45//!
46//! Quotations (`[ ... ]`) compile to function pointers pushed onto the stack.
47//! - **Pure quotations**: No captured variables, just a function pointer.
48//! - **Closures**: Capture variables from enclosing scope. The runtime allocates
49//!   a closure struct containing the function pointer and captured values.
50//!
51//! Each quotation generates a wrapper function (C convention, for `call` builtin)
52//! and an impl function. Closure captures are analyzed at compile time by
53//! `capture_analysis.rs`.
54//!
55//! # Module Structure
56//!
57//! - `state.rs`: Core types (CodeGen, VirtualValue, TailPosition)
58//! - `program.rs`: Main entry points (codegen_program*)
59//! - `words.rs`: Word and quotation code generation
60//! - `statements.rs`: Statement dispatch and main function
61//! - `inline/`: Inline operation code generation (no runtime calls)
62//!   - `dispatch.rs`: Main inline dispatch logic
63//!   - `ops.rs`: Individual inline operations
64//! - `control_flow.rs`: If/else, match statements
65//! - `virtual_stack.rs`: Virtual register optimization
66//! - `types.rs`: Type helpers and exhaustiveness checking
67//! - `globals.rs`: String and symbol constants
68//! - `runtime.rs`: Runtime function declarations
69//! - `ffi_wrappers.rs`: FFI wrapper generation
70//! - `platform.rs`: Platform detection
71//! - `error.rs`: Error types
72
73// Submodules
74mod 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
90// Public re-exports
91pub 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
96// Internal re-exports for submodules
97use 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        // main uses C calling convention (no tailcc) since it's called from C runtime
137        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        // Test io.write (write without newline)
146        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        // Test inline tagged stack arithmetic with virtual registers (Issue #189)
178        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        // Issue #189: With virtual registers, integers are kept in SSA variables
204        // Using identity add: %n = add i64 0, <value>
205        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        // The add operation uses virtual registers directly
208        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        // Simple program: 5 3 add (should return 8)
216        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        // Pure inline test mode should:
240        // 1. NOT CALL the scheduler (declarations are ok, calls are not)
241        assert!(!ir.contains("call void @patch_seq_scheduler_init"));
242        assert!(!ir.contains("call i64 @patch_seq_strand_spawn"));
243
244        // 2. Have main allocate tagged stack and call seq_main directly
245        assert!(ir.contains("call ptr @seq_stack_new_default()"));
246        assert!(ir.contains("call ptr @seq_main(ptr %stack_base)"));
247
248        // 3. Read result from stack and return as exit code
249        // SSA name is a dynamic temp (not hardcoded %result), so check line-level
250        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        // 4. Use inline push with virtual registers (Issue #189)
258        assert!(!ir.contains("call ptr @patch_seq_push_int"));
259        // Values are kept in SSA variables via identity add
260        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        // 5. Use inline add with virtual registers (add i64 %, not call patch_seq_add)
264        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)] // Testing codegen in isolation, not full pipeline
278    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, // Codegen doesn't check effects
289                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        // Should declare the external builtin
309        assert!(
310            ir.contains("declare ptr @test_runtime_my_op(ptr)"),
311            "IR should declare external builtin"
312        );
313
314        // Should call the external builtin
315        assert!(
316            ir.contains("call ptr @test_runtime_my_op"),
317            "IR should call external builtin"
318        );
319    }
320
321    #[test]
322    #[allow(deprecated)] // Testing codegen in isolation, not full pipeline
323    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, // Codegen doesn't check effects
334                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        // Should declare both external builtins
361        assert!(ir.contains("declare ptr @seq_actors_self(ptr)"));
362        assert!(ir.contains("declare ptr @seq_actors_journal_append(ptr)"));
363
364        // Should call both
365        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)] // Testing config builder, not full pipeline
371    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        // Test that external builtins work through the full compile pipeline
387        // including parser, AST validation, type checker, and codegen
388        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        // External builtins must have explicit effects (v2.0 requirement)
400        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        // This should succeed - the external builtin is registered
408        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        // Test that using an external builtin without config fails validation
423        use crate::compile_to_ir;
424
425        let source = r#"
426            : main ( -- Int )
427              42 unknown-builtin
428              0
429            ;
430        "#;
431
432        // This should fail - unknown-builtin is not registered
433        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        // Test symbol literal codegen
491        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        // Issue #166: Test that duplicate symbol literals share the same global
527        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                    // Use :hello twice - should share the same .sym global
537                    Statement::Symbol("hello".to_string()),
538                    Statement::Symbol("hello".to_string()),
539                    Statement::Symbol("world".to_string()), // Different symbol
540                ],
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        // Should have exactly one .sym global for "hello" and one for "world"
551        // Count occurrences of symbol global definitions (lines starting with @.sym)
552        let sym_defs: Vec<_> = ir
553            .lines()
554            .filter(|l| l.trim().starts_with("@.sym."))
555            .collect();
556
557        // There should be 2 definitions: .sym.0 for "hello" and .sym.1 for "world"
558        assert_eq!(
559            sym_defs.len(),
560            2,
561            "Expected 2 symbol globals, got: {:?}",
562            sym_defs
563        );
564
565        // Verify deduplication: :hello appears twice but .sym.0 is reused
566        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        // The IR should contain static symbol structure with capacity=0
573        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        // Test that dup on Int uses optimized load/store instead of clone_value
582        // This verifies the Issue #186 optimization actually fires
583        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), // stmt 0: push Int
596                        Statement::WordCall {
597                            // stmt 1: dup
598                            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        // Provide type info: before statement 1 (dup), top of stack is Int
627        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        // Extract just the test_dup function
635        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        // The optimized path should use load/store directly (no clone_value call)
640        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        // The optimized path should NOT call clone_value
652        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        // Test Issue #195: dup after literal push uses optimized path
662        // Pattern: `42 dup` should be optimized even without type map info
663        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), // Previous statement is Int literal
674                        Statement::WordCall {
675                            // dup should be optimized
676                            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        // No type info provided - but literal heuristic should optimize
705        let ir = codegen
706            .codegen_program(&program, HashMap::new(), HashMap::new())
707            .unwrap();
708
709        // Extract just the test_dup function
710        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        // With literal heuristic, should use optimized path
715        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        // Test that dup after word call (unknown type) uses safe clone_value path
735        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                            // Previous statement is word call (unknown type)
754                            name: "get_value".to_string(),
755                            span: None,
756                        },
757                        Statement::WordCall {
758                            // dup should NOT be optimized
759                            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        // No type info provided and no literal before dup
788        let ir = codegen
789            .codegen_program(&program, HashMap::new(), HashMap::new())
790            .unwrap();
791
792        // Extract just the test_dup function
793        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        // Without literal or type info, should call clone_value (safe path)
798        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        // Test Issue #192: roll with constant N uses optimized inline code
808        // Pattern: `2 roll` should generate rot-like inline code
809        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), // Constant N for roll
823                        Statement::WordCall {
824                            // 2 roll = rot
825                            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        // Extract just the test_roll function
862        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        // With constant N=2, should NOT do dynamic calculation
867        // Should NOT have dynamic add/sub for offset calculation
868        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        // Should NOT call memmove for small N (n=2 uses direct loads/stores)
875        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        // Test Issue #192: pick with constant N uses constant offset
885        // Pattern: `1 pick` should generate code with constant -3 offset
886        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), // Constant N for pick
899                        Statement::WordCall {
900                            // 1 pick = over
901                            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        // Extract just the test_pick function
938        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        // With constant N=1, should use constant offset -3
943        // Should NOT have dynamic add/sub for offset calculation
944        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        // Should have the constant offset -3 in getelementptr
951        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        // Test Issue #187: Small words get alwaysinline attribute
961        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(), // Small word: dup i.+
969                    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        // Small word 'double' should have alwaysinline attribute
1004        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        // main should NOT have alwaysinline (uses C calling convention)
1014        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        // Test Issue #187: Recursive words should NOT get alwaysinline
1027        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(), // Recursive
1035                    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(), // Recursive call
1050                                    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        // Recursive word should NOT have alwaysinline
1081        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        // Test Issue #187: Recursive calls inside match arms should prevent inlining
1094        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(), // Recursive in match arm
1119                    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(), // Recursive call
1126                                    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        // Recursive word (via match arm) should NOT have alwaysinline
1159        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        // Issue #338: When a specialized function is called in an if-then branch,
1172        // the generated IR was missing a terminator instruction because:
1173        // 1. will_emit_tail_call returned true (expecting musttail + ret)
1174        // 2. But try_specialized_dispatch took the specialized path instead
1175        // 3. The specialized path doesn't emit ret, leaving the basic block unterminated
1176        //
1177        // The fix skips specialized dispatch in tail position for user-defined words.
1178        use crate::types::{Effect, StackType, Type};
1179
1180        let mut codegen = CodeGen::new();
1181
1182        // Create a specializable word: get-value ( Int -- Int )
1183        // This will get a specialized version that returns i64 directly
1184        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        // Create a word that calls get-value in an if-then branch
1197        // This pattern triggered the bug in issue #338
1198        let program = Program {
1199            includes: vec![],
1200            unions: vec![],
1201            words: vec![
1202                // : get-value ( Int -- Int ) dup ;
1203                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                // : test-caller ( Bool Int -- Int )
1214                //   if get-value else drop 0 then ;
1215                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                // : main ( -- ) true 42 test-caller drop ;
1236                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        // This should NOT panic with "basic block lacks terminator"
1258        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        // Verify the specialized version was generated
1263        assert!(
1264            ir.contains("@seq_get_value_i64"),
1265            "Should generate specialized version of get-value"
1266        );
1267
1268        // Verify the test-caller function has proper structure
1269        // (both branches should have terminators leading to merge or return)
1270        assert!(
1271            ir.contains("define tailcc ptr @seq_test_caller"),
1272            "Should generate test-caller function"
1273        );
1274
1275        // The then branch should use tail call (musttail + ret) for get-value
1276        // NOT the specialized dispatch (which would leave the block unterminated)
1277        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        // Normal mode should call patch_seq_report after scheduler_run
1309        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        // Pure inline test mode should NOT call patch_seq_report
1335        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        // Should emit counter array
1378        assert!(
1379            ir.contains("@seq_word_counters = global [2 x i64] zeroinitializer"),
1380            "Should emit counter array for 2 words"
1381        );
1382
1383        // Should emit word name strings
1384        assert!(
1385            ir.contains("@seq_word_name_"),
1386            "Should emit word name constants"
1387        );
1388
1389        // Should emit name pointer table
1390        assert!(
1391            ir.contains("@seq_word_names = private constant [2 x ptr]"),
1392            "Should emit name pointer table"
1393        );
1394
1395        // Should emit atomicrmw in each word
1396        assert!(
1397            ir.contains("atomicrmw add ptr %instr_ptr_"),
1398            "Should emit atomicrmw add for word counters"
1399        );
1400
1401        // Should emit report_init call
1402        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        // Should NOT emit counter array
1431        assert!(
1432            !ir.contains("@seq_word_counters"),
1433            "Should not emit counters when instrument=false"
1434        );
1435
1436        // Should NOT emit atomicrmw
1437        assert!(
1438            !ir.contains("atomicrmw"),
1439            "Should not emit atomicrmw when instrument=false"
1440        );
1441
1442        // Should NOT emit report_init call
1443        assert!(
1444            !ir.contains("call void @patch_seq_report_init"),
1445            "Should not emit report_init when instrument=false"
1446        );
1447    }
1448}