repo_description: Otterlang programming language 🦦
repo_topics: compiler general-purpose language llvm otter otterlang programming-language pythonic rust scripting-language
code: #![expect(
      clippy::print_stdout,
      clippy::print_stderr,
      reason = "TODO: Use robust logging"
  )]
  
  use std::fs;
  use std::path::{Path, PathBuf};
  use std::process::Command as ProcessCommand;
  use std::time::Duration;
  
  use anyhow::{Context, Result, anyhow, bail};
  use clap::{Parser, Subcommand};
  use colored::Colorize;
  use tracing::{debug, info, warn};
  
  const TASK_RUNTIME_ENABLED: bool = cfg!(feature = "task-runtime");
  
  use otterc_cache::{CacheBuildOptions, CacheEntry, CacheManager, CacheMetadata, CompilationInputs};
  use otterc_codegen::{BuildArtifact, build_executable};
  use otterc_config::{CodegenOptLevel, CodegenOptions, LanguageFeatureFlags, TargetTriple, VERSION};
  use otterc_ffi::{BridgeSymbolRegistry, FunctionSpec, TypeSpec};
  use otterc_jit::{ExecutorStats, JitExecutor};
  use otterc_lexer::{LexerError, tokenize};
  use otterc_module::ModuleProcessor;
  use otterc_parser::{ParserError, parse};
  use otterc_runtime::memory::config::GcStrategy;
  use otterc_symbol::registry::SymbolRegistry;
  use otterc_typecheck::TypeChecker;
  use otterc_utils::errors::{Diagnostic, emit_diagnostics};
  use otterc_utils::logger;
  use otterc_utils::profiler::{PhaseTiming, Profiler};
  use std::collections::{HashMap, HashSet};
  
  #[derive(Parser, Debug)]
  #[command(name = "otter", version = VERSION, about = "OtterLang compiler")]
  pub struct OtterCli {
      #[arg(long, global = true)]
      /// Dump the token stream before parsing.
      dump_tokens: bool,
  
      #[arg(long, global = true)]
      /// Dump the parsed AST before code generation.
      dump_ast: bool,
  
      #[arg(long, global = true)]
      /// Dump the generated LLVM IR.
      dump_ir: bool,
  
      #[arg(long, global = true)]
      /// Display phase timing information.
      time: bool,
  
      #[arg(long, global = true)]
      /// Emit profiling summary for the compilation.
      profile: bool,
  
      #[arg(long, global = true)]
      /// Enable release mode (O3 + LTO) when building binaries.
      release: bool,
  
      #[arg(long, global = true)]
      /// Enable the experimental async task runtime when executing programs.
      tasks: bool,
  
      #[arg(long, global = true)]
      /// Emit verbose scheduler diagnostics from the task runtime.
      tasks_debug: bool,
  
      #[arg(long, global = true)]
      /// Trace task lifecycle events from the runtime.
      tasks_trace: bool,
  
      #[arg(long, global = true)]
      /// Execute programs via the experimental JIT instead of spawning a separate binary.
      jit: bool,
  
      #[arg(long, global = true)]
      /// Enable debug mode with stack traces.
      debug: bool,
  
      #[arg(long, global = true)]
      /// Disable cache for this compilation.
      no_cache: bool,
  
      #[arg(long, global = true, value_name = "list")]
      /// Enable experimental language features (comma-separated names or use OTTER_FEATURES env var).
      features: Option<String>,
  
      #[arg(long, global = true)]
      /// Target triple for cross-compilation (e.g., wasm32-unknown-unknown, thumbv7m-none-eabi)
      target: Option<String>,
  
      #[arg(long, global = true, value_name = "strategy")]
      /// Select the GC strategy (rc, mark-sweep, generational, none)
      gc_strategy: Option<String>,
  
      #[arg(long, global = true, value_name = "fraction")]
      /// Override the GC heap usage threshold before triggering a cycle (0.0-1.0)
      gc_threshold: Option<f64>,
  
      #[arg(long, global = true, value_name = "ms")]
      /// Force a fixed GC interval in milliseconds (0 disables interval-based cycles)
      gc_interval_ms: Option<u64>,
  
      #[arg(long, global = true, value_name = "bytes")]
      /// Limit the number of bytes that may be allocated while GC is disabled
      gc_disabled_max_bytes: Option<usize>,
  
      #[command(subcommand)]
      command: Command,
  }
  
  impl OtterCli {
      pub fn command(&self) -> &Command {
          &self.command
      }
  }
  
  #[derive(Subcommand, Debug)]
  pub enum Command {
      /// Lexes, parses, and executes the specified source file via the cached native pipeline.
      #[command(alias = "r")]
      Run { path: PathBuf },
      /// Builds a native executable from the specified source file.
      #[command(alias = "b")]
      Build {
          path: PathBuf,
          #[arg(short, long)]
          output: Option<PathBuf>,
      },
      /// Checks the source file for errors without generating code.
      #[command(alias = "c")]
      Check { path: PathBuf },
      /// Format OtterLang source code.
      Fmt {
          /// Files to format (defaults to all .ot files in current directory)
          #[arg(default_value = ".")]
          paths: Vec<PathBuf>,
      },
      /// Profile OtterLang programs (memory or performance)
      Profile {
          #[command(subcommand)]
          subcommand: crate::tools::profiler::ProfileCommand,
      },
      /// Run tests in OtterLang source files
      #[command(alias = "t")]
      Test {
          /// Test files or directories to run (defaults to current directory)
          #[arg(default_value = ".")]
          paths: Vec<PathBuf>,
          /// Run tests in parallel
          #[arg(short, long)]
          parallel: bool,
          /// Show output from passing tests
          #[arg(short, long)]
          verbose: bool,
          /// Update snapshots instead of comparing
          #[arg(long)]
          update_snapshots: bool,
      },
  }
  
  pub fn run() -> Result<()> {
      logger::init_logging();
      maybe_auto_update()?;
      otterc_ffi::bootstrap_stdlib();
      let cli = OtterCli::parse();
      enforce_task_runtime_flags(&cli)?;
  
      if cli.jit && !matches!(cli.command, Command::Run { .. }) {
          bail!("--jit is currently only supported with the `run` command");
      }
  
      match &cli.command {
          Command::Run { path } => handle_run(&cli, path),
          Command::Build { path, output } => handle_build(&cli, path, output.clone()),
          Command::Check { path } => handle_check(&cli, path),
          Command::Fmt { paths } => handle_fmt(paths),
          Command::Profile { subcommand } => {
              crate::tools::profiler::run_profiler_subcommand(subcommand)
          }
          Command::Test {
              paths,
              parallel,
              verbose,
              update_snapshots,
          } => handle_test(&cli, paths, *parallel, *verbose, *update_snapshots),
      }
  }
  
  fn enforce_task_runtime_flags(cli: &OtterCli) -> Result<()> {
      if TASK_RUNTIME_ENABLED {
          return Ok(());
      }
  
      if cli.tasks || cli.tasks_debug || cli.tasks_trace {
          bail!(
              "task runtime diagnostics require building otterlang with the 'task-runtime' feature (try `cargo run --features task-runtime -- ...`)."
          );
      }
  
      Ok(())
  }
  
  fn maybe_auto_update() -> Result<()> {
      let source_dir = match std::env::var("OTTER_DEV_AUTOUPDATE") {
          Ok(val) if val == "0" || val.eq_ignore_ascii_case("false") => return Ok(()),
          Ok(path) if !path.is_empty() => PathBuf::from(path),
          _ => return Ok(()),
      };
  
      let cargo_toml = source_dir.join("Cargo.toml");
      if !cargo_toml.exists() {
          warn!(
              "OTTER_DEV_AUTOUPDATE is set but {} is missing; skipping auto-install",
              cargo_toml.display()
          );
          return Ok(());
      }
  
      let cargo_lock = source_dir.join("Cargo.lock");
      let lock_mtime = cargo_lock
          .metadata()
          .and_then(|meta| meta.modified())
          .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
  
      let stamp_path = source_dir.join(".otter/cli-autoupdate.stamp");
      fs::create_dir_all(stamp_path.parent().unwrap())?;
  
      let mut needs_install = true;
      if let Some(prev) = fs::read_to_string(&stamp_path)
          .ok()
          .and_then(|c| c.trim().parse::<u128>().ok())
      {
          let current = lock_mtime
              .duration_since(std::time::SystemTime::UNIX_EPOCH)
              .map(|d| d.as_millis())
              .unwrap_or(0);
          if current <= prev {
              needs_install = false;
          }
      }
  
      if !needs_install {
          return Ok(());
      }
  
      info!(
          "Auto-updating otter CLI from {} (set OTTER_DEV_AUTOUPDATE=0 to disable)",
          source_dir.display()
      );
  
      let status = ProcessCommand::new("cargo")
          .arg("install")
          .arg("--path")
          .arg(&source_dir)
          .arg("--force")
          .status()
          .with_context(|| "failed to spawn cargo install for auto-update")?;
  
      if !status.success() {
          warn!("cargo install exited with status {status:?}; continuing without updating");
          return Ok(());
      }
  
      let millis = lock_mtime
          .duration_since(std::time::SystemTime::UNIX_EPOCH)
          .map(|d| d.as_millis())
          .unwrap_or(0);
      fs::write(&stamp_path, format!("{}\n", millis))?;
      Ok(())
  }
  
  fn handle_run(cli: &OtterCli, path: &Path) -> Result<()> {
      let settings = CompilationSettings::from_cli(cli)?;
      let source = read_source(path)?;
      let stage = compile_pipeline(path, &source, &settings)?;
  
      if settings.jit_enabled() {
          match &stage.result {
              CompilationResult::PreparedProgram { program } => {
                  run_program_with_jit(program, &settings)?;
              }
              _ => {
                  bail!(
                      "unexpected compilation result for JIT execution; was the --jit flag removed?"
                  );
              }
          }
      } else {
          match &stage.result {
              CompilationResult::CacheHit(entry) => {
                  println!(
                      "{} ({} bytes)",
                      "Cache hit".cyan().bold(),
                      entry.metadata.binary_size
                  );
                  if settings.profile {
                      print_profile(&entry.metadata);
                  }
                  execute_binary(&entry.binary_path, &settings)?;
              }
              CompilationResult::Compiled { artifact, metadata } => {
                  println!("{} {}", "Building".blue().bold(), artifact.binary.display());
                  execute_binary(&artifact.binary, &settings)?;
                  if settings.dump_ir
                      && let Some(ir) = &artifact.ir
                  {
                      println!("\n{}", "== LLVM IR ==".bold());
                      println!("{ir}");
                  }
                  if settings.profile {
                      print_profile(metadata);
                  }
              }
              CompilationResult::Checked => {
                  unreachable!("check_only should be false for run command")
              }
              CompilationResult::PreparedProgram { .. } => {
                  unreachable!("prepared program should only be returned when --jit is enabled")
              }
          }
      }
  
      if settings.time {
          print_timings(&stage);
      }
  
      Ok(())
  }
  
  fn handle_build(cli: &OtterCli, path: &Path, output: Option<PathBuf>) -> Result<()> {
      let settings = CompilationSettings::from_cli(cli)?;
      let source = read_source(path)?;
      let stage = compile_pipeline(path, &source, &settings)?;
  
      let output_path = resolve_output_path(path, output);
      if let Some(parent) = output_path.parent() {
          fs::create_dir_all(parent)
              .with_context(|| format!("failed to create output directory {}", parent.display()))?;
      }
  
      let cached_binary = match &stage.result {
          CompilationResult::CacheHit(entry) => &entry.binary_path,
          CompilationResult::Compiled { artifact, .. } => &artifact.binary,
          CompilationResult::Checked => unreachable!("check_only should be false for build command"),
          CompilationResult::PreparedProgram { .. } => {
              unreachable!("--jit is not supported for the build command")
          }
      };
  
      fs::copy(cached_binary, &output_path).with_context(|| {
          format!(
              "failed to copy cached binary {} to {}",
              cached_binary.display(),
              output_path.display()
          )
      })?;
  
      println!("{} {}", "Built".green().bold(), output_path.display());
  
      match &stage.result {
          CompilationResult::Compiled { artifact, metadata } => {
              if settings.dump_ir
                  && let Some(ir) = &artifact.ir
              {
                  println!("\n{}", "== LLVM IR ==".bold());
                  println!("{ir}");
              }
              if settings.profile {
                  print_profile(metadata);
              }
          }
          CompilationResult::CacheHit(entry) => {
              if settings.profile {
                  print_profile(&entry.metadata);
              }
          }
          CompilationResult::Checked => unreachable!("check_only should be false for build command"),
          CompilationResult::PreparedProgram { .. } => {
              unreachable!("--jit is not supported for the build command")
          }
      }
  
      if settings.time {
          print_timings(&stage);
      }
  
      Ok(())
  }
  
  fn handle_check(cli: &OtterCli, path: &Path) -> Result<()> {
      let mut settings = CompilationSettings::from_cli(cli)?;
      settings.check_only = true;
      let source = read_source(path)?;
      let stage = compile_pipeline(path, &source, &settings)?;
  
      if settings.time {
          print_timings(&stage);
      }
  
      println!("{} {}", "Checked".green().bold(), path.display());
      Ok(())
  }
  
  pub fn compile_pipeline(
      path: &Path,
      source: &str,
      settings: &CompilationSettings,
  ) -> Result<CompilationStage> {
      let mut cache_manager = CacheManager::new();
      let cache_options = settings.cache_build_options();
      let mut profiler = Profiler::new();
      let source_id = path.display().to_string();
      let source_dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
  
      // Try to find stdlib directory
      let stdlib_dir = find_stdlib_dir().ok();
  
      // Initial inputs without module dependencies (will be updated after parsing)
      let mut inputs = CompilationInputs::new(path.to_path_buf(), Vec::new());
  
      // Generate initial cache key for quick lookup check
      let initial_cache_key = profiler.record_phase("Fingerprint", || {
          cache_manager.fingerprint(&inputs, &cache_options, VERSION)
      });
  
      if settings.allow_cache()
          && let Some(entry) =
              profiler.record_phase("Cache lookup", || cache_manager.lookup(&initial_cache_key))
      {
          debug!(cache_hit = %entry.binary_path.display());
          profiler.push_phase("Compile skipped", Duration::from_millis(0));
          return Ok(CompilationStage {
              profiler,
              result: CompilationResult::CacheHit(entry),
          });
      }
  
      let tokens = match profiler.record_phase("Lexing", || tokenize(source)) {
          Ok(tokens) => tokens,
          Err(errors) => {
              emit_lexer_errors(&source_id, source, &errors);
              bail!("lexing failed");
          }
      };
  
      if settings.dump_tokens {
          println!("\n{}", "== Tokens ==".bold());
          for token in &tokens {
              println!("  {:?} @ {:?}", token.kind(), token.span());
          }
      }
  
      let program = match profiler.record_phase("Parsing", || parse(&tokens)) {
          Ok(program) => {
              if settings.debug {
                  println!("Parsed successfully");
              }
              program
          }
          Err(errors) => {
              emit_parser_errors(&source_id, source, &errors);
              bail!("parsing failed");
          }
      };
  
      if settings.dump_ast {
          println!("\n{}", "== AST ==".bold());
          println!("{:#?}", program);
      }
  
      // Process module imports
      let mut module_processor = ModuleProcessor::new(source_dir.clone(), stdlib_dir.clone());
      let module_deps = profiler.record_phase("Module Resolution", || {
          module_processor.process_imports(&program)
      })?;
  
      // Resolve re-exports after all modules are loaded
      profiler.record_phase("Re-export Resolution", || {
          module_processor.resolve_all_re_exports()
      })?;
  
      // Register Rust FFI functions for type checking (before type checking)
      let registry = otterc_symbol::registry::SymbolRegistry::global();
      profiler.record_phase("Register FFI Functions", || {
          register_rust_ffi_functions_for_typecheck(&program, registry)
      })?;
  
      // Type check the program
      let mut type_checker =
          TypeChecker::with_language_features(settings.language_features().clone())
              .with_registry(registry);
  
      for module in module_processor.modules() {
          type_checker.register_module_definitions(&module.program);
      }
      let type_check_result =
          profiler.record_phase("Type Checking", || type_checker.check_program(&program));
  
      if let Err(err) = type_check_result {
          let diagnostics = otterc_typecheck::diagnostics_from_type_errors(
              type_checker.errors(),
              &source_id,
              source,
          );
          emit_diagnostics(&diagnostics, source);
          return Err(err).with_context(|| "type checking failed");
      }
  
      if settings.check_only {
          profiler.push_phase("Codegen skipped", Duration::from_millis(0));
          return Ok(CompilationStage {
              profiler,
              result: CompilationResult::Checked,
          });
      }
  
      if settings.jit_enabled() {
          profiler.push_phase("Codegen skipped (JIT)", Duration::from_millis(0));
          return Ok(CompilationStage {
              profiler,
              result: CompilationResult::PreparedProgram { program },
          });
      }
  
      let enum_layouts = type_checker.enum_layouts();
      let (expr_types, expr_types_by_span, comprehension_var_types) = type_checker.into_type_maps();
  
      // Update inputs with module dependencies for accurate cache fingerprinting
      inputs.imports = module_deps
          .iter()
          .map(|p| p.display().to_string())
          .collect();
      let cache_key = profiler.record_phase("Fingerprint (with modules)", || {
          cache_manager.fingerprint(&inputs, &cache_options, VERSION)
      });
  
      // Check cache again with module dependencies included
      if settings.allow_cache()
          && let Some(entry) = profiler.record_phase("Cache lookup (with modules)", || {
              cache_manager.lookup(&cache_key)
          })
      {
          debug!(cache_hit = %entry.binary_path.display());
          profiler.push_phase("Compile skipped", Duration::from_millis(0));
          return Ok(CompilationStage {
              profiler,
              result: CompilationResult::CacheHit(entry),
          });
      }
  
      let codegen_options = settings.codegen_options();
      let binary_path = if let Some(path) = cache_manager.binary_path(&cache_key) {
          ensure_output_directory(&path)?;
          path
      } else {
          let fallback = PathBuf::from("./target/tmp_binary");
          ensure_output_directory(&fallback)?;
          fallback
      };
  
      let artifact = profiler.record_phase("Codegen", || {
          build_executable(
              &program,
              &expr_types,
              &expr_types_by_span,
              &comprehension_var_types,
              &enum_layouts,
              &binary_path,
              &codegen_options,
          )
      })?;
  
      let build_duration_ms = profiler
          .phases()
          .last()
          .map(|phase| phase.duration.as_millis())
          .unwrap_or_default();
  
      let binary_size = std::fs::metadata(&artifact.binary)?.len();
  
      let metadata = CacheMetadata::new(
          cache_key.clone(),
          canonical_or(path),
          inputs.dependencies.clone(),
          artifact.binary.clone(),
          binary_size,
          build_duration_ms as u64,
          PathBuf::from("./cache"), // cache_path
      )
      .with_llvm_version(otterc_codegen::current_llvm_version());
  
      if let Err(e) = cache_manager.store(&metadata) {
          warn!("Failed to store cache entry: {}", e);
      }
  
      info!(compiled = %artifact.binary.display(), size = binary_size);
  
      Ok(CompilationStage {
          profiler,
          result: CompilationResult::Compiled { artifact, metadata },
      })
  }
  
  fn ensure_output_directory(path: &Path) -> Result<()> {
      if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
          fs::create_dir_all(parent)
              .with_context(|| format!("failed to create output directory {}", parent.display()))?;
      }
  
      Ok(())
  }
  
  pub struct CompilationStage {
      profiler: Profiler,
      pub result: CompilationResult,
  }
  
  pub enum CompilationResult {
      CacheHit(CacheEntry),
      Checked,
      Compiled {
          artifact: BuildArtifact,
          metadata: CacheMetadata,
      },
      PreparedProgram {
          program: otterc_ast::nodes::Program,
      },
  }
  
  impl CompilationStage {
      fn timings(&self) -> &[PhaseTiming] {
          self.profiler.phases()
      }
  }
  
  #[derive(Clone)]
  pub struct CompilationSettings {
      dump_tokens: bool,
      dump_ast: bool,
      dump_ir: bool,
      time: bool,
      profile: bool,
      release: bool,
      tasks: bool,
      tasks_debug: bool,
      tasks_trace: bool,
      jit: bool,
      debug: bool,
      target: Option<String>,
      no_cache: bool,
      enable_cache: bool,
      cache_dir: PathBuf,
      max_cache_size: usize,
      check_only: bool,
      language_features: LanguageFeatureFlags,
      gc: GcCliOptions,
  }
  
  #[derive(Clone, Default)]
  struct GcCliOptions {
      strategy: Option<GcStrategy>,
      threshold: Option<f64>,
      interval_ms: Option<u64>,
      disabled_limit: Option<usize>,
  }
  
  impl GcCliOptions {
      fn from_cli(cli: &OtterCli) -> Result<Self> {
          let strategy = match &cli.gc_strategy {
              Some(value) => Some(
                  value
                      .parse::<GcStrategy>()
                      .or(Err(anyhow!("invalid GC strategy '{}'", value)))?,
              ),
              None => None,
          };
  
          Ok(Self {
              strategy,
              threshold: cli.gc_threshold.map(|value| value.clamp(0.0, 1.0)),
              interval_ms: cli.gc_interval_ms,
              disabled_limit: cli.gc_disabled_max_bytes,
          })
      }
  
      fn env_pairs(&self) -> Vec<(&'static str, String)> {
          let mut pairs = Vec::new();
          if let Some(strategy) = self.strategy {
              pairs.push((
                  "OTTER_GC_STRATEGY",
                  Self::strategy_env_value(strategy).to_string(),
              ));
          }
          if let Some(threshold) = self.threshold {
              pairs.push(("OTTER_GC_THRESHOLD", threshold.to_string()));
          }
          if let Some(interval) = self.interval_ms {
              pairs.push(("OTTER_GC_INTERVAL", interval.to_string()));
          }
          if let Some(limit) = self.disabled_limit {
              pairs.push(("OTTER_GC_DISABLED_MAX_BYTES", limit.to_string()));
          }
          pairs
      }
  
      fn strategy_env_value(strategy: GcStrategy) -> &'static str {
          match strategy {
              GcStrategy::ReferenceCounting => "reference-counting",
              GcStrategy::MarkSweep => "mark-sweep",
              GcStrategy::Generational => "generational",
              GcStrategy::None => "none",
          }
      }
  }
  
  impl CompilationSettings {
      fn from_cli(cli: &OtterCli) -> Result<Self> {
          let language_features = resolve_language_features(cli);
          let gc = GcCliOptions::from_cli(cli)?;
          Ok(Self {
              dump_tokens: cli.dump_tokens,
              dump_ast: cli.dump_ast,
              dump_ir: cli.dump_ir,
              time: cli.time,
              profile: cli.profile,
              release: cli.release,
              tasks: cli.tasks,
              tasks_debug: cli.tasks_debug,
              tasks_trace: cli.tasks_trace,
              jit: cli.jit,
              debug: cli.debug,
              target: cli.target.clone(),
              no_cache: cli.no_cache,
              enable_cache: !cli.no_cache,
              cache_dir: PathBuf::from("./cache"),
              max_cache_size: 1024 * 1024 * 1024, // 1GB default
              check_only: false,
              language_features,
              gc,
          })
      }
  
      fn allow_cache(&self) -> bool {
          !(self.dump_tokens
              || self.dump_ast
              || self.dump_ir
              || self.no_cache
              || self.check_only
              || self.jit)
      }
  
      pub fn jit_enabled(&self) -> bool {
          self.jit
      }
  
      pub fn apply_runtime_env(&self, command: &mut std::process::Command) {
          for (key, value) in self.runtime_env_pairs() {
              command.env(key, value);
          }
      }
  
      fn runtime_env_pairs(&self) -> Vec<(String, String)> {
          let mut pairs = Vec::new();
          if self.tasks {
              pairs.push(("OTTER_TASKS_DIAGNOSTICS".into(), "1".into()));
          }
          if self.tasks_debug {
              pairs.push(("OTTER_TASKS_DEBUG".into(), "1".into()));
          }
          if self.tasks_trace {
              pairs.push(("OTTER_TASKS_TRACE".into(), "1".into()));
          }
          if self.debug {
              pairs.push(("RUST_BACKTRACE".into(), "1".into()));
              pairs.push(("OTTER_DEBUG".into(), "1".into()));
          }
          for (key, value) in self.gc.env_pairs() {
              pairs.push((key.to_string(), value));
          }
          pairs
      }
  
      fn cache_build_options(&self) -> CacheBuildOptions {
          CacheBuildOptions {
              enable_cache: self.enable_cache,
              cache_dir: self.cache_dir.clone(),
              max_cache_size: self.max_cache_size,
              release: self.release,
              lto: self.release,
              emit_ir: self.dump_ir,
          }
      }
  
      fn codegen_options(&self) -> CodegenOptions {
          let target = self.target.as_ref().and_then(|t| {
              TargetTriple::parse(t)
                  .map_err(|e| {
                      eprintln!("Warning: Invalid target triple '{}': {}", t, e);
                  })
                  .ok()
          });
  
          CodegenOptions {
              emit_ir: self.dump_ir,
              opt_level: if self.release {
                  CodegenOptLevel::Aggressive
              } else {
                  CodegenOptLevel::Default
              },
              enable_lto: self.release,
              enable_pgo: false,
              pgo_profile_file: None,
              inline_threshold: None,
              target,
          }
      }
  
      fn language_features(&self) -> &LanguageFeatureFlags {
          &self.language_features
      }
  }
  
  fn resolve_language_features(cli: &OtterCli) -> LanguageFeatureFlags {
      let mut flags = LanguageFeatureFlags::default();
  
      if let Ok(env_value) = std::env::var("OTTER_FEATURES") {
          apply_feature_list(&env_value, &mut flags, "OTTER_FEATURES");
      }
  
      if let Some(cli_value) = cli.features.as_deref() {
          apply_feature_list(cli_value, &mut flags, "--features");
      }
  
      if flags.any_enabled() {
          let enabled = collect_enabled_feature_names(&flags).join(", ");
          info!("language features enabled: {}", enabled);
      }
  
      flags
  }
  
  fn apply_feature_list(source: &str, flags: &mut LanguageFeatureFlags, label: &str) {
      for raw in source.split(',') {
          for token in raw.split_whitespace() {
              let feature = token.trim();
              if feature.is_empty() {
                  continue;
              }
              if !flags.enable(feature) {
                  warn!("unknown language feature '{}' from {}", feature, label);
              }
          }
      }
  }
  
  fn collect_enabled_feature_names(flags: &LanguageFeatureFlags) -> Vec<&'static str> {
      let mut names = Vec::new();
      if flags.result_option_core {
          names.push(LanguageFeatureFlags::RESULT_OPTION_CORE);
      }
      if flags.match_exhaustiveness {
          names.push(LanguageFeatureFlags::MATCH_EXHAUSTIVENESS);
      }
      if flags.newtype_aliases {
          names.push(LanguageFeatureFlags::NEWTYPE_ALIASES);
      }
      names
  }
  
  pub fn read_source(path: &Path) -> Result<String> {
      fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))
  }
  
  fn resolve_output_path(path: &Path, output: Option<PathBuf>) -> PathBuf {
      output.unwrap_or_else(|| {
          let mut candidate = path.with_extension("");
          if candidate.file_name().is_none() {
              candidate = PathBuf::from("otter.out");
          }
  
          #[cfg(target_os = "windows")]
          {
              candidate.set_extension("exe");
          }
  
          candidate
      })
  }
  
  fn canonical_or(path: &Path) -> PathBuf {
      path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
  }
  
  fn find_stdlib_dir() -> Result<PathBuf> {
      // Try environment variable first
      if let Ok(dir) = std::env::var("OTTER_STDLIB_DIR") {
          let path = PathBuf::from(dir);
          if path.exists() {
              return Ok(path);
          }
      }
  
      // Try relative to executable (for development)
      if let Ok(exe) = std::env::current_exe()
          && let Some(exe_dir) = exe.parent()
      {
          let stdlib = exe_dir
              .parent()
              .unwrap_or(exe_dir)
              .join("stdlib")
              .join("otter");
          if stdlib.exists() {
              return Ok(stdlib);
          }
      }
  
      // Try relative to current directory (for development)
      let stdlib = PathBuf::from("stdlib").join("otter");
      if stdlib.exists() {
          return Ok(stdlib);
      }
  
      bail!(
          "stdlib directory not found. Set OTTER_STDLIB_DIR environment variable or ensure stdlib/otter exists"
      )
  }
  
  fn execute_binary(path: &Path, settings: &CompilationSettings) -> Result<()> {
      if settings.debug {
          println!("Running program: {}", path.display());
      }
  
      let mut command = ProcessCommand::new(path);
      settings.apply_runtime_env(&mut command);
  
      let status = command
          .status()
          .with_context(|| format!("failed to execute {}", path.display()))?;
  
      if !status.success() {
          if settings.debug {
              eprintln!("\nStack trace:");
              eprintln!("  Exit status: {}", status);
          }
          bail!("program exited with status {status}");
      }
  
      Ok(())
  }
  
  fn run_program_with_jit(
      program: &otterc_ast::nodes::Program,
      settings: &CompilationSettings,
  ) -> Result<()> {
      let _env_guard = RuntimeEnvGuard::apply(settings);
      let registry = SymbolRegistry::global();
      let mut executor = JitExecutor::new(program, registry)?;
      executor.execute_main()?;
  
      if settings.profile {
          let stats = executor.get_stats();
          print_jit_stats(&stats);
      }
  
      Ok(())
  }
  
  struct RuntimeEnvGuard {
      previous: Vec<(String, Option<String>)>,
  }
  
  impl RuntimeEnvGuard {
      fn apply(settings: &CompilationSettings) -> Self {
          let mut previous = Vec::new();
          for (key, value) in settings.runtime_env_pairs() {
              let prior = std::env::var(&key).ok();
              // SAFETY: setting process environment variables is the intended side effect here.
              unsafe {
                  std::env::set_var(&key, &value);
              }
              previous.push((key, prior));
          }
          Self { previous }
      }
  }
  
  impl Drop for RuntimeEnvGuard {
      fn drop(&mut self) {
          for (key, value) in self.previous.drain(..).rev() {
              if let Some(v) = value {
                  // SAFETY: restoring the previous variable mirrors the earlier mutation.
                  unsafe {
                      std::env::set_var(&key, v);
                  }
              } else {
                  // SAFETY: removing the variable reverts the temporary override above.
                  unsafe {
                      std::env::remove_var(&key);
                  }
              }
          }
      }
  }
  
  fn print_timings(stage: &CompilationStage) {
      println!("\nTimings:");
      let mut total = Duration::ZERO;
      for PhaseTiming { name: _, duration } in stage.timings() {
          total += *duration;
      }
      for PhaseTiming { name, duration } in stage.timings() {
          let pct = if total.as_secs_f64() > 0.0 {
              (duration.as_secs_f64() / total.as_secs_f64()) * 100.0
          } else {
              0.0
          };
          println!(
              "  {:20} {:8.2}ms ({:5.1}%)",
              name,
              duration.as_secs_f64() * 1000.0,
              pct
          );
      }
      println!("  {:20} {:8.2}ms", "Total", total.as_secs_f64() * 1000.0);
  }
  
  fn handle_fmt(paths: &[PathBuf]) -> Result<()> {
      use glob::glob;
      use otterc_fmt::Formatter;
      use otterc_lexer::tokenize;
      use otterc_parser::parse;
  
      println!("Formatting OtterLang files...");
  
      let formatter = Formatter::new();
      let mut formatted_count = 0;
  
      // Collect all .ot files
      let mut files = Vec::new();
      if paths.is_empty() || (paths.len() == 1 && paths[0].to_str() == Some(".")) {
          // Default: format all .ot files in current directory recursively
          for path in (glob("**/*.ot")?).flatten() {
              files.push(path);
          }
      } else {
          for path in paths {
              if path.is_dir() {
                  for p in (glob(&format!("{}/**/*.ot", path.display()))?).flatten() {
                      files.push(p);
                  }
              } else if path.extension().is_some_and(|ext| ext == "ot") {
                  files.push(path.clone());
              }
          }
      }
  
      for file_path in files {
          let source = fs::read_to_string(&file_path)
              .with_context(|| format!("failed to read {}", file_path.display()))?;
  
          #[expect(
              clippy::map_err_ignore,
              reason = "TODO: Use the provided error when reporting"
          )]
          let tokens = tokenize(&source)
              .map_err(|_| anyhow::anyhow!("failed to tokenize {}", file_path.display()))?;
  
          #[expect(
              clippy::map_err_ignore,
              reason = "TODO: Use the provided error when reporting"
          )]
          let program = parse(&tokens)
              .map_err(|_| anyhow::anyhow!("failed to parse {}", file_path.display()))?;
  
          let formatted = formatter.format_program(&program);
  
          if formatted != source {
              fs::write(&file_path, formatted)
                  .with_context(|| format!("failed to write {}", file_path.display()))?;
              println!("  {}", file_path.display());
              formatted_count += 1;
          }
      }
  
      if formatted_count == 0 {
          println!("All files are already formatted");
      } else {
          println!("\nFormatted {} file(s)", formatted_count);
      }
  
      Ok(())
  }
  
  fn print_profile(metadata: &CacheMetadata) {
      println!("\nProfile:");
      println!("  Binary: {}", metadata.binary_path.display());
      println!("  Size:   {} bytes", metadata.binary_size);
      println!("  Build:  {} ms", metadata.build_time_ms);
      if let Some(version) = &metadata.llvm_version {
          println!("  LLVM:   {}", version);
      }
  }
  
  fn print_jit_stats(stats: &ExecutorStats) {
      println!("\nJIT profile:");
      if stats.profiler_metrics.is_empty() {
          println!("  No functions executed yet");
      } else {
          for metric in &stats.profiler_metrics {
              println!(
                  "  {:20} calls: {:6} avg: {:>8.3}µs total: {:>8.3}ms",
                  metric.name,
                  metric.call_count,
                  metric.avg_time.as_secs_f64() * 1_000_000.0,
                  metric.total_time.as_secs_f64() * 1000.0
              );
          }
      }
      println!(
          "  Cache: {} function(s), {} bytes",
          stats.cache_stats.total_functions, stats.cache_stats.total_size
      );
  }
  
  fn emit_lexer_errors(source_id: &str, source: &str, errors: &[LexerError]) {
      println!("\nLexical errors:");
      let diagnostics: Vec<Diagnostic> = errors
          .iter()
          .map(|err| err.to_diagnostic(source_id))
          .collect();
      emit_diagnostics(&diagnostics, source);
  }
  
  fn emit_parser_errors(source_id: &str, source: &str, errors: &[ParserError]) {
      println!("\nParsing errors:");
      let diagnostics: Vec<Diagnostic> = errors
          .iter()
          .map(|err| err.to_diagnostic(source_id))
          .collect();
      emit_diagnostics(&diagnostics, source);
  }
  
  fn handle_test(
      cli: &OtterCli,
      paths: &[PathBuf],
      parallel: bool,
      verbose: bool,
      update_snapshots: bool,
  ) -> Result<()> {
      use crate::test::{TestDiscovery, TestReporter, TestRunner};
      use rayon::prelude::*;
  
      let settings = CompilationSettings::from_cli(cli)?;
      let mut discovery = TestDiscovery::new();
      discovery.discover_files(paths)?;
  
      let tests = discovery.discover_all_tests()?;
  
      if tests.is_empty() {
          println!("No tests found");
          return Ok(());
      }
  
      println!("Running {} test(s)...\n", tests.len());
  
      let runner = TestRunner::new(settings, update_snapshots);
      let mut reporter = TestReporter::new(verbose);
  
      if parallel {
          // Run tests in parallel
          let results: Vec<_> = tests
              .par_iter()
              .map(|test| {
                  let result = runner.run_test(test);
                  (test.clone(), result)
              })
              .collect();
  
          for (test, result) in results {
              reporter.print_result(&test, &result);
              reporter.record_result(test, result);
          }
      } else {
          // Run tests sequentially
          for test in tests {
              let result = runner.run_test(&test);
              reporter.print_result(&test, &result);
              reporter.record_result(test, result);
          }
      }
  
      reporter.print_summary();
  
      if reporter.has_failures() {
          #[expect(clippy::exit, reason = "It's desired to exit immediately here")]
          std::process::exit(1);
      }
  
      Ok(())
  }
  
  fn register_rust_ffi_functions_for_typecheck(
      program: &otterc_ast::nodes::Program,
      registry: &'static SymbolRegistry,
  ) -> Result<()> {
      let imports = collect_rust_imports_for_typecheck(program);
      if imports.is_empty() {
          return Ok(());
      }
  
      let bridge_registry = BridgeSymbolRegistry::global();
  
      for (crate_name, aliases) in imports {
          let metadata = bridge_registry.ensure_metadata(&crate_name)?;
          register_bridge_functions_for_typecheck(
              &crate_name,
              &aliases,
              &metadata.functions,
              registry,
          )?;
      }
  
      Ok(())
  }
  
  fn collect_rust_imports_for_typecheck(
      program: &otterc_ast::nodes::Program,
  ) -> HashMap<String, HashSet<String>> {
      use otterc_ast::nodes::Statement;
      let mut imports: HashMap<String, HashSet<String>> = HashMap::new();
  
      for statement in &program.statements {
          if let Statement::Use {
              imports: use_imports,
          } = statement.as_ref()
          {
              for import in use_imports {
                  if let Some((namespace, crate_name)) = import.as_ref().module.split_once(':')
                      && namespace == "rust"
                  {
                      let aliases = imports.entry(crate_name.to_string()).or_default();
                      aliases.insert(crate_name.to_string());
                      if let Some(alias_name) = &import.as_ref().alias {
                          aliases.insert(alias_name.clone());
                      }
                  }
              }
          }
      }
  
      imports
  }
  
  fn register_bridge_functions_for_typecheck(
      crate_name: &str,
      aliases: &HashSet<String>,
      functions: &[FunctionSpec],
      registry: &SymbolRegistry,
  ) -> Result<()> {
      use otterc_symbol::registry::{FfiFunction, FfiSignature, FfiType};
  
      if functions.is_empty() {
          return Ok(());
      }
  
      for function in functions {
          let canonical_name = if function.name.contains(':') || function.name.contains('.') {
              function.name.clone()
          } else {
              format!("{crate_name}:{}", function.name)
          };
  
          let params = function
              .params
              .iter()
              .enumerate()
              .map(|(idx, param)| {
                  type_spec_to_ffi_helper(param, "parameter", &canonical_name).with_context(|| {
                      format!("parameter {idx} in `{canonical_name}` is not FFI compatible")
                  })
              })
              .collect::<Result<Vec<_>>>()?;
  
          let result = type_spec_to_ffi_helper(&function.result, "return", &canonical_name)?;
          let signature = FfiSignature::new(params.clone(), result.clone());
  
          registry.register(FfiFunction {
              name: canonical_name.clone(),
              symbol: function.symbol.clone(),
              signature: signature.clone(),
          });
  
          for alias in aliases {
              let alias_name = alias_name_helper(alias, crate_name, &canonical_name);
              registry.register(FfiFunction {
                  name: alias_name,
                  symbol: function.symbol.clone(),
                  signature: FfiSignature::new(params.clone(), result.clone()),
              });
          }
      }
  
      registry.register(FfiFunction {
          name: format!("{crate_name}.__call_json"),
          symbol: "otter_call_json".into(),
          signature: FfiSignature::new(vec![FfiType::Str, FfiType::Str], FfiType::Str),
      });
  
      for alias in aliases {
          registry.register(FfiFunction {
              name: format!("{alias}.__call_json"),
              symbol: "otter_call_json".into(),
              signature: FfiSignature::new(vec![FfiType::Str, FfiType::Str], FfiType::Str),
          });
      }
  
      Ok(())
  }
  
  fn type_spec_to_ffi_helper(
      spec: &TypeSpec,
      position: &str,
      function_name: &str,
  ) -> Result<otterc_symbol::registry::FfiType> {
      use otterc_symbol::registry::FfiType;
  
      match spec {
          TypeSpec::Unit => {
              if position == "return" {
                  Ok(FfiType::Unit)
              } else {
                  bail!("`{function_name}` cannot accept a unit value in parameter position")
              }
          }
          TypeSpec::Bool => Ok(FfiType::Bool),
          TypeSpec::I32 => Ok(FfiType::I32),
          TypeSpec::I64 => Ok(FfiType::I64),
          TypeSpec::F64 => Ok(FfiType::F64),
          TypeSpec::Str => Ok(FfiType::Str),
          TypeSpec::Opaque | TypeSpec::Option(_) => Ok(FfiType::Opaque),
          TypeSpec::List(_) => Ok(FfiType::List),
          TypeSpec::Map(_, _) => Ok(FfiType::Map),
      }
  }
  
  fn alias_name_helper(alias: &str, crate_name: &str, canonical: &str) -> String {
      if let Some(rest) = canonical.strip_prefix(&format!("{}:", crate_name)) {
          format!("{alias}.{rest}")
      } else {
          format!("{alias}.{canonical}")
      }
  }
  