defmodule Firebird.Target.Config do @moduledoc """ Configuration for the Firebird WASM compilation target. Holds all settings needed to discover, compile, and output WASM modules from Elixir source files. ## Creating a Config # From keyword options config = Firebird.Target.Config.new( output_dir: "_build/wasm", sources: ["lib/wasm_modules"], optimize: true ) # From Mix project config config = Firebird.Target.Config.from_mix_project() # Merge CLI options on top of project config config = Firebird.Target.Config.from_mix_project() |> Firebird.Target.Config.merge(cli_opts) ## Options - `:output_dir` - Directory for compiled WASM output (default: `"_build/wasm"`) - `:sources` - List of source directories to scan (default: `["lib"]`) - `:files` - Explicit list of files to compile (overrides source scanning) - `:optimize` - Enable IR optimizations like constant folding (default: `false`) - `:tco` - Enable tail call optimization (default: `false`) - `:inline` - Enable function inlining (default: `false`) - `:cse` - Enable common subexpression elimination (default: `false`) - `:licm` - Enable loop-invariant code motion (default: `false`) - `:wat_only` - Only generate WAT text, skip binary compilation (default: `false`) - `:verify` - Load and verify compiled WASM modules (default: `false`) - `:source_map` - Generate source maps (default: `false`) - `:verbose` - Enable verbose output (default: `false`) - `:force` - Force recompilation of all files (default: `false`) """ @type t :: %__MODULE__{ output_dir: String.t(), sources: [String.t()], files: [String.t()] | nil, optimize: boolean(), tco: boolean(), inline: boolean(), cse: boolean(), licm: boolean(), wat_only: boolean(), verify: boolean(), source_map: boolean(), verbose: boolean(), force: boolean() } defstruct output_dir: "_build/wasm", sources: ["lib"], files: nil, optimize: false, tco: false, inline: false, cse: false, licm: false, wat_only: false, verify: false, source_map: false, verbose: false, force: false @doc """ Create a new Config from keyword options. ## Examples config = Config.new(output_dir: "priv/wasm", optimize: true) config = Config.new([]) # all defaults """ @spec new(keyword()) :: t() def new(opts \\ []) when is_list(opts) do struct(__MODULE__, opts) end @doc """ Create a Config from the current Mix project's `:firebird` configuration. Reads from `mix.exs`: def project do [ firebird: [ wasm_output: "_build/wasm", wasm_sources: ["lib/wasm_modules"], optimize: true ] ] end """ @spec from_mix_project() :: t() def from_mix_project do config = get_project_firebird_config() new( output_dir: Keyword.get(config, :wasm_output, "_build/wasm"), sources: Keyword.get(config, :wasm_sources, ["lib"]), optimize: Keyword.get(config, :optimize, false), tco: Keyword.get(config, :tco, false), inline: Keyword.get(config, :inline, false), cse: Keyword.get(config, :cse, false), licm: Keyword.get(config, :licm, false), wat_only: Keyword.get(config, :wat_only, false), verify: Keyword.get(config, :verify, false), source_map: Keyword.get(config, :source_map, false) ) end @doc """ Merge additional options into an existing config. CLI options override existing config values. Only non-nil values from `opts` are merged. ## Examples config = Config.from_mix_project() config = Config.merge(config, output_dir: "custom/output", verbose: true) """ @spec merge(t(), keyword()) :: t() def merge(%__MODULE__{} = config, opts) when is_list(opts) do # Only merge non-nil values filtered = opts |> Enum.filter(fn {_k, v} -> v != nil end) struct(config, filtered) end @doc """ Validate the config and return a list of warnings. Checks for common misconfigurations like nonexistent directories or missing prerequisites. """ @spec validate(t()) :: {:ok, [String.t()]} def validate(%__MODULE__{} = config) do warnings = [] # Check output dir parent exists warnings = if String.starts_with?(config.output_dir, "/") and not File.dir?(Path.dirname(config.output_dir)) do ["Output directory parent doesn't exist: #{Path.dirname(config.output_dir)}" | warnings] else warnings end # Check source dirs exist warnings = Enum.reduce(config.sources || [], warnings, fn dir, acc -> if not File.dir?(dir) do ["Source directory not found: #{dir}" | acc] else acc end end) # Check explicit files exist warnings = Enum.reduce(config.files || [], warnings, fn file, acc -> if not File.regular?(file) do ["Source file not found: #{file}" | acc] else acc end end) # Check wat2wasm availability warnings = if not config.wat_only and not Firebird.Compiler.wat2wasm_available?() do ["wat2wasm not found - binary compilation will fail" | warnings] else warnings end {:ok, Enum.reverse(warnings)} end defp get_project_firebird_config do case Mix.Project.config() do config when is_list(config) -> Keyword.get(config, :firebird, []) _ -> [] end rescue _ -> [] end end