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 platform;
80mod program;
81mod runtime;
82mod state;
83mod statements;
84mod types;
85mod virtual_stack;
86mod words;
87
88// Public re-exports
89pub use error::CodeGenError;
90pub use platform::{ffi_c_args, ffi_return_type, get_target_triple};
91pub use runtime::{BUILTIN_SYMBOLS, RUNTIME_DECLARATIONS, emit_runtime_decls};
92pub use state::CodeGen;
93
94// Internal re-exports for submodules
95use state::{
96    BranchResult, MAX_VIRTUAL_STACK, QuotationFunctions, TailPosition, UNREACHABLE_PREDECESSOR,
97    VirtualValue, mangle_name,
98};
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::ast::{Program, Statement, WordDef};
104    use std::collections::HashMap;
105
106    #[test]
107    fn test_codegen_hello_world() {
108        let mut codegen = CodeGen::new();
109
110        let program = Program {
111            includes: vec![],
112            unions: vec![],
113            words: vec![WordDef {
114                name: "main".to_string(),
115                effect: None,
116                body: vec![
117                    Statement::StringLiteral("Hello, World!".to_string()),
118                    Statement::WordCall {
119                        name: "io.write-line".to_string(),
120                        span: None,
121                    },
122                ],
123                source: None,
124            }],
125        };
126
127        let ir = codegen
128            .codegen_program(&program, HashMap::new(), HashMap::new())
129            .unwrap();
130
131        assert!(ir.contains("define i32 @main(i32 %argc, ptr %argv)"));
132        // main uses C calling convention (no tailcc) since it's called from C runtime
133        assert!(ir.contains("define ptr @seq_main(ptr %stack)"));
134        assert!(ir.contains("call ptr @patch_seq_push_string"));
135        assert!(ir.contains("call ptr @patch_seq_write_line"));
136        assert!(ir.contains("\"Hello, World!\\00\""));
137    }
138
139    #[test]
140    fn test_codegen_io_write() {
141        // Test io.write (write without newline)
142        let mut codegen = CodeGen::new();
143
144        let program = Program {
145            includes: vec![],
146            unions: vec![],
147            words: vec![WordDef {
148                name: "main".to_string(),
149                effect: None,
150                body: vec![
151                    Statement::StringLiteral("no newline".to_string()),
152                    Statement::WordCall {
153                        name: "io.write".to_string(),
154                        span: None,
155                    },
156                ],
157                source: None,
158            }],
159        };
160
161        let ir = codegen
162            .codegen_program(&program, HashMap::new(), HashMap::new())
163            .unwrap();
164
165        assert!(ir.contains("call ptr @patch_seq_push_string"));
166        assert!(ir.contains("call ptr @patch_seq_write"));
167        assert!(ir.contains("\"no newline\\00\""));
168    }
169
170    #[test]
171    fn test_codegen_arithmetic() {
172        // Test inline tagged stack arithmetic with virtual registers (Issue #189)
173        let mut codegen = CodeGen::new();
174
175        let program = Program {
176            includes: vec![],
177            unions: vec![],
178            words: vec![WordDef {
179                name: "main".to_string(),
180                effect: None,
181                body: vec![
182                    Statement::IntLiteral(2),
183                    Statement::IntLiteral(3),
184                    Statement::WordCall {
185                        name: "i.add".to_string(),
186                        span: None,
187                    },
188                ],
189                source: None,
190            }],
191        };
192
193        let ir = codegen
194            .codegen_program(&program, HashMap::new(), HashMap::new())
195            .unwrap();
196
197        // Issue #189: With virtual registers, integers are kept in SSA variables
198        // Using identity add: %n = add i64 0, <value>
199        assert!(ir.contains("add i64 0, 2"), "Should create SSA var for 2");
200        assert!(ir.contains("add i64 0, 3"), "Should create SSA var for 3");
201        // The add operation uses virtual registers directly
202        assert!(ir.contains("add i64 %"), "Should add SSA variables");
203    }
204
205    #[test]
206    fn test_pure_inline_test_mode() {
207        let mut codegen = CodeGen::new_pure_inline_test();
208
209        // Simple program: 5 3 add (should return 8)
210        let program = Program {
211            includes: vec![],
212            unions: vec![],
213            words: vec![WordDef {
214                name: "main".to_string(),
215                effect: None,
216                body: vec![
217                    Statement::IntLiteral(5),
218                    Statement::IntLiteral(3),
219                    Statement::WordCall {
220                        name: "i.add".to_string(),
221                        span: None,
222                    },
223                ],
224                source: None,
225            }],
226        };
227
228        let ir = codegen
229            .codegen_program(&program, HashMap::new(), HashMap::new())
230            .unwrap();
231
232        // Pure inline test mode should:
233        // 1. NOT CALL the scheduler (declarations are ok, calls are not)
234        assert!(!ir.contains("call void @patch_seq_scheduler_init"));
235        assert!(!ir.contains("call i64 @patch_seq_strand_spawn"));
236
237        // 2. Have main allocate tagged stack and call seq_main directly
238        assert!(ir.contains("call ptr @seq_stack_new_default()"));
239        assert!(ir.contains("call ptr @seq_main(ptr %stack_base)"));
240
241        // 3. Read result from stack and return as exit code
242        assert!(ir.contains("trunc i64 %result to i32"));
243        assert!(ir.contains("ret i32 %exit_code"));
244
245        // 4. Use inline push with virtual registers (Issue #189)
246        assert!(!ir.contains("call ptr @patch_seq_push_int"));
247        // Values are kept in SSA variables via identity add
248        assert!(ir.contains("add i64 0, 5"), "Should create SSA var for 5");
249        assert!(ir.contains("add i64 0, 3"), "Should create SSA var for 3");
250
251        // 5. Use inline add with virtual registers (add i64 %, not call patch_seq_add)
252        assert!(!ir.contains("call ptr @patch_seq_add"));
253        assert!(ir.contains("add i64 %"), "Should add SSA variables");
254    }
255
256    #[test]
257    fn test_escape_llvm_string() {
258        assert_eq!(CodeGen::escape_llvm_string("hello").unwrap(), "hello");
259        assert_eq!(CodeGen::escape_llvm_string("a\nb").unwrap(), r"a\0Ab");
260        assert_eq!(CodeGen::escape_llvm_string("a\tb").unwrap(), r"a\09b");
261        assert_eq!(CodeGen::escape_llvm_string("a\"b").unwrap(), r"a\22b");
262    }
263
264    #[test]
265    #[allow(deprecated)] // Testing codegen in isolation, not full pipeline
266    fn test_external_builtins_declared() {
267        use crate::config::{CompilerConfig, ExternalBuiltin};
268
269        let mut codegen = CodeGen::new();
270
271        let program = Program {
272            includes: vec![],
273            unions: vec![],
274            words: vec![WordDef {
275                name: "main".to_string(),
276                effect: None, // Codegen doesn't check effects
277                body: vec![
278                    Statement::IntLiteral(42),
279                    Statement::WordCall {
280                        name: "my-external-op".to_string(),
281                        span: None,
282                    },
283                ],
284                source: None,
285            }],
286        };
287
288        let config = CompilerConfig::new()
289            .with_builtin(ExternalBuiltin::new("my-external-op", "test_runtime_my_op"));
290
291        let ir = codegen
292            .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
293            .unwrap();
294
295        // Should declare the external builtin
296        assert!(
297            ir.contains("declare ptr @test_runtime_my_op(ptr)"),
298            "IR should declare external builtin"
299        );
300
301        // Should call the external builtin
302        assert!(
303            ir.contains("call ptr @test_runtime_my_op"),
304            "IR should call external builtin"
305        );
306    }
307
308    #[test]
309    #[allow(deprecated)] // Testing codegen in isolation, not full pipeline
310    fn test_multiple_external_builtins() {
311        use crate::config::{CompilerConfig, ExternalBuiltin};
312
313        let mut codegen = CodeGen::new();
314
315        let program = Program {
316            includes: vec![],
317            unions: vec![],
318            words: vec![WordDef {
319                name: "main".to_string(),
320                effect: None, // Codegen doesn't check effects
321                body: vec![
322                    Statement::WordCall {
323                        name: "actor-self".to_string(),
324                        span: None,
325                    },
326                    Statement::WordCall {
327                        name: "journal-append".to_string(),
328                        span: None,
329                    },
330                ],
331                source: None,
332            }],
333        };
334
335        let config = CompilerConfig::new()
336            .with_builtin(ExternalBuiltin::new("actor-self", "seq_actors_self"))
337            .with_builtin(ExternalBuiltin::new(
338                "journal-append",
339                "seq_actors_journal_append",
340            ));
341
342        let ir = codegen
343            .codegen_program_with_config(&program, HashMap::new(), HashMap::new(), &config)
344            .unwrap();
345
346        // Should declare both external builtins
347        assert!(ir.contains("declare ptr @seq_actors_self(ptr)"));
348        assert!(ir.contains("declare ptr @seq_actors_journal_append(ptr)"));
349
350        // Should call both
351        assert!(ir.contains("call ptr @seq_actors_self"));
352        assert!(ir.contains("call ptr @seq_actors_journal_append"));
353    }
354
355    #[test]
356    #[allow(deprecated)] // Testing config builder, not full pipeline
357    fn test_external_builtins_with_library_paths() {
358        use crate::config::{CompilerConfig, ExternalBuiltin};
359
360        let config = CompilerConfig::new()
361            .with_builtin(ExternalBuiltin::new("my-op", "runtime_my_op"))
362            .with_library_path("/custom/lib")
363            .with_library("myruntime");
364
365        assert_eq!(config.external_builtins.len(), 1);
366        assert_eq!(config.library_paths, vec!["/custom/lib"]);
367        assert_eq!(config.libraries, vec!["myruntime"]);
368    }
369
370    #[test]
371    fn test_external_builtin_full_pipeline() {
372        // Test that external builtins work through the full compile pipeline
373        // including parser, AST validation, type checker, and codegen
374        use crate::compile_to_ir_with_config;
375        use crate::config::{CompilerConfig, ExternalBuiltin};
376        use crate::types::{Effect, StackType, Type};
377
378        let source = r#"
379            : main ( -- Int )
380              42 my-transform
381              0
382            ;
383        "#;
384
385        // External builtins must have explicit effects (v2.0 requirement)
386        let effect = Effect::new(StackType::singleton(Type::Int), StackType::Empty);
387        let config = CompilerConfig::new().with_builtin(ExternalBuiltin::with_effect(
388            "my-transform",
389            "ext_runtime_transform",
390            effect,
391        ));
392
393        // This should succeed - the external builtin is registered
394        let result = compile_to_ir_with_config(source, &config);
395        assert!(
396            result.is_ok(),
397            "Compilation should succeed: {:?}",
398            result.err()
399        );
400
401        let ir = result.unwrap();
402        assert!(ir.contains("declare ptr @ext_runtime_transform(ptr)"));
403        assert!(ir.contains("call ptr @ext_runtime_transform"));
404    }
405
406    #[test]
407    fn test_external_builtin_without_config_fails() {
408        // Test that using an external builtin without config fails validation
409        use crate::compile_to_ir;
410
411        let source = r#"
412            : main ( -- Int )
413              42 unknown-builtin
414              0
415            ;
416        "#;
417
418        // This should fail - unknown-builtin is not registered
419        let result = compile_to_ir(source);
420        assert!(result.is_err());
421        assert!(result.unwrap_err().contains("unknown-builtin"));
422    }
423
424    #[test]
425    fn test_match_exhaustiveness_error() {
426        use crate::compile_to_ir;
427
428        let source = r#"
429            union Result { Ok { value: Int } Err { msg: String } }
430
431            : handle ( Variant -- Int )
432              match
433                Ok -> drop 1
434                # Missing Err arm!
435              end
436            ;
437
438            : main ( -- ) 42 Make-Ok handle drop ;
439        "#;
440
441        let result = compile_to_ir(source);
442        assert!(result.is_err());
443        let err = result.unwrap_err();
444        assert!(err.contains("Non-exhaustive match"));
445        assert!(err.contains("Result"));
446        assert!(err.contains("Err"));
447    }
448
449    #[test]
450    fn test_match_exhaustive_compiles() {
451        use crate::compile_to_ir;
452
453        let source = r#"
454            union Result { Ok { value: Int } Err { msg: String } }
455
456            : handle ( Variant -- Int )
457              match
458                Ok -> drop 1
459                Err -> drop 0
460              end
461            ;
462
463            : main ( -- ) 42 Make-Ok handle drop ;
464        "#;
465
466        let result = compile_to_ir(source);
467        assert!(
468            result.is_ok(),
469            "Exhaustive match should compile: {:?}",
470            result
471        );
472    }
473
474    #[test]
475    fn test_codegen_symbol() {
476        // Test symbol literal codegen
477        let mut codegen = CodeGen::new();
478
479        let program = Program {
480            includes: vec![],
481            unions: vec![],
482            words: vec![WordDef {
483                name: "main".to_string(),
484                effect: None,
485                body: vec![
486                    Statement::Symbol("hello".to_string()),
487                    Statement::WordCall {
488                        name: "symbol->string".to_string(),
489                        span: None,
490                    },
491                    Statement::WordCall {
492                        name: "io.write-line".to_string(),
493                        span: None,
494                    },
495                ],
496                source: None,
497            }],
498        };
499
500        let ir = codegen
501            .codegen_program(&program, HashMap::new(), HashMap::new())
502            .unwrap();
503
504        assert!(ir.contains("call ptr @patch_seq_push_interned_symbol"));
505        assert!(ir.contains("call ptr @patch_seq_symbol_to_string"));
506        assert!(ir.contains("\"hello\\00\""));
507    }
508
509    #[test]
510    fn test_symbol_interning_dedup() {
511        // Issue #166: Test that duplicate symbol literals share the same global
512        let mut codegen = CodeGen::new();
513
514        let program = Program {
515            includes: vec![],
516            unions: vec![],
517            words: vec![WordDef {
518                name: "main".to_string(),
519                effect: None,
520                body: vec![
521                    // Use :hello twice - should share the same .sym global
522                    Statement::Symbol("hello".to_string()),
523                    Statement::Symbol("hello".to_string()),
524                    Statement::Symbol("world".to_string()), // Different symbol
525                ],
526                source: None,
527            }],
528        };
529
530        let ir = codegen
531            .codegen_program(&program, HashMap::new(), HashMap::new())
532            .unwrap();
533
534        // Should have exactly one .sym global for "hello" and one for "world"
535        // Count occurrences of symbol global definitions (lines starting with @.sym)
536        let sym_defs: Vec<_> = ir
537            .lines()
538            .filter(|l| l.trim().starts_with("@.sym."))
539            .collect();
540
541        // There should be 2 definitions: .sym.0 for "hello" and .sym.1 for "world"
542        assert_eq!(
543            sym_defs.len(),
544            2,
545            "Expected 2 symbol globals, got: {:?}",
546            sym_defs
547        );
548
549        // Verify deduplication: :hello appears twice but .sym.0 is reused
550        let hello_uses: usize = ir.matches("@.sym.0").count();
551        assert_eq!(
552            hello_uses, 3,
553            "Expected 3 occurrences of .sym.0 (1 def + 2 uses)"
554        );
555
556        // The IR should contain static symbol structure with capacity=0
557        assert!(
558            ir.contains("i64 0, i8 1"),
559            "Symbol global should have capacity=0 and global=1"
560        );
561    }
562
563    #[test]
564    fn test_dup_optimization_for_int() {
565        // Test that dup on Int uses optimized load/store instead of clone_value
566        // This verifies the Issue #186 optimization actually fires
567        let mut codegen = CodeGen::new();
568
569        use crate::types::Type;
570
571        let program = Program {
572            includes: vec![],
573            unions: vec![],
574            words: vec![
575                WordDef {
576                    name: "test_dup".to_string(),
577                    effect: None,
578                    body: vec![
579                        Statement::IntLiteral(42), // stmt 0: push Int
580                        Statement::WordCall {
581                            // stmt 1: dup
582                            name: "dup".to_string(),
583                            span: None,
584                        },
585                        Statement::WordCall {
586                            name: "drop".to_string(),
587                            span: None,
588                        },
589                        Statement::WordCall {
590                            name: "drop".to_string(),
591                            span: None,
592                        },
593                    ],
594                    source: None,
595                },
596                WordDef {
597                    name: "main".to_string(),
598                    effect: None,
599                    body: vec![Statement::WordCall {
600                        name: "test_dup".to_string(),
601                        span: None,
602                    }],
603                    source: None,
604                },
605            ],
606        };
607
608        // Provide type info: before statement 1 (dup), top of stack is Int
609        let mut statement_types = HashMap::new();
610        statement_types.insert(("test_dup".to_string(), 1), Type::Int);
611
612        let ir = codegen
613            .codegen_program(&program, HashMap::new(), statement_types)
614            .unwrap();
615
616        // Extract just the test_dup function
617        let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
618        let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
619        let test_dup_fn = &ir[func_start..func_end];
620
621        // The optimized path should use load/store directly (no clone_value call)
622        assert!(
623            test_dup_fn.contains("load %Value"),
624            "Optimized dup should use 'load %Value', got:\n{}",
625            test_dup_fn
626        );
627        assert!(
628            test_dup_fn.contains("store %Value"),
629            "Optimized dup should use 'store %Value', got:\n{}",
630            test_dup_fn
631        );
632
633        // The optimized path should NOT call clone_value
634        assert!(
635            !test_dup_fn.contains("@patch_seq_clone_value"),
636            "Optimized dup should NOT call clone_value for Int, got:\n{}",
637            test_dup_fn
638        );
639    }
640
641    #[test]
642    fn test_dup_optimization_after_literal() {
643        // Test Issue #195: dup after literal push uses optimized path
644        // Pattern: `42 dup` should be optimized even without type map info
645        let mut codegen = CodeGen::new();
646
647        let program = Program {
648            includes: vec![],
649            unions: vec![],
650            words: vec![
651                WordDef {
652                    name: "test_dup".to_string(),
653                    effect: None,
654                    body: vec![
655                        Statement::IntLiteral(42), // Previous statement is Int literal
656                        Statement::WordCall {
657                            // dup should be optimized
658                            name: "dup".to_string(),
659                            span: None,
660                        },
661                        Statement::WordCall {
662                            name: "drop".to_string(),
663                            span: None,
664                        },
665                        Statement::WordCall {
666                            name: "drop".to_string(),
667                            span: None,
668                        },
669                    ],
670                    source: None,
671                },
672                WordDef {
673                    name: "main".to_string(),
674                    effect: None,
675                    body: vec![Statement::WordCall {
676                        name: "test_dup".to_string(),
677                        span: None,
678                    }],
679                    source: None,
680                },
681            ],
682        };
683
684        // No type info provided - but literal heuristic should optimize
685        let ir = codegen
686            .codegen_program(&program, HashMap::new(), HashMap::new())
687            .unwrap();
688
689        // Extract just the test_dup function
690        let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
691        let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
692        let test_dup_fn = &ir[func_start..func_end];
693
694        // With literal heuristic, should use optimized path
695        assert!(
696            test_dup_fn.contains("load %Value"),
697            "Dup after int literal should use optimized load, got:\n{}",
698            test_dup_fn
699        );
700        assert!(
701            test_dup_fn.contains("store %Value"),
702            "Dup after int literal should use optimized store, got:\n{}",
703            test_dup_fn
704        );
705        assert!(
706            !test_dup_fn.contains("@patch_seq_clone_value"),
707            "Dup after int literal should NOT call clone_value, got:\n{}",
708            test_dup_fn
709        );
710    }
711
712    #[test]
713    fn test_dup_no_optimization_after_word_call() {
714        // Test that dup after word call (unknown type) uses safe clone_value path
715        let mut codegen = CodeGen::new();
716
717        let program = Program {
718            includes: vec![],
719            unions: vec![],
720            words: vec![
721                WordDef {
722                    name: "get_value".to_string(),
723                    effect: None,
724                    body: vec![Statement::IntLiteral(42)],
725                    source: None,
726                },
727                WordDef {
728                    name: "test_dup".to_string(),
729                    effect: None,
730                    body: vec![
731                        Statement::WordCall {
732                            // Previous statement is word call (unknown type)
733                            name: "get_value".to_string(),
734                            span: None,
735                        },
736                        Statement::WordCall {
737                            // dup should NOT be optimized
738                            name: "dup".to_string(),
739                            span: None,
740                        },
741                        Statement::WordCall {
742                            name: "drop".to_string(),
743                            span: None,
744                        },
745                        Statement::WordCall {
746                            name: "drop".to_string(),
747                            span: None,
748                        },
749                    ],
750                    source: None,
751                },
752                WordDef {
753                    name: "main".to_string(),
754                    effect: None,
755                    body: vec![Statement::WordCall {
756                        name: "test_dup".to_string(),
757                        span: None,
758                    }],
759                    source: None,
760                },
761            ],
762        };
763
764        // No type info provided and no literal before dup
765        let ir = codegen
766            .codegen_program(&program, HashMap::new(), HashMap::new())
767            .unwrap();
768
769        // Extract just the test_dup function
770        let func_start = ir.find("define tailcc ptr @seq_test_dup").unwrap();
771        let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
772        let test_dup_fn = &ir[func_start..func_end];
773
774        // Without literal or type info, should call clone_value (safe path)
775        assert!(
776            test_dup_fn.contains("@patch_seq_clone_value"),
777            "Dup after word call should call clone_value, got:\n{}",
778            test_dup_fn
779        );
780    }
781
782    #[test]
783    fn test_roll_constant_optimization() {
784        // Test Issue #192: roll with constant N uses optimized inline code
785        // Pattern: `2 roll` should generate rot-like inline code
786        let mut codegen = CodeGen::new();
787
788        let program = Program {
789            includes: vec![],
790            unions: vec![],
791            words: vec![
792                WordDef {
793                    name: "test_roll".to_string(),
794                    effect: None,
795                    body: vec![
796                        Statement::IntLiteral(1),
797                        Statement::IntLiteral(2),
798                        Statement::IntLiteral(3),
799                        Statement::IntLiteral(2), // Constant N for roll
800                        Statement::WordCall {
801                            // 2 roll = rot
802                            name: "roll".to_string(),
803                            span: None,
804                        },
805                        Statement::WordCall {
806                            name: "drop".to_string(),
807                            span: None,
808                        },
809                        Statement::WordCall {
810                            name: "drop".to_string(),
811                            span: None,
812                        },
813                        Statement::WordCall {
814                            name: "drop".to_string(),
815                            span: None,
816                        },
817                    ],
818                    source: None,
819                },
820                WordDef {
821                    name: "main".to_string(),
822                    effect: None,
823                    body: vec![Statement::WordCall {
824                        name: "test_roll".to_string(),
825                        span: None,
826                    }],
827                    source: None,
828                },
829            ],
830        };
831
832        let ir = codegen
833            .codegen_program(&program, HashMap::new(), HashMap::new())
834            .unwrap();
835
836        // Extract just the test_roll function
837        let func_start = ir.find("define tailcc ptr @seq_test_roll").unwrap();
838        let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
839        let test_roll_fn = &ir[func_start..func_end];
840
841        // With constant N=2, should NOT do dynamic calculation
842        // Should NOT have dynamic add/sub for offset calculation
843        assert!(
844            !test_roll_fn.contains("= add i64 %"),
845            "Constant roll should use constant offset, not dynamic add, got:\n{}",
846            test_roll_fn
847        );
848
849        // Should NOT call memmove for small N (n=2 uses direct loads/stores)
850        assert!(
851            !test_roll_fn.contains("@llvm.memmove"),
852            "2 roll should not use memmove, got:\n{}",
853            test_roll_fn
854        );
855    }
856
857    #[test]
858    fn test_pick_constant_optimization() {
859        // Test Issue #192: pick with constant N uses constant offset
860        // Pattern: `1 pick` should generate code with constant -3 offset
861        let mut codegen = CodeGen::new();
862
863        let program = Program {
864            includes: vec![],
865            unions: vec![],
866            words: vec![
867                WordDef {
868                    name: "test_pick".to_string(),
869                    effect: None,
870                    body: vec![
871                        Statement::IntLiteral(10),
872                        Statement::IntLiteral(20),
873                        Statement::IntLiteral(1), // Constant N for pick
874                        Statement::WordCall {
875                            // 1 pick = over
876                            name: "pick".to_string(),
877                            span: None,
878                        },
879                        Statement::WordCall {
880                            name: "drop".to_string(),
881                            span: None,
882                        },
883                        Statement::WordCall {
884                            name: "drop".to_string(),
885                            span: None,
886                        },
887                        Statement::WordCall {
888                            name: "drop".to_string(),
889                            span: None,
890                        },
891                    ],
892                    source: None,
893                },
894                WordDef {
895                    name: "main".to_string(),
896                    effect: None,
897                    body: vec![Statement::WordCall {
898                        name: "test_pick".to_string(),
899                        span: None,
900                    }],
901                    source: None,
902                },
903            ],
904        };
905
906        let ir = codegen
907            .codegen_program(&program, HashMap::new(), HashMap::new())
908            .unwrap();
909
910        // Extract just the test_pick function
911        let func_start = ir.find("define tailcc ptr @seq_test_pick").unwrap();
912        let func_end = ir[func_start..].find("\n}\n").unwrap() + func_start + 3;
913        let test_pick_fn = &ir[func_start..func_end];
914
915        // With constant N=1, should use constant offset -3
916        // Should NOT have dynamic add/sub for offset calculation
917        assert!(
918            !test_pick_fn.contains("= add i64 %"),
919            "Constant pick should use constant offset, not dynamic add, got:\n{}",
920            test_pick_fn
921        );
922
923        // Should have the constant offset -3 in getelementptr
924        assert!(
925            test_pick_fn.contains("i64 -3"),
926            "1 pick should use offset -3 (-(1+2)), got:\n{}",
927            test_pick_fn
928        );
929    }
930
931    #[test]
932    fn test_small_word_marked_alwaysinline() {
933        // Test Issue #187: Small words get alwaysinline attribute
934        let mut codegen = CodeGen::new();
935
936        let program = Program {
937            includes: vec![],
938            unions: vec![],
939            words: vec![
940                WordDef {
941                    name: "double".to_string(), // Small word: dup i.+
942                    effect: None,
943                    body: vec![
944                        Statement::WordCall {
945                            name: "dup".to_string(),
946                            span: None,
947                        },
948                        Statement::WordCall {
949                            name: "i.+".to_string(),
950                            span: None,
951                        },
952                    ],
953                    source: None,
954                },
955                WordDef {
956                    name: "main".to_string(),
957                    effect: None,
958                    body: vec![
959                        Statement::IntLiteral(21),
960                        Statement::WordCall {
961                            name: "double".to_string(),
962                            span: None,
963                        },
964                    ],
965                    source: None,
966                },
967            ],
968        };
969
970        let ir = codegen
971            .codegen_program(&program, HashMap::new(), HashMap::new())
972            .unwrap();
973
974        // Small word 'double' should have alwaysinline attribute
975        assert!(
976            ir.contains("define tailcc ptr @seq_double(ptr %stack) alwaysinline"),
977            "Small word should have alwaysinline attribute, got:\n{}",
978            ir.lines()
979                .filter(|l| l.contains("define"))
980                .collect::<Vec<_>>()
981                .join("\n")
982        );
983
984        // main should NOT have alwaysinline (uses C calling convention)
985        assert!(
986            ir.contains("define ptr @seq_main(ptr %stack) {"),
987            "main should not have alwaysinline, got:\n{}",
988            ir.lines()
989                .filter(|l| l.contains("define"))
990                .collect::<Vec<_>>()
991                .join("\n")
992        );
993    }
994
995    #[test]
996    fn test_recursive_word_not_inlined() {
997        // Test Issue #187: Recursive words should NOT get alwaysinline
998        let mut codegen = CodeGen::new();
999
1000        let program = Program {
1001            includes: vec![],
1002            unions: vec![],
1003            words: vec![
1004                WordDef {
1005                    name: "countdown".to_string(), // Recursive
1006                    effect: None,
1007                    body: vec![
1008                        Statement::WordCall {
1009                            name: "dup".to_string(),
1010                            span: None,
1011                        },
1012                        Statement::If {
1013                            then_branch: vec![
1014                                Statement::IntLiteral(1),
1015                                Statement::WordCall {
1016                                    name: "i.-".to_string(),
1017                                    span: None,
1018                                },
1019                                Statement::WordCall {
1020                                    name: "countdown".to_string(), // Recursive call
1021                                    span: None,
1022                                },
1023                            ],
1024                            else_branch: Some(vec![]),
1025                        },
1026                    ],
1027                    source: None,
1028                },
1029                WordDef {
1030                    name: "main".to_string(),
1031                    effect: None,
1032                    body: vec![
1033                        Statement::IntLiteral(5),
1034                        Statement::WordCall {
1035                            name: "countdown".to_string(),
1036                            span: None,
1037                        },
1038                    ],
1039                    source: None,
1040                },
1041            ],
1042        };
1043
1044        let ir = codegen
1045            .codegen_program(&program, HashMap::new(), HashMap::new())
1046            .unwrap();
1047
1048        // Recursive word should NOT have alwaysinline
1049        assert!(
1050            ir.contains("define tailcc ptr @seq_countdown(ptr %stack) {"),
1051            "Recursive word should NOT have alwaysinline, got:\n{}",
1052            ir.lines()
1053                .filter(|l| l.contains("define"))
1054                .collect::<Vec<_>>()
1055                .join("\n")
1056        );
1057    }
1058
1059    #[test]
1060    fn test_recursive_word_in_match_not_inlined() {
1061        // Test Issue #187: Recursive calls inside match arms should prevent inlining
1062        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
1063
1064        let mut codegen = CodeGen::new();
1065
1066        let program = Program {
1067            includes: vec![],
1068            unions: vec![UnionDef {
1069                name: "Option".to_string(),
1070                variants: vec![
1071                    UnionVariant {
1072                        name: "Some".to_string(),
1073                        fields: vec![],
1074                        source: None,
1075                    },
1076                    UnionVariant {
1077                        name: "None".to_string(),
1078                        fields: vec![],
1079                        source: None,
1080                    },
1081                ],
1082                source: None,
1083            }],
1084            words: vec![
1085                WordDef {
1086                    name: "process".to_string(), // Recursive in match arm
1087                    effect: None,
1088                    body: vec![Statement::Match {
1089                        arms: vec![
1090                            MatchArm {
1091                                pattern: Pattern::Variant("Some".to_string()),
1092                                body: vec![Statement::WordCall {
1093                                    name: "process".to_string(), // Recursive call
1094                                    span: None,
1095                                }],
1096                            },
1097                            MatchArm {
1098                                pattern: Pattern::Variant("None".to_string()),
1099                                body: vec![],
1100                            },
1101                        ],
1102                    }],
1103                    source: None,
1104                },
1105                WordDef {
1106                    name: "main".to_string(),
1107                    effect: None,
1108                    body: vec![Statement::WordCall {
1109                        name: "process".to_string(),
1110                        span: None,
1111                    }],
1112                    source: None,
1113                },
1114            ],
1115        };
1116
1117        let ir = codegen
1118            .codegen_program(&program, HashMap::new(), HashMap::new())
1119            .unwrap();
1120
1121        // Recursive word (via match arm) should NOT have alwaysinline
1122        assert!(
1123            ir.contains("define tailcc ptr @seq_process(ptr %stack) {"),
1124            "Recursive word in match should NOT have alwaysinline, got:\n{}",
1125            ir.lines()
1126                .filter(|l| l.contains("define"))
1127                .collect::<Vec<_>>()
1128                .join("\n")
1129        );
1130    }
1131}