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