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