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, a 40-byte Rust enum with `#[repr(C)]`.
11//! Layout: `{ i64, i64, i64, i64, i64 }` (discriminant + largest variant payload).
12//! This fixed size allows pass-by-value, required for Alpine/musl compatibility.
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        codegen.tagged_ptr = false; // Test asserts 40-byte IR patterns
585
586        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), // stmt 0: push Int
597                        Statement::WordCall {
598                            // stmt 1: dup
599                            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        // Provide type info: before statement 1 (dup), top of stack is Int
628        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        // Extract just the test_dup function
636        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        // The optimized path should use load/store directly (no clone_value call)
641        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        // The optimized path should NOT call clone_value
653        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        // Test Issue #195: dup after literal push uses optimized path
663        // Pattern: `42 dup` should be optimized even without type map info
664        let mut codegen = CodeGen::new();
665        codegen.tagged_ptr = false; // Test asserts 40-byte IR patterns
666
667        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), // Previous statement is Int literal
676                        Statement::WordCall {
677                            // dup should be optimized
678                            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        // No type info provided - but literal heuristic should optimize
707        let ir = codegen
708            .codegen_program(&program, HashMap::new(), HashMap::new())
709            .unwrap();
710
711        // Extract just the test_dup function
712        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        // With literal heuristic, should use optimized path
717        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        // Test that dup after word call (unknown type) uses safe clone_value path
737        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                            // Previous statement is word call (unknown type)
756                            name: "get_value".to_string(),
757                            span: None,
758                        },
759                        Statement::WordCall {
760                            // dup should NOT be optimized
761                            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        // No type info provided and no literal before dup
790        let ir = codegen
791            .codegen_program(&program, HashMap::new(), HashMap::new())
792            .unwrap();
793
794        // Extract just the test_dup function
795        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        // Without literal or type info, should call clone_value (safe path)
800        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        // Test Issue #192: roll with constant N uses optimized inline code
810        // Pattern: `2 roll` should generate rot-like inline code
811        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), // Constant N for roll
825                        Statement::WordCall {
826                            // 2 roll = rot
827                            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        // Extract just the test_roll function
864        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        // With constant N=2, should NOT do dynamic calculation
869        // Should NOT have dynamic add/sub for offset calculation
870        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        // Should NOT call memmove for small N (n=2 uses direct loads/stores)
877        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        // Test Issue #192: pick with constant N uses constant offset
887        // Pattern: `1 pick` should generate code with constant -3 offset
888        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), // Constant N for pick
901                        Statement::WordCall {
902                            // 1 pick = over
903                            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        // Extract just the test_pick function
940        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        // With constant N=1, should use constant offset -3
945        // Should NOT have dynamic add/sub for offset calculation
946        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        // Should have the constant offset -3 in getelementptr
953        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        // Test Issue #187: Small words get alwaysinline attribute
963        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(), // Small word: dup i.+
971                    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        // Small word 'double' should have alwaysinline attribute
1006        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        // main should NOT have alwaysinline (uses C calling convention)
1016        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        // Test Issue #187: Recursive words should NOT get alwaysinline
1029        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(), // Recursive
1037                    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(), // Recursive call
1052                                    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        // Recursive word should NOT have alwaysinline
1083        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        // Test Issue #187: Recursive calls inside match arms should prevent inlining
1096        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(), // Recursive in match arm
1121                    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(), // Recursive call
1128                                    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        // Recursive word (via match arm) should NOT have alwaysinline
1161        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        // Issue #338: When a specialized function is called in an if-then branch,
1174        // the generated IR was missing a terminator instruction because:
1175        // 1. will_emit_tail_call returned true (expecting musttail + ret)
1176        // 2. But try_specialized_dispatch took the specialized path instead
1177        // 3. The specialized path doesn't emit ret, leaving the basic block unterminated
1178        //
1179        // The fix skips specialized dispatch in tail position for user-defined words.
1180        use crate::types::{Effect, StackType, Type};
1181
1182        let mut codegen = CodeGen::new();
1183        codegen.tagged_ptr = false; // Test asserts 40-byte IR patterns
1184
1185        // Create a specializable word: get-value ( Int -- Int )
1186        // This will get a specialized version that returns i64 directly
1187        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        // Create a word that calls get-value in an if-then branch
1200        // This pattern triggered the bug in issue #338
1201        let program = Program {
1202            includes: vec![],
1203            unions: vec![],
1204            words: vec![
1205                // : get-value ( Int -- Int ) dup ;
1206                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                // : test-caller ( Bool Int -- Int )
1217                //   if get-value else drop 0 then ;
1218                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                // : main ( -- ) true 42 test-caller drop ;
1239                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        // This should NOT panic with "basic block lacks terminator"
1261        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        // Verify the specialized version was generated
1266        assert!(
1267            ir.contains("@seq_get_value_i64"),
1268            "Should generate specialized version of get-value"
1269        );
1270
1271        // Verify the test-caller function has proper structure
1272        // (both branches should have terminators leading to merge or return)
1273        assert!(
1274            ir.contains("define tailcc ptr @seq_test_caller"),
1275            "Should generate test-caller function"
1276        );
1277
1278        // The then branch should use tail call (musttail + ret) for get-value
1279        // NOT the specialized dispatch (which would leave the block unterminated)
1280        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        // Normal mode should call patch_seq_report after scheduler_run
1312        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        // Pure inline test mode should NOT call patch_seq_report
1338        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        // Should emit counter array
1381        assert!(
1382            ir.contains("@seq_word_counters = global [2 x i64] zeroinitializer"),
1383            "Should emit counter array for 2 words"
1384        );
1385
1386        // Should emit word name strings
1387        assert!(
1388            ir.contains("@seq_word_name_"),
1389            "Should emit word name constants"
1390        );
1391
1392        // Should emit name pointer table
1393        assert!(
1394            ir.contains("@seq_word_names = private constant [2 x ptr]"),
1395            "Should emit name pointer table"
1396        );
1397
1398        // Should emit atomicrmw in each word
1399        assert!(
1400            ir.contains("atomicrmw add ptr %instr_ptr_"),
1401            "Should emit atomicrmw add for word counters"
1402        );
1403
1404        // Should emit report_init call
1405        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        // Should NOT emit counter array
1434        assert!(
1435            !ir.contains("@seq_word_counters"),
1436            "Should not emit counters when instrument=false"
1437        );
1438
1439        // Should NOT emit atomicrmw
1440        assert!(
1441            !ir.contains("atomicrmw"),
1442            "Should not emit atomicrmw when instrument=false"
1443        );
1444
1445        // Should NOT emit report_init call
1446        assert!(
1447            !ir.contains("call void @patch_seq_report_init"),
1448            "Should not emit report_init when instrument=false"
1449        );
1450    }
1451}