gruel_compiler/
lib.rs

1//! Gruel compiler driver.
2//!
3//! This crate orchestrates the compilation pipeline:
4//! Source -> Lexer -> Parser -> AstGen -> Sema -> CodeGen -> ELF
5//!
6//! It re-exports types from the component crates for convenience.
7//!
8//! # Diagnostic Formatting
9//!
10//! The [`MultiFileFormatter`] provides a clean API for formatting errors and warnings:
11//!
12//! ```ignore
13//! use gruel_compiler::{MultiFileFormatter, SourceInfo, FileId};
14//!
15//! let sources = vec![(FileId::new(1), SourceInfo::new(&source, "example.gruel"))];
16//! let formatter = MultiFileFormatter::new(sources);
17//!
18//! // Format an error
19//! let error_output = formatter.format_error(&error);
20//! eprintln!("{}", error_output);
21//! ```
22//!
23//! # Tracing
24//!
25//! This crate is instrumented with `tracing` spans for performance analysis.
26//! Use `--log-level info` or `--time-passes` to see timing information.
27
28mod diagnostic;
29mod drop_glue;
30mod unit;
31
32pub use unit::CompilationUnit;
33
34use rayon::prelude::*;
35use tracing::{info, info_span};
36
37pub use diagnostic::{
38    ColorChoice, JsonDiagnostic, JsonSpan, JsonSuggestion, MultiFileFormatter,
39    MultiFileJsonFormatter, SourceInfo,
40};
41
42use std::io::Write;
43use std::path::PathBuf;
44use std::process::Command;
45use std::sync::atomic::{AtomicU64, Ordering};
46
47// ============================================================================
48// Error Helper Functions
49// ============================================================================
50
51/// Convert an I/O result into a `CompileResult` with a contextual message.
52///
53/// This helper wraps `std::io::Error` with a descriptive message explaining
54/// what operation failed.
55///
56/// # Example
57/// ```ignore
58/// std::fs::create_dir_all(&path).map_err(|e| io_link_error("failed to create temp directory", e))?;
59/// ```
60fn io_link_error(context: &str, err: std::io::Error) -> CompileError {
61    CompileError::without_span(ErrorKind::LinkError(format!("{}: {}", context, err)))
62}
63
64/// Counter for generating unique temp directory names.
65static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
66
67/// A temporary directory for linking that automatically cleans up on drop.
68///
69/// This struct manages the creation of a unique temporary directory for the
70/// linking process and automatically removes it when dropped (whether via
71/// normal completion or early error return).
72struct TempLinkDir {
73    /// Path to the temporary directory.
74    path: PathBuf,
75    /// Paths to the object files written to the directory.
76    obj_paths: Vec<PathBuf>,
77    /// Path to the runtime archive in the directory.
78    runtime_path: PathBuf,
79    /// Path where the linked executable will be written.
80    output_path: PathBuf,
81}
82
83impl TempLinkDir {
84    /// Create a new temporary directory for linking.
85    ///
86    /// Creates a unique directory in the system temp directory with the
87    /// format `gruel-<pid>-<counter>` to ensure uniqueness even in parallel
88    /// test execution.
89    fn new() -> CompileResult<Self> {
90        let unique_id = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
91        let path = std::env::temp_dir().join(format!("gruel-{}-{}", std::process::id(), unique_id));
92        std::fs::create_dir_all(&path)
93            .map_err(|e| io_link_error("failed to create temp directory", e))?;
94
95        let runtime_path = path.join("libgruel_runtime.a");
96        let output_path = path.join("output");
97
98        Ok(Self {
99            path,
100            obj_paths: Vec::new(),
101            runtime_path,
102            output_path,
103        })
104    }
105
106    /// Write object files to the temporary directory.
107    ///
108    /// Each object file is written to a file named `obj{N}.o` where N is
109    /// the index. The paths are stored in `self.obj_paths`.
110    fn write_object_files(&mut self, object_files: &[Vec<u8>]) -> CompileResult<()> {
111        for (i, obj_bytes) in object_files.iter().enumerate() {
112            let obj_path = self.path.join(format!("obj{}.o", i));
113            let mut file = std::fs::File::create(&obj_path)
114                .map_err(|e| io_link_error("failed to create temp object file", e))?;
115            file.write_all(obj_bytes)
116                .map_err(|e| io_link_error("failed to write temp object file", e))?;
117            self.obj_paths.push(obj_path);
118        }
119        Ok(())
120    }
121
122    /// Write the runtime archive to the temporary directory.
123    fn write_runtime(&self, runtime_bytes: &[u8]) -> CompileResult<()> {
124        std::fs::write(&self.runtime_path, runtime_bytes)
125            .map_err(|e| io_link_error("failed to write runtime archive", e))
126    }
127
128    /// Read the linked executable from the output path.
129    fn read_output(&self) -> CompileResult<Vec<u8>> {
130        std::fs::read(&self.output_path)
131            .map_err(|e| io_link_error("failed to read linked executable", e))
132    }
133}
134
135impl Drop for TempLinkDir {
136    fn drop(&mut self) {
137        // Best-effort cleanup; ignore errors
138        let _ = std::fs::remove_dir_all(&self.path);
139    }
140}
141
142/// The gruel-runtime staticlib archive bytes, embedded at compile time.
143/// This is linked into every Gruel executable.
144static RUNTIME_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libgruel_runtime.a"));
145
146// Re-export commonly used types
147pub use gruel_air::{Air, AnalyzedFunction, Sema, SemaOutput, StructDef, Type, TypeInternPool};
148pub use gruel_cfg::{Cfg, CfgBuilder, CfgOutput, OptLevel};
149pub use gruel_error::{
150    Applicability, CompileError, CompileErrors, CompileResult, CompileWarning, Diagnostic,
151    ErrorCode, ErrorKind, MultiErrorResult, PreviewFeature, PreviewFeatures, Suggestion,
152    WarningKind,
153};
154pub use gruel_lexer::{Lexer, Token, TokenKind};
155pub use gruel_parser::{Ast, Expr, Function, Item, Parser};
156pub use gruel_rir::{AstGen, Rir, RirPrinter};
157pub use gruel_span::{FileId, Span};
158pub use gruel_target::{Arch, Target};
159pub use lasso::{Spur, ThreadedRodeo};
160
161// ============================================================================
162// Multi-file Compilation Types
163// ============================================================================
164
165/// A source file with its path and content.
166///
167/// Used for multi-file compilation to associate source content with file paths.
168#[derive(Debug, Clone)]
169pub struct SourceFile<'a> {
170    /// Path to the source file (used for error messages).
171    pub path: &'a str,
172    /// Source code content.
173    pub source: &'a str,
174    /// Unique identifier for this file.
175    pub file_id: FileId,
176}
177
178impl<'a> SourceFile<'a> {
179    /// Create a new source file.
180    pub fn new(path: &'a str, source: &'a str, file_id: FileId) -> Self {
181        Self {
182            path,
183            source,
184            file_id,
185        }
186    }
187}
188
189/// Result of parsing a single file.
190///
191/// Contains the AST and interner from parsing. The interner contains all
192/// string literals and identifiers interned during lexing.
193#[derive(Debug)]
194pub struct ParsedFile {
195    /// Path to the source file.
196    pub path: String,
197    /// File identifier for error reporting.
198    pub file_id: FileId,
199    /// The parsed abstract syntax tree.
200    pub ast: Ast,
201    /// String interner from lexing.
202    pub interner: ThreadedRodeo,
203}
204
205/// Result of parsing all source files.
206///
207/// Contains all parsed files and a merged interner for use in later phases.
208#[derive(Debug)]
209pub struct ParsedProgram {
210    /// Parsed files with their ASTs.
211    pub files: Vec<ParsedFile>,
212    /// Merged interner containing all symbols from all files.
213    pub interner: ThreadedRodeo,
214}
215
216/// Parse multiple source files with a shared interner.
217///
218/// Each file is lexed and parsed sequentially with a single shared interner.
219/// This ensures Spur values are consistent across all files, enabling cross-file
220/// symbol resolution during semantic analysis.
221///
222/// Note: This uses sequential parsing rather than parallel to share the interner.
223/// A future optimization could add parallel parsing with interner merging and
224/// AST Spur remapping.
225///
226/// # Arguments
227///
228/// * `sources` - Slice of source files to parse
229///
230/// # Returns
231///
232/// A `ParsedProgram` containing all parsed files and the shared interner,
233/// or errors from any file that failed to parse.
234///
235/// # Example
236///
237/// ```ignore
238/// use gruel_compiler::{SourceFile, parse_all_files};
239/// use gruel_span::FileId;
240///
241/// let sources = vec![
242///     SourceFile::new("main.gruel", "fn main() -> i32 { 0 }", FileId::new(1)),
243///     SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
244/// ];
245/// let program = parse_all_files(&sources)?;
246/// ```
247pub fn parse_all_files(sources: &[SourceFile<'_>]) -> MultiErrorResult<ParsedProgram> {
248    // Parse all files sequentially with a shared interner
249    // This ensures Spur values are consistent across files for cross-file symbol resolution
250    let mut parsed_files = Vec::with_capacity(sources.len());
251    let mut interner = ThreadedRodeo::new();
252
253    for source in sources {
254        // Create lexer with shared interner and file ID for proper error reporting
255        let lexer = Lexer::with_interner_and_file_id(source.source, interner, source.file_id);
256
257        // Tokenize - propagate error immediately (interner is consumed)
258        let (tokens, returned_interner) = lexer.tokenize().map_err(CompileErrors::from)?;
259        interner = returned_interner;
260
261        // Parse the tokens - propagate error immediately (interner is consumed)
262        let parser = Parser::new(tokens, interner);
263        let (ast, returned_interner) = parser.parse()?;
264        interner = returned_interner;
265
266        parsed_files.push(ParsedFile {
267            path: source.path.to_string(),
268            file_id: source.file_id,
269            ast,
270            // Note: interner is shared, but we store a dummy here for API compatibility
271            // The real interner is in the returned ParsedProgram
272            interner: ThreadedRodeo::new(),
273        });
274    }
275
276    Ok(ParsedProgram {
277        files: parsed_files,
278        interner,
279    })
280}
281
282/// Result of merging symbols from multiple parsed files.
283///
284/// Contains a merged AST with all items from all files and the merged interner.
285/// Used as input to RIR generation for multi-file compilation.
286#[derive(Debug)]
287pub struct MergedProgram {
288    /// The merged AST containing items from all files.
289    pub ast: Ast,
290    /// Merged interner containing all symbols from all files.
291    pub interner: ThreadedRodeo,
292}
293
294/// Result of validating and generating RIR from multiple parsed files.
295///
296/// This is the parallel-optimized path: RIR is generated per-file in parallel,
297/// then merged. Used by `compile_multi_file_with_options`.
298pub struct ValidatedProgram {
299    /// The merged RIR from all files.
300    pub rir: Rir,
301    /// Merged interner containing all symbols from all files.
302    pub interner: ThreadedRodeo,
303    /// Maps FileId to source file path (for module resolution).
304    pub file_paths: std::collections::HashMap<FileId, String>,
305}
306
307/// Information about a symbol definition for duplicate detection.
308#[derive(Debug, Clone)]
309struct SymbolDef {
310    /// Span of the first definition.
311    span: Span,
312    /// File path where the first definition was found.
313    file_path: String,
314}
315
316/// Merge symbols from all parsed files into a unified program.
317///
318/// This function:
319/// 1. Combines all items from all files into a single merged AST
320/// 2. Detects duplicate function, struct, and enum definitions
321/// 3. Reports errors with both locations for any duplicates found
322///
323/// # Arguments
324///
325/// * `program` - The parsed program containing all files
326///
327/// # Returns
328///
329/// A `MergedProgram` ready for RIR generation, or errors if duplicates are found.
330///
331/// # Example
332///
333/// ```ignore
334/// use gruel_compiler::{parse_all_files, merge_symbols, SourceFile};
335/// use gruel_span::FileId;
336///
337/// let sources = vec![
338///     SourceFile::new("main.gruel", "fn main() -> i32 { helper() }", FileId::new(1)),
339///     SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
340/// ];
341/// let parsed = parse_all_files(&sources)?;
342/// let merged = merge_symbols(parsed)?;
343/// // merged.ast now contains both functions
344/// ```
345pub fn merge_symbols(program: ParsedProgram) -> MultiErrorResult<MergedProgram> {
346    use std::collections::HashMap;
347
348    let _span = info_span!("merge_symbols", file_count = program.files.len()).entered();
349
350    // Track seen symbols for duplicate detection.
351    // Key: symbol name (resolved string), Value: first definition info
352    let mut functions: HashMap<String, SymbolDef> = HashMap::new();
353    let mut structs: HashMap<String, SymbolDef> = HashMap::new();
354    let mut enums: HashMap<String, SymbolDef> = HashMap::new();
355
356    // Collect all items and detect duplicates
357    let mut all_items = Vec::new();
358    let mut errors: Vec<CompileError> = Vec::new();
359
360    // Use the shared interner for resolving all symbol names
361    let interner = &program.interner;
362
363    for file in &program.files {
364        for item in &file.ast.items {
365            match item {
366                Item::Function(func) => {
367                    // Use shared interner for consistent Spur resolution
368                    let name = interner.resolve(&func.name.name).to_string();
369                    if let Some(first) = functions.get(&name) {
370                        // Duplicate function definition
371                        let err = CompileError::new(
372                            ErrorKind::DuplicateTypeDefinition {
373                                type_name: format!("function `{}`", name),
374                            },
375                            func.span,
376                        )
377                        .with_label(format!("first defined in {}", first.file_path), first.span);
378                        errors.push(err);
379                    } else {
380                        functions.insert(
381                            name.clone(),
382                            SymbolDef {
383                                span: func.span,
384                                file_path: file.path.clone(),
385                            },
386                        );
387                    }
388                }
389                Item::Struct(s) => {
390                    // Use shared interner for consistent Spur resolution
391                    let name = interner.resolve(&s.name.name).to_string();
392                    if let Some(first) = structs.get(&name) {
393                        // Duplicate struct definition
394                        let err = CompileError::new(
395                            ErrorKind::DuplicateTypeDefinition {
396                                type_name: format!("struct `{}`", name),
397                            },
398                            s.span,
399                        )
400                        .with_label(format!("first defined in {}", first.file_path), first.span);
401                        errors.push(err);
402                    } else if let Some(first) = enums.get(&name) {
403                        // Struct name conflicts with enum
404                        let err = CompileError::new(
405                            ErrorKind::DuplicateTypeDefinition {
406                                type_name: format!("struct `{}` (conflicts with enum)", name),
407                            },
408                            s.span,
409                        )
410                        .with_label(
411                            format!("enum first defined in {}", first.file_path),
412                            first.span,
413                        );
414                        errors.push(err);
415                    } else {
416                        structs.insert(
417                            name.clone(),
418                            SymbolDef {
419                                span: s.span,
420                                file_path: file.path.clone(),
421                            },
422                        );
423                    }
424                }
425                Item::Enum(e) => {
426                    // Use shared interner for consistent Spur resolution
427                    let name = interner.resolve(&e.name.name).to_string();
428                    if let Some(first) = enums.get(&name) {
429                        // Duplicate enum definition
430                        let err = CompileError::new(
431                            ErrorKind::DuplicateTypeDefinition {
432                                type_name: format!("enum `{}`", name),
433                            },
434                            e.span,
435                        )
436                        .with_label(format!("first defined in {}", first.file_path), first.span);
437                        errors.push(err);
438                    } else if let Some(first) = structs.get(&name) {
439                        // Enum name conflicts with struct
440                        let err = CompileError::new(
441                            ErrorKind::DuplicateTypeDefinition {
442                                type_name: format!("enum `{}` (conflicts with struct)", name),
443                            },
444                            e.span,
445                        )
446                        .with_label(
447                            format!("struct first defined in {}", first.file_path),
448                            first.span,
449                        );
450                        errors.push(err);
451                    } else {
452                        enums.insert(
453                            name.clone(),
454                            SymbolDef {
455                                span: e.span,
456                                file_path: file.path.clone(),
457                            },
458                        );
459                    }
460                }
461                Item::DropFn(_) | Item::Const(_) => {
462                    // Drop fns and const declarations are validated in Sema, not here.
463                    // Const declarations are checked for duplicates in the declarations phase.
464                }
465                Item::Error(_) => {
466                    // Error nodes from parser recovery are skipped - errors were already reported
467                }
468            }
469            all_items.push(item.clone());
470        }
471    }
472
473    // If there are any duplicate definitions, return all errors
474    if !errors.is_empty() {
475        return Err(CompileErrors::from(errors));
476    }
477
478    info!(
479        function_count = functions.len(),
480        struct_count = structs.len(),
481        enum_count = enums.len(),
482        "symbol merging complete"
483    );
484
485    Ok(MergedProgram {
486        ast: Ast { items: all_items },
487        interner: program.interner,
488    })
489}
490
491/// Validate symbols and generate RIR in parallel for multi-file compilation.
492///
493/// This is the optimized path for multi-file compilation:
494/// 1. Validates that there are no duplicate symbol definitions across files
495/// 2. Generates RIR for each file in parallel using Rayon
496/// 3. Merges the per-file RIRs into a single RIR with renumbered references
497///
498/// This is more efficient than the sequential path for projects with many files,
499/// as RIR generation is embarrassingly parallel (no cross-file dependencies
500/// at the RIR level).
501///
502/// # Arguments
503///
504/// * `program` - The parsed program containing all files and shared interner
505///
506/// # Returns
507///
508/// A `ValidatedProgram` containing the merged RIR, or errors if duplicates are found.
509pub fn validate_and_generate_rir_parallel(
510    program: ParsedProgram,
511) -> MultiErrorResult<ValidatedProgram> {
512    use std::collections::HashMap;
513
514    let _span = info_span!(
515        "validate_and_generate_rir",
516        file_count = program.files.len()
517    )
518    .entered();
519
520    // Step 0: Build file_id -> path mapping for module resolution
521    let file_paths: HashMap<FileId, String> = program
522        .files
523        .iter()
524        .map(|f| (f.file_id, f.path.clone()))
525        .collect();
526
527    // Step 1: Validate symbols for duplicates (same logic as merge_symbols)
528    let mut functions: HashMap<String, SymbolDef> = HashMap::new();
529    let mut structs: HashMap<String, SymbolDef> = HashMap::new();
530    let mut enums: HashMap<String, SymbolDef> = HashMap::new();
531    let mut errors: Vec<CompileError> = Vec::new();
532
533    let interner = &program.interner;
534
535    for file in &program.files {
536        for item in &file.ast.items {
537            match item {
538                Item::Function(func) => {
539                    let name = interner.resolve(&func.name.name).to_string();
540                    if let Some(first) = functions.get(&name) {
541                        let err = CompileError::new(
542                            ErrorKind::DuplicateTypeDefinition {
543                                type_name: format!("function `{}`", name),
544                            },
545                            func.span,
546                        )
547                        .with_label(format!("first defined in {}", first.file_path), first.span);
548                        errors.push(err);
549                    } else {
550                        functions.insert(
551                            name.clone(),
552                            SymbolDef {
553                                span: func.span,
554                                file_path: file.path.clone(),
555                            },
556                        );
557                    }
558                }
559                Item::Struct(s) => {
560                    let name = interner.resolve(&s.name.name).to_string();
561                    if let Some(first) = structs.get(&name) {
562                        let err = CompileError::new(
563                            ErrorKind::DuplicateTypeDefinition {
564                                type_name: format!("struct `{}`", name),
565                            },
566                            s.span,
567                        )
568                        .with_label(format!("first defined in {}", first.file_path), first.span);
569                        errors.push(err);
570                    } else if let Some(first) = enums.get(&name) {
571                        let err = CompileError::new(
572                            ErrorKind::DuplicateTypeDefinition {
573                                type_name: format!("struct `{}` (conflicts with enum)", name),
574                            },
575                            s.span,
576                        )
577                        .with_label(
578                            format!("enum first defined in {}", first.file_path),
579                            first.span,
580                        );
581                        errors.push(err);
582                    } else {
583                        structs.insert(
584                            name.clone(),
585                            SymbolDef {
586                                span: s.span,
587                                file_path: file.path.clone(),
588                            },
589                        );
590                    }
591                }
592                Item::Enum(e) => {
593                    let name = interner.resolve(&e.name.name).to_string();
594                    if let Some(first) = enums.get(&name) {
595                        let err = CompileError::new(
596                            ErrorKind::DuplicateTypeDefinition {
597                                type_name: format!("enum `{}`", name),
598                            },
599                            e.span,
600                        )
601                        .with_label(format!("first defined in {}", first.file_path), first.span);
602                        errors.push(err);
603                    } else if let Some(first) = structs.get(&name) {
604                        let err = CompileError::new(
605                            ErrorKind::DuplicateTypeDefinition {
606                                type_name: format!("enum `{}` (conflicts with struct)", name),
607                            },
608                            e.span,
609                        )
610                        .with_label(
611                            format!("struct first defined in {}", first.file_path),
612                            first.span,
613                        );
614                        errors.push(err);
615                    } else {
616                        enums.insert(
617                            name.clone(),
618                            SymbolDef {
619                                span: e.span,
620                                file_path: file.path.clone(),
621                            },
622                        );
623                    }
624                }
625                Item::DropFn(_) | Item::Const(_) => {
626                    // Validated in Sema
627                }
628                Item::Error(_) => {
629                    // Error nodes from parser recovery are skipped
630                }
631            }
632        }
633    }
634
635    if !errors.is_empty() {
636        return Err(CompileErrors::from(errors));
637    }
638
639    info!(
640        function_count = functions.len(),
641        struct_count = structs.len(),
642        enum_count = enums.len(),
643        "symbol validation complete"
644    );
645
646    // Step 2: Generate RIR per-file in parallel
647    let interner = program.interner;
648    let rirs: Vec<Rir> = {
649        let _span = info_span!("parallel_astgen").entered();
650
651        program
652            .files
653            .par_iter()
654            .map(|file| {
655                let astgen = AstGen::new(&file.ast, &interner);
656                astgen.generate()
657            })
658            .collect()
659    };
660
661    // Step 3: Merge RIRs
662    let rir = {
663        let _span = info_span!("merge_rirs", rir_count = rirs.len()).entered();
664        Rir::merge(&rirs)
665    };
666
667    info!(instruction_count = rir.len(), "RIR generation complete");
668
669    Ok(ValidatedProgram {
670        rir,
671        interner,
672        file_paths,
673    })
674}
675
676/// Which linker to use for the final linking phase.
677///
678/// The Gruel compiler can either use its built-in ELF linker or delegate to
679/// an external system linker like `clang`, `gcc`, or `ld`.
680#[derive(Debug, Clone, PartialEq, Eq, Default)]
681pub enum LinkerMode {
682    /// Use the internal linker (default).
683    #[default]
684    Internal,
685    /// Use an external system linker (e.g., "clang", "ld", "gcc").
686    System(String),
687}
688
689/// Configuration options for compilation.
690///
691/// Controls target architecture, linker selection, optimization level, and feature flags.
692///
693/// # Example
694///
695/// ```ignore
696/// let options = CompileOptions {
697///     target: Target::host(),
698///     linker: LinkerMode::System("cc".to_string()),
699///     opt_level: OptLevel::O1,
700///     preview_features: PreviewFeatures::new(),
701///     jobs: 0, // 0 = auto-detect
702/// };
703/// let output = compile_with_options(source, &options)?;
704/// ```
705#[derive(Debug, Clone)]
706pub struct CompileOptions {
707    /// The target architecture and OS.
708    pub target: Target,
709    /// Which linker to use.
710    pub linker: LinkerMode,
711    /// Optimization level.
712    pub opt_level: OptLevel,
713    /// Enabled preview features.
714    pub preview_features: PreviewFeatures,
715    /// Number of parallel jobs (0 = auto-detect, use all cores).
716    pub jobs: usize,
717    /// When true, suppress the on-the-fly stderr print of comptime `@dbg`
718    /// output. The output is still collected on `SemaOutput.comptime_dbg_output`
719    /// and a warning is still emitted for each call. Used by fuzz harnesses.
720    pub capture_comptime_dbg: bool,
721}
722
723impl Default for CompileOptions {
724    fn default() -> Self {
725        Self {
726            target: Target::host(),
727            linker: LinkerMode::Internal,
728            opt_level: OptLevel::default(),
729            preview_features: PreviewFeatures::new(),
730            jobs: 0,
731            capture_comptime_dbg: false,
732        }
733    }
734}
735
736/// A function with its typed IR (AIR) and control flow graph (CFG).
737///
738/// This combines the output of semantic analysis with CFG construction.
739#[derive(Debug)]
740pub struct FunctionWithCfg {
741    /// The analyzed function from semantic analysis.
742    pub analyzed: AnalyzedFunction,
743    /// The control flow graph built from the AIR.
744    pub cfg: Cfg,
745}
746
747/// Intermediate compilation state after frontend processing.
748///
749/// This allows inspection of the IR at each stage, useful for
750/// debugging and the `--emit` CLI flags.
751pub struct CompileState {
752    /// The abstract syntax tree.
753    pub ast: Ast,
754    /// String interner used during compilation.
755    pub interner: ThreadedRodeo,
756    /// The untyped IR (RIR).
757    pub rir: Rir,
758    /// Analyzed functions with typed IR and control flow graphs.
759    pub functions: Vec<FunctionWithCfg>,
760    /// Type intern pool containing all struct and enum definitions.
761    pub type_pool: TypeInternPool,
762    /// String literals indexed by their string_const index.
763    pub strings: Vec<String>,
764    /// Warnings collected during compilation.
765    pub warnings: Vec<CompileWarning>,
766    /// Lines of `@dbg` output collected during comptime evaluation.
767    pub comptime_dbg_output: Vec<String>,
768}
769
770/// Output from successful compilation.
771///
772/// Contains the compiled executable binary and any warnings generated
773/// during compilation. The binary format depends on the target platform
774/// (ELF for Linux, Mach-O for macOS).
775#[derive(Debug)]
776pub struct CompileOutput {
777    /// The compiled ELF binary.
778    pub elf: Vec<u8>,
779    /// Warnings generated during compilation.
780    pub warnings: Vec<CompileWarning>,
781}
782
783/// Compile source code through all frontend phases (up to but not including codegen).
784///
785/// This runs: lexing → parsing → AST to RIR → semantic analysis → CFG construction.
786/// Returns the compile state which can be inspected for debugging.
787///
788/// This function collects errors from multiple functions instead of stopping at the
789/// first error, allowing users to see all issues at once.
790///
791/// Uses default optimization level (O0) and no preview features. For custom options,
792/// use [`compile_frontend_with_options`].
793pub fn compile_frontend(source: &str) -> MultiErrorResult<CompileState> {
794    compile_frontend_with_options(source, &PreviewFeatures::new())
795}
796
797/// Compile source code through all frontend phases.
798///
799/// This runs: lexing → parsing → AST to RIR → semantic analysis → CFG construction.
800/// Returns the compile state which can be inspected for debugging.
801///
802/// This function collects errors from multiple functions instead of stopping at the
803/// first error, allowing users to see all issues at once.
804pub fn compile_frontend_with_options(
805    source: &str,
806    preview_features: &PreviewFeatures,
807) -> MultiErrorResult<CompileState> {
808    compile_frontend_with_options_full(source, preview_features, false)
809}
810
811/// Like [`compile_frontend_with_options`], but additionally lets the caller
812/// suppress the on-the-fly stderr print of comptime `@dbg` output. When
813/// `suppress_comptime_dbg_print` is true, output is only collected in the
814/// `SemaOutput.comptime_dbg_output` buffer. Used by fuzz harnesses that
815/// consume the buffer directly and don't want noise on stderr.
816pub fn compile_frontend_with_options_full(
817    source: &str,
818    preview_features: &PreviewFeatures,
819    suppress_comptime_dbg_print: bool,
820) -> MultiErrorResult<CompileState> {
821    let _span = info_span!("frontend", source_bytes = source.len()).entered();
822
823    // Lexing - errors here are fatal (can't continue without tokens)
824    let (tokens, interner) = {
825        let _span = info_span!("lexer").entered();
826        let lexer = Lexer::new(source);
827        let (tokens, interner) = lexer.tokenize().map_err(CompileErrors::from)?;
828        info!(token_count = tokens.len(), "lexing complete");
829        (tokens, interner)
830    };
831
832    // Parsing - errors here are fatal (can't continue without AST)
833    let (ast, interner) = {
834        let _span = info_span!("parser").entered();
835        let parser = Parser::new(tokens, interner);
836        let (ast, interner) = parser.parse()?;
837        info!(item_count = ast.items.len(), "parsing complete");
838        (ast, interner)
839    };
840
841    compile_frontend_from_ast_with_options_full(
842        ast,
843        interner,
844        preview_features,
845        suppress_comptime_dbg_print,
846    )
847}
848
849/// Compile from an already-parsed AST through all remaining frontend phases.
850///
851/// This runs: AST to RIR → semantic analysis → CFG construction.
852/// Use this when you already have a parsed AST (e.g., for `--emit` modes that
853/// need both AST output and later stage output without double-parsing).
854///
855/// Uses no preview features. For custom options, use [`compile_frontend_from_ast_with_options`].
856pub fn compile_frontend_from_ast(
857    ast: Ast,
858    interner: ThreadedRodeo,
859) -> MultiErrorResult<CompileState> {
860    compile_frontend_from_ast_with_options(ast, interner, &PreviewFeatures::new())
861}
862
863/// Compile from an already-parsed AST through all remaining frontend phases.
864///
865/// This runs: AST to RIR → semantic analysis → CFG construction.
866/// Use this when you already have a parsed AST (e.g., for `--emit` modes that
867/// need both AST output and later stage output without double-parsing).
868///
869/// This function collects errors from multiple functions instead of stopping at the
870/// first error, allowing users to see all issues at once.
871pub fn compile_frontend_from_ast_with_options(
872    ast: Ast,
873    interner: ThreadedRodeo,
874    preview_features: &PreviewFeatures,
875) -> MultiErrorResult<CompileState> {
876    compile_frontend_from_ast_with_options_full(ast, interner, preview_features, false)
877}
878
879/// Like [`compile_frontend_from_ast_with_options`], but additionally lets the
880/// caller suppress the on-the-fly stderr print of comptime `@dbg` output.
881pub fn compile_frontend_from_ast_with_options_full(
882    ast: Ast,
883    interner: ThreadedRodeo,
884    preview_features: &PreviewFeatures,
885    suppress_comptime_dbg_print: bool,
886) -> MultiErrorResult<CompileState> {
887    // AST to RIR (untyped IR)
888    let (rir, interner) = {
889        let _span = info_span!("astgen").entered();
890        let astgen = AstGen::new(&ast, &interner);
891        let rir = astgen.generate();
892        info!(instruction_count = rir.len(), "AST generation complete");
893        (rir, interner)
894    };
895
896    // Semantic analysis (RIR to AIR) - this now collects multiple errors
897    let sema_output = {
898        let _span = info_span!("sema").entered();
899        let mut sema = Sema::new(&rir, &interner, preview_features.clone());
900        sema.set_suppress_comptime_dbg_print(suppress_comptime_dbg_print);
901        let output = sema.analyze_all()?;
902        info!(
903            function_count = output.functions.len(),
904            struct_count = output.type_pool.stats().struct_count,
905            "semantic analysis complete"
906        );
907        output
908    };
909
910    // Synthesize drop glue functions for structs that need them
911    let drop_glue_functions = drop_glue::synthesize_drop_glue(&sema_output.type_pool, &interner);
912
913    // Combine user functions with synthesized drop glue functions
914    // Filter out comptime-only functions (those returning `type`) as they don't generate runtime code
915    let all_functions: Vec<_> = sema_output
916        .functions
917        .into_iter()
918        .filter(|f| f.air.return_type() != Type::COMPTIME_TYPE)
919        .chain(drop_glue_functions)
920        .collect();
921
922    // Build CFGs from AIR (one per function) in parallel, collecting warnings
923    let (functions, warnings) = {
924        let _span = info_span!("cfg_construction").entered();
925
926        // Build CFGs in parallel - each function is independent
927        let results: Vec<(FunctionWithCfg, Vec<CompileWarning>)> = all_functions
928            .into_par_iter()
929            .map(|func| {
930                let cfg_output = CfgBuilder::build(&func, &sema_output.type_pool);
931
932                (
933                    FunctionWithCfg {
934                        analyzed: func,
935                        cfg: cfg_output.cfg,
936                    },
937                    cfg_output.warnings,
938                )
939            })
940            .collect();
941
942        // Unzip the results and collect all warnings
943        let mut functions = Vec::with_capacity(results.len());
944        let mut warnings = sema_output.warnings;
945        for (func, func_warnings) in results {
946            functions.push(func);
947            warnings.extend(func_warnings);
948        }
949
950        info!(
951            function_count = functions.len(),
952            "CFG construction complete"
953        );
954        (functions, warnings)
955    };
956
957    Ok(CompileState {
958        ast,
959        interner,
960        rir,
961        functions,
962        type_pool: sema_output.type_pool,
963        strings: sema_output.strings,
964        warnings,
965        comptime_dbg_output: sema_output.comptime_dbg_output,
966    })
967}
968
969/// Compile from already-generated RIR through remaining frontend phases.
970///
971/// This runs: semantic analysis → CFG construction → optimization.
972/// This is the optimized path used by parallel multi-file compilation, where
973/// RIR has already been generated per-file in parallel and merged.
974///
975/// # Arguments
976///
977/// * `rir` - The RIR (already merged if from multiple files)
978/// * `interner` - The shared string interner
979/// * `opt_level` - Optimization level
980/// * `preview_features` - Enabled preview features
981///
982/// # Returns
983///
984/// A `CompileStateFromRir` containing the compilation state.
985pub fn compile_frontend_from_rir_with_options(
986    rir: Rir,
987    interner: ThreadedRodeo,
988    preview_features: &PreviewFeatures,
989) -> MultiErrorResult<CompileStateFromRir> {
990    compile_frontend_from_rir_with_file_paths(
991        rir,
992        interner,
993        preview_features,
994        std::collections::HashMap::new(),
995    )
996}
997
998/// Compile frontend from RIR with file paths for module resolution.
999///
1000/// This is the full version that accepts file_id -> path mapping for
1001/// multi-file compilation with module imports.
1002pub fn compile_frontend_from_rir_with_file_paths(
1003    rir: Rir,
1004    interner: ThreadedRodeo,
1005    preview_features: &PreviewFeatures,
1006    file_paths: std::collections::HashMap<FileId, String>,
1007) -> MultiErrorResult<CompileStateFromRir> {
1008    // Semantic analysis (RIR to AIR)
1009    let sema_output = {
1010        let _span = info_span!("sema").entered();
1011        let mut sema = Sema::new(&rir, &interner, preview_features.clone());
1012        sema.set_file_paths(file_paths);
1013        let output = sema.analyze_all()?;
1014        info!(
1015            function_count = output.functions.len(),
1016            struct_count = output.type_pool.stats().struct_count,
1017            "semantic analysis complete"
1018        );
1019        output
1020    };
1021
1022    // Synthesize drop glue functions for structs that need them
1023    let drop_glue_functions = drop_glue::synthesize_drop_glue(&sema_output.type_pool, &interner);
1024
1025    // Combine user functions with synthesized drop glue functions
1026    // Filter out comptime-only functions (those returning `type`) as they don't generate runtime code
1027    let all_functions: Vec<_> = sema_output
1028        .functions
1029        .into_iter()
1030        .filter(|f| f.air.return_type() != Type::COMPTIME_TYPE)
1031        .chain(drop_glue_functions)
1032        .collect();
1033
1034    // Build CFGs from AIR (one per function) in parallel, collecting warnings
1035    let (functions, warnings) = {
1036        let _span = info_span!("cfg_construction").entered();
1037
1038        // Build CFGs in parallel - each function is independent
1039        let results: Vec<(FunctionWithCfg, Vec<CompileWarning>)> = all_functions
1040            .into_par_iter()
1041            .map(|func| {
1042                let cfg_output = CfgBuilder::build(&func, &sema_output.type_pool);
1043
1044                (
1045                    FunctionWithCfg {
1046                        analyzed: func,
1047                        cfg: cfg_output.cfg,
1048                    },
1049                    cfg_output.warnings,
1050                )
1051            })
1052            .collect();
1053
1054        // Unzip the results and collect all warnings
1055        let mut functions = Vec::with_capacity(results.len());
1056        let mut warnings = sema_output.warnings;
1057        for (func, func_warnings) in results {
1058            functions.push(func);
1059            warnings.extend(func_warnings);
1060        }
1061
1062        info!(
1063            function_count = functions.len(),
1064            "CFG construction complete"
1065        );
1066        (functions, warnings)
1067    };
1068
1069    Ok(CompileStateFromRir {
1070        interner,
1071        rir,
1072        functions,
1073        type_pool: sema_output.type_pool,
1074        strings: sema_output.strings,
1075        warnings,
1076        comptime_dbg_output: sema_output.comptime_dbg_output,
1077    })
1078}
1079
1080/// Intermediate compilation state after frontend processing from RIR.
1081///
1082/// Similar to `CompileState` but without the AST (since we started from RIR directly
1083/// in the parallel compilation path).
1084pub struct CompileStateFromRir {
1085    /// String interner used during compilation.
1086    pub interner: ThreadedRodeo,
1087    /// The untyped IR (RIR).
1088    pub rir: Rir,
1089    /// Analyzed functions with typed IR and control flow graphs.
1090    pub functions: Vec<FunctionWithCfg>,
1091    /// Type intern pool containing all struct and enum definitions.
1092    pub type_pool: TypeInternPool,
1093    /// String literals indexed by their string_const index.
1094    pub strings: Vec<String>,
1095    /// Warnings collected during compilation.
1096    pub warnings: Vec<CompileWarning>,
1097    /// Lines of `@dbg` output collected during comptime evaluation.
1098    pub comptime_dbg_output: Vec<String>,
1099}
1100
1101/// Compile source code to an ELF binary.
1102///
1103/// This is the main entry point for compilation.
1104/// Returns the ELF binary along with any warnings generated during compilation.
1105///
1106/// This function collects errors from multiple functions instead of stopping at the
1107/// first error, allowing users to see all issues at once.
1108pub fn compile(source: &str) -> MultiErrorResult<CompileOutput> {
1109    compile_with_options(source, &CompileOptions::default())
1110}
1111
1112/// Compile source code to an ELF binary with the given options.
1113///
1114/// This allows specifying the target architecture, optimization level, and other compilation options.
1115///
1116/// This function collects errors from multiple functions instead of stopping at the
1117/// first error, allowing users to see all issues at once.
1118pub fn compile_with_options(
1119    source: &str,
1120    options: &CompileOptions,
1121) -> MultiErrorResult<CompileOutput> {
1122    // Delegate to multi-file compilation with a single source file
1123    let sources = vec![SourceFile::new("<source>", source, FileId::new(1))];
1124    compile_multi_file_with_options(&sources, options)
1125}
1126
1127/// Compile multiple source files to an ELF binary.
1128///
1129/// This is the main entry point for multi-file compilation. It:
1130/// 1. Parses all files in parallel
1131/// 2. Merges symbols into a unified program
1132/// 3. Performs semantic analysis across all files
1133/// 4. Generates code for the combined program
1134///
1135/// Cross-file references (function calls, struct/enum usage) are resolved during
1136/// semantic analysis since all symbols are visible in the merged program.
1137///
1138/// # Arguments
1139///
1140/// * `sources` - Slice of source files to compile
1141/// * `options` - Compilation options (target, linker, optimization level, etc.)
1142///
1143/// # Returns
1144///
1145/// A `CompileOutput` containing the ELF binary and any warnings,
1146/// or errors if compilation fails.
1147///
1148/// # Example
1149///
1150/// ```ignore
1151/// use gruel_compiler::{SourceFile, CompileOptions, compile_multi_file_with_options};
1152/// use gruel_span::FileId;
1153///
1154/// let sources = vec![
1155///     SourceFile::new("main.gruel", "fn main() -> i32 { helper() }", FileId::new(1)),
1156///     SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
1157/// ];
1158/// let options = CompileOptions::default();
1159/// let output = compile_multi_file_with_options(&sources, &options)?;
1160/// ```
1161pub fn compile_multi_file_with_options(
1162    sources: &[SourceFile<'_>],
1163    options: &CompileOptions,
1164) -> MultiErrorResult<CompileOutput> {
1165    // Configure Rayon's global thread pool based on the jobs setting.
1166    // This must happen before any parallel operations.
1167    // 0 means auto-detect (use all cores), which is Rayon's default.
1168    if options.jobs > 0 {
1169        // Ignore the error if the pool has already been initialized (e.g., in tests).
1170        // This is safe because we're just trying to set the thread count.
1171        let _ = rayon::ThreadPoolBuilder::new()
1172            .num_threads(options.jobs)
1173            .build_global();
1174    }
1175
1176    let total_source_bytes: usize = sources.iter().map(|s| s.source.len()).sum();
1177    let _span = info_span!(
1178        "compile",
1179        target = %options.target,
1180        file_count = sources.len(),
1181        source_bytes = total_source_bytes
1182    )
1183    .entered();
1184
1185    // Use CompilationUnit for the entire pipeline
1186    let mut unit = CompilationUnit::new(sources.to_vec(), options.clone());
1187    unit.run_all()
1188}
1189
1190/// Link using an external system linker.
1191fn link_system_with_warnings(
1192    options: &CompileOptions,
1193    object_files: &[Vec<u8>],
1194    linker_cmd: &str,
1195    warnings: &[CompileWarning],
1196) -> MultiErrorResult<CompileOutput> {
1197    let _span = info_span!("linker", mode = "system", command = linker_cmd).entered();
1198
1199    // Set up temporary directory with object files and runtime
1200    let mut temp_dir = TempLinkDir::new().map_err(CompileErrors::from)?;
1201    temp_dir
1202        .write_object_files(object_files)
1203        .map_err(CompileErrors::from)?;
1204    temp_dir
1205        .write_runtime(RUNTIME_BYTES)
1206        .map_err(CompileErrors::from)?;
1207
1208    // Build the linker command
1209    let mut cmd = Command::new(linker_cmd);
1210
1211    // Add target-specific linker flags.
1212    // We use -nostartfiles (not -nostdlib) because the runtime provides its own
1213    // _start/_main entry points but relies on libc for syscalls.
1214    if options.target.is_macho() {
1215        // macOS-specific flags
1216        cmd.arg("-nostartfiles");
1217        cmd.arg("-arch").arg("arm64");
1218        cmd.arg("-e").arg("__main");
1219    } else {
1220        // Linux/ELF-specific flags
1221        // Dynamic linking lets ld.so initialize libc (TLS, malloc, stdio)
1222        // before jumping to our _start. We only skip the C startup files
1223        // since the runtime provides its own _start entry point.
1224        cmd.arg("-nostartfiles");
1225    }
1226
1227    cmd.arg("-o");
1228    cmd.arg(&temp_dir.output_path);
1229
1230    // Add object files
1231    for path in &temp_dir.obj_paths {
1232        cmd.arg(path);
1233    }
1234
1235    // Add the runtime library
1236    cmd.arg(&temp_dir.runtime_path);
1237
1238    // macOS requires libSystem for syscalls
1239    if options.target.is_macho() {
1240        cmd.arg("-lSystem");
1241    }
1242
1243    // Run the linker
1244    let output = cmd.output().map_err(|e| {
1245        CompileErrors::from(CompileError::without_span(ErrorKind::LinkError(format!(
1246            "failed to execute linker '{}': {}",
1247            linker_cmd, e
1248        ))))
1249    })?;
1250
1251    if !output.status.success() {
1252        let stderr = String::from_utf8_lossy(&output.stderr);
1253        // temp_dir is dropped here, cleaning up automatically
1254        return Err(CompileErrors::from(CompileError::without_span(
1255            ErrorKind::LinkError(format!("linker '{}' failed: {}", linker_cmd, stderr)),
1256        )));
1257    }
1258
1259    // Read the resulting executable
1260    let elf = temp_dir.read_output().map_err(CompileErrors::from)?;
1261    info!(
1262        object_count = object_files.len(),
1263        output_bytes = elf.len(),
1264        "linking complete"
1265    );
1266
1267    // temp_dir is dropped here, cleaning up automatically
1268    Ok(CompileOutput {
1269        elf,
1270        warnings: warnings.to_vec(),
1271    })
1272}
1273
1274// ============================================================================
1275// Unified Backend (Codegen + Linking)
1276// ============================================================================
1277
1278/// Compile analyzed functions to a binary.
1279///
1280/// This is the unified backend that handles both architectures. It:
1281/// 1. Generates machine code for each function in parallel
1282/// 2. Creates object files with relocations
1283/// 3. Links them into an executable
1284///
1285/// This function is used by `CompilationUnit::compile()` and the legacy
1286/// compile functions.
1287pub fn compile_backend(
1288    functions: &[FunctionWithCfg],
1289    type_pool: &TypeInternPool,
1290    strings: &[String],
1291    interner: &ThreadedRodeo,
1292    options: &CompileOptions,
1293    warnings: &[CompileWarning],
1294) -> MultiErrorResult<CompileOutput> {
1295    // Check for main function
1296    let _main_fn = functions
1297        .iter()
1298        .find(|f| f.analyzed.name == "main")
1299        .ok_or_else(|| {
1300            CompileErrors::from(CompileError::without_span(ErrorKind::NoMainFunction))
1301        })?;
1302
1303    generate_llvm_objects_and_link(functions, type_pool, strings, interner, options, warnings)
1304}
1305
1306/// Generate a single LLVM object file from all functions and link it.
1307///
1308/// Unlike the native backends, which produce one object file per function,
1309/// the LLVM backend compiles all functions into a single LLVM module and emits
1310/// one object file. Linking always uses the system linker because LLVM emits
1311/// platform-native object formats (ELF on Linux, Mach-O on macOS).
1312fn generate_llvm_objects_and_link(
1313    functions: &[FunctionWithCfg],
1314    type_pool: &TypeInternPool,
1315    strings: &[String],
1316    interner: &ThreadedRodeo,
1317    options: &CompileOptions,
1318    warnings: &[CompileWarning],
1319) -> MultiErrorResult<CompileOutput> {
1320    let _span = info_span!("codegen", backend = "llvm").entered();
1321
1322    let cfgs: Vec<&Cfg> = functions.iter().map(|f| &f.cfg).collect();
1323    let object_bytes =
1324        gruel_codegen_llvm::generate(&cfgs, type_pool, strings, interner, options.opt_level)
1325            .map_err(CompileErrors::from)?;
1326
1327    // LLVM produces a single object file; wrap it as a one-element slice.
1328    let object_files = vec![object_bytes];
1329
1330    // The LLVM backend always uses the system linker. If the user specified
1331    // --linker internal, fall back to "cc" (the platform's default C compiler).
1332    let linker_cmd = match &options.linker {
1333        LinkerMode::System(cmd) => cmd.clone(),
1334        LinkerMode::Internal => "cc".to_string(),
1335    };
1336    link_system_with_warnings(options, &object_files, &linker_cmd, warnings)
1337}
1338
1339/// Generate LLVM textual IR from analyzed functions.
1340///
1341/// Returns the LLVM IR in human-readable `.ll` format. Used by `--emit asm`
1342/// to produce inspectable IR in place of native assembly.
1343pub fn generate_llvm_ir(
1344    functions: &[FunctionWithCfg],
1345    type_pool: &TypeInternPool,
1346    strings: &[String],
1347    interner: &ThreadedRodeo,
1348    opt_level: OptLevel,
1349) -> CompileResult<String> {
1350    let cfgs: Vec<&Cfg> = functions.iter().map(|f| &f.cfg).collect();
1351    gruel_codegen_llvm::generate_ir(&cfgs, type_pool, strings, interner, opt_level)
1352}
1353
1354// ============================================================================
1355// Test Helper Functions
1356// ============================================================================
1357
1358/// Output from semantic analysis (compile_to_air).
1359///
1360/// This struct provides access to the typed IR (AIR) for each function,
1361/// useful for testing semantic analysis without generating machine code.
1362#[derive(Debug)]
1363pub struct AirOutput {
1364    /// The abstract syntax tree.
1365    pub ast: Ast,
1366    /// String interner used during compilation.
1367    pub interner: ThreadedRodeo,
1368    /// The untyped IR (RIR).
1369    pub rir: Rir,
1370    /// Analyzed functions with typed IR.
1371    pub functions: Vec<AnalyzedFunction>,
1372    /// Type intern pool containing all struct and enum definitions.
1373    pub type_pool: TypeInternPool,
1374    /// String literals.
1375    pub strings: Vec<String>,
1376    /// Warnings collected during analysis.
1377    pub warnings: Vec<CompileWarning>,
1378}
1379
1380/// Compile source code up to AIR (typed IR) without building CFG.
1381///
1382/// This is a test helper that runs: lexing → parsing → AST to RIR → semantic analysis.
1383/// It stops before CFG construction, making it fast for testing type checking
1384/// and semantic analysis.
1385///
1386/// # Example
1387///
1388/// ```ignore
1389/// use gruel_compiler::compile_to_air;
1390///
1391/// let result = compile_to_air("fn main() -> i32 { 1 + 2 * 3 }");
1392/// assert!(result.is_ok());
1393/// ```
1394pub fn compile_to_air(source: &str) -> MultiErrorResult<AirOutput> {
1395    // Lexing
1396    let lexer = Lexer::new(source);
1397    let (tokens, interner) = lexer.tokenize().map_err(CompileErrors::from)?;
1398
1399    // Parsing
1400    let parser = Parser::new(tokens, interner);
1401    let (ast, interner) = parser.parse()?;
1402
1403    // AST to RIR (untyped IR)
1404    let astgen = AstGen::new(&ast, &interner);
1405    let rir = astgen.generate();
1406
1407    // Semantic analysis (RIR to AIR)
1408    let sema = Sema::new(&rir, &interner, PreviewFeatures::new());
1409    let sema_output = sema.analyze_all()?;
1410
1411    Ok(AirOutput {
1412        ast,
1413        interner,
1414        rir,
1415        functions: sema_output.functions,
1416        type_pool: sema_output.type_pool,
1417        strings: sema_output.strings,
1418        warnings: sema_output.warnings,
1419    })
1420}
1421
1422/// Compile source code up to CFG (control flow graph).
1423///
1424/// This is an alias for `compile_frontend` that provides a more intuitive name
1425/// for test code. It runs the full frontend pipeline:
1426/// lexing → parsing → AST to RIR → semantic analysis → CFG construction.
1427///
1428/// # Example
1429///
1430/// ```ignore
1431/// use gruel_compiler::compile_to_cfg;
1432///
1433/// let result = compile_to_cfg("fn main() -> i32 { if true { 1 } else { 2 } }");
1434/// assert!(result.is_ok());
1435/// let state = result.unwrap();
1436/// assert_eq!(state.functions.len(), 1);
1437/// ```
1438pub fn compile_to_cfg(source: &str) -> MultiErrorResult<CompileState> {
1439    compile_frontend(source)
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444    use super::*;
1445
1446    #[test]
1447    fn test_compile_simple() {
1448        let output = compile("fn main() -> i32 { 42 }").unwrap();
1449        // Should produce a valid executable (ELF on Linux, Mach-O on macOS)
1450        let magic = &output.elf[0..4];
1451        let is_elf = magic == [0x7F, b'E', b'L', b'F'];
1452        let is_macho = magic == 0xFEEDFACF_u32.to_le_bytes();
1453        assert!(
1454            is_elf || is_macho,
1455            "should produce valid ELF or Mach-O binary"
1456        );
1457    }
1458
1459    #[test]
1460    fn test_compile_no_main() {
1461        let result = compile("fn foo() -> i32 { 42 }");
1462        assert!(result.is_err());
1463    }
1464
1465    #[test]
1466    fn test_unused_variable_warning() {
1467        let output = compile("fn main() -> i32 { let x = 42; 0 }").unwrap();
1468        assert_eq!(output.warnings.len(), 1);
1469        assert!(output.warnings[0].to_string().contains("unused variable"));
1470        assert!(output.warnings[0].to_string().contains("'x'"));
1471    }
1472
1473    #[test]
1474    fn test_underscore_prefix_no_warning() {
1475        let output = compile("fn main() -> i32 { let _x = 42; 0 }").unwrap();
1476        assert_eq!(output.warnings.len(), 0);
1477    }
1478
1479    #[test]
1480    fn test_used_variable_no_warning() {
1481        let output = compile("fn main() -> i32 { let x = 42; x }").unwrap();
1482        assert_eq!(output.warnings.len(), 0);
1483    }
1484
1485    #[test]
1486    fn test_compile_frontend_includes_warnings() {
1487        let state = compile_frontend("fn main() -> i32 { let x = 42; 0 }").unwrap();
1488        assert_eq!(state.warnings.len(), 1);
1489        assert!(state.warnings[0].to_string().contains("unused variable"));
1490    }
1491
1492    #[test]
1493    fn test_multiple_errors_collected() {
1494        // Test that errors from multiple functions are collected together
1495        // Use examples that both result in type mismatch errors
1496        // Note: Functions must be called from main() to be analyzed (lazy analysis)
1497        let source = r#"
1498            fn foo() -> i32 { true }
1499            fn bar() -> i32 { false }
1500            fn main() -> i32 { foo() + bar() }
1501        "#;
1502        let result = compile_frontend(source);
1503        let errors = match result {
1504            Ok(_) => panic!("expected error, got success"),
1505            Err(e) => e,
1506        };
1507
1508        // Should have at least 2 errors (one from foo, one from bar)
1509        assert!(
1510            errors.len() >= 2,
1511            "expected at least 2 errors, got {}",
1512            errors.len()
1513        );
1514
1515        // All errors should be type mismatches (returning bool where i32 expected)
1516        for error in errors.iter() {
1517            assert!(
1518                error.to_string().contains("type mismatch"),
1519                "expected type mismatch error, got: {}",
1520                error
1521            );
1522        }
1523    }
1524
1525    #[test]
1526    fn test_multiple_errors_display() {
1527        // Use examples that both result in type mismatch errors
1528        // Note: Functions must be called from main() to be analyzed (lazy analysis)
1529        let source = r#"
1530            fn foo() -> i32 { true }
1531            fn bar() -> i32 { false }
1532            fn main() -> i32 { foo() + bar() }
1533        "#;
1534        let errors = match compile_frontend(source) {
1535            Ok(_) => panic!("expected error, got success"),
1536            Err(e) => e,
1537        };
1538
1539        // Display should show both errors
1540        let display = errors.to_string();
1541        assert!(
1542            display.contains("type mismatch"),
1543            "display should contain error message"
1544        );
1545        if errors.len() > 1 {
1546            assert!(
1547                display.contains("more error"),
1548                "display should indicate more errors"
1549            );
1550        }
1551    }
1552
1553    #[test]
1554    fn test_single_error_still_works() {
1555        // Single error should still be collected and returned properly
1556        let source = "fn main() -> i32 { true }";
1557        let errors = match compile_frontend(source) {
1558            Ok(_) => panic!("expected error, got success"),
1559            Err(e) => e,
1560        };
1561
1562        assert_eq!(errors.len(), 1);
1563        assert!(
1564            errors
1565                .first()
1566                .unwrap()
1567                .to_string()
1568                .contains("type mismatch")
1569        );
1570    }
1571
1572    // ========================================================================
1573    // Multi-file Symbol Merging Tests
1574    // ========================================================================
1575
1576    #[test]
1577    fn test_merge_symbols_no_duplicates() {
1578        let sources = vec![
1579            SourceFile::new("main.gruel", "fn main() -> i32 { 0 }", FileId::new(1)),
1580            SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
1581        ];
1582        let parsed = parse_all_files(&sources).unwrap();
1583        let merged = merge_symbols(parsed);
1584        assert!(merged.is_ok(), "merge should succeed with no duplicates");
1585
1586        let program = merged.unwrap();
1587        assert_eq!(program.ast.items.len(), 2, "should have 2 items");
1588    }
1589
1590    #[test]
1591    fn test_merge_symbols_duplicate_function() {
1592        let sources = vec![
1593            SourceFile::new("a.gruel", "fn foo() -> i32 { 1 }", FileId::new(1)),
1594            SourceFile::new("b.gruel", "fn foo() -> i32 { 2 }", FileId::new(2)),
1595        ];
1596        let parsed = parse_all_files(&sources).unwrap();
1597        let result = merge_symbols(parsed);
1598        assert!(result.is_err(), "merge should fail with duplicate function");
1599
1600        let errors = result.unwrap_err();
1601        assert_eq!(errors.len(), 1, "should have 1 error");
1602        let err_msg = errors.first().unwrap().to_string();
1603        assert!(
1604            err_msg.contains("function `foo`"),
1605            "error should mention the function name"
1606        );
1607    }
1608
1609    #[test]
1610    fn test_merge_symbols_duplicate_struct() {
1611        let sources = vec![
1612            SourceFile::new(
1613                "a.gruel",
1614                "struct Point { x: i32 } fn main() -> i32 { 0 }",
1615                FileId::new(1),
1616            ),
1617            SourceFile::new("b.gruel", "struct Point { y: i32 }", FileId::new(2)),
1618        ];
1619        let parsed = parse_all_files(&sources).unwrap();
1620        let result = merge_symbols(parsed);
1621        assert!(result.is_err(), "merge should fail with duplicate struct");
1622
1623        let errors = result.unwrap_err();
1624        assert_eq!(errors.len(), 1, "should have 1 error");
1625        let err_msg = errors.first().unwrap().to_string();
1626        assert!(
1627            err_msg.contains("struct `Point`"),
1628            "error should mention the struct name"
1629        );
1630    }
1631
1632    #[test]
1633    fn test_merge_symbols_duplicate_enum() {
1634        let sources = vec![
1635            SourceFile::new(
1636                "a.gruel",
1637                "enum Color { Red } fn main() -> i32 { 0 }",
1638                FileId::new(1),
1639            ),
1640            SourceFile::new("b.gruel", "enum Color { Blue }", FileId::new(2)),
1641        ];
1642        let parsed = parse_all_files(&sources).unwrap();
1643        let result = merge_symbols(parsed);
1644        assert!(result.is_err(), "merge should fail with duplicate enum");
1645
1646        let errors = result.unwrap_err();
1647        assert_eq!(errors.len(), 1, "should have 1 error");
1648        let err_msg = errors.first().unwrap().to_string();
1649        assert!(
1650            err_msg.contains("enum `Color`"),
1651            "error should mention the enum name"
1652        );
1653    }
1654
1655    #[test]
1656    fn test_merge_symbols_struct_enum_conflict() {
1657        // Struct and enum with the same name should conflict
1658        let sources = vec![
1659            SourceFile::new(
1660                "a.gruel",
1661                "struct Foo { x: i32 } fn main() -> i32 { 0 }",
1662                FileId::new(1),
1663            ),
1664            SourceFile::new("b.gruel", "enum Foo { Bar }", FileId::new(2)),
1665        ];
1666        let parsed = parse_all_files(&sources).unwrap();
1667        let result = merge_symbols(parsed);
1668        assert!(
1669            result.is_err(),
1670            "merge should fail when struct and enum have same name"
1671        );
1672
1673        let errors = result.unwrap_err();
1674        assert_eq!(errors.len(), 1, "should have 1 error");
1675        let err_msg = errors.first().unwrap().to_string();
1676        assert!(
1677            err_msg.contains("Foo") && err_msg.contains("conflicts"),
1678            "error should mention the conflict: {}",
1679            err_msg
1680        );
1681    }
1682
1683    #[test]
1684    fn test_merge_symbols_multiple_duplicates() {
1685        // Multiple duplicates should report multiple errors
1686        let sources = vec![
1687            SourceFile::new(
1688                "a.gruel",
1689                "fn foo() -> i32 { 1 } fn bar() -> i32 { 2 }",
1690                FileId::new(1),
1691            ),
1692            SourceFile::new(
1693                "b.gruel",
1694                "fn foo() -> i32 { 3 } fn bar() -> i32 { 4 }",
1695                FileId::new(2),
1696            ),
1697        ];
1698        let parsed = parse_all_files(&sources).unwrap();
1699        let result = merge_symbols(parsed);
1700        assert!(
1701            result.is_err(),
1702            "merge should fail with duplicate functions"
1703        );
1704
1705        let errors = result.unwrap_err();
1706        assert_eq!(errors.len(), 2, "should have 2 errors for 2 duplicates");
1707    }
1708
1709    #[test]
1710    fn test_merge_symbols_with_struct_methods() {
1711        // Structs with inline methods from different files should be allowed
1712        let sources = vec![
1713            SourceFile::new(
1714                "a.gruel",
1715                "struct Point { x: i32, fn get_x(self) -> i32 { self.x } } fn main() -> i32 { 0 }",
1716                FileId::new(1),
1717            ),
1718            SourceFile::new("b.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
1719        ];
1720        let parsed = parse_all_files(&sources).unwrap();
1721        let result = merge_symbols(parsed);
1722        assert!(result.is_ok(), "struct methods should not cause conflicts");
1723    }
1724
1725    // ========================================================================
1726    // Cross-File Semantic Analysis Tests
1727    // ========================================================================
1728
1729    #[test]
1730    fn test_cross_file_function_call() {
1731        // Function in main.gruel calls function in utils.gruel
1732        let sources = vec![
1733            SourceFile::new(
1734                "main.gruel",
1735                "fn main() -> i32 { helper() }",
1736                FileId::new(1),
1737            ),
1738            SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
1739        ];
1740        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1741        assert!(
1742            result.is_ok(),
1743            "cross-file function call should compile: {:?}",
1744            result.err()
1745        );
1746    }
1747
1748    #[test]
1749    fn test_cross_file_function_call_with_args() {
1750        // Function in main.gruel calls function in utils.gruel with arguments
1751        let sources = vec![
1752            SourceFile::new(
1753                "main.gruel",
1754                "fn main() -> i32 { add(10, 32) }",
1755                FileId::new(1),
1756            ),
1757            SourceFile::new(
1758                "utils.gruel",
1759                "fn add(a: i32, b: i32) -> i32 { a + b }",
1760                FileId::new(2),
1761            ),
1762        ];
1763        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1764        assert!(
1765            result.is_ok(),
1766            "cross-file function call with args should compile: {:?}",
1767            result.err()
1768        );
1769    }
1770
1771    #[test]
1772    fn test_cross_file_struct_usage() {
1773        // Struct defined in types.gruel, used in main.gruel
1774        let sources = vec![
1775            SourceFile::new(
1776                "main.gruel",
1777                "fn main() -> i32 { let p = Point { x: 1, y: 2 }; p.x + p.y }",
1778                FileId::new(1),
1779            ),
1780            SourceFile::new(
1781                "types.gruel",
1782                "struct Point { x: i32, y: i32 }",
1783                FileId::new(2),
1784            ),
1785        ];
1786        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1787        assert!(
1788            result.is_ok(),
1789            "cross-file struct usage should compile: {:?}",
1790            result.err()
1791        );
1792    }
1793
1794    #[test]
1795    fn test_cross_file_struct_as_function_param() {
1796        // Struct defined in types.gruel, function in utils.gruel takes it as param
1797        let sources = vec![
1798            SourceFile::new(
1799                "main.gruel",
1800                "fn main() -> i32 { let p = Point { x: 10, y: 5 }; get_sum(p) }",
1801                FileId::new(1),
1802            ),
1803            SourceFile::new(
1804                "types.gruel",
1805                "struct Point { x: i32, y: i32 }",
1806                FileId::new(2),
1807            ),
1808            SourceFile::new(
1809                "utils.gruel",
1810                "fn get_sum(p: Point) -> i32 { p.x + p.y }",
1811                FileId::new(3),
1812            ),
1813        ];
1814        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1815        assert!(
1816            result.is_ok(),
1817            "cross-file struct as function param should compile: {:?}",
1818            result.err()
1819        );
1820    }
1821
1822    #[test]
1823    fn test_cross_file_enum_usage() {
1824        // Enum defined in types.gruel, used in main.gruel
1825        let sources = vec![
1826            SourceFile::new(
1827                "main.gruel",
1828                r#"fn main() -> i32 {
1829                    let c = Color::Red;
1830                    match c { Color::Red => 1, Color::Green => 2, Color::Blue => 3 }
1831                }"#,
1832                FileId::new(1),
1833            ),
1834            SourceFile::new(
1835                "types.gruel",
1836                "enum Color { Red, Green, Blue }",
1837                FileId::new(2),
1838            ),
1839        ];
1840        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1841        assert!(
1842            result.is_ok(),
1843            "cross-file enum usage should compile: {:?}",
1844            result.err()
1845        );
1846    }
1847
1848    #[test]
1849    fn test_cross_file_no_main_function() {
1850        // No main function in any file
1851        let sources = vec![
1852            SourceFile::new("a.gruel", "fn foo() -> i32 { 1 }", FileId::new(1)),
1853            SourceFile::new("b.gruel", "fn bar() -> i32 { 2 }", FileId::new(2)),
1854        ];
1855        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1856        assert!(result.is_err(), "should fail without main function");
1857
1858        let errors = result.unwrap_err();
1859        let err_msg = errors.first().unwrap().to_string();
1860        assert!(
1861            err_msg.contains("main") && err_msg.contains("function"),
1862            "error should mention missing main function: {}",
1863            err_msg
1864        );
1865    }
1866
1867    #[test]
1868    fn test_cross_file_duplicate_main() {
1869        // main() defined in multiple files
1870        let sources = vec![
1871            SourceFile::new("a.gruel", "fn main() -> i32 { 1 }", FileId::new(1)),
1872            SourceFile::new("b.gruel", "fn main() -> i32 { 2 }", FileId::new(2)),
1873        ];
1874        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1875        assert!(result.is_err(), "should fail with duplicate main");
1876
1877        let errors = result.unwrap_err();
1878        let err_msg = errors.first().unwrap().to_string();
1879        assert!(
1880            err_msg.contains("main"),
1881            "error should mention duplicate main: {}",
1882            err_msg
1883        );
1884    }
1885
1886    #[test]
1887    fn test_cross_file_undefined_function() {
1888        // main.gruel calls function that doesn't exist
1889        let sources = vec![
1890            SourceFile::new(
1891                "main.gruel",
1892                "fn main() -> i32 { nonexistent() }",
1893                FileId::new(1),
1894            ),
1895            SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
1896        ];
1897        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1898        assert!(result.is_err(), "should fail with undefined function");
1899
1900        let errors = result.unwrap_err();
1901        let err_msg = errors.first().unwrap().to_string();
1902        assert!(
1903            err_msg.contains("nonexistent") || err_msg.contains("undefined"),
1904            "error should mention undefined function: {}",
1905            err_msg
1906        );
1907    }
1908
1909    #[test]
1910    fn test_cross_file_three_files_chain() {
1911        // main.gruel -> utils.gruel -> math.gruel chain of calls
1912        let sources = vec![
1913            SourceFile::new(
1914                "main.gruel",
1915                "fn main() -> i32 { compute(6, 7) }",
1916                FileId::new(1),
1917            ),
1918            SourceFile::new(
1919                "utils.gruel",
1920                "fn compute(a: i32, b: i32) -> i32 { multiply(a, b) }",
1921                FileId::new(2),
1922            ),
1923            SourceFile::new(
1924                "math.gruel",
1925                "fn multiply(x: i32, y: i32) -> i32 { x * y }",
1926                FileId::new(3),
1927            ),
1928        ];
1929        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1930        assert!(
1931            result.is_ok(),
1932            "chain of cross-file calls should compile: {:?}",
1933            result.err()
1934        );
1935    }
1936
1937    #[test]
1938    fn test_cross_file_mutual_calls() {
1939        // Two files calling each other (mutual recursion possible)
1940        let sources = vec![
1941            SourceFile::new(
1942                "main.gruel",
1943                r#"fn main() -> i32 { is_even(4) }
1944                fn is_even(n: i32) -> i32 { if n == 0 { 1 } else { is_odd(n - 1) } }"#,
1945                FileId::new(1),
1946            ),
1947            SourceFile::new(
1948                "utils.gruel",
1949                "fn is_odd(n: i32) -> i32 { if n == 0 { 0 } else { is_even(n - 1) } }",
1950                FileId::new(2),
1951            ),
1952        ];
1953        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1954        assert!(
1955            result.is_ok(),
1956            "mutual cross-file calls should compile: {:?}",
1957            result.err()
1958        );
1959    }
1960
1961    // ========================================================================
1962    // Module Import Tests
1963    // ========================================================================
1964
1965    #[test]
1966    fn test_module_member_access() {
1967        // Test that @import returns a module type and member access works
1968        // Note: In Phase 1, all files are merged into the same namespace,
1969        // so math.add() looks up "add" in the global function table.
1970        let sources = vec![
1971            SourceFile::new(
1972                "main.gruel",
1973                r#"fn main() -> i32 {
1974                    let math = @import("math.gruel");
1975                    math.add(1, 2)
1976                }"#,
1977                FileId::new(1),
1978            ),
1979            SourceFile::new(
1980                "math.gruel",
1981                "fn add(a: i32, b: i32) -> i32 { a + b }",
1982                FileId::new(2),
1983            ),
1984        ];
1985        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
1986        assert!(
1987            result.is_ok(),
1988            "module member access should compile: {:?}",
1989            result.err()
1990        );
1991    }
1992
1993    #[test]
1994    fn test_module_member_access_multiple_functions() {
1995        // Test accessing multiple functions from an imported module
1996        let sources = vec![
1997            SourceFile::new(
1998                "main.gruel",
1999                r#"fn main() -> i32 {
2000                    let math = @import("math.gruel");
2001                    let sum = math.add(10, 20);
2002                    let diff = math.sub(sum, 5);
2003                    diff
2004                }"#,
2005                FileId::new(1),
2006            ),
2007            SourceFile::new(
2008                "math.gruel",
2009                r#"fn add(a: i32, b: i32) -> i32 { a + b }
2010                fn sub(a: i32, b: i32) -> i32 { a - b }"#,
2011                FileId::new(2),
2012            ),
2013        ];
2014        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
2015        assert!(
2016            result.is_ok(),
2017            "module with multiple functions should compile: {:?}",
2018            result.err()
2019        );
2020    }
2021
2022    #[test]
2023    fn test_module_undefined_function_error() {
2024        // Test that accessing an undefined function in a module produces an error
2025        let sources = vec![
2026            SourceFile::new(
2027                "main.gruel",
2028                r#"fn main() -> i32 {
2029                    let math = @import("math.gruel");
2030                    math.nonexistent(1, 2)
2031                }"#,
2032                FileId::new(1),
2033            ),
2034            SourceFile::new(
2035                "math.gruel",
2036                "fn add(a: i32, b: i32) -> i32 { a + b }",
2037                FileId::new(2),
2038            ),
2039        ];
2040        let result = compile_multi_file_with_options(&sources, &CompileOptions::default());
2041        assert!(
2042            result.is_err(),
2043            "undefined module function should fail to compile"
2044        );
2045        let err = result.err().unwrap().to_string();
2046        assert!(
2047            err.contains("undefined function") || err.contains("nonexistent"),
2048            "error should mention undefined function: {}",
2049            err
2050        );
2051    }
2052}
2053
2054// ============================================================================
2055// Integration Unit Tests
2056// ============================================================================
2057//
2058// These tests verify the compilation pipeline without execution. They test:
2059// - Type checking and semantic analysis
2060// - CFG construction
2061// - Error message quality
2062//
2063// Benefits:
2064// - Fast: No file I/O, no process spawning, no execution
2065// - Comprehensive: Tests full parse→sema→codegen pipeline
2066// - Debuggable: Can inspect intermediate IRs in tests
2067
2068#[cfg(test)]
2069mod integration_tests {
2070    use super::*;
2071
2072    // ========================================================================
2073    // Integer Types
2074    // ========================================================================
2075
2076    mod integer_types {
2077        use super::*;
2078
2079        #[test]
2080        fn signed_integer_return() {
2081            assert!(compile_to_air("fn main() -> i8 { 42 }").is_ok());
2082            assert!(compile_to_air("fn main() -> i16 { 42 }").is_ok());
2083            assert!(compile_to_air("fn main() -> i32 { 42 }").is_ok());
2084            assert!(compile_to_air("fn main() -> i64 { 42 }").is_ok());
2085        }
2086
2087        #[test]
2088        fn unsigned_integer_return() {
2089            assert!(compile_to_air("fn main() -> u8 { 42 }").is_ok());
2090            assert!(compile_to_air("fn main() -> u16 { 42 }").is_ok());
2091            assert!(compile_to_air("fn main() -> u32 { 42 }").is_ok());
2092            assert!(compile_to_air("fn main() -> u64 { 42 }").is_ok());
2093        }
2094
2095        #[test]
2096        fn integer_type_mismatch() {
2097            let result = compile_to_air("fn main() -> i32 { let x: i64 = 1; x }");
2098            assert!(result.is_err());
2099            let err = result.unwrap_err().to_string();
2100            assert!(err.contains("type mismatch") || err.contains("expected"));
2101        }
2102
2103        #[test]
2104        fn integer_literal_type_inference() {
2105            // Type inferred from return type
2106            assert!(compile_to_air("fn main() -> i64 { 100 }").is_ok());
2107            // Type inferred from annotation
2108            assert!(compile_to_air("fn main() -> i32 { let x: i64 = 100; 0 }").is_ok());
2109        }
2110    }
2111
2112    // ========================================================================
2113    // Boolean Type
2114    // ========================================================================
2115
2116    mod boolean_type {
2117        use super::*;
2118
2119        #[test]
2120        fn boolean_literals() {
2121            assert!(compile_to_air("fn main() -> bool { true }").is_ok());
2122            assert!(compile_to_air("fn main() -> bool { false }").is_ok());
2123        }
2124
2125        #[test]
2126        fn boolean_in_condition() {
2127            assert!(compile_to_cfg("fn main() -> i32 { if true { 1 } else { 0 } }").is_ok());
2128        }
2129
2130        #[test]
2131        fn non_boolean_condition_rejected() {
2132            let result = compile_to_air("fn main() -> i32 { if 1 { 1 } else { 0 } }");
2133            assert!(result.is_err());
2134        }
2135    }
2136
2137    // ========================================================================
2138    // Unit Type
2139    // ========================================================================
2140
2141    mod unit_type {
2142        use super::*;
2143
2144        #[test]
2145        fn unit_return_type() {
2146            assert!(compile_to_air("fn main() -> () { () }").is_ok());
2147        }
2148
2149        #[test]
2150        fn unit_in_expression() {
2151            assert!(compile_to_air("fn main() -> () { let _x = (); () }").is_ok());
2152        }
2153
2154        #[test]
2155        fn implicit_unit_return() {
2156            assert!(compile_to_air("fn foo() -> () { } fn main() -> i32 { 0 }").is_ok());
2157        }
2158    }
2159
2160    // ========================================================================
2161    // Arithmetic Operations
2162    // ========================================================================
2163
2164    mod arithmetic {
2165        use super::*;
2166
2167        #[test]
2168        fn basic_addition() {
2169            assert!(compile_to_air("fn main() -> i32 { 1 + 2 }").is_ok());
2170        }
2171
2172        #[test]
2173        fn basic_subtraction() {
2174            assert!(compile_to_air("fn main() -> i32 { 5 - 3 }").is_ok());
2175        }
2176
2177        #[test]
2178        fn basic_multiplication() {
2179            assert!(compile_to_air("fn main() -> i32 { 3 * 4 }").is_ok());
2180        }
2181
2182        #[test]
2183        fn basic_division() {
2184            assert!(compile_to_air("fn main() -> i32 { 10 / 2 }").is_ok());
2185        }
2186
2187        #[test]
2188        fn basic_modulo() {
2189            assert!(compile_to_air("fn main() -> i32 { 10 % 3 }").is_ok());
2190        }
2191
2192        #[test]
2193        fn unary_negation() {
2194            assert!(compile_to_air("fn main() -> i32 { -42 }").is_ok());
2195        }
2196
2197        #[test]
2198        fn operator_precedence() {
2199            // Multiplication before addition
2200            let state = compile_to_cfg("fn main() -> i32 { 1 + 2 * 3 }").unwrap();
2201            assert_eq!(state.functions.len(), 1);
2202        }
2203
2204        #[test]
2205        fn chained_operations() {
2206            assert!(compile_to_air("fn main() -> i32 { 1 + 2 + 3 + 4 }").is_ok());
2207        }
2208
2209        #[test]
2210        fn mixed_type_arithmetic_rejected() {
2211            let result = compile_to_air("fn main() -> i32 { 1 + true }");
2212            assert!(result.is_err());
2213        }
2214
2215        #[test]
2216        fn unsigned_arithmetic() {
2217            assert!(compile_to_air("fn main() -> u32 { 10 + 5 }").is_ok());
2218            assert!(compile_to_air("fn main() -> u32 { 10 - 5 }").is_ok());
2219            assert!(compile_to_air("fn main() -> u32 { 10 * 5 }").is_ok());
2220        }
2221    }
2222
2223    // ========================================================================
2224    // Comparison Operations
2225    // ========================================================================
2226
2227    mod comparison {
2228        use super::*;
2229
2230        #[test]
2231        fn equality_comparison() {
2232            assert!(compile_to_air("fn main() -> bool { 1 == 1 }").is_ok());
2233            assert!(compile_to_air("fn main() -> bool { 1 != 2 }").is_ok());
2234        }
2235
2236        #[test]
2237        fn ordering_comparison() {
2238            assert!(compile_to_air("fn main() -> bool { 1 < 2 }").is_ok());
2239            assert!(compile_to_air("fn main() -> bool { 2 > 1 }").is_ok());
2240            assert!(compile_to_air("fn main() -> bool { 1 <= 2 }").is_ok());
2241            assert!(compile_to_air("fn main() -> bool { 2 >= 1 }").is_ok());
2242        }
2243
2244        #[test]
2245        fn boolean_equality() {
2246            assert!(compile_to_air("fn main() -> bool { true == true }").is_ok());
2247            assert!(compile_to_air("fn main() -> bool { true != false }").is_ok());
2248        }
2249
2250        #[test]
2251        fn comparison_returns_bool() {
2252            let result = compile_to_air("fn main() -> i32 { 1 < 2 }");
2253            assert!(result.is_err()); // Type mismatch: bool vs i32
2254        }
2255
2256        #[test]
2257        fn mixed_type_comparison_rejected() {
2258            let result = compile_to_air("fn main() -> bool { 1 == true }");
2259            assert!(result.is_err());
2260        }
2261    }
2262
2263    // ========================================================================
2264    // Logical Operations
2265    // ========================================================================
2266
2267    mod logical {
2268        use super::*;
2269
2270        #[test]
2271        fn logical_and() {
2272            assert!(compile_to_cfg("fn main() -> bool { true && false }").is_ok());
2273        }
2274
2275        #[test]
2276        fn logical_or() {
2277            assert!(compile_to_cfg("fn main() -> bool { true || false }").is_ok());
2278        }
2279
2280        #[test]
2281        fn logical_not() {
2282            assert!(compile_to_air("fn main() -> bool { !true }").is_ok());
2283        }
2284
2285        #[test]
2286        fn chained_logical() {
2287            assert!(compile_to_cfg("fn main() -> bool { true && false || true }").is_ok());
2288        }
2289
2290        #[test]
2291        fn logical_with_non_bool_rejected() {
2292            let result = compile_to_air("fn main() -> bool { 1 && true }");
2293            assert!(result.is_err());
2294        }
2295    }
2296
2297    // ========================================================================
2298    // Bitwise Operations
2299    // ========================================================================
2300
2301    mod bitwise {
2302        use super::*;
2303
2304        #[test]
2305        fn bitwise_and() {
2306            assert!(compile_to_air("fn main() -> i32 { 5 & 3 }").is_ok());
2307        }
2308
2309        #[test]
2310        fn bitwise_or() {
2311            assert!(compile_to_air("fn main() -> i32 { 5 | 3 }").is_ok());
2312        }
2313
2314        #[test]
2315        fn bitwise_xor() {
2316            assert!(compile_to_air("fn main() -> i32 { 5 ^ 3 }").is_ok());
2317        }
2318
2319        #[test]
2320        fn bitwise_not() {
2321            assert!(compile_to_air("fn main() -> i32 { ~5 }").is_ok());
2322        }
2323
2324        #[test]
2325        fn shift_left() {
2326            assert!(compile_to_air("fn main() -> i32 { 1 << 4 }").is_ok());
2327        }
2328
2329        #[test]
2330        fn shift_right() {
2331            assert!(compile_to_air("fn main() -> i32 { 16 >> 2 }").is_ok());
2332        }
2333
2334        #[test]
2335        fn bitwise_on_bool_rejected() {
2336            let result = compile_to_air("fn main() -> bool { true & false }");
2337            assert!(result.is_err());
2338        }
2339    }
2340
2341    // ========================================================================
2342    // Control Flow - If Expressions
2343    // ========================================================================
2344
2345    mod if_expressions {
2346        use super::*;
2347
2348        #[test]
2349        fn basic_if_else() {
2350            assert!(compile_to_cfg("fn main() -> i32 { if true { 1 } else { 0 } }").is_ok());
2351        }
2352
2353        #[test]
2354        fn if_with_condition_expr() {
2355            assert!(compile_to_cfg("fn main() -> i32 { if 1 < 2 { 1 } else { 0 } }").is_ok());
2356        }
2357
2358        #[test]
2359        fn nested_if() {
2360            let src = "fn main() -> i32 { if true { if false { 1 } else { 2 } } else { 3 } }";
2361            assert!(compile_to_cfg(src).is_ok());
2362        }
2363
2364        #[test]
2365        fn if_branches_must_match_type() {
2366            let result = compile_to_air("fn main() -> i32 { if true { 1 } else { true } }");
2367            assert!(result.is_err());
2368        }
2369
2370        #[test]
2371        fn if_result_type_checked() {
2372            let result = compile_to_air("fn main() -> bool { if true { 1 } else { 0 } }");
2373            assert!(result.is_err());
2374        }
2375    }
2376
2377    // ========================================================================
2378    // Control Flow - Match Expressions
2379    // ========================================================================
2380
2381    mod match_expressions {
2382        use super::*;
2383
2384        #[test]
2385        fn match_on_integer() {
2386            let src = r#"
2387                fn main() -> i32 {
2388                    let x = 1;
2389                    match x {
2390                        1 => 10,
2391                        2 => 20,
2392                        _ => 0,
2393                    }
2394                }
2395            "#;
2396            assert!(compile_to_cfg(src).is_ok());
2397        }
2398
2399        #[test]
2400        fn match_on_boolean() {
2401            let src = r#"
2402                fn main() -> i32 {
2403                    match true {
2404                        true => 1,
2405                        false => 0,
2406                    }
2407                }
2408            "#;
2409            assert!(compile_to_cfg(src).is_ok());
2410        }
2411
2412        #[test]
2413        fn match_exhaustiveness_required() {
2414            // Missing case should error
2415            let result = compile_to_air(
2416                r#"
2417                fn main() -> i32 {
2418                    match 1 {
2419                        1 => 10,
2420                    }
2421                }
2422            "#,
2423            );
2424            assert!(result.is_err());
2425        }
2426
2427        #[test]
2428        fn match_branches_must_match_type() {
2429            let result = compile_to_air(
2430                r#"
2431                fn main() -> i32 {
2432                    match true {
2433                        true => 1,
2434                        false => true,
2435                    }
2436                }
2437            "#,
2438            );
2439            assert!(result.is_err());
2440        }
2441    }
2442
2443    // ========================================================================
2444    // Control Flow - Loops
2445    // ========================================================================
2446
2447    mod loops {
2448        use super::*;
2449
2450        #[test]
2451        fn while_loop_basic() {
2452            let src = r#"
2453                fn main() -> i32 {
2454                    let mut x = 0;
2455                    while x < 10 {
2456                        x = x + 1;
2457                    }
2458                    x
2459                }
2460            "#;
2461            assert!(compile_to_cfg(src).is_ok());
2462        }
2463
2464        #[test]
2465        fn while_with_break() {
2466            let src = r#"
2467                fn main() -> i32 {
2468                    let mut x = 0;
2469                    while true {
2470                        x = x + 1;
2471                        if x == 5 {
2472                            break;
2473                        }
2474                    }
2475                    x
2476                }
2477            "#;
2478            assert!(compile_to_cfg(src).is_ok());
2479        }
2480
2481        #[test]
2482        fn while_with_continue() {
2483            let src = r#"
2484                fn main() -> i32 {
2485                    let mut x = 0;
2486                    let mut sum = 0;
2487                    while x < 10 {
2488                        x = x + 1;
2489                        if x == 5 {
2490                            continue;
2491                        }
2492                        sum = sum + x;
2493                    }
2494                    sum
2495                }
2496            "#;
2497            assert!(compile_to_cfg(src).is_ok());
2498        }
2499
2500        #[test]
2501        fn break_outside_loop_rejected() {
2502            let result = compile_to_air("fn main() -> i32 { break; 0 }");
2503            assert!(result.is_err());
2504        }
2505
2506        #[test]
2507        fn continue_outside_loop_rejected() {
2508            let result = compile_to_air("fn main() -> i32 { continue; 0 }");
2509            assert!(result.is_err());
2510        }
2511    }
2512
2513    // ========================================================================
2514    // Let Bindings
2515    // ========================================================================
2516
2517    mod let_bindings {
2518        use super::*;
2519
2520        #[test]
2521        fn basic_let() {
2522            assert!(compile_to_air("fn main() -> i32 { let x = 42; x }").is_ok());
2523        }
2524
2525        #[test]
2526        fn let_with_type_annotation() {
2527            assert!(compile_to_air("fn main() -> i32 { let x: i32 = 42; x }").is_ok());
2528        }
2529
2530        #[test]
2531        fn mutable_let() {
2532            let src = "fn main() -> i32 { let mut x = 1; x = 2; x }";
2533            assert!(compile_to_air(src).is_ok());
2534        }
2535
2536        #[test]
2537        fn immutable_assignment_rejected() {
2538            let result = compile_to_air("fn main() -> i32 { let x = 1; x = 2; x }");
2539            assert!(result.is_err());
2540        }
2541
2542        #[test]
2543        fn shadowing_allowed() {
2544            let src = "fn main() -> i32 { let x = 1; let x = 2; x }";
2545            assert!(compile_to_air(src).is_ok());
2546        }
2547
2548        #[test]
2549        fn shadowing_can_change_type() {
2550            let src = "fn main() -> bool { let x = 1; let x = true; x }";
2551            assert!(compile_to_air(src).is_ok());
2552        }
2553
2554        #[test]
2555        fn undefined_variable_rejected() {
2556            let result = compile_to_air("fn main() -> i32 { x }");
2557            assert!(result.is_err());
2558        }
2559    }
2560
2561    // ========================================================================
2562    // Functions
2563    // ========================================================================
2564
2565    mod functions {
2566        use super::*;
2567
2568        #[test]
2569        fn function_call() {
2570            let src = r#"
2571                fn add(a: i32, b: i32) -> i32 { a + b }
2572                fn main() -> i32 { add(1, 2) }
2573            "#;
2574            assert!(compile_to_air(src).is_ok());
2575        }
2576
2577        #[test]
2578        fn function_forward_reference() {
2579            let src = r#"
2580                fn main() -> i32 { foo() }
2581                fn foo() -> i32 { 42 }
2582            "#;
2583            assert!(compile_to_air(src).is_ok());
2584        }
2585
2586        #[test]
2587        fn recursion() {
2588            let src = r#"
2589                fn factorial(n: i32) -> i32 {
2590                    if n <= 1 { 1 } else { n * factorial(n - 1) }
2591                }
2592                fn main() -> i32 { factorial(5) }
2593            "#;
2594            assert!(compile_to_cfg(src).is_ok());
2595        }
2596
2597        #[test]
2598        fn mutual_recursion() {
2599            let src = r#"
2600                fn is_even(n: i32) -> bool {
2601                    if n == 0 { true } else { is_odd(n - 1) }
2602                }
2603                fn is_odd(n: i32) -> bool {
2604                    if n == 0 { false } else { is_even(n - 1) }
2605                }
2606                fn main() -> i32 { if is_even(4) { 1 } else { 0 } }
2607            "#;
2608            assert!(compile_to_cfg(src).is_ok());
2609        }
2610
2611        #[test]
2612        fn wrong_argument_count_rejected() {
2613            let src = r#"
2614                fn add(a: i32, b: i32) -> i32 { a + b }
2615                fn main() -> i32 { add(1) }
2616            "#;
2617            let result = compile_to_air(src);
2618            assert!(result.is_err());
2619        }
2620
2621        #[test]
2622        fn wrong_argument_type_rejected() {
2623            let src = r#"
2624                fn foo(x: i32) -> i32 { x }
2625                fn main() -> i32 { foo(true) }
2626            "#;
2627            let result = compile_to_air(src);
2628            assert!(result.is_err());
2629        }
2630
2631        #[test]
2632        fn undefined_function_rejected() {
2633            let result = compile_to_air("fn main() -> i32 { unknown() }");
2634            assert!(result.is_err());
2635        }
2636
2637        #[test]
2638        fn return_type_mismatch_rejected() {
2639            let result = compile_to_air("fn main() -> i32 { true }");
2640            assert!(result.is_err());
2641        }
2642    }
2643
2644    // ========================================================================
2645    // Structs
2646    // ========================================================================
2647
2648    mod structs {
2649        use super::*;
2650
2651        #[test]
2652        fn struct_definition() {
2653            let src = r#"
2654                struct Point { x: i32, y: i32 }
2655                fn main() -> i32 { 0 }
2656            "#;
2657            let result = compile_to_air(src).unwrap();
2658            // type_pool includes builtin types (String) plus user-defined structs
2659            // There's 1 builtin (String) + 1 user-defined (Point) = 2 total structs
2660            let all_struct_ids = result.type_pool.all_struct_ids();
2661            assert_eq!(all_struct_ids.len(), 2);
2662            // Verify Point is present
2663            let point_name = result.interner.get_or_intern("Point");
2664            let point_interned = result.type_pool.get_struct_by_name(point_name);
2665            assert!(
2666                point_interned.is_some(),
2667                "Point struct should exist in pool"
2668            );
2669        }
2670
2671        #[test]
2672        fn struct_literal() {
2673            let src = r#"
2674                struct Point { x: i32, y: i32 }
2675                fn main() -> i32 {
2676                    let _p = Point { x: 1, y: 2 };
2677                    0
2678                }
2679            "#;
2680            assert!(compile_to_air(src).is_ok());
2681        }
2682
2683        #[test]
2684        fn struct_field_access() {
2685            let src = r#"
2686                struct Point { x: i32, y: i32 }
2687                fn main() -> i32 {
2688                    let p = Point { x: 10, y: 20 };
2689                    p.x + p.y
2690                }
2691            "#;
2692            assert!(compile_to_air(src).is_ok());
2693        }
2694
2695        #[test]
2696        fn struct_field_order_independent() {
2697            let src = r#"
2698                struct Point { x: i32, y: i32 }
2699                fn main() -> i32 {
2700                    let p = Point { y: 2, x: 1 };
2701                    p.x
2702                }
2703            "#;
2704            assert!(compile_to_air(src).is_ok());
2705        }
2706
2707        #[test]
2708        fn struct_unknown_field_rejected() {
2709            let src = r#"
2710                struct Point { x: i32, y: i32 }
2711                fn main() -> i32 {
2712                    let p = Point { x: 1, z: 2 };
2713                    0
2714                }
2715            "#;
2716            let result = compile_to_air(src);
2717            assert!(result.is_err());
2718        }
2719
2720        #[test]
2721        fn struct_equality() {
2722            let src = r#"
2723                struct Point { x: i32, y: i32 }
2724                fn main() -> bool {
2725                    let a = Point { x: 1, y: 2 };
2726                    let b = Point { x: 1, y: 2 };
2727                    a == b
2728                }
2729            "#;
2730            assert!(compile_to_air(src).is_ok());
2731        }
2732
2733        #[test]
2734        fn struct_move_semantics() {
2735            // After moving a struct, it should not be usable
2736            let src = r#"
2737                struct Point { x: i32, y: i32 }
2738                fn consume(p: Point) -> i32 { p.x }
2739                fn main() -> i32 {
2740                    let p = Point { x: 1, y: 2 };
2741                    let _a = consume(p);
2742                    p.x
2743                }
2744            "#;
2745            let result = compile_to_air(src);
2746            assert!(result.is_err());
2747        }
2748    }
2749
2750    // ========================================================================
2751    // Enums
2752    // ========================================================================
2753
2754    mod enums {
2755        use super::*;
2756
2757        #[test]
2758        fn enum_definition() {
2759            let src = r#"
2760                enum Color { Red, Green, Blue }
2761                fn main() -> i32 { 0 }
2762            "#;
2763            assert!(compile_to_air(src).is_ok());
2764        }
2765
2766        #[test]
2767        fn enum_variant_access() {
2768            let src = r#"
2769                enum Color { Red, Green, Blue }
2770                fn main() -> i32 {
2771                    let _c = Color::Red;
2772                    0
2773                }
2774            "#;
2775            assert!(compile_to_air(src).is_ok());
2776        }
2777
2778        #[test]
2779        fn enum_match() {
2780            let src = r#"
2781                enum Color { Red, Green, Blue }
2782                fn main() -> i32 {
2783                    let c = Color::Green;
2784                    match c {
2785                        Color::Red => 1,
2786                        Color::Green => 2,
2787                        Color::Blue => 3,
2788                    }
2789                }
2790            "#;
2791            assert!(compile_to_cfg(src).is_ok());
2792        }
2793
2794        #[test]
2795        fn enum_comparison_via_match() {
2796            // Enum equality comparison is done via match, not ==
2797            // (== is not yet implemented for enums)
2798            let src = r#"
2799                enum Color { Red, Green, Blue }
2800                fn eq(a: Color, b: Color) -> bool {
2801                    match a {
2802                        Color::Red => match b { Color::Red => true, _ => false },
2803                        Color::Green => match b { Color::Green => true, _ => false },
2804                        Color::Blue => match b { Color::Blue => true, _ => false },
2805                    }
2806                }
2807                fn main() -> i32 { if eq(Color::Red, Color::Red) { 1 } else { 0 } }
2808            "#;
2809            assert!(compile_to_cfg(src).is_ok());
2810        }
2811
2812        #[test]
2813        fn unknown_enum_variant_rejected() {
2814            let src = r#"
2815                enum Color { Red, Green, Blue }
2816                fn main() -> i32 {
2817                    let _c = Color::Yellow;
2818                    0
2819                }
2820            "#;
2821            let result = compile_to_air(src);
2822            assert!(result.is_err());
2823        }
2824    }
2825
2826    // ========================================================================
2827    // Arrays
2828    // ========================================================================
2829
2830    mod arrays {
2831        use super::*;
2832
2833        #[test]
2834        fn array_literal() {
2835            let src = "fn main() -> i32 { let _arr: [i32; 3] = [1, 2, 3]; 0 }";
2836            assert!(compile_to_air(src).is_ok());
2837        }
2838
2839        #[test]
2840        fn array_indexing() {
2841            let src = "fn main() -> i32 { let arr: [i32; 3] = [1, 2, 3]; arr[1] }";
2842            assert!(compile_to_air(src).is_ok());
2843        }
2844
2845        #[test]
2846        fn array_element_assignment() {
2847            let src = r#"
2848                fn main() -> i32 {
2849                    let mut arr: [i32; 3] = [1, 2, 3];
2850                    arr[0] = 10;
2851                    arr[0]
2852                }
2853            "#;
2854            assert!(compile_to_air(src).is_ok());
2855        }
2856
2857        #[test]
2858        fn array_wrong_length_rejected() {
2859            let src = "fn main() -> i32 { let _arr: [i32; 3] = [1, 2]; 0 }";
2860            let result = compile_to_air(src);
2861            assert!(result.is_err());
2862        }
2863
2864        #[test]
2865        fn array_mixed_types_rejected() {
2866            let src = "fn main() -> i32 { let _arr: [i32; 2] = [1, true]; 0 }";
2867            let result = compile_to_air(src);
2868            assert!(result.is_err());
2869        }
2870    }
2871
2872    // ========================================================================
2873    // Strings
2874    // ========================================================================
2875
2876    mod strings {
2877        use super::*;
2878
2879        #[test]
2880        fn string_literal() {
2881            let src = r#"fn main() -> i32 { let _s = "hello"; 0 }"#;
2882            assert!(compile_to_air(src).is_ok());
2883        }
2884
2885        #[test]
2886        fn string_with_quote_escape() {
2887            // String escape sequences: \" is supported
2888            let src = r#"fn main() -> i32 { let _s = "hello\"world"; 0 }"#;
2889            assert!(compile_to_air(src).is_ok());
2890        }
2891
2892        #[test]
2893        fn string_with_backslash_escape() {
2894            // String escape sequences: \\ is supported
2895            let src = r#"fn main() -> i32 { let _s = "hello\\world"; 0 }"#;
2896            assert!(compile_to_air(src).is_ok());
2897        }
2898    }
2899
2900    // ========================================================================
2901    // Block Expressions
2902    // ========================================================================
2903
2904    mod blocks {
2905        use super::*;
2906
2907        #[test]
2908        fn block_returns_final_expression() {
2909            let src = "fn main() -> i32 { { 1; 2; 3 } }";
2910            assert!(compile_to_air(src).is_ok());
2911        }
2912
2913        #[test]
2914        fn block_with_let_bindings() {
2915            let src = "fn main() -> i32 { { let x = 1; let y = 2; x + y } }";
2916            assert!(compile_to_air(src).is_ok());
2917        }
2918
2919        #[test]
2920        fn nested_blocks() {
2921            let src = "fn main() -> i32 { { { { 42 } } } }";
2922            assert!(compile_to_air(src).is_ok());
2923        }
2924
2925        #[test]
2926        fn block_scoping() {
2927            // Variable should not be accessible outside block
2928            let result = compile_to_air("fn main() -> i32 { { let x = 1; } x }");
2929            assert!(result.is_err());
2930        }
2931    }
2932
2933    // ========================================================================
2934    // Never Type
2935    // ========================================================================
2936
2937    mod never_type {
2938        use super::*;
2939
2940        #[test]
2941        fn return_is_never() {
2942            let src = "fn main() -> i32 { return 42; }";
2943            assert!(compile_to_cfg(src).is_ok());
2944        }
2945
2946        #[test]
2947        fn break_is_never() {
2948            let src = r#"
2949                fn main() -> i32 {
2950                    while true {
2951                        break;
2952                    }
2953                    0
2954                }
2955            "#;
2956            assert!(compile_to_cfg(src).is_ok());
2957        }
2958
2959        #[test]
2960        fn never_in_if_branch() {
2961            let src = "fn main() -> i32 { if true { 1 } else { return 2; } }";
2962            assert!(compile_to_cfg(src).is_ok());
2963        }
2964    }
2965
2966    // ========================================================================
2967    // Type Intrinsics
2968    // ========================================================================
2969
2970    mod intrinsics {
2971        use super::*;
2972
2973        #[test]
2974        fn size_of_intrinsic() {
2975            // @size_of returns i32
2976            let src = "fn main() -> i32 { @size_of(i32) }";
2977            assert!(compile_to_air(src).is_ok());
2978        }
2979
2980        #[test]
2981        fn align_of_intrinsic() {
2982            // @align_of returns i32
2983            let src = "fn main() -> i32 { @align_of(i64) }";
2984            assert!(compile_to_air(src).is_ok());
2985        }
2986    }
2987
2988    // ========================================================================
2989    // CFG Construction
2990    // ========================================================================
2991
2992    mod cfg_construction {
2993        use super::*;
2994
2995        #[test]
2996        fn cfg_has_correct_function_count() {
2997            let src = r#"
2998                fn foo() -> i32 { 1 }
2999                fn bar() -> i32 { 2 }
3000                fn main() -> i32 { foo() + bar() }
3001            "#;
3002            let state = compile_to_cfg(src).unwrap();
3003            assert_eq!(state.functions.len(), 3);
3004        }
3005
3006        #[test]
3007        fn cfg_branches_for_if() {
3008            let src = "fn main() -> i32 { if true { 1 } else { 0 } }";
3009            let state = compile_to_cfg(src).unwrap();
3010            // CFG should have multiple blocks for branching
3011            let main_cfg = &state.functions[0].cfg;
3012            assert!(main_cfg.blocks().len() >= 3); // entry, then, else, merge
3013        }
3014
3015        #[test]
3016        fn cfg_loop_for_while() {
3017            let src = r#"
3018                fn main() -> i32 {
3019                    let mut x = 0;
3020                    while x < 10 { x = x + 1; }
3021                    x
3022                }
3023            "#;
3024            let state = compile_to_cfg(src).unwrap();
3025            let main_cfg = &state.functions[0].cfg;
3026            assert!(main_cfg.blocks().len() >= 3); // header, body, exit
3027        }
3028    }
3029
3030    // ========================================================================
3031    // Error Messages
3032    // ========================================================================
3033
3034    mod error_messages {
3035        use super::*;
3036
3037        #[test]
3038        fn type_mismatch_error_is_descriptive() {
3039            let result = compile_to_air("fn main() -> i32 { true }");
3040            let err = result.unwrap_err().to_string();
3041            assert!(err.contains("type mismatch") || err.contains("expected"));
3042            assert!(err.contains("i32") || err.contains("bool"));
3043        }
3044
3045        #[test]
3046        fn undefined_variable_error_is_descriptive() {
3047            let result = compile_to_air("fn main() -> i32 { unknown_var }");
3048            let err = result.unwrap_err().to_string();
3049            assert!(err.contains("undefined") || err.contains("unknown"));
3050        }
3051
3052        #[test]
3053        fn missing_field_error_is_descriptive() {
3054            let src = r#"
3055                struct Point { x: i32, y: i32 }
3056                fn main() -> i32 {
3057                    let p = Point { x: 1 };
3058                    0
3059                }
3060            "#;
3061            let result = compile_to_air(src);
3062            let err = result.unwrap_err().to_string();
3063            assert!(err.contains("missing") || err.contains("field"));
3064        }
3065    }
3066
3067    // ========================================================================
3068    // Warnings
3069    // ========================================================================
3070
3071    mod warnings {
3072        use super::*;
3073
3074        #[test]
3075        fn unused_variable_warning() {
3076            let result = compile_to_air("fn main() -> i32 { let x = 42; 0 }").unwrap();
3077            assert_eq!(result.warnings.len(), 1);
3078            assert!(result.warnings[0].to_string().contains("unused"));
3079        }
3080
3081        #[test]
3082        fn underscore_prefix_suppresses_warning() {
3083            let result = compile_to_air("fn main() -> i32 { let _x = 42; 0 }").unwrap();
3084            assert_eq!(result.warnings.len(), 0);
3085        }
3086
3087        #[test]
3088        fn used_variable_no_warning() {
3089            let result = compile_to_air("fn main() -> i32 { let x = 42; x }").unwrap();
3090            assert_eq!(result.warnings.len(), 0);
3091        }
3092    }
3093
3094    // ========================================================================
3095    // Edge Cases
3096    // ========================================================================
3097
3098    mod edge_cases {
3099        use super::*;
3100
3101        #[test]
3102        fn empty_function_body() {
3103            assert!(compile_to_air("fn main() -> () { }").is_ok());
3104        }
3105
3106        #[test]
3107        fn deeply_nested_expressions() {
3108            let src = "fn main() -> i32 { ((((((1 + 2)))))) }";
3109            assert!(compile_to_air(src).is_ok());
3110        }
3111
3112        #[test]
3113        fn many_parameters() {
3114            let src = r#"
3115                fn many(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) -> i32 {
3116                    a + b + c + d + e + f
3117                }
3118                fn main() -> i32 { many(1, 2, 3, 4, 5, 6) }
3119            "#;
3120            assert!(compile_to_air(src).is_ok());
3121        }
3122
3123        #[test]
3124        fn long_chain_of_operations() {
3125            let src = "fn main() -> i32 { 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 }";
3126            assert!(compile_to_air(src).is_ok());
3127        }
3128
3129        #[test]
3130        fn multiple_functions_same_local_names() {
3131            let src = r#"
3132                fn foo() -> i32 { let x = 1; x }
3133                fn bar() -> i32 { let x = 2; x }
3134                fn main() -> i32 { foo() + bar() }
3135            "#;
3136            assert!(compile_to_air(src).is_ok());
3137        }
3138    }
3139
3140    // ========================================================================
3141    // Multi-file Parsing
3142    // ========================================================================
3143
3144    mod multi_file_parsing {
3145        use super::*;
3146
3147        #[test]
3148        fn parse_single_file() {
3149            let sources = vec![SourceFile::new(
3150                "main.gruel",
3151                "fn main() -> i32 { 42 }",
3152                FileId::new(1),
3153            )];
3154            let program = parse_all_files(&sources).unwrap();
3155            assert_eq!(program.files.len(), 1);
3156            assert_eq!(program.files[0].path, "main.gruel");
3157            assert_eq!(program.files[0].file_id, FileId::new(1));
3158        }
3159
3160        #[test]
3161        fn parse_multiple_files() {
3162            let sources = vec![
3163                SourceFile::new(
3164                    "main.gruel",
3165                    "fn main() -> i32 { helper() }",
3166                    FileId::new(1),
3167                ),
3168                SourceFile::new("utils.gruel", "fn helper() -> i32 { 42 }", FileId::new(2)),
3169            ];
3170            let program = parse_all_files(&sources).unwrap();
3171            assert_eq!(program.files.len(), 2);
3172
3173            // Check that both files were parsed
3174            let paths: Vec<_> = program.files.iter().map(|f| f.path.as_str()).collect();
3175            assert!(paths.contains(&"main.gruel"));
3176            assert!(paths.contains(&"utils.gruel"));
3177        }
3178
3179        #[test]
3180        fn parse_many_files_in_parallel() {
3181            // Create 10 files to exercise parallel parsing
3182            let sources: Vec<_> = (1..=10)
3183                .map(|i| {
3184                    SourceFile::new(
3185                        // Leak the string to get a &'static str
3186                        Box::leak(format!("file{}.gruel", i).into_boxed_str()),
3187                        Box::leak(format!("fn func{}() -> i32 {{ {} }}", i, i).into_boxed_str()),
3188                        FileId::new(i as u32),
3189                    )
3190                })
3191                .collect();
3192
3193            let program = parse_all_files(&sources).unwrap();
3194            assert_eq!(program.files.len(), 10);
3195
3196            // All functions should be in their respective ASTs
3197            for (i, file) in program.files.iter().enumerate() {
3198                assert!(!file.ast.items.is_empty(), "File {} has no items", i);
3199            }
3200        }
3201
3202        #[test]
3203        fn parse_error_in_single_file() {
3204            let sources = vec![SourceFile::new(
3205                "bad.gruel",
3206                "fn main( { }", // Missing closing paren
3207                FileId::new(1),
3208            )];
3209
3210            let result = parse_all_files(&sources);
3211            assert!(result.is_err());
3212
3213            let errors = result.unwrap_err();
3214            assert!(!errors.is_empty());
3215        }
3216
3217        #[test]
3218        fn parse_error_in_multiple_files() {
3219            let sources = vec![
3220                SourceFile::new("good.gruel", "fn good() -> i32 { 42 }", FileId::new(1)),
3221                SourceFile::new(
3222                    "bad.gruel",
3223                    "fn bad( { }", // Parse error
3224                    FileId::new(2),
3225                ),
3226            ];
3227
3228            let result = parse_all_files(&sources);
3229            assert!(result.is_err());
3230
3231            // The error should still report, and we should get at least one error
3232            let errors = result.unwrap_err();
3233            assert!(!errors.is_empty());
3234        }
3235
3236        #[test]
3237        fn lexer_error_in_file() {
3238            let sources = vec![SourceFile::new(
3239                "lexer_error.gruel",
3240                "fn main() -> i32 { $ }", // '$' is not a valid token
3241                FileId::new(1),
3242            )];
3243
3244            let result = parse_all_files(&sources);
3245            assert!(result.is_err());
3246        }
3247
3248        #[test]
3249        fn interner_merges_across_files() {
3250            let sources = vec![
3251                SourceFile::new("a.gruel", "fn foo() -> i32 { 1 }", FileId::new(1)),
3252                SourceFile::new("b.gruel", "fn bar() -> i32 { 2 }", FileId::new(2)),
3253            ];
3254
3255            let program = parse_all_files(&sources).unwrap();
3256
3257            // The merged interner should contain both "foo" and "bar"
3258            let has_foo = program.interner.iter().any(|(_, s)| s == "foo");
3259            let has_bar = program.interner.iter().any(|(_, s)| s == "bar");
3260
3261            assert!(has_foo, "Interner should contain 'foo'");
3262            assert!(has_bar, "Interner should contain 'bar'");
3263        }
3264
3265        #[test]
3266        fn empty_file_parses_ok() {
3267            let sources = vec![SourceFile::new("empty.gruel", "", FileId::new(1))];
3268
3269            let program = parse_all_files(&sources).unwrap();
3270            assert_eq!(program.files.len(), 1);
3271            assert!(program.files[0].ast.items.is_empty());
3272        }
3273
3274        #[test]
3275        fn file_ids_preserved() {
3276            let sources = vec![
3277                SourceFile::new("a.gruel", "fn a() -> i32 { 1 }", FileId::new(42)),
3278                SourceFile::new("b.gruel", "fn b() -> i32 { 2 }", FileId::new(99)),
3279            ];
3280
3281            let program = parse_all_files(&sources).unwrap();
3282
3283            let file_ids: Vec<_> = program.files.iter().map(|f| f.file_id).collect();
3284            assert!(file_ids.contains(&FileId::new(42)));
3285            assert!(file_ids.contains(&FileId::new(99)));
3286        }
3287    }
3288}