Current section
Files
Jump to
Current section
Files
lib/mix/tasks/firebird.target.ex
defmodule Mix.Tasks.Firebird.Target do
@moduledoc """
Configure and compile an Elixir project's WASM target.
This task provides a complete workflow for compiling Elixir modules
to WebAssembly. It discovers all modules with `@wasm true` annotations,
compiles them through the Firebird pipeline, and produces `.wasm` binaries.
## Usage
# Discover and compile all @wasm annotated modules
mix firebird.target
# Compile specific files
mix firebird.target --files lib/math.ex,lib/crypto.ex
# Target specific directories
mix firebird.target --dirs lib/wasm_modules
# Output to custom directory
mix firebird.target --output priv/wasm
# WAT only (no binary compilation)
mix firebird.target --wat-only
# Verbose output showing WAT and compilation details
mix firebird.target --verbose
# Verify compiled WASM modules load correctly
mix firebird.target --verify
# Clean WASM build artifacts
mix firebird.target --clean
# Show compilation stats
mix firebird.target --stats
# Profile compilation phases
mix firebird.target --profile
# Link all modules into a single WASM binary
mix firebird.target --link --link-name MyApp
# Watch for changes and recompile
mix firebird.target --watch
# Force recompile (ignore incremental cache)
mix firebird.target --force
## Configuration
Add to your `mix.exs`:
def project do
[
firebird: [
wasm_output: "_build/wasm",
wasm_sources: ["lib/wasm_modules"],
wat_only: false
]
]
end
## Module Annotations
Mark functions for WASM compilation with `@wasm true`:
defmodule MyMath do
@wasm true
def add(a, b), do: a + b
@wasm true
def fibonacci(0), do: 0
def fibonacci(1), do: 1
def fibonacci(n), do: fibonacci(n - 1) + fibonacci(n - 2)
end
If no `@wasm` annotations are present in a file, all public functions
will be compiled.
## Compilation Pipeline
Elixir Source (.ex)
â Elixir AST (Code.string_to_quoted)
â Firebird IR (normalized intermediate representation)
â Type Inference (i64/f64 type assignment)
â Validation (check compilable subset)
â WAT (WebAssembly Text Format)
â WASM binary (via wat2wasm)
## Supported Elixir Subset
- Integer arithmetic: `+`, `-`, `*`, `div`, `rem`
- Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=`
- Boolean: `and`, `or`, `not`
- Control flow: `if/else`, `cond`, `case`
- Pattern matching on integer literals
- Multi-clause functions with guards
- Recursive and tail-recursive functions
## Limitations
- Only numeric types (i64 integers, f64 floats)
- No lists, maps, tuples, strings, or atoms
- No IO, processes, or OTP features
- No standard library functions
- No hot code reloading
"""
use Mix.Task
@shortdoc "Compile Elixir project to WebAssembly target"
@switches [
output: :string,
files: :string,
dirs: :string,
wat_only: :boolean,
verbose: :boolean,
verify: :boolean,
clean: :boolean,
stats: :boolean,
force: :boolean,
optimize: :boolean,
tco: :boolean,
inline: :boolean,
profile: :boolean,
link: :boolean,
link_name: :string,
link_prefix: :boolean,
watch: :boolean,
source_map: :boolean,
dry_run: :boolean,
json: :boolean
]
@aliases [
o: :output,
f: :files,
d: :dirs,
v: :verbose
]
@impl Mix.Task
def run(args) do
{opts, _rest, _invalid} = OptionParser.parse(args, switches: @switches, aliases: @aliases)
if Keyword.get(opts, :clean, false) do
clean(opts)
else
if Keyword.get(opts, :watch, false) do
watch(opts)
else
compile(opts)
end
end
end
@doc """
Compile WASM target with the given options.
"""
@spec compile(keyword()) :: :ok | {:error, term()}
def compile(opts) do
# Merge project config with CLI opts
project_config = get_project_config()
opts = merge_config(project_config, opts)
# Validate config
validate_config(opts)
output_dir = Keyword.get(opts, :output, "_build/wasm")
wat_only = Keyword.get(opts, :wat_only, false)
verbose = Keyword.get(opts, :verbose, false)
verify = Keyword.get(opts, :verify, false)
show_stats = Keyword.get(opts, :stats, false)
optimize = Keyword.get(opts, :optimize, false)
tco = Keyword.get(opts, :tco, false)
do_inline = Keyword.get(opts, :inline, false)
profile = Keyword.get(opts, :profile, false)
do_link = Keyword.get(opts, :link, false)
force = Keyword.get(opts, :force, false)
dry_run = Keyword.get(opts, :dry_run, false)
# For dry run, skip prerequisites check
unless dry_run do
check_prerequisites!(wat_only)
end
# Discover source files
sources = discover_sources(opts)
if Enum.empty?(sources) do
Mix.shell().info("ðĨ Firebird WASM Target: No compilable sources found")
Mix.shell().info(" Add @wasm true annotations to your Elixir functions")
:ok
else
if dry_run do
run_dry_run(sources)
else
# Filter to only stale sources unless --force
sources_to_compile =
if force do
sources
else
filter_stale(sources, output_dir)
end
if Enum.empty?(sources_to_compile) and not do_link do
Mix.shell().info("ðĨ Firebird WASM Target: All #{length(sources)} file(s) up to date")
Mix.shell().info(" Use --force to recompile")
:ok
else
Mix.shell().info("ðĨ Firebird WASM Target")
Mix.shell().info(
" Sources: #{length(sources_to_compile)} file(s)#{if not force and length(sources_to_compile) < length(sources), do: " (#{length(sources) - length(sources_to_compile)} cached)", else: ""}"
)
Mix.shell().info(" Output: #{output_dir}/")
Mix.shell().info("")
File.mkdir_p!(output_dir)
start_time = System.monotonic_time(:millisecond)
source_map = Keyword.get(opts, :source_map, false)
compile_opts = [optimize: optimize, tco: tco, inline: do_inline, source_map: source_map]
results =
Enum.map(sources_to_compile, fn source ->
compile_source(source, output_dir, wat_only, verbose, verify, compile_opts, profile)
end)
elapsed = System.monotonic_time(:millisecond) - start_time
successes = Enum.filter(results, &match?({:ok, _}, &1))
failures = Enum.filter(results, &match?({:error, _}, &1))
# Update manifest
update_manifest(successes, output_dir)
Mix.shell().info("")
# JSON output mode
json_mode = Keyword.get(opts, :json, false)
if json_mode do
summary = Firebird.Compiler.Summary.generate(results)
summary_with_time = Map.put(summary, :elapsed_ms, elapsed)
Mix.shell().info(Firebird.Compiler.Summary.to_json(summary_with_time))
else
if show_stats do
print_stats(successes)
end
Mix.shell().info(
"ð Results: #{length(successes)} compiled, #{length(failures)} failed (#{elapsed}ms)"
)
end
# Link if requested
link_result =
if do_link and Enum.any?(successes) do
link_modules(sources, opts)
else
:ok
end
cond do
Enum.any?(failures) ->
{:error, {:compilation_failed, length(failures)}}
link_result != :ok ->
link_result
true ->
:ok
end
end
end
# dry_run else
end
end
@doc """
Run a dry-run analysis without compiling.
Shows which files would be compiled and analyzes each for compilability.
"""
@spec run_dry_run([String.t()]) :: :ok
def run_dry_run(sources) do
Mix.shell().info("ðĨ Firebird WASM Target â Dry Run")
Mix.shell().info(" Would compile #{length(sources)} file(s):")
Mix.shell().info("")
Enum.each(sources, fn source ->
Mix.shell().info(" ð #{source}")
case File.read(source) do
{:ok, code} ->
case Firebird.Compiler.Analyzer.analyze_source(code) do
{:ok, analysis} ->
if analysis.compilable do
fns = length(analysis.functions)
Mix.shell().info(" â
#{analysis.module}: #{fns} function(s) compilable")
else
Mix.shell().info(" â #{analysis.module}: has issues")
Enum.each(analysis.issues, fn issue ->
Mix.shell().info(" âĒ #{issue}")
end)
end
Enum.each(analysis.suggestions, fn sug ->
Mix.shell().info(" ðĄ #{sug}")
end)
{:error, reason} ->
Mix.shell().error(" â Analysis failed: #{inspect(reason)}")
end
{:error, reason} ->
Mix.shell().error(" â Cannot read: #{reason}")
end
end)
Mix.shell().info("")
:ok
end
@doc """
Watch for source file changes and recompile.
"""
@spec watch(keyword()) :: no_return()
def watch(opts) do
Mix.shell().info("ðĨ Firebird WASM Target â Watch Mode")
Mix.shell().info(" Watching for changes... (Ctrl+C to stop)")
Mix.shell().info("")
# Initial compile
compile(Keyword.delete(opts, :watch))
# Poll loop
watch_loop(opts, snapshot_mtimes(opts))
end
defp watch_loop(opts, prev_mtimes) do
Process.sleep(1_000)
current_mtimes = snapshot_mtimes(opts)
changed =
Enum.filter(current_mtimes, fn {path, mtime} ->
Map.get(prev_mtimes, path) != mtime
end)
|> Enum.map(fn {path, _} -> path end)
if Enum.any?(changed) do
Mix.shell().info("")
Mix.shell().info("ð Changes detected in #{length(changed)} file(s):")
Enum.each(changed, fn f -> Mix.shell().info(" #{f}") end)
Mix.shell().info("")
compile(opts |> Keyword.delete(:watch) |> Keyword.put(:files, Enum.join(changed, ",")))
end
watch_loop(opts, current_mtimes)
end
defp snapshot_mtimes(opts) do
sources = discover_sources(opts)
Map.new(sources, fn path ->
mtime =
case File.stat(path) do
{:ok, %{mtime: mtime}} -> mtime
_ -> nil
end
{path, mtime}
end)
end
@doc """
Clean WASM build artifacts.
"""
@spec clean(keyword()) :: :ok
def clean(opts) do
output_dir = Keyword.get(opts, :output, "_build/wasm")
if File.dir?(output_dir) do
files = Path.wildcard(Path.join(output_dir, "*.{wat,wasm}"))
Enum.each(files, &File.rm/1)
Mix.shell().info("ð§đ Cleaned #{length(files)} WASM artifact(s) from #{output_dir}/")
else
Mix.shell().info("ð§đ Nothing to clean (#{output_dir}/ doesn't exist)")
end
# Also clean the compiler manifest
Mix.Compilers.FirebirdWasm.clean()
:ok
end
@doc """
Validate project configuration and print warnings for common mistakes.
"""
@spec validate_config(keyword()) :: :ok
def validate_config(opts) do
# Check output directory
output = Keyword.get(opts, :output, "_build/wasm")
if String.starts_with?(output, "/") and not File.dir?(Path.dirname(output)) do
Mix.shell().info("â Output directory parent doesn't exist: #{Path.dirname(output)}")
end
# Check source dirs exist
case Keyword.get(opts, :dirs) do
nil ->
:ok
dirs_str ->
dirs_str
|> String.split(",", trim: true)
|> Enum.each(fn dir ->
unless File.dir?(dir) do
Mix.shell().info("â Source directory not found: #{dir}")
end
end)
end
# Check files exist
case Keyword.get(opts, :files) do
nil ->
:ok
files_str ->
files_str
|> String.split(",", trim: true)
|> Enum.each(fn file ->
unless File.regular?(file) do
Mix.shell().info("â Source file not found: #{file}")
end
end)
end
:ok
end
@doc """
Link multiple modules into a single WASM binary.
"""
@spec link_modules([String.t()], keyword()) :: :ok | {:error, term()}
def link_modules(sources, opts) do
output_dir = Keyword.get(opts, :output, "_build/wasm")
link_name = Keyword.get(opts, :link_name, "Combined")
prefix = Keyword.get(opts, :link_prefix, false)
wat_only = Keyword.get(opts, :wat_only, false)
Mix.shell().info("")
Mix.shell().info("ð Linking #{length(sources)} module(s) into #{link_name}.wasm")
combiner_opts = [
name: link_name,
prefix: prefix,
output_dir: output_dir,
wat_only: wat_only,
optimize: Keyword.get(opts, :optimize, false),
tco: Keyword.get(opts, :tco, false),
inline: Keyword.get(opts, :inline, false)
]
case Firebird.Compiler.Combiner.combine(sources, combiner_opts) do
{:ok, result} ->
export_count = length(result.exports || [])
Mix.shell().info(" â
Linked: #{export_count} export(s)")
if result.wasm do
Mix.shell().info(" ðĶ #{link_name}.wasm: #{byte_size(result.wasm)} bytes")
end
:ok
{:error, reason} ->
Mix.shell().error(" â Link failed: #{inspect(reason)}")
{:error, {:link_failed, reason}}
end
end
# Filter to only stale sources by checking file mtimes against output
defp filter_stale(sources, output_dir) do
Enum.filter(sources, fn source ->
source_mtime =
case File.stat(source) do
{:ok, %{mtime: mtime}} -> mtime
_ -> {{2000, 1, 1}, {0, 0, 0}}
end
# Check if any output file is newer than source
basename = Path.basename(source, Path.extname(source))
wat_path = Path.join(output_dir, "*.wat")
wasm_path = Path.join(output_dir, "*.wasm")
output_files =
(Path.wildcard(wat_path) ++ Path.wildcard(wasm_path))
|> Enum.filter(fn f -> String.contains?(Path.basename(f), basename) end)
if Enum.empty?(output_files) do
true
else
# Source is stale if it's newer than any output
Enum.any?(output_files, fn output ->
case File.stat(output) do
{:ok, %{mtime: output_mtime}} ->
:calendar.datetime_to_gregorian_seconds(source_mtime) >
:calendar.datetime_to_gregorian_seconds(output_mtime)
_ ->
true
end
end)
end
end)
end
defp update_manifest(successes, _output_dir) do
entries =
Enum.map(successes, fn {:ok, result} ->
%{
module: result.module,
compiled_at: :calendar.datetime_to_gregorian_seconds(:calendar.local_time())
}
end)
# Store in a simple ETS-like approach via manifest file
manifest_path = Path.join(Mix.Project.manifest_path(), "firebird_target.manifest")
try do
File.mkdir_p!(Path.dirname(manifest_path))
File.write!(manifest_path, :erlang.term_to_binary(entries))
rescue
_ -> :ok
end
end
# Discover source files from CLI options or project config
defp discover_sources(opts) do
files =
case Keyword.get(opts, :files) do
nil -> []
files_str -> String.split(files_str, ",", trim: true)
end
dirs =
case Keyword.get(opts, :dirs) do
nil ->
if Enum.empty?(files) do
Keyword.get(opts, :wasm_sources, ["lib"])
else
[]
end
dirs_str ->
String.split(dirs_str, ",", trim: true)
end
explicit_files = Enum.filter(files, &File.regular?/1)
discovered =
dirs
|> Enum.flat_map(fn dir ->
if File.dir?(dir) do
Path.wildcard(Path.join(dir, "**/*.{ex,exs}"))
else
[]
end
end)
|> Enum.filter(&has_wasm_content?/1)
(explicit_files ++ discovered) |> Enum.sort() |> Enum.uniq()
end
defp has_wasm_content?(path) do
case File.read(path) do
{:ok, content} ->
# Either has @wasm annotations or is in a wasm_modules directory
String.contains?(content, "@wasm true") or
String.contains?(path, "wasm_modules")
_ ->
false
end
end
defp compile_source(source, output_dir, wat_only, verbose, verify, compile_opts, profile) do
Mix.shell().info(" ð #{source}")
compiler_opts = Keyword.merge([output_dir: output_dir, wat_only: wat_only], compile_opts)
result =
if profile do
compile_with_profile(source, compiler_opts, verbose, verify)
else
compile_without_profile(source, compiler_opts, verbose, verify)
end
# Generate source map if requested
case result do
{:ok, compile_result} ->
source_map_enabled = Keyword.get(compile_opts, :source_map, false)
if source_map_enabled, do: generate_source_map(source, compile_result, output_dir)
result
_ ->
result
end
end
defp generate_source_map(source_path, compile_result, output_dir) do
case File.read(source_path) do
{:ok, code} ->
with {:ok, ast} <- Firebird.Compiler.parse(code),
{:ok, ir} <- Firebird.Compiler.IRGen.generate(ast),
{:ok, sm} <- Firebird.Compiler.SourceMap.generate(ir, source_path) do
json = Firebird.Compiler.SourceMap.to_json(sm)
base = Atom.to_string(compile_result.module) |> String.replace(".", "_")
map_path = Path.join(output_dir, "#{base}.source_map.json")
File.write!(map_path, Jason.encode!(json, pretty: true))
Mix.shell().info(" ð Source map: #{map_path}")
end
_ ->
:ok
end
rescue
_ -> :ok
end
defp compile_without_profile(source, compiler_opts, verbose, verify) do
case Firebird.Compiler.compile(source, compiler_opts) do
{:ok, result} ->
print_verbose(result, verbose)
maybe_verify(result, verify)
Mix.shell().info(" â
#{result.module}")
{:ok, result}
{:error, reason} ->
Mix.shell().error(" â #{format_error(reason)}")
{:error, {source, reason}}
end
end
defp compile_with_profile(source, compiler_opts, verbose, verify) do
# Time each phase individually
phases = []
# Phase 1: Read file
{read_us, source_code} =
:timer.tc(fn ->
case File.read(source) do
{:ok, code} -> code
{:error, reason} -> {:error, {:file_error, reason, source}}
end
end)
phases = [{:read, read_us} | phases]
if is_tuple(source_code) and elem(source_code, 0) == :error do
Mix.shell().error(" â #{format_error(source_code)}")
{:error, {source, source_code}}
else
# Phase 2: Parse
{parse_us, parse_result} =
:timer.tc(fn ->
Firebird.Compiler.parse(source_code)
end)
phases = [{:parse, parse_us} | phases]
case parse_result do
{:ok, ast} ->
# Phase 3: IR Generation
{ir_us, ir_result} =
:timer.tc(fn ->
Firebird.Compiler.IRGen.generate(ast)
end)
phases = [{:ir_gen, ir_us} | phases]
case ir_result do
{:ok, module_ir} ->
# Phase 4: Type Inference
{type_us, type_result} =
:timer.tc(fn ->
with :ok <- Firebird.Compiler.Validator.validate(module_ir),
{:ok, typed} <- Firebird.Compiler.TypeInference.infer(module_ir) do
{:ok, typed}
end
end)
phases = [{:type_infer, type_us} | phases]
case type_result do
{:ok, typed_ir} ->
# Phase 5: WAT Generation
{wat_us, wat_result} =
:timer.tc(fn ->
Firebird.Compiler.WATGen.generate(typed_ir)
end)
phases = [{:wat_gen, wat_us} | phases]
case wat_result do
{:ok, wat} ->
# Phase 6: WASM binary (if not wat_only)
{wasm_us, wasm_result} =
if Keyword.get(compiler_opts, :wat_only, false) do
{0, {:ok, nil}}
else
:timer.tc(fn -> Firebird.Compiler.wat_to_wasm(wat) end)
end
phases = [{:wat2wasm, wasm_us} | phases]
case wasm_result do
{:ok, wasm_binary} ->
result = %{
wat: wat,
wasm: wasm_binary,
module: typed_ir.name
}
# Write output
output_dir = Keyword.get(compiler_opts, :output_dir)
if output_dir do
File.mkdir_p!(output_dir)
base = Atom.to_string(result.module) |> String.replace(".", "_")
File.write!(Path.join(output_dir, "#{base}.wat"), wat)
if wasm_binary do
File.write!(Path.join(output_dir, "#{base}.wasm"), wasm_binary)
end
end
print_verbose(result, verbose)
maybe_verify(result, verify)
print_profile(phases)
Mix.shell().info(" â
#{result.module}")
{:ok, result}
{:error, reason} ->
print_profile(phases)
Mix.shell().error(" â #{format_error(reason)}")
{:error, {source, reason}}
end
{:error, reason} ->
print_profile(phases)
Mix.shell().error(" â #{format_error(reason)}")
{:error, {source, reason}}
end
{:error, reason} ->
print_profile(phases)
Mix.shell().error(" â #{format_error(reason)}")
{:error, {source, reason}}
end
{:error, reason} ->
print_profile(phases)
Mix.shell().error(" â #{format_error(reason)}")
{:error, {source, reason}}
end
{:error, reason} ->
print_profile(phases)
Mix.shell().error(" â #{format_error(reason)}")
{:error, {source, reason}}
end
end
end
defp print_profile(phases) do
phases = Enum.reverse(phases)
total = Enum.map(phases, fn {_, us} -> us end) |> Enum.sum()
Mix.shell().info(" âą Profile:")
Enum.each(phases, fn {phase, us} ->
pct = if total > 0, do: Float.round(us / total * 100, 1), else: 0.0
name = phase |> Atom.to_string() |> String.pad_trailing(12)
time = format_microseconds(us) |> String.pad_leading(10)
Mix.shell().info(" #{name} #{time} (#{pct}%)")
end)
Mix.shell().info(
" #{"total" |> String.pad_trailing(12)} #{format_microseconds(total) |> String.pad_leading(10)}"
)
end
defp format_microseconds(us) when us < 1000, do: "#{us}Ξs"
defp format_microseconds(us) when us < 1_000_000, do: "#{Float.round(us / 1000, 1)}ms"
defp format_microseconds(us), do: "#{Float.round(us / 1_000_000, 2)}s"
defp print_verbose(result, false), do: result
defp print_verbose(result, true) do
Mix.shell().info(" Module: #{result.module}")
Mix.shell().info(" WAT: #{byte_size(result.wat)} bytes")
if result.wasm do
Mix.shell().info(" WASM: #{byte_size(result.wasm)} bytes")
ratio = Float.round(byte_size(result.wasm) / byte_size(result.wat) * 100, 1)
Mix.shell().info(" Compression: #{ratio}% of WAT size")
end
Mix.shell().info("")
Mix.shell().info(" WAT output:")
result.wat
|> String.split("\n")
|> Enum.take(20)
|> Enum.each(fn line -> Mix.shell().info(" #{line}") end)
if String.split(result.wat, "\n") |> length() > 20 do
Mix.shell().info(" ... (truncated)")
end
Mix.shell().info("")
result
end
defp maybe_verify(_result, false), do: :ok
defp maybe_verify(result, true) do
if result.wasm do
verify_module(result)
end
end
defp verify_module(result) do
case Firebird.load(result.wasm) do
{:ok, instance} ->
exports = Firebird.exports(instance)
Mix.shell().info(" ð Verified: exports #{inspect(exports)}")
Firebird.stop(instance)
{:error, reason} ->
Mix.shell().error(" â Verification failed: #{inspect(reason)}")
end
end
defp print_stats(successes) do
results = Enum.map(successes, fn {:ok, r} -> r end)
total_wat = Enum.map(results, fn r -> byte_size(r.wat) end) |> Enum.sum()
total_wasm =
results
|> Enum.filter(& &1.wasm)
|> Enum.map(fn r -> byte_size(r.wasm) end)
|> Enum.sum()
modules = Enum.map(results, fn r -> r.module end)
Mix.shell().info("")
Mix.shell().info("ð Compilation Statistics:")
Mix.shell().info(" ââââââââââââââââââââââââââââââŽâââââââââââââââŽâââââââââââââââ")
Mix.shell().info(" â Module â WAT (bytes) â WASM (bytes) â")
Mix.shell().info(" ââââââââââââââââââââââââââââââžâââââââââââââââžâââââââââââââââĪ")
Enum.each(results, fn r ->
name = inspect(r.module) |> String.pad_trailing(26) |> String.slice(0, 26)
wat_sz = byte_size(r.wat) |> to_string() |> String.pad_leading(12)
wasm_sz =
if(r.wasm, do: byte_size(r.wasm), else: 0) |> to_string() |> String.pad_leading(12)
Mix.shell().info(" â #{name} â #{wat_sz} â #{wasm_sz} â")
end)
Mix.shell().info(" ââââââââââââââââââââââââââââââžâââââââââââââââžâââââââââââââââĪ")
total_wat_s = total_wat |> to_string() |> String.pad_leading(12)
total_wasm_s = total_wasm |> to_string() |> String.pad_leading(12)
Mix.shell().info(
" â #{"TOTAL" |> String.pad_trailing(26)} â #{total_wat_s} â #{total_wasm_s} â"
)
Mix.shell().info(" ââââââââââââââââââââââââââââââīâââââââââââââââīâââââââââââââââ")
if total_wat > 0 and total_wasm > 0 do
ratio = Float.round(total_wasm / total_wat * 100, 1)
Mix.shell().info(" Ratio: #{ratio}% (WASM/WAT)")
end
Mix.shell().info(" Modules: #{length(modules)}")
Mix.shell().info("")
end
defp check_prerequisites!(wat_only) do
unless wat_only do
unless Firebird.Compiler.wat2wasm_available?() do
Mix.raise("""
wat2wasm not found! Install the WebAssembly Binary Toolkit (wabt):
# macOS
brew install wabt
# Ubuntu/Debian
apt-get install wabt
# Or use --wat-only to skip binary compilation
""")
end
end
end
defp get_project_config do
case Mix.Project.config() do
config when is_list(config) -> Keyword.get(config, :firebird, [])
_ -> []
end
rescue
_ -> []
end
defp merge_config(project_config, cli_opts) do
Keyword.merge(project_config, cli_opts)
end
defp format_error({:parse_error, {line, col}, msg, token}) do
"Parse error at #{line}:#{col}: #{msg}#{token}"
end
defp format_error({:validation_errors, errors}) do
details =
Enum.map(errors, fn {func, errs} ->
" #{func}: #{Enum.join(errs, ", ")}"
end)
"Validation errors:\n#{Enum.join(details, "\n")}"
end
defp format_error({:wat2wasm_error, output}), do: "wat2wasm: #{output}"
defp format_error({:file_error, reason, path}), do: "Cannot read #{path}: #{reason}"
defp format_error(other), do: inspect(other)
end