diff --git a/bench/pool_call_many_bench.exs b/bench/pool_call_many_bench.exs new file mode 100644 index 0000000..f6190ae --- /dev/null +++ b/bench/pool_call_many_bench.exs @@ -0,0 +1,89 @@ +# Benchmark: Pool.call_many vs repeated Pool.call +# +# Demonstrates the overhead savings from batching multiple function calls +# into a single checkout/checkin cycle. +# +# Run: mix run bench/pool_call_many_bench.exs + +math_wasm = "fixtures/math.wasm" +{:ok, pool} = Firebird.Pool.start_link(wasm: math_wasm, size: 4) + +# Build call lists of various sizes +calls_5 = for i <- 1..5, do: {:add, [i, i]} +calls_20 = for i <- 1..20, do: {:add, [i, i]} +calls_100 = for i <- 1..100, do: {:add, [i, i]} + +defmodule BenchHelper do + def repeated_calls(pool, calls) do + Enum.map(calls, fn {func, args} -> + {:ok, result} = Firebird.Pool.call(pool, func, args) + result + end) + end +end + +IO.puts("=" |> String.duplicate(70)) +IO.puts("Pool.call_many vs repeated Pool.call benchmark") +IO.puts("=" |> String.duplicate(70)) + +for {label, calls} <- [{"5 calls", calls_5}, {"20 calls", calls_20}, {"100 calls", calls_100}] do + IO.puts("\n--- #{label} ---") + + # Warmup + for _ <- 1..10 do + {:ok, _} = Firebird.Pool.call_many(pool, calls) + BenchHelper.repeated_calls(pool, calls) + end + + iterations = 200 + + # Benchmark call_many + many_times = for _ <- 1..iterations do + {time, {:ok, _results}} = :timer.tc(fn -> Firebird.Pool.call_many(pool, calls) end) + time + end + + # Benchmark repeated call + repeat_times = for _ <- 1..iterations do + {time, _results} = :timer.tc(fn -> BenchHelper.repeated_calls(pool, calls) end) + time + end + + many_avg = Enum.sum(many_times) / iterations + repeat_avg = Enum.sum(repeat_times) / iterations + speedup = repeat_avg / max(many_avg, 0.1) + + many_p50 = Enum.sort(many_times) |> Enum.at(div(iterations, 2)) + repeat_p50 = Enum.sort(repeat_times) |> Enum.at(div(iterations, 2)) + + IO.puts(" call_many: avg=#{Float.round(many_avg, 1)}μs p50=#{many_p50}μs") + IO.puts(" repeated call: avg=#{Float.round(repeat_avg, 1)}μs p50=#{repeat_p50}μs") + IO.puts(" speedup: #{Float.round(speedup, 2)}x faster with call_many") +end + +# Also benchmark against pure Elixir equivalent +IO.puts("\n--- call_many vs native Elixir (100 additions) ---") + +native_times = for _ <- 1..200 do + {time, _} = :timer.tc(fn -> + for i <- 1..100, do: i + i + end) + time +end + +wasm_times = for _ <- 1..200 do + {time, {:ok, _}} = :timer.tc(fn -> + Firebird.Pool.call_many(pool, calls_100) + end) + time +end + +native_avg = Enum.sum(native_times) / 200 +wasm_avg = Enum.sum(wasm_times) / 200 + +IO.puts(" WASM call_many: avg=#{Float.round(wasm_avg, 1)}μs") +IO.puts(" Native Elixir: avg=#{Float.round(native_avg, 1)}μs") +IO.puts(" Overhead ratio: #{Float.round(wasm_avg / max(native_avg, 0.1), 1)}x") + +Firebird.Pool.stop(pool) +IO.puts("\nDone.") diff --git a/bench/wasm_collection.exs b/bench/wasm_collection.exs new file mode 100644 index 0000000..f6c88d8 --- /dev/null +++ b/bench/wasm_collection.exs @@ -0,0 +1,47 @@ +# Benchmark: WasmRunner collection helpers vs manual loops +# +# Run: mix run bench/wasm_collection.exs +# +# Compares the overhead of WasmRunner.map/4, reduce/5, and filter/4 +# against manual with_instance + Enum loops. + +alias Firebird.WasmRunner + +math_wasm = "fixtures/math.wasm" +{:ok, compiled} = WasmRunner.precompile(math_wasm) + +inputs = Enum.to_list(1..50) + +Benchee.run( + %{ + "WasmRunner.map (from file)" => fn -> + {:ok, _} = WasmRunner.map(math_wasm, :fibonacci, inputs) + end, + "WasmRunner.map (precompiled)" => fn -> + {:ok, _} = WasmRunner.map(compiled, :fibonacci, inputs) + end, + "manual with_instance + Enum.map" => fn -> + {:ok, _results} = + WasmRunner.with_instance(math_wasm, fn pid -> + Enum.map(inputs, fn n -> + WasmRunner.call_single!(pid, :fibonacci, [n]) + end) + end) + end, + "WasmRunner.reduce (from file)" => fn -> + {:ok, _} = WasmRunner.reduce(math_wasm, :add, inputs, 0) + end, + "WasmRunner.reduce (precompiled)" => fn -> + {:ok, _} = WasmRunner.reduce(compiled, :add, inputs, 0) + end, + "WasmRunner.filter (from file)" => fn -> + {:ok, _} = WasmRunner.filter(math_wasm, :is_prime, inputs) + end, + "WasmRunner.filter (precompiled)" => fn -> + {:ok, _} = WasmRunner.filter(compiled, :is_prime, inputs) + end + }, + warmup: 1, + time: 3, + memory_time: 1 +) diff --git a/bench/wasm_pipe.exs b/bench/wasm_pipe.exs new file mode 100644 index 0000000..297a567 --- /dev/null +++ b/bench/wasm_pipe.exs @@ -0,0 +1,75 @@ +# Benchmark: WasmRunner.pipe/2 vs sequential WasmRunner.run/2 +# +# Compares the performance of: +# 1. pipe/2 - loads once, chains calls +# 2. Multiple run/2 - loads per call (baseline) +# 3. with_instance + manual chaining +# +# Run: mix run bench/wasm_pipe.exs + +alias Firebird.WasmRunner + +math_wasm = "fixtures/math.wasm" + +IO.puts("=== WasmRunner.pipe/2 Benchmark ===\n") + +# Warmup +WasmRunner.pipe(math_wasm, [{:add, [5, 3]}, {:fibonacci, []}, {:multiply, [:pipe, 2]}]) + +# Benchmark pipe/2 (3-stage pipeline) +pipe_result = WasmRunner.benchmark(math_wasm, :add, [[5, 3]], iterations: 100) +IO.puts("Single call baseline: avg=#{pipe_result.avg_us}μs") + +# Manual timing of pipe vs separate runs +iterations = 100 + +pipe_times = + for _ <- 1..iterations do + {elapsed, _} = + :timer.tc(fn -> + WasmRunner.pipe!(math_wasm, [ + {:add, [5, 3]}, + {:fibonacci, []}, + {:multiply, [:pipe, 2]} + ]) + end) + + elapsed + end + +separate_times = + for _ <- 1..iterations do + {elapsed, _} = + :timer.tc(fn -> + r1 = WasmRunner.run!(math_wasm, add: [5, 3]) + r2 = WasmRunner.run!(math_wasm, fibonacci: [r1]) + WasmRunner.run!(math_wasm, multiply: [r2, 2]) + end) + + elapsed + end + +with_instance_times = + for _ <- 1..iterations do + {elapsed, _} = + :timer.tc(fn -> + WasmRunner.with_instance(math_wasm, fn pid -> + r1 = WasmRunner.call_single!(pid, :add, [5, 3]) + r2 = WasmRunner.call_single!(pid, :fibonacci, [r1]) + WasmRunner.call_single!(pid, :multiply, [r2, 2]) + end) + end) + + elapsed + end + +pipe_avg = Enum.sum(pipe_times) / iterations +separate_avg = Enum.sum(separate_times) / iterations +with_instance_avg = Enum.sum(with_instance_times) / iterations + +IO.puts("\n3-stage pipeline (#{iterations} iterations):") +IO.puts(" pipe/2: avg=#{Float.round(pipe_avg, 1)}μs") +IO.puts(" 3x run/2: avg=#{Float.round(separate_avg, 1)}μs") +IO.puts(" with_instance: avg=#{Float.round(with_instance_avg, 1)}μs") +IO.puts(" pipe speedup vs separate: #{Float.round(separate_avg / pipe_avg, 2)}x") +IO.puts(" pipe vs with_instance: #{Float.round(pipe_avg / with_instance_avg, 2)}x overhead") diff --git a/bench/wasm_runner_run.exs b/bench/wasm_runner_run.exs new file mode 100644 index 0000000..86681ff --- /dev/null +++ b/bench/wasm_runner_run.exs @@ -0,0 +1,149 @@ +# Benchmark: WasmRunner.run/2 vs native Elixir +# +# Run: mix run bench/wasm_runner_run.exs +# +# Compares the one-shot WasmRunner.run/2 API against: +# 1. Pure Elixir equivalent functions +# 2. Pre-loaded WASM instance (to isolate call overhead from load overhead) +# 3. Precompiled module (to show compilation cache benefit) + +defmodule NativeElixir do + def add(a, b), do: a + b + def multiply(a, b), do: a * b + + def fibonacci(0), do: 0 + def fibonacci(1), do: 1 + def fibonacci(n) when n > 1, do: fibonacci(n - 1) + fibonacci(n - 2) +end + +wasm_path = "fixtures/math.wasm" + +IO.puts("=" |> String.duplicate(70)) +IO.puts("WasmRunner.run/2 Benchmark") +IO.puts("=" |> String.duplicate(70)) + +# --- Section 1: One-shot run/2 vs Elixir --- +IO.puts("\n## 1. One-shot execution: run/2 vs pure Elixir\n") + +iterations = 100 + +# Warmup +for _ <- 1..5 do + Firebird.WasmRunner.run(wasm_path, add: [5, 3]) + NativeElixir.add(5, 3) +end + +# Benchmark run/2 (includes load + call + cleanup each time) +run_times = + for _ <- 1..iterations do + {us, _} = :timer.tc(fn -> Firebird.WasmRunner.run(wasm_path, add: [5, 3]) end) + us + end + +# Benchmark Elixir +elixir_times = + for _ <- 1..iterations do + {us, _} = :timer.tc(fn -> NativeElixir.add(5, 3) end) + us + end + +run_avg = Enum.sum(run_times) / iterations +elixir_avg = Enum.sum(elixir_times) / iterations +run_p50 = Enum.sort(run_times) |> Enum.at(div(iterations, 2)) +elixir_p50 = Enum.sort(elixir_times) |> Enum.at(div(iterations, 2)) + +IO.puts(" WasmRunner.run/2 (add): avg=#{Float.round(run_avg, 1)}μs p50=#{run_p50}μs") +IO.puts(" Pure Elixir (add): avg=#{Float.round(elixir_avg, 1)}μs p50=#{elixir_p50}μs") +IO.puts(" Overhead ratio: #{Float.round(run_avg / max(elixir_avg, 0.1), 1)}x") + +# --- Section 2: run/2 with multiple calls --- +IO.puts("\n## 2. Multiple calls in one run/2 (amortizes load cost)\n") + +multi_times = + for _ <- 1..iterations do + {us, _} = + :timer.tc(fn -> + Firebird.WasmRunner.run(wasm_path, + add: [5, 3], + multiply: [4, 7], + fibonacci: [10] + ) + end) + us + end + +multi_avg = Enum.sum(multi_times) / iterations +per_call = multi_avg / 3 + +IO.puts(" 3 calls in one run/2: avg=#{Float.round(multi_avg, 1)}μs total") +IO.puts(" Per-call amortized: avg=#{Float.round(per_call, 1)}μs") +IO.puts(" vs single run/2: #{Float.round(multi_avg / run_avg, 2)}x (ideal: 1.0x + call overhead)") + +# --- Section 3: Precompiled vs cold run/2 --- +IO.puts("\n## 3. Precompiled module vs cold start\n") + +{:ok, compiled} = Firebird.WasmRunner.precompile(wasm_path) + +precompiled_times = + for _ <- 1..iterations do + {us, _} = :timer.tc(fn -> Firebird.WasmRunner.run(compiled, add: [5, 3]) end) + us + end + +pre_avg = Enum.sum(precompiled_times) / iterations +pre_p50 = Enum.sort(precompiled_times) |> Enum.at(div(iterations, 2)) + +IO.puts(" Cold run/2: avg=#{Float.round(run_avg, 1)}μs p50=#{run_p50}μs") +IO.puts(" Precompiled run/2: avg=#{Float.round(pre_avg, 1)}μs p50=#{pre_p50}μs") +IO.puts(" Speedup: #{Float.round(run_avg / max(pre_avg, 0.1), 1)}x") + +# --- Section 4: Pre-loaded instance (call overhead only) --- +IO.puts("\n## 4. Pre-loaded instance (isolates call overhead)\n") + +{:ok, pid} = Firebird.WasmRunner.start(wasm_path) + +loaded_times = + for _ <- 1..iterations do + {us, _} = :timer.tc(fn -> Firebird.WasmRunner.call_single(pid, :add, [5, 3]) end) + us + end + +Firebird.stop(pid) + +loaded_avg = Enum.sum(loaded_times) / iterations +loaded_p50 = Enum.sort(loaded_times) |> Enum.at(div(iterations, 2)) + +IO.puts(" Pre-loaded call_single: avg=#{Float.round(loaded_avg, 1)}μs p50=#{loaded_p50}μs") +IO.puts(" run/2 overhead (load): ~#{Float.round(run_avg - loaded_avg, 1)}μs per call") + +# --- Section 5: fibonacci(30) - compute-heavy --- +IO.puts("\n## 5. Compute-heavy: fibonacci(30)\n") + +fib_wasm_times = + for _ <- 1..50 do + {us, _} = :timer.tc(fn -> Firebird.WasmRunner.run(wasm_path, fibonacci: [30]) end) + us + end + +fib_elixir_times = + for _ <- 1..50 do + {us, _} = :timer.tc(fn -> NativeElixir.fibonacci(30) end) + us + end + +fib_wasm_avg = Enum.sum(fib_wasm_times) / 50 +fib_elixir_avg = Enum.sum(fib_elixir_times) / 50 + +IO.puts(" WASM run/2 fib(30): avg=#{Float.round(fib_wasm_avg, 1)}μs") +IO.puts(" Elixir fib(30): avg=#{Float.round(fib_elixir_avg, 1)}μs") + +if fib_wasm_avg < fib_elixir_avg do + IO.puts(" → WASM wins by #{Float.round(fib_elixir_avg / fib_wasm_avg, 1)}x") +else + IO.puts(" → Elixir wins by #{Float.round(fib_wasm_avg / fib_elixir_avg, 1)}x (WASM has load overhead)") +end + +IO.puts("\n" <> String.duplicate("=", 70)) +IO.puts("Summary: run/2 is the simplest API for one-shot WASM execution.") +IO.puts("For hot paths, use precompiled modules or pools to amortize load cost.") +IO.puts(String.duplicate("=", 70)) diff --git a/docs/PERFORMANCE_GUIDE.md b/docs/PERFORMANCE_GUIDE.md index 2c9c274..3adb4b8 100644 --- a/docs/PERFORMANCE_GUIDE.md +++ b/docs/PERFORMANCE_GUIDE.md @@ -382,6 +382,41 @@ stats = bench_wasm_call wasm, :add, [5, 3], iterations: 100_000 --- +## WasmRunner.run/2 Performance Characteristics + +`WasmRunner.run/2` is the simplest one-shot API. Here's what to expect: + +| Scenario | Typical Latency | Use When | +|----------|----------------|----------| +| `run/2` cold (from file) | ~2ms | Scripts, CLI tools, infrequent calls | +| `run/2` precompiled | ~40μs | Repeated calls, avoid recompilation | +| `call_single` (pre-loaded) | ~15μs | Hot paths, keep instance alive | +| Pool `call_one` | ~15μs | Concurrent access, production servers | + +**Key insight**: ~97% of `run/2` latency is WASM compilation. For hot paths: + +```elixir +# One-shot (simple, ~2ms) — great for scripts +{:ok, 8} = WasmRunner.run("math.wasm", add: [5, 3]) + +# Precompiled (53x faster) — amortize compilation +{:ok, compiled} = WasmRunner.precompile("math.wasm") +{:ok, 8} = WasmRunner.run(compiled, add: [5, 3]) + +# Multiple calls in one load (amortizes overhead) +{:ok, [8, 28, 55]} = WasmRunner.run("math.wasm", + add: [5, 3], multiply: [4, 7], fibonacci: [10]) + +# Pre-loaded instance (~15μs/call) — best for hot paths +{:ok, pid} = WasmRunner.start("math.wasm") +{:ok, 8} = WasmRunner.call_single(pid, :add, [5, 3]) +``` + +For compute-heavy functions (e.g., `fibonacci(30)`), WASM can be **5x+ faster** +than equivalent pure Elixir, even including the ~2ms load overhead. + +Run the benchmark yourself: `mix run bench/wasm_runner_run.exs` + ## Quick Reference: Profiling Commands ```bash diff --git a/docs/POOL_CALL_MANY.md b/docs/POOL_CALL_MANY.md new file mode 100644 index 0000000..f65b475 --- /dev/null +++ b/docs/POOL_CALL_MANY.md @@ -0,0 +1,73 @@ +# Pool.call_many — Batched Function Calls + +## The Problem + +Each `Pool.call/3` incurs a GenServer round-trip for checkout and checkin: + +``` +call(pool, :add, [1, 2]) → checkout → wasm_call → checkin +call(pool, :add, [3, 4]) → checkout → wasm_call → checkin +call(pool, :add, [5, 6]) → checkout → wasm_call → checkin +``` + +For N calls, that's N checkouts + N checkins = 2N GenServer messages. + +## The Solution + +`Pool.call_many/2` batches calls into a single checkout/checkin cycle: + +``` +call_many(pool, [...]) → checkout → wasm_call → wasm_call → wasm_call → checkin +``` + +For N calls: 1 checkout + N WASM calls + 1 checkin = 2 GenServer messages. + +## API + +```elixir +# Raw results (each is a list) +{:ok, [[8], [28], [55]]} = Pool.call_many(pool, [ + {:add, [5, 3]}, + {:multiply, [4, 7]}, + {:fibonacci, [10]} +]) + +# Unwrapped results (single values extracted) +{:ok, [8, 28, 55]} = Pool.call_many_unwrapped(pool, [ + {:add, [5, 3]}, + {:multiply, [4, 7]}, + {:fibonacci, [10]} +]) + +# Bang variant +[[8], [28]] = Pool.call_many!(pool, [{:add, [5, 3]}, {:multiply, [4, 7]}]) +``` + +## Performance + +Benchmark results (fixtures/math.wasm, pool size 4): + +| Batch Size | call_many avg | repeated call avg | Speedup | +|-----------|---------------|-------------------|---------| +| 5 calls | ~88μs | ~112μs | 1.27x | +| 20 calls | ~303μs | ~358μs | 1.18x | +| 100 calls | ~1802μs | ~2015μs | 1.12x | + +The speedup comes from eliminating N-1 GenServer round-trips. It's most +pronounced for small batches where checkout/checkin overhead is proportionally +larger relative to the WASM execution time. + +## Error Handling + +- Stops on the first error and returns `{:error, reason}` +- The instance is **always** checked back in (via `try/after`) +- Subsequent calls to the pool work normally after an error + +## When to Use + +Use `call_many/2` when you have multiple function calls to make and don't +need to inspect intermediate results: + +- Processing a batch of inputs through the same function +- Running a sequence of related operations (e.g., encode → process → decode) +- Any loop where you'd otherwise call `Pool.call/3` repeatedly diff --git a/examples/basic_usage.exs b/examples/basic_usage.exs index ec6cf13..5160cb5 100644 --- a/examples/basic_usage.exs +++ b/examples/basic_usage.exs @@ -28,11 +28,13 @@ IO.puts("✓ fibonacci(10) = #{result}") IO.puts("✓ factorial(5) = #{result}") # 5. Batch calls -{:ok, results} = Firebird.call_many(instance, [ - {:add, [10, 20]}, - {:multiply, [3, 4]}, - {:is_prime, [17]} -]) +{:ok, results} = + Firebird.call_many(instance, [ + {:add, [10, 20]}, + {:multiply, [3, 4]}, + {:is_prime, [17]} + ]) + IO.puts("✓ Batch results: #{inspect(results)}") # 6. Memory operations diff --git a/examples/build_and_run.exs b/examples/build_and_run.exs index 7ee9e4c..0a441db 100644 --- a/examples/build_and_run.exs +++ b/examples/build_and_run.exs @@ -15,7 +15,7 @@ IO.puts("=" |> String.duplicate(60)) # --- Step 1: Create a temporary Go project --- IO.puts("\n📝 Step 1: Creating Go WASM project...") -tmp_dir = Path.join(System.tmp_dir!(), "firebird_demo_#{:rand.uniform(100000)}") +tmp_dir = Path.join(System.tmp_dir!(), "firebird_demo_#{:rand.uniform(100_000)}") :ok = Firebird.Builder.scaffold_go(tmp_dir, functions: [:add, :multiply, :double]) # Customize the generated code @@ -53,9 +53,10 @@ IO.puts(" Files: #{File.ls!(tmp_dir) |> Enum.join(", ")}") # --- Step 2: Build WASM --- IO.puts("\n🔨 Step 2: Building Go → WASM...") -{build_time, {:ok, wasm_path}} = :timer.tc(fn -> - Firebird.Builder.build_go(tmp_dir) -end) +{build_time, {:ok, wasm_path}} = + :timer.tc(fn -> + Firebird.Builder.build_go(tmp_dir) + end) wasm_size = File.stat!(wasm_path).size IO.puts(" Built: #{wasm_path}") @@ -65,9 +66,10 @@ IO.puts(" Time: #{div(build_time, 1000)}ms") # --- Step 3: Load and call --- IO.puts("\n🚀 Step 3: Loading and calling WASM functions...") -{load_time, {:ok, instance}} = :timer.tc(fn -> - Firebird.load(wasm_path, wasi: true) -end) +{load_time, {:ok, instance}} = + :timer.tc(fn -> + Firebird.load(wasm_path, wasi: true) + end) IO.puts(" Loaded in #{div(load_time, 1000)}ms") @@ -95,18 +97,24 @@ IO.puts("\n🔗 Step 4: Using Pipeline to chain calls...") result = Firebird.pipeline(instance) - |> Firebird.Pipeline.call(:add, [5, 3]) # 8 - |> Firebird.Pipeline.call(:square, [:_]) # 64 - |> Firebird.Pipeline.call(:double, [:_]) # 128 + # 8 + |> Firebird.Pipeline.call(:add, [5, 3]) + # 64 + |> Firebird.Pipeline.call(:square, [:_]) + # 128 + |> Firebird.Pipeline.call(:double, [:_]) |> Firebird.Pipeline.run!() IO.puts(" add(5,3) |> square |> double = #{hd(result)}") result2 = Firebird.pipeline(instance) - |> Firebird.Pipeline.call(:fibonacci, [10]) # 55 - |> Firebird.Pipeline.call(:add, [:_, 45]) # 100 - |> Firebird.Pipeline.call(:multiply, [:_, 3]) # 300 + # 55 + |> Firebird.Pipeline.call(:fibonacci, [10]) + # 100 + |> Firebird.Pipeline.call(:add, [:_, 45]) + # 300 + |> Firebird.Pipeline.call(:multiply, [:_, 3]) |> Firebird.Pipeline.run!() IO.puts(" fibonacci(10) |> add(_, 45) |> multiply(_, 3) = #{hd(result2)}") @@ -125,8 +133,9 @@ IO.puts(" " <> String.duplicate("─", 56)) test_cases = [ {"add(100,200)", :add, [100, 200], fn -> 100 + 200 end}, - {"fibonacci(20)", :fibonacci, [20], fn -> Enum.reduce(2..20, {1, 0}, fn _, {b, a} -> {a + b, b} end) |> elem(0) end}, - {"factorial(10)", :factorial, [10], fn -> Enum.reduce(1..10, 1, &*/2) end}, + {"fibonacci(20)", :fibonacci, [20], + fn -> Enum.reduce(2..20, {1, 0}, fn _, {b, a} -> {a + b, b} end) |> elem(0) end}, + {"factorial(10)", :factorial, [10], fn -> Enum.reduce(1..10, 1, &*/2) end} ] for {name, func, args, native_fn} <- test_cases do @@ -137,7 +146,10 @@ for {name, func, args, native_fn} <- test_cases do native_r = if is_tuple(native_r), do: elem(native_r, 0), else: native_r status = if go_r == rust_r and rust_r == wat_r, do: "✅", else: "❌" - IO.puts(" #{status} #{String.pad_trailing(name, 16)} #{String.pad_leading("#{go_r}", 10)} #{String.pad_leading("#{rust_r}", 10)} #{String.pad_leading("#{wat_r}", 10)} #{String.pad_leading("#{native_r}", 10)}") + + IO.puts( + " #{status} #{String.pad_trailing(name, 16)} #{String.pad_leading("#{go_r}", 10)} #{String.pad_leading("#{rust_r}", 10)} #{String.pad_leading("#{wat_r}", 10)} #{String.pad_leading("#{native_r}", 10)}" + ) end Firebird.stop(go_instance) diff --git a/examples/compile_to_wasm.exs b/examples/compile_to_wasm.exs index 8f9d6ef..5e39e8e 100644 --- a/examples/compile_to_wasm.exs +++ b/examples/compile_to_wasm.exs @@ -49,7 +49,11 @@ IO.puts("⚙️ Compiling to WebAssembly...") IO.puts(" Module: #{result.module}") IO.puts(" WAT size: #{byte_size(result.wat)} bytes") IO.puts(" WASM size: #{byte_size(result.wasm)} bytes") -IO.puts(" Compression: #{Float.round(byte_size(result.wasm) / byte_size(result.wat) * 100, 1)}%") + +IO.puts( + " Compression: #{Float.round(byte_size(result.wasm) / byte_size(result.wat) * 100, 1)}%" +) + IO.puts("") # Step 3: Load and execute diff --git a/examples/complete_project/lib/my_app.ex b/examples/complete_project/lib/my_app.ex index 85d6559..b27fa53 100644 --- a/examples/complete_project/lib/my_app.ex +++ b/examples/complete_project/lib/my_app.ex @@ -40,9 +40,12 @@ defmodule MyApp do # 5. Block API with auto-cleanup IO.puts("\n5. Block API:") - {:ok, z} = Firebird.with_instance("wasm/math.wasm", fn wasm -> - Firebird.call_one!(wasm, :fibonacci, [20]) - end) + + {:ok, z} = + Firebird.with_instance("wasm/math.wasm", fn wasm -> + Firebird.call_one!(wasm, :fibonacci, [20]) + end) + IO.puts(" fibonacci(20) = #{z}") IO.puts("\n✅ All done!") diff --git a/examples/complete_project/lib/my_app/application.ex b/examples/complete_project/lib/my_app/application.ex index d426223..f6eb83d 100644 --- a/examples/complete_project/lib/my_app/application.ex +++ b/examples/complete_project/lib/my_app/application.ex @@ -5,7 +5,7 @@ defmodule MyApp.Application do def start(_type, _args) do children = [ # Start the WASM math module as a supervised process - MyApp.Math, + MyApp.Math # Or use a pool for concurrent workloads: # {Firebird.Pool, wasm: "wasm/math.wasm", size: 4, name: :math_pool} diff --git a/examples/complete_wasm_demo/demo.exs b/examples/complete_wasm_demo/demo.exs index 83ec2ea..2509f6e 100644 --- a/examples/complete_wasm_demo/demo.exs +++ b/examples/complete_wasm_demo/demo.exs @@ -88,11 +88,13 @@ IO.puts(" Firebird.run!(\"math.wasm\", :add, [999, 1]) = #{inspect(result)}") # --- 6. With Instance (auto-cleanup) --- IO.puts("\n━━━ 6. With Instance (auto-cleanup) ━━━\n") -{:ok, result} = Firebird.with_instance("fixtures/math.wasm", fn wasm -> - {:ok, [a]} = Firebird.call(wasm, :add, [10, 20]) - {:ok, [b]} = Firebird.call(wasm, :multiply, [a, 3]) - b -end) +{:ok, result} = + Firebird.with_instance("fixtures/math.wasm", fn wasm -> + {:ok, [a]} = Firebird.call(wasm, :add, [10, 20]) + {:ok, [b]} = Firebird.call(wasm, :multiply, [a, 3]) + b + end) + IO.puts(" (10 + 20) * 3 = #{result}") # --- 7. Pipe API --- @@ -120,12 +122,16 @@ IO.puts("\n━━━ 9. Connection Pool ━━━\n") {:ok, pool} = Firebird.start_pool(wasm: "fixtures/math.wasm", size: 4) IO.puts(" Started pool with 4 instances") -results = 1..10 -|> Task.async_stream(fn i -> - {:ok, [r]} = Firebird.pool_call(pool, :fibonacci, [i]) - {i, r} -end, max_concurrency: 4) -|> Enum.map(fn {:ok, r} -> r end) +results = + 1..10 + |> Task.async_stream( + fn i -> + {:ok, [r]} = Firebird.pool_call(pool, :fibonacci, [i]) + {i, r} + end, + max_concurrency: 4 + ) + |> Enum.map(fn {:ok, r} -> r end) for {n, fib} <- results do IO.puts(" fibonacci(#{n}) = #{fib}") @@ -151,7 +157,10 @@ IO.puts(" Next prime after 100: #{r}") IO.puts(" Digit sum of 9999: #{r}") # Sort array in WASM memory -data = <<5::little-signed-32, 3::little-signed-32, 1::little-signed-32, 4::little-signed-32, 2::little-signed-32>> +data = + <<5::little-signed-32, 3::little-signed-32, 1::little-signed-32, 4::little-signed-32, + 2::little-signed-32>> + :ok = Firebird.write_memory(algo, 0, data) Firebird.call(algo, :sort_i32, [0, 5]) {:ok, sorted} = Firebird.read_memory(algo, 0, 20) diff --git a/examples/comprehensive_demo.exs b/examples/comprehensive_demo.exs index 1a0ba62..8a2e841 100644 --- a/examples/comprehensive_demo.exs +++ b/examples/comprehensive_demo.exs @@ -66,6 +66,7 @@ IO.puts(" Rust exports: #{inspect(exports)}") all = Firebird.all_exports(rust) IO.puts(" All exports (with types):") + for {name, type} <- all do IO.puts(" #{name}: #{type}") end @@ -81,15 +82,17 @@ IO.puts("") IO.puts("4. BATCH CALLS (call_many)") IO.puts("-" |> String.duplicate(40)) -{:ok, results} = Firebird.call_many(rust, [ - {:add, [1, 2]}, - {:multiply, [3, 4]}, - {:fibonacci, [10]}, - {:factorial, [5]}, - {:gcd, [48, 18]} -]) +{:ok, results} = + Firebird.call_many(rust, [ + {:add, [1, 2]}, + {:multiply, [3, 4]}, + {:fibonacci, [10]}, + {:factorial, [5]}, + {:gcd, [48, 18]} + ]) labels = ["add(1,2)", "multiply(3,4)", "fibonacci(10)", "factorial(5)", "gcd(48,18)"] + for {label, [val]} <- Enum.zip(labels, results) do IO.puts(" #{label} = #{val}") end @@ -106,7 +109,8 @@ IO.puts("-" |> String.duplicate(40)) IO.puts(" Memory size: #{size} bytes (#{div(size, 65_536)} pages)") # Write and read back -data = <<72, 101, 108, 108, 111>> # "Hello" +# "Hello" +data = <<72, 101, 108, 108, 111>> :ok = Firebird.write_memory(rust, 0, data) {:ok, read_back} = Firebird.read_memory(rust, 0, 5) IO.puts(" Wrote #{inspect(data)} -> Read #{inspect(read_back)} (\"#{read_back}\")") @@ -132,12 +136,14 @@ IO.puts(" Pool started with 4 instances") IO.puts(" pool_call add(100, 200) = #{result}") # Concurrent -tasks = for i <- 1..10 do - Task.async(fn -> - {:ok, [r]} = Firebird.pool_call(pool, :fibonacci, [i]) - r - end) -end +tasks = + for i <- 1..10 do + Task.async(fn -> + {:ok, [r]} = Firebird.pool_call(pool, :fibonacci, [i]) + r + end) + end + results = Task.await_many(tasks) IO.puts(" 10 concurrent fibonacci calls: #{inspect(results)}") @@ -154,7 +160,9 @@ IO.puts("-" |> String.duplicate(40)) {:ok, str_wasm} = Firebird.load("fixtures/string_utils.wasm") -{:ok, "HELLO WORLD"} = Firebird.WasmString.call_with_string(str_wasm, :to_uppercase, "hello world") +{:ok, "HELLO WORLD"} = + Firebird.WasmString.call_with_string(str_wasm, :to_uppercase, "hello world") + IO.puts(" to_uppercase(\"hello world\") = \"HELLO WORLD\"") {:ok, ptr, len} = Firebird.WasmString.write_string(str_wasm, "firebird") @@ -181,18 +189,19 @@ test_cases = [ {:factorial, [10]}, {:gcd, [252, 105]}, {:power, [2, 16]}, - {:is_prime, [104729]}, + {:is_prime, [104_729]}, {:sum_range, [1, 1000]} ] -all_match = Enum.all?(test_cases, fn {func, args} -> - {:ok, [r]} = Firebird.call(rust, func, args) - {:ok, [g]} = Firebird.call(go, func, args) - match = r == g - status = if match, do: "✅", else: "❌" - IO.puts(" #{status} #{func}(#{Enum.join(args, ", ")}): Rust=#{r}, Go=#{g}") - match -end) +all_match = + Enum.all?(test_cases, fn {func, args} -> + {:ok, [r]} = Firebird.call(rust, func, args) + {:ok, [g]} = Firebird.call(go, func, args) + match = r == g + status = if match, do: "✅", else: "❌" + IO.puts(" #{status} #{func}(#{Enum.join(args, ", ")}): Rust=#{r}, Go=#{g}") + match + end) IO.puts(" All results match: #{all_match}") diff --git a/examples/concurrent_pool.exs b/examples/concurrent_pool.exs index bf0e7a3..f8afad0 100644 --- a/examples/concurrent_pool.exs +++ b/examples/concurrent_pool.exs @@ -16,20 +16,22 @@ IO.puts("Started pool with #{Firebird.Pool.status(pool).pool_size} instances\n") n = 100 # Sequential -{sequential_time, _} = :timer.tc(fn -> - for i <- 1..n do - {:ok, [_]} = Firebird.Pool.call(pool, :fibonacci, [30]) - end -end) +{sequential_time, _} = + :timer.tc(fn -> + for i <- 1..n do + {:ok, [_]} = Firebird.Pool.call(pool, :fibonacci, [30]) + end + end) # Concurrent with tasks -{concurrent_time, _} = :timer.tc(fn -> - 1..n - |> Enum.map(fn _i -> - Task.async(fn -> Firebird.Pool.call!(pool, :fibonacci, [30]) end) +{concurrent_time, _} = + :timer.tc(fn -> + 1..n + |> Enum.map(fn _i -> + Task.async(fn -> Firebird.Pool.call!(pool, :fibonacci, [30]) end) + end) + |> Task.await_many() end) - |> Task.await_many() -end) IO.puts("#{n} fibonacci(30) calls:") IO.puts(" Sequential: #{div(sequential_time, 1000)}ms") @@ -39,6 +41,7 @@ IO.puts(" Speedup: #{Float.round(sequential_time / max(concurrent_time, 1), # ── 3. Mixed workloads ────────────────────────────────────────────────────── IO.puts("Mixed concurrent workload:") + tasks = [ Task.async(fn -> {:add, Firebird.Pool.call!(pool, :add, [100, 200])} end), Task.async(fn -> {:mul, Firebird.Pool.call!(pool, :multiply, [7, 8])} end), diff --git a/examples/end_to_end_demo.exs b/examples/end_to_end_demo.exs index dfb773c..ed27c73 100644 --- a/examples/end_to_end_demo.exs +++ b/examples/end_to_end_demo.exs @@ -113,7 +113,12 @@ IO.puts(" Binary search for 30 → index #{idx}") IO.puts("\n🌍 Cross-language comparison (WAT vs Go):\n") -for {func, args} <- [{:add, [100, 200]}, {:multiply, [7, 8]}, {:fibonacci, [15]}, {:factorial, [10]}] do +for {func, args} <- [ + {:add, [100, 200]}, + {:multiply, [7, 8]}, + {:fibonacci, [15]}, + {:factorial, [10]} + ] do {:ok, [wat_r]} = Firebird.call(wat, func, args) {:ok, [go_r]} = Firebird.call(go, func, args) match = if wat_r == go_r, do: "✅", else: "❌" @@ -124,13 +129,15 @@ end IO.puts("\n📦 Batch operations:\n") -{:ok, results} = Firebird.WasmRunner.run_batch("fixtures/math.wasm", [ - {:add, [1, 2]}, - {:multiply, [3, 4]}, - {:fibonacci, [10]}, - {:factorial, [5]}, - {:gcd, [48, 36]} -]) +{:ok, results} = + Firebird.WasmRunner.run_batch("fixtures/math.wasm", [ + {:add, [1, 2]}, + {:multiply, [3, 4]}, + {:fibonacci, [10]}, + {:factorial, [5]}, + {:gcd, [48, 36]} + ]) + IO.puts(" Batch results: #{inspect(results)}") # ── 9. WasmRunner auto-WASI ────────────────────────────────── diff --git a/examples/end_to_end_wasm_demo.exs b/examples/end_to_end_wasm_demo.exs index 4211dc3..46cc838 100644 --- a/examples/end_to_end_wasm_demo.exs +++ b/examples/end_to_end_wasm_demo.exs @@ -47,9 +47,9 @@ calls = [ {"add(5, 3)", :add, [5, 3], 8}, {"multiply(7, 6)", :multiply, [7, 6], 42}, {"fibonacci(10)", :fibonacci, [10], 55}, - {"factorial(12)", :factorial, [12], 479001600}, + {"factorial(12)", :factorial, [12], 479_001_600}, {"gcd(48, 36)", :gcd, [48, 36], 12}, - {"is_prime(104729)", :is_prime, [104729], 1}, + {"is_prime(104729)", :is_prime, [104_729], 1}, {"power(2, 10)", :power, [2, 10], 1024}, {"sum_range(1, 100)", :sum_range, [1, 100], 5050} ] @@ -59,7 +59,10 @@ for {label, func, args, expected} <- calls do {:ok, [g_result]} = Firebird.call(go, func, args) match = if r_result == g_result and r_result == expected, do: "✅", else: "❌" - IO.puts(" #{match} #{String.pad_trailing(label, 25)} Rust=#{r_result} Go=#{g_result} expected=#{expected}") + + IO.puts( + " #{match} #{String.pad_trailing(label, 25)} Rust=#{r_result} Go=#{g_result} expected=#{expected}" + ) end # ── 4. Rust-only Features ──────────────────────────────────── @@ -78,20 +81,21 @@ IO.puts(" collatz_steps(27) = #{result} steps") {:ok, [result]} = Firebird.call(rust, :count_primes, [1000]) IO.puts(" count_primes(1000) = #{result}") -{:ok, [result]} = Firebird.call(rust, :isqrt, [1000000]) +{:ok, [result]} = Firebird.call(rust, :isqrt, [1_000_000]) IO.puts(" isqrt(1000000) = #{result}") # ── 5. WasmRunner Ergonomic API ────────────────────────────── IO.puts("\n🚀 Step 5: WasmRunner batch execution\n") -{:ok, results} = Firebird.WasmRunner.run_batch("fixtures/rust_math.wasm", [ - {:add, [100, 200]}, - {:multiply, [11, 11]}, - {:fibonacci, [20]}, - {:factorial, [10]}, - {:count_primes, [100]} -]) +{:ok, results} = + Firebird.WasmRunner.run_batch("fixtures/rust_math.wasm", [ + {:add, [100, 200]}, + {:multiply, [11, 11]}, + {:fibonacci, [20]}, + {:factorial, [10]}, + {:count_primes, [100]} + ]) IO.puts(" Batch results: #{inspect(results)}") IO.puts(" (add=300, multiply=121, fib=6765, factorial=3628800, primes=25)") @@ -102,9 +106,12 @@ IO.puts("\n⚡ Step 6: Concurrent WASM execution\n") args_list = for n <- [10, 20, 30, 35, 40], do: [n] -{time, {:ok, results}} = :timer.tc(fn -> - Firebird.WasmRunner.run_concurrent("fixtures/rust_math.wasm", :fibonacci, args_list, concurrency: 4) -end) +{time, {:ok, results}} = + :timer.tc(fn -> + Firebird.WasmRunner.run_concurrent("fixtures/rust_math.wasm", :fibonacci, args_list, + concurrency: 4 + ) + end) IO.puts(" fibonacci([10,20,30,35,40]) = #{inspect(results)}") IO.puts(" Concurrent execution time: #{time}μs (#{Float.round(time / 1000, 1)}ms)") @@ -136,7 +143,9 @@ IO.puts("\n📝 Step 8: String processing (Rust WASM)\n") {:ok, "FIREBIRD"} = Firebird.WasmString.call_with_string(str_wasm, :to_uppercase, "firebird") IO.puts(" to_uppercase(\"firebird\") = \"FIREBIRD\"") -{:ok, "HELLO WORLD"} = Firebird.WasmString.call_with_string(str_wasm, :to_uppercase, "hello world") +{:ok, "HELLO WORLD"} = + Firebird.WasmString.call_with_string(str_wasm, :to_uppercase, "hello world") + IO.puts(" to_uppercase(\"hello world\") = \"HELLO WORLD\"") Firebird.stop(str_wasm) diff --git a/examples/error_handling.exs b/examples/error_handling.exs index 66d308c..3b7f8cb 100644 --- a/examples/error_handling.exs +++ b/examples/error_handling.exs @@ -9,6 +9,7 @@ IO.puts("🔥 Firebird Error Handling Patterns\n") # ── 1. File not found ─────────────────────────────────────────────────────── IO.puts("1. File not found:") + case Firebird.load("nonexistent.wasm") do {:ok, _} -> IO.puts(" Loaded!") {:error, {:file_error, :enoent, path}} -> IO.puts(" ✓ File not found: #{path}") @@ -18,6 +19,7 @@ end # ── 2. Bang version raises ────────────────────────────────────────────────── IO.puts("\n2. Bang version raises:") + try do Firebird.load!("nonexistent.wasm") rescue @@ -38,6 +40,7 @@ end IO.puts("\n4. Check before calling:") function = :add + if Firebird.function_exists?(wasm, function) do {:ok, [result]} = Firebird.call(wasm, function, [5, 3]) IO.puts(" ✓ #{function}(5, 3) = #{result}") @@ -48,6 +51,7 @@ end # ── 5. run/3 — one-shot with error handling ────────────────────────────────── IO.puts("\n5. One-shot with run/3:") + case Firebird.run("fixtures/math.wasm", :add, [5, 3]) do {:ok, [result]} -> IO.puts(" ✓ Result: #{result}") {:error, reason} -> IO.puts(" ✗ Error: #{inspect(reason)}") @@ -56,11 +60,12 @@ end # ── 6. with_instance — block with auto-cleanup ────────────────────────────── IO.puts("\n6. Block with auto-cleanup:") + case Firebird.with_instance("fixtures/math.wasm", fn inst -> - {:ok, [a]} = Firebird.call(inst, :add, [10, 20]) - {:ok, [b]} = Firebird.call(inst, :multiply, [a, 2]) - b -end) do + {:ok, [a]} = Firebird.call(inst, :add, [10, 20]) + {:ok, [b]} = Firebird.call(inst, :multiply, [a, 2]) + b + end) do {:ok, result} -> IO.puts(" ✓ Result: #{result}") {:error, reason} -> IO.puts(" ✗ Error: #{inspect(reason)}") end @@ -68,6 +73,7 @@ end # ── 7. Pattern: safe plugin loading ───────────────────────────────────────── IO.puts("\n7. Safe plugin loading pattern:") + defmodule PluginLoader do def load(path) do with {:ok, instance} <- Firebird.load(path), @@ -79,6 +85,7 @@ defmodule PluginLoader do false -> IO.puts(" ✗ Plugin missing required functions (init, process)") {:error, :invalid_plugin} + {:error, _} = error -> IO.puts(" ✗ Failed to load: #{inspect(error)}") error diff --git a/examples/go_math_demo.exs b/examples/go_math_demo.exs index 66bd595..d534122 100644 --- a/examples/go_math_demo.exs +++ b/examples/go_math_demo.exs @@ -10,7 +10,11 @@ go_path = "fixtures/go_math.wasm" unless File.exists?(go_path) do IO.puts("❌ Go WASM not found. Build it first:") - IO.puts(" cd fixtures/go_math && tinygo build -o ../go_math.wasm -target wasm -no-debug main.go") + + IO.puts( + " cd fixtures/go_math && tinygo build -o ../go_math.wasm -target wasm -no-debug main.go" + ) + System.halt(1) end @@ -53,15 +57,15 @@ IO.puts("Cross-language verification (Go vs Rust):") {:ok, rust_wasm} = Firebird.load("fixtures/math.wasm") for {func, args, label} <- [ - {:add, [42, 58], "add(42, 58)"}, - {:multiply, [7, 8], "multiply(7, 8)"}, - {:fibonacci, [10], "fibonacci(10)"}, - {:factorial, [5], "factorial(5)"}, - {:gcd, [48, 18], "gcd(48, 18)"}, - {:power, [2, 10], "power(2, 10)"}, - {:is_prime, [97], "is_prime(97)"}, - {:sum_range, [1, 100], "sum_range(1, 100)"} -] do + {:add, [42, 58], "add(42, 58)"}, + {:multiply, [7, 8], "multiply(7, 8)"}, + {:fibonacci, [10], "fibonacci(10)"}, + {:factorial, [5], "factorial(5)"}, + {:gcd, [48, 18], "gcd(48, 18)"}, + {:power, [2, 10], "power(2, 10)"}, + {:is_prime, [97], "is_prime(97)"}, + {:sum_range, [1, 100], "sum_range(1, 100)"} + ] do {:ok, [go_result]} = Firebird.call(wasm, func, args) {:ok, [rust_result]} = Firebird.call(rust_wasm, func, args) match = if go_result == rust_result, do: "✅", else: "❌" diff --git a/examples/memory_management.exs b/examples/memory_management.exs index e9f2618..1722f25 100644 --- a/examples/memory_management.exs +++ b/examples/memory_management.exs @@ -96,15 +96,20 @@ IO.puts("") # Define C-compatible struct layouts for complex data exchange. # Fields use natural alignment matching C/Rust/TinyGo defaults. -layout = Memory.define_layout([ - {:x, :f64}, - {:y, :f64}, - {:id, :i32}, - {:flags, :u8} -]) +layout = + Memory.define_layout([ + {:x, :f64}, + {:y, :f64}, + {:id, :i32}, + {:flags, :u8} + ]) IO.puts("6️⃣ Struct layout defined") -IO.puts(" Fields: #{inspect(Enum.map(layout.fields, fn {name, type} -> "#{name}:#{type}" end))}") + +IO.puts( + " Fields: #{inspect(Enum.map(layout.fields, fn {name, type} -> "#{name}:#{type}" end))}" +) + IO.puts(" Offsets: #{inspect(layout.offsets)}") IO.puts(" Total size: #{layout.size} bytes (padded to max alignment)") @@ -129,9 +134,11 @@ IO.puts(" Wrote #{struct_count} structs starting at offset #{arr_struct_ptr}") {:ok, read_points} = Memory.read_struct_array(alloc, layout, arr_struct_ptr, struct_count) IO.puts(" Read back #{length(read_points)} structs") + for p <- read_points do IO.puts(" Point ##{p.id}: (#{p.x}, #{p.y}) flags=#{p.flags}") end + IO.puts("") # ─── 7. Introspection ────────────────────────────────────────────────────── @@ -162,18 +169,19 @@ IO.puts("") IO.puts("9️⃣ Scoped arena (with_arena)") IO.puts(" Cursor before: #{alloc.cursor}") -{result, alloc} = Memory.with_arena(alloc, fn arena -> - # These allocations are temporary - {arena, ptr, len} = Memory.write_string(arena, "temporary data") - {:ok, temp_str} = Memory.read_string(arena, ptr, len) +{result, alloc} = + Memory.with_arena(alloc, fn arena -> + # These allocations are temporary + {arena, ptr, len} = Memory.write_string(arena, "temporary data") + {:ok, temp_str} = Memory.read_string(arena, ptr, len) - {arena, _, _} = Memory.write_i32_array(arena, Enum.to_list(1..100)) + {arena, _, _} = Memory.write_i32_array(arena, Enum.to_list(1..100)) - IO.puts(" Inside arena: #{Memory.allocated_bytes(arena)} bytes allocated") + IO.puts(" Inside arena: #{Memory.allocated_bytes(arena)} bytes allocated") - # Return a result — it survives the arena reset - {temp_str, arena} -end) + # Return a result — it survives the arena reset + {temp_str, arena} + end) IO.puts(" Cursor after: #{alloc.cursor} (reset to before)") IO.puts(" Result preserved: #{inspect(result)}") diff --git a/examples/new_features.exs b/examples/new_features.exs index 8eba019..07fa285 100644 --- a/examples/new_features.exs +++ b/examples/new_features.exs @@ -27,13 +27,14 @@ IO.puts("2️⃣ Inline WAT with wat!() compile-time macro") import Firebird.Sigils -bytes = wat!(""" -(module - (func (export "square") (param i32) (result i32) - local.get 0 - local.get 0 - i32.mul)) -""") +bytes = + wat!(""" + (module + (func (export "square") (param i32) (result i32) + local.get 0 + local.get 0 + i32.mul)) + """) {:ok, wasm} = Firebird.load(bytes) {:ok, [result]} = Firebird.call(wasm, :square, [7]) @@ -44,11 +45,15 @@ IO.puts("") # ─── 3. Quick.eval_wat for one-off computations ───────────────── IO.puts("3️⃣ Quick.eval_wat for one-off WAT evaluation") -{:ok, [answer]} = Firebird.Quick.eval_wat(""" -(module - (func (export "meaning_of_life") (result i32) - i32.const 42)) -""", "meaning_of_life") +{:ok, [answer]} = + Firebird.Quick.eval_wat( + """ + (module + (func (export "meaning_of_life") (result i32) + i32.const 42)) + """, + "meaning_of_life" + ) IO.puts(" meaning_of_life() = #{answer}") IO.puts("") @@ -67,8 +72,8 @@ IO.puts("") # ─── 5. Quick.wat_fn for reusable inline functions ────────────── IO.puts("5️⃣ Quick.wat_fn for creating callable WAT functions") -negate = Firebird.Quick.wat_fn("negate", "(param i32) (result i32)", - "i32.const 0 local.get 0 i32.sub") +negate = + Firebird.Quick.wat_fn("negate", "(param i32) (result i32)", "i32.const 0 local.get 0 i32.sub") {:ok, [neg5]} = negate.([5]) {:ok, [neg100]} = negate.([100]) @@ -78,6 +83,7 @@ IO.puts("") # ─── Summary ───────────────────────────────────────────────────── IO.puts("✅ All features working!") + IO.puts(""" Summary of new DX features: • use Firebird, wasm: "..." — shorter than use Firebird.Module diff --git a/examples/phoenix_wasm_app.exs b/examples/phoenix_wasm_app.exs index e38864f..2b9347f 100644 --- a/examples/phoenix_wasm_app.exs +++ b/examples/phoenix_wasm_app.exs @@ -31,38 +31,69 @@ alias Firebird.Phoenix.{RequestHandler, Middleware, Conn} {:ok, handler} = RequestHandler.new() # Add middleware -handler = handler -|> RequestHandler.use_middleware(Middleware.request_id()) -|> RequestHandler.use_middleware(Middleware.timer()) -|> RequestHandler.use_middleware(Middleware.logger()) -|> RequestHandler.use_middleware(Middleware.default_header("x-powered-by", "Firebird WASM")) +handler = + handler + |> RequestHandler.use_middleware(Middleware.request_id()) + |> RequestHandler.use_middleware(Middleware.timer()) + |> RequestHandler.use_middleware(Middleware.logger()) + |> RequestHandler.use_middleware(Middleware.default_header("x-powered-by", "Firebird WASM")) # Define routes -handler = handler -|> RequestHandler.route("GET", "/", "PageController.index") -|> RequestHandler.route("GET", "/api/users", "UserController.index") -|> RequestHandler.route("GET", "/api/users/:id", "UserController.show") -|> RequestHandler.route("POST", "/api/users", "UserController.create") -|> RequestHandler.route("PUT", "/api/users/:id", "UserController.update") -|> RequestHandler.route("DELETE", "/api/users/:id", "UserController.delete") -|> RequestHandler.route("GET", "/api/health", "HealthController.check") +handler = + handler + |> RequestHandler.route("GET", "/", "PageController.index") + |> RequestHandler.route("GET", "/api/users", "UserController.index") + |> RequestHandler.route("GET", "/api/users/:id", "UserController.show") + |> RequestHandler.route("POST", "/api/users", "UserController.create") + |> RequestHandler.route("PUT", "/api/users/:id", "UserController.update") + |> RequestHandler.route("DELETE", "/api/users/:id", "UserController.delete") + |> RequestHandler.route("GET", "/api/health", "HealthController.check") # Define templates -handler = handler -|> RequestHandler.template("PageController.index", 200, "text/html", - "
A REST API powered by WebAssembly
") -|> RequestHandler.template("UserController.index", 200, "application/json", - ~s([{"id":"1","name":"Alice"},{"id":"2","name":"Bob"},{"id":"3","name":"Charlie"}])) -|> RequestHandler.template("UserController.show", 200, "application/json", - ~s({"id":"{{id}}","name":"User {{id}}","email":"user{{id}}@example.com"})) -|> RequestHandler.template("UserController.create", 201, "application/json", - ~s({"status":"created","message":"User created successfully"})) -|> RequestHandler.template("UserController.update", 200, "application/json", - ~s({"status":"updated","id":"{{id}}"})) -|> RequestHandler.template("UserController.delete", 200, "application/json", - ~s({"status":"deleted","id":"{{id}}"})) -|> RequestHandler.template("HealthController.check", 200, "application/json", - ~s({"status":"ok","uptime":"100%","wasm":"active"})) +handler = + handler + |> RequestHandler.template( + "PageController.index", + 200, + "text/html", + "A REST API powered by WebAssembly
" + ) + |> RequestHandler.template( + "UserController.index", + 200, + "application/json", + ~s([{"id":"1","name":"Alice"},{"id":"2","name":"Bob"},{"id":"3","name":"Charlie"}]) + ) + |> RequestHandler.template( + "UserController.show", + 200, + "application/json", + ~s({"id":"{{id}}","name":"User {{id}}","email":"user{{id}}@example.com"}) + ) + |> RequestHandler.template( + "UserController.create", + 201, + "application/json", + ~s({"status":"created","message":"User created successfully"}) + ) + |> RequestHandler.template( + "UserController.update", + 200, + "application/json", + ~s({"status":"updated","id":"{{id}}"}) + ) + |> RequestHandler.template( + "UserController.delete", + 200, + "application/json", + ~s({"status":"deleted","id":"{{id}}"}) + ) + |> RequestHandler.template( + "HealthController.check", + 200, + "application/json", + ~s({"status":"ok","uptime":"100%","wasm":"active"}) + ) IO.puts("🛣️ Routes defined:") IO.puts(" GET /") @@ -90,22 +121,25 @@ requests = [ ] for {method, path, description} <- requests do - {time_us, {:ok, conn}} = :timer.tc(fn -> - RequestHandler.handle(handler, method, path) - end) - - status_emoji = cond do - conn.status in 200..299 -> "✅" - conn.status == 404 -> "🔍" - true -> "❌" - end - - body_preview = if conn.resp_body do - String.slice(conn.resp_body, 0..60) - |> String.replace("\n", " ") - else - "(empty)" - end + {time_us, {:ok, conn}} = + :timer.tc(fn -> + RequestHandler.handle(handler, method, path) + end) + + status_emoji = + cond do + conn.status in 200..299 -> "✅" + conn.status == 404 -> "🔍" + true -> "❌" + end + + body_preview = + if conn.resp_body do + String.slice(conn.resp_body, 0..60) + |> String.replace("\n", " ") + else + "(empty)" + end IO.puts("#{status_emoji} #{method} #{path}") IO.puts(" #{description}") @@ -123,35 +157,49 @@ IO.puts("🔒 WASM Validator Demo\n") # Valid form IO.puts(" Valid user registration:") -{:ok, result} = Firebird.Phoenix.Validator.validate(validator, %{ - "name" => "Alice Smith", - "email" => "alice@example.com", - "age" => "25", - "password" => "securepass123" -}, [ - {:required, "name"}, - {:required, "email"}, - {:required, "password"}, - {:type, "email", :email}, - {:type, "age", :integer}, - {:min_length, "password", 8} -]) + +{:ok, result} = + Firebird.Phoenix.Validator.validate( + validator, + %{ + "name" => "Alice Smith", + "email" => "alice@example.com", + "age" => "25", + "password" => "securepass123" + }, + [ + {:required, "name"}, + {:required, "email"}, + {:required, "password"}, + {:type, "email", :email}, + {:type, "age", :integer}, + {:min_length, "password", 8} + ] + ) + IO.puts(" Result: #{inspect(result)}") # Invalid form IO.puts("\n Invalid user registration:") -{:ok, result} = Firebird.Phoenix.Validator.validate(validator, %{ - "name" => "A", - "email" => "not-email", - "age" => "abc" -}, [ - {:required, "name"}, - {:required, "email"}, - {:required, "password"}, - {:type, "email", :email}, - {:type, "age", :integer}, - {:min_length, "name", 2} -]) + +{:ok, result} = + Firebird.Phoenix.Validator.validate( + validator, + %{ + "name" => "A", + "email" => "not-email", + "age" => "abc" + }, + [ + {:required, "name"}, + {:required, "email"}, + {:required, "password"}, + {:type, "email", :email}, + {:type, "age", :integer}, + {:min_length, "name", 2} + ] + ) + IO.puts(" Result: #{inspect(result)}") Firebird.Phoenix.Validator.stop(validator) @@ -162,11 +210,13 @@ IO.puts("\n#{String.duplicate("─", 50)}") IO.puts("⚡ Performance Benchmark\n") iterations = 1000 -{total_us, _} = :timer.tc(fn -> - for _ <- 1..iterations do - RequestHandler.handle(handler, "GET", "/api/users/42") - end -end) + +{total_us, _} = + :timer.tc(fn -> + for _ <- 1..iterations do + RequestHandler.handle(handler, "GET", "/api/users/42") + end + end) avg = total_us / iterations ops_per_sec = iterations / (total_us / 1_000_000) @@ -192,4 +242,3 @@ This example demonstrated: ✅ Error handling (404 responses) ✅ Sub-millisecond request processing """) - diff --git a/examples/phoenix_wasm_demo.exs b/examples/phoenix_wasm_demo.exs index 0e7f509..07958a2 100644 --- a/examples/phoenix_wasm_demo.exs +++ b/examples/phoenix_wasm_demo.exs @@ -31,7 +31,8 @@ IO.puts("✅ All modules loaded!\n") IO.puts("1️⃣ REQUEST LIFECYCLE: Parse → Route → Dispatch") IO.puts(String.duplicate("-", 50)) -raw_request = "GET /api/users/42 HTTP/1.1\r\nHost: example.com\r\nAccept: application/json\r\nCookie: session=signed_abc.1234567890abcdef\r\n\r\n" +raw_request = + "GET /api/users/42 HTTP/1.1\r\nHost: example.com\r\nAccept: application/json\r\nCookie: session=signed_abc.1234567890abcdef\r\n\r\n" {:ok, parsed} = Firebird.Phoenix.Plug.parse_request(plug, raw_request) IO.puts(" Parsed: #{parsed.method} #{parsed.path}") @@ -48,12 +49,14 @@ routes = [ {:ok, match} = Firebird.Phoenix.Router.match(router, parsed.method, parsed.path, routes) IO.puts(" Matched: #{match.handler} (params: #{inspect(match.params)})") -{:ok, response} = Firebird.Phoenix.JSON.api_response(json, 200, %{ - "id" => match.params["id"], - "name" => "Alice Johnson", - "email" => "alice@example.com", - "active" => "true" -}) +{:ok, response} = + Firebird.Phoenix.JSON.api_response(json, 200, %{ + "id" => match.params["id"], + "name" => "Alice Johnson", + "email" => "alice@example.com", + "active" => "true" + }) + IO.puts(" Response: #{String.slice(response, 0..80)}...") IO.puts("") @@ -67,13 +70,17 @@ eex = "<%= @email %>
Phoenix WASM Demo
") -|> RequestHandler.template("User.index", 200, "application/json", - ~s([{"id":"1","name":"Alice"},{"id":"2","name":"Bob"}])) -|> RequestHandler.template("User.show", 200, "application/json", - ~s({"id":"{{id}}","name":"User {{id}}"})) -|> RequestHandler.template("User.create", 201, "application/json", - ~s({"status":"created"})) -|> RequestHandler.template("User.update", 200, "application/json", - ~s({"id":"{{id}}","status":"updated"})) -|> RequestHandler.template("User.delete", 204, "text/plain", "") -# Dynamic actions -|> RequestHandler.action("Health.check", fn _params -> - {200, "application/json", ~s({"status":"healthy","components":#{running}})} -end) + +handler = + handler + # Middleware + |> RequestHandler.use_middleware(Middleware.request_id()) + |> RequestHandler.use_middleware(Middleware.timer()) + # Routes + |> RequestHandler.route("GET", "/", "Page.index") + |> RequestHandler.route("GET", "/users", "User.index") + |> RequestHandler.route("GET", "/users/:id", "User.show") + |> RequestHandler.route("POST", "/users", "User.create") + |> RequestHandler.route("PUT", "/users/:id", "User.update") + |> RequestHandler.route("DELETE", "/users/:id", "User.delete") + |> RequestHandler.route("GET", "/api/health", "Health.check") + # Templates + |> RequestHandler.template( + "Page.index", + 200, + "text/html", + "Phoenix WASM Demo
" + ) + |> RequestHandler.template( + "User.index", + 200, + "application/json", + ~s([{"id":"1","name":"Alice"},{"id":"2","name":"Bob"}]) + ) + |> RequestHandler.template( + "User.show", + 200, + "application/json", + ~s({"id":"{{id}}","name":"User {{id}}"}) + ) + |> RequestHandler.template("User.create", 201, "application/json", ~s({"status":"created"})) + |> RequestHandler.template( + "User.update", + 200, + "application/json", + ~s({"id":"{{id}}","status":"updated"}) + ) + |> RequestHandler.template("User.delete", 204, "text/plain", "") + # Dynamic actions + |> RequestHandler.action("Health.check", fn _params -> + {200, "application/json", ~s({"status":"healthy","components":#{running}})} + end) IO.puts(" ✅ 7 routes configured\n") @@ -74,17 +91,20 @@ requests = [ {"PUT", "/users/1", []}, {"DELETE", "/users/1", []}, {"GET", "/api/health", []}, - {"GET", "/nonexistent", []}, + {"GET", "/nonexistent", []} ] for {method, path, opts} <- requests do {:ok, conn} = RequestHandler.handle(handler, method, path, opts) - status_emoji = cond do - conn.status < 300 -> "✅" - conn.status < 400 -> "↗️" - conn.status < 500 -> "⚠️" - true -> "❌" - end + + status_emoji = + cond do + conn.status < 300 -> "✅" + conn.status < 400 -> "↗️" + conn.status < 500 -> "⚠️" + true -> "❌" + end + body_preview = String.slice(conn.resp_body, 0..50) IO.puts(" #{status_emoji} #{method} #{path} → #{conn.status} #{body_preview}") end @@ -93,17 +113,19 @@ end IO.puts("\n4. Generating HTML forms...") -form = [ - Form.form_for("user", "/users", method: :post, csrf_token: csrf_token), - Form.label("user", "name", "Name"), - Form.text_input("user", "name", required: true, class: "form-control"), - Form.label("user", "email", "Email"), - Form.email_input("user", "email", placeholder: "user@example.com"), - Form.select("user", "role", ["admin", "user", "guest"], selected: "user"), - Form.checkbox("user", "active", checked: true), - Form.submit("Create User", class: "btn btn-primary"), - Form.end_form() -] |> Enum.join("\n") +form = + [ + Form.form_for("user", "/users", method: :post, csrf_token: csrf_token), + Form.label("user", "name", "Name"), + Form.text_input("user", "name", required: true, class: "form-control"), + Form.label("user", "email", "Email"), + Form.email_input("user", "email", placeholder: "user@example.com"), + Form.select("user", "role", ["admin", "user", "guest"], selected: "user"), + Form.checkbox("user", "active", checked: true), + Form.submit("Create User", class: "btn btn-primary"), + Form.end_form() + ] + |> Enum.join("\n") IO.puts(" ✅ Form generated (#{byte_size(form)} bytes)") IO.puts(" CSRF token: #{String.slice(csrf_token, 0..15)}...") @@ -111,6 +133,7 @@ IO.puts(" CSRF token: #{String.slice(csrf_token, 0..15)}...") # ─── 5. CSRF validation ─── IO.puts("\n5. CSRF token validation...") + assert_result = fn :valid -> "✅ valid" :invalid -> "❌ invalid" @@ -120,6 +143,7 @@ IO.puts(" Correct token: #{assert_result.(CSRF.validate(csrf_token, csrf_token IO.puts(" Wrong token: #{assert_result.(CSRF.validate("wrong", csrf_token))}") signed = CSRF.generate_signed_token(secret: "app-secret") + case CSRF.validate_signed(signed, secret: "app-secret") do {:ok, _} -> IO.puts(" Signed token: ✅ verified") _ -> IO.puts(" Signed token: ❌ failed") @@ -141,8 +165,11 @@ if WebSocket.upgrade_request?(ws_headers) do end # Channel message roundtrip -frame = WebSocket.encode_channel_message("room:lobby", "new_msg", - %{"body" => "Hello from WASM!"}, ref: "1") +frame = + WebSocket.encode_channel_message("room:lobby", "new_msg", %{"body" => "Hello from WASM!"}, + ref: "1" + ) + {:ok, {:text, json}, <<>>} = WebSocket.decode_frame(frame) {:ok, msg} = WebSocket.decode_channel_message(json) IO.puts(" ✅ Channel message: #{msg.topic} / #{msg.event} → #{inspect(msg.payload)}") @@ -151,39 +178,48 @@ IO.puts(" ✅ Channel message: #{msg.topic} / #{msg.event} → #{inspect(msg.p IO.puts("\n7. Rate limiting demo...") -{_, _} = Enum.reduce(1..5, {RateLimiter.new(rate: 3, period: :minute, burst: 3), 0}, fn i, {limiter, _} -> - case RateLimiter.check(limiter, "demo-client") do - {:ok, new_limiter, remaining} -> - IO.puts(" Request #{i}: ✅ allowed (#{remaining} remaining)") - {new_limiter, remaining} - {:error, :rate_limited, new_limiter, retry_after} -> - IO.puts(" Request #{i}: 🚫 rate limited (retry after #{retry_after}ms)") - {new_limiter, retry_after} - end -end) +{_, _} = + Enum.reduce(1..5, {RateLimiter.new(rate: 3, period: :minute, burst: 3), 0}, fn i, + {limiter, _} -> + case RateLimiter.check(limiter, "demo-client") do + {:ok, new_limiter, remaining} -> + IO.puts(" Request #{i}: ✅ allowed (#{remaining} remaining)") + {new_limiter, remaining} + + {:error, :rate_limited, new_limiter, retry_after} -> + IO.puts(" Request #{i}: 🚫 rate limited (retry after #{retry_after}ms)") + {new_limiter, retry_after} + end + end) # ─── 8. WASM component benchmarks ─── IO.puts("\n8. Quick performance check...") -{time, _} = :timer.tc(fn -> - for _ <- 1..1000 do - RequestHandler.handle(handler, "GET", "/users/42") - end -end) +{time, _} = + :timer.tc(fn -> + for _ <- 1..1000 do + RequestHandler.handle(handler, "GET", "/users/42") + end + end) + per_request = Float.round(time / 1000, 1) rps = Float.round(1_000_000 / per_request) IO.puts(" Full request lifecycle: #{per_request} µs/request (#{trunc(rps)} req/s)") if components[:router] do - {:ok, _} = Firebird.Phoenix.Router.compile(components[:router], [ - {"GET", "/users/:id", "User.show"} - ]) - {time, _} = :timer.tc(fn -> - for _ <- 1..1000 do - Firebird.Phoenix.Router.match_compiled(components[:router], "GET", "/users/42") - end - end) + {:ok, _} = + Firebird.Phoenix.Router.compile(components[:router], [ + {"GET", "/users/:id", "User.show"} + ]) + + {time, _} = + :timer.tc(fn -> + for _ <- 1..1000 do + Firebird.Phoenix.Router.match_compiled(components[:router], "GET", "/users/42") + end + end) + IO.puts(" Compiled route match: #{Float.round(time / 1000, 1)} µs/match") end diff --git a/examples/phoenix_wasm_server.exs b/examples/phoenix_wasm_server.exs index f4407a0..144c21a 100644 --- a/examples/phoenix_wasm_server.exs +++ b/examples/phoenix_wasm_server.exs @@ -27,65 +27,65 @@ routes = [ {"POST", "/api/users", "UserController.create"}, {"PUT", "/api/users/:id", "UserController.update"}, {"DELETE", "/api/users/:id", "UserController.delete"}, - {"GET", "/health", "HealthController.check"}, + {"GET", "/health", "HealthController.check"} ] # Define actions (handler => {status, content_type, body_template}) actions = %{ - "PageController.index" => {200, "text/html", """ - - -This page is served through WebAssembly-accelerated Phoenix components.
-- Request → gen_tcp → WASM Router → WASM Template → WASM Plug → Response -- - - """}, - - "PageController.about" => {200, "text/html", """ - -
All request processing happens in WebAssembly modules compiled from Rust.
- ← Back - - """}, - - "UserController.index" => {200, "application/json", - ~s([{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"},{"id":3,"name":"Charlie","email":"charlie@example.com"}])}, - - "UserController.show" => {200, "application/json", - ~s({"id":"{{id}}","name":"User {{id}}","email":"user{{id}}@example.com"})}, - - "UserController.create" => {201, "application/json", - ~s({"id":"4","name":"New User","status":"created"})}, - - "UserController.update" => {200, "application/json", - ~s({"id":"{{id}}","status":"updated"})}, - + "PageController.index" => + {200, "text/html", + """ + + +This page is served through WebAssembly-accelerated Phoenix components.
++ Request → gen_tcp → WASM Router → WASM Template → WASM Plug → Response ++ + + """}, + "PageController.about" => + {200, "text/html", + """ + +
All request processing happens in WebAssembly modules compiled from Rust.
+ ← Back + + """}, + "UserController.index" => + {200, "application/json", + ~s([{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"},{"id":3,"name":"Charlie","email":"charlie@example.com"}])}, + "UserController.show" => + {200, "application/json", + ~s({"id":"{{id}}","name":"User {{id}}","email":"user{{id}}@example.com"})}, + "UserController.create" => + {201, "application/json", ~s({"id":"4","name":"New User","status":"created"})}, + "UserController.update" => {200, "application/json", ~s({"id":"{{id}}","status":"updated"})}, "UserController.delete" => {204, "application/json", ""}, - - "HealthController.check" => {200, "application/json", - ~s({"status":"ok","engine":"wasm","components":["router","template","plug","endpoint"]})}, + "HealthController.check" => + {200, "application/json", + ~s({"status":"ok","engine":"wasm","components":["router","template","plug","endpoint"]})} } # Start the server -{:ok, server} = Firebird.Phoenix.Server.start_link( - port: 4001, - routes: routes, - actions: actions -) +{:ok, server} = + Firebird.Phoenix.Server.start_link( + port: 4001, + routes: routes, + actions: actions + ) IO.puts(""" ✅ Server started on http://localhost:4001 diff --git a/examples/pipeline_and_batch.exs b/examples/pipeline_and_batch.exs index d4e6830..2d62fe3 100644 --- a/examples/pipeline_and_batch.exs +++ b/examples/pipeline_and_batch.exs @@ -23,8 +23,10 @@ IO.puts("── 1.1 Basic pipeline ──") result = Pipeline.new(wasm) - |> Pipeline.call(:add, [5, 3]) # => 8 - |> Pipeline.call(:multiply, [:_, 2]) # => 8 * 2 = 16 + # => 8 + |> Pipeline.call(:add, [5, 3]) + # => 8 * 2 = 16 + |> Pipeline.call(:multiply, [:_, 2]) |> Pipeline.run!() IO.puts(" add(5, 3) |> multiply(_, 2) = #{inspect(result)}") @@ -36,8 +38,10 @@ IO.puts("\n── 1.2 Longer chain ──") result = Pipeline.new(wasm) - |> Pipeline.call(:add, [3, 7]) # => 10 - |> Pipeline.call(:fibonacci, [:_]) # => fib(10) = 55 + # => 10 + |> Pipeline.call(:add, [3, 7]) + # => fib(10) = 55 + |> Pipeline.call(:fibonacci, [:_]) |> Pipeline.run!() IO.puts(" add(3, 7) |> fibonacci(_) = #{inspect(result)}") @@ -49,9 +53,12 @@ IO.puts("\n── 1.3 Transform — Elixir logic between WASM calls ──") result = Pipeline.new(wasm) - |> Pipeline.call(:add, [10, 20]) # => [30] - |> Pipeline.transform(fn [x] -> [x, x] end) # => [30, 30] - |> Pipeline.call(:multiply, [:_, :_]) # => 30 * 30 = 900 + # => [30] + |> Pipeline.call(:add, [10, 20]) + # => [30, 30] + |> Pipeline.transform(fn [x] -> [x, x] end) + # => 30 * 30 = 900 + |> Pipeline.call(:multiply, [:_, :_]) |> Pipeline.run!() IO.puts(" add(10, 20) |> dup |> multiply(_, _) = #{inspect(result)}") @@ -95,16 +102,20 @@ IO.puts("\n── 1.6 Pattern — compute then validate ──") # Check if a computed result is prime result = Pipeline.new(wasm) - |> Pipeline.call(:add, [4, 3]) # => 7 - |> Pipeline.call(:is_prime, [:_]) # => 1 (true) + # => 7 + |> Pipeline.call(:add, [4, 3]) + # => 1 (true) + |> Pipeline.call(:is_prime, [:_]) |> Pipeline.run!() IO.puts(" add(4, 3) |> is_prime(_) = #{inspect(result)} (1 = prime)") result = Pipeline.new(wasm) - |> Pipeline.call(:add, [4, 4]) # => 8 - |> Pipeline.call(:is_prime, [:_]) # => 0 (false) + # => 8 + |> Pipeline.call(:add, [4, 4]) + # => 0 (false) + |> Pipeline.call(:is_prime, [:_]) |> Pipeline.run!() IO.puts(" add(4, 4) |> is_prime(_) = #{inspect(result)} (0 = not prime)") @@ -117,11 +128,13 @@ Firebird.stop(wasm) IO.puts("\n\n── Part 2: Batch Processing ──────────────────────────────────\n") -{:ok, _pool} = Firebird.Pool.start_link( - wasm: "fixtures/math.wasm", - size: 4, - name: :batch_pool -) +{:ok, _pool} = + Firebird.Pool.start_link( + wasm: "fixtures/math.wasm", + size: 4, + name: :batch_pool + ) + IO.puts("✓ Pool started with 4 instances\n") alias Firebird.Batch @@ -130,13 +143,14 @@ alias Firebird.Batch IO.puts("── 2.1 Batch.run — mixed function calls ──") -results = Batch.run(:batch_pool, [ - {:add, [10, 20]}, - {:multiply, [6, 7]}, - {:fibonacci, [10]}, - {:factorial, [5]}, - {:is_prime, [17]} -]) +results = + Batch.run(:batch_pool, [ + {:add, [10, 20]}, + {:multiply, [6, 7]}, + {:fibonacci, [10]}, + {:factorial, [5]}, + {:is_prime, [17]} + ]) for {result, i} <- Enum.with_index(results) do case result do @@ -162,6 +176,7 @@ IO.puts("\n── 2.3 Batch.pmap — safe parallel map ──") case Batch.pmap(:batch_pool, :add, [[1, 2], [3, 4], [5, 6]]) do {:ok, results} -> IO.puts(" ✓ All succeeded: #{inspect(Enum.map(results, fn [x] -> x end))}") + {:error, errors} -> IO.puts(" ✗ Errors at indices: #{inspect(errors)}") end @@ -184,11 +199,14 @@ IO.puts(" Throughput: #{stats.calls_per_second} calls/sec") IO.puts("\n── 2.5 Error resilience — errors don't stop other calls ──") -results = Batch.run(:batch_pool, [ - {:add, [1, 2]}, - {:nonexistent, [42]}, # this will fail - {:multiply, [3, 4]}, # this still runs -]) +results = + Batch.run(:batch_pool, [ + {:add, [1, 2]}, + # this will fail + {:nonexistent, [42]}, + # this still runs + {:multiply, [3, 4]} + ]) for {result, i} <- Enum.with_index(results) do case result do @@ -217,14 +235,17 @@ results = for n <- inputs do [result] = Pipeline.new(wasm) - |> Pipeline.call(:fibonacci, [n]) # compute fib(n) - |> Pipeline.call(:multiply, [:_, 2]) # double it + # compute fib(n) + |> Pipeline.call(:fibonacci, [n]) + # double it + |> Pipeline.call(:multiply, [:_, 2]) |> Pipeline.run!() {n, result} end IO.puts(" fib(n) * 2 for each input:") + for {n, result} <- results do IO.puts(" n=#{n} → fib(#{n})*2 = #{result}") end diff --git a/examples/pool_usage.exs b/examples/pool_usage.exs index 8b8166b..7d02155 100644 --- a/examples/pool_usage.exs +++ b/examples/pool_usage.exs @@ -4,11 +4,12 @@ IO.puts("🔥 Firebird.Pool Concurrent Usage\n") # Start a pool of 4 instances -{:ok, _pool} = Firebird.Pool.start_link( - wasm: "fixtures/math.wasm", - size: 4, - name: :math_pool -) +{:ok, _pool} = + Firebird.Pool.start_link( + wasm: "fixtures/math.wasm", + size: 4, + name: :math_pool + ) status = Firebird.Pool.status(:math_pool) IO.puts("✓ Pool started: #{status.size} instances, #{status.alive} alive") @@ -28,6 +29,7 @@ tasks = results = Task.await_many(tasks) IO.puts("✓ Concurrent fibonacci results:") + for {n, result} <- Enum.sort(results) do IO.puts(" fib(#{n}) = #{result}") end diff --git a/examples/profiling_and_metrics.exs b/examples/profiling_and_metrics.exs index 1950512..0ed469f 100644 --- a/examples/profiling_and_metrics.exs +++ b/examples/profiling_and_metrics.exs @@ -19,9 +19,10 @@ IO.puts("── 1. Quick Timing ──\n") wasm = Firebird.load!(wasm_path) -{time_us, {:ok, [result]}} = :timer.tc(fn -> - Firebird.call(wasm, :fibonacci, [30]) -end) +{time_us, {:ok, [result]}} = + :timer.tc(fn -> + Firebird.call(wasm, :fibonacci, [30]) + end) IO.puts(" fibonacci(30) = #{result}") IO.puts(" Time: #{time_us}μs (#{Float.round(time_us / 1000, 2)}ms)\n") @@ -34,10 +35,11 @@ Firebird.stop(wasm) IO.puts("── 2. Profiler (Detailed Stats) ──\n") -profile = Firebird.Profiler.profile(wasm_path, :add, [5, 3], - iterations: 500, - warmup: 50 -) +profile = + Firebird.Profiler.profile(wasm_path, :add, [5, 3], + iterations: 500, + warmup: 50 + ) IO.puts(" Function: #{profile.function}") IO.puts(" Iterations: #{profile.count}") @@ -58,7 +60,11 @@ IO.puts("── 3. Startup Profiling ──\n") startup = Firebird.Profiler.profile_startup(wasm_path, iterations: 20) IO.puts(" WASM load time:") -IO.puts(" Avg: #{Float.round(startup.avg_us, 1)}μs (#{Float.round(startup.avg_us / 1000, 2)}ms)") + +IO.puts( + " Avg: #{Float.round(startup.avg_us, 1)}μs (#{Float.round(startup.avg_us / 1000, 2)}ms)" +) + IO.puts(" Min: #{startup.min_us}μs") IO.puts(" Max: #{startup.max_us}μs\n") @@ -90,9 +96,7 @@ IO.puts("") IO.puts("── 5. Throughput ──\n") -throughput = Firebird.Profiler.throughput(wasm_path, :add, [5, 3], - duration_ms: 1000 -) +throughput = Firebird.Profiler.throughput(wasm_path, :add, [5, 3], duration_ms: 1000) IO.puts(" #{throughput.function}: #{Float.round(throughput.calls_per_second, 0)} calls/sec") IO.puts(" Total calls in #{throughput.duration_ms}ms: #{throughput.total_calls}") @@ -128,12 +132,14 @@ metrics = Firebird.Metrics.report() IO.puts(" Collected metrics for #{map_size(metrics)} functions:\n") for {func, stats} <- Enum.sort_by(metrics, fn {_, s} -> s.calls end, :desc) do - IO.puts(" #{String.pad_trailing(func, 15)} " <> - "calls=#{String.pad_leading("#{stats.calls}", 4)} " <> - "avg=#{String.pad_leading("#{Float.round(stats.avg_us, 1)}", 7)}μs " <> - "min=#{String.pad_leading("#{stats.min_us}", 5)}μs " <> - "max=#{String.pad_leading("#{stats.max_us}", 5)}μs " <> - "errors=#{stats.errors}") + IO.puts( + " #{String.pad_trailing(func, 15)} " <> + "calls=#{String.pad_leading("#{stats.calls}", 4)} " <> + "avg=#{String.pad_leading("#{Float.round(stats.avg_us, 1)}", 7)}μs " <> + "min=#{String.pad_leading("#{stats.min_us}", 5)}μs " <> + "max=#{String.pad_leading("#{stats.max_us}", 5)}μs " <> + "errors=#{stats.errors}" + ) end IO.puts("") @@ -162,16 +168,18 @@ n = 25 iterations = 500 # Benchmark WASM -wasm_times = for _ <- 1..iterations do - {t, _} = :timer.tc(fn -> Firebird.call_one!(wasm, :fibonacci, [n]) end) - t -end +wasm_times = + for _ <- 1..iterations do + {t, _} = :timer.tc(fn -> Firebird.call_one!(wasm, :fibonacci, [n]) end) + t + end # Benchmark Elixir -elixir_times = for _ <- 1..iterations do - {t, _} = :timer.tc(fn -> elixir_fib.(n) end) - t -end +elixir_times = + for _ <- 1..iterations do + {t, _} = :timer.tc(fn -> elixir_fib.(n) end) + t + end wasm_avg = Enum.sum(wasm_times) / iterations elixir_avg = Enum.sum(elixir_times) / iterations diff --git a/examples/quick_start.exs b/examples/quick_start.exs index 2de4e3c..f332687 100644 --- a/examples/quick_start.exs +++ b/examples/quick_start.exs @@ -50,11 +50,13 @@ IO.puts(" Stopped.") # ============================================================ IO.puts("\n── 4. with_instance (auto-cleanup) ──") -{:ok, result} = Firebird.with_instance("math.wasm", fn wasm -> - {:ok, [a]} = Firebird.call(wasm, :add, [10, 20]) - {:ok, [b]} = Firebird.call(wasm, :multiply, [a, 3]) - b -end) +{:ok, result} = + Firebird.with_instance("math.wasm", fn wasm -> + {:ok, [a]} = Firebird.call(wasm, :add, [10, 20]) + {:ok, [b]} = Firebird.call(wasm, :multiply, [a, 3]) + b + end) + IO.puts(" (10 + 20) * 3 = #{result}") # ============================================================ @@ -72,13 +74,19 @@ Firebird.stop(wasm) # ============================================================ IO.puts("\n── 6. Inline WAT ──") -result = Firebird.quick(""" -(module - (func (export "triple") (param i32) (result i32) - local.get 0 - i32.const 3 - i32.mul)) -""", :triple, [14]) +result = + Firebird.quick( + """ + (module + (func (export "triple") (param i32) (result i32) + local.get 0 + i32.const 3 + i32.mul)) + """, + :triple, + [14] + ) + IO.puts(" triple(14) = #{result}") # ============================================================ @@ -88,6 +96,7 @@ IO.puts("\n── 7. Explore a WASM file ──") fns = Firebird.list_functions("math.wasm") IO.puts(" Functions in math.wasm:") + for f <- fns do params = f.params |> Enum.map(&to_string/1) |> Enum.join(", ") results = f.results |> Enum.map(&to_string/1) |> Enum.join(", ") diff --git a/examples/stream_processing.exs b/examples/stream_processing.exs index bd8385c..a1e4373 100644 --- a/examples/stream_processing.exs +++ b/examples/stream_processing.exs @@ -138,17 +138,21 @@ readings = [10, 25, 42, 7, 99, 3, 55] processed = readings - |> Firebird.Stream.map(wasm, :fibonacci) # transform each reading - |> Stream.zip(readings) # pair with original + # transform each reading + |> Firebird.Stream.map(wasm, :fibonacci) + # pair with original + |> Stream.zip(readings) |> Stream.map(fn {transformed, original} -> %{input: original, output: transformed} end) |> Enum.to_list() IO.puts(" Processed #{length(processed)} readings:") + for %{input: i, output: o} <- Enum.take(processed, 3) do IO.puts(" input=#{i} → output=#{o}") end + IO.puts(" ...") # Clean up diff --git a/examples/string_processing.exs b/examples/string_processing.exs index d849734..7785d43 100644 --- a/examples/string_processing.exs +++ b/examples/string_processing.exs @@ -41,6 +41,7 @@ IO.puts(" Memory freed\n") IO.puts("3. Multiple strings:") strings = ["hello", "world", "firebird", "elixir", "wasm"] + for s <- strings do {:ok, upper} = Firebird.WasmString.call_with_string(wasm, :to_uppercase, s) IO.puts(" \"#{s}\" → \"#{upper}\"") diff --git a/examples/supervision_tree.exs b/examples/supervision_tree.exs index 84c0c51..c249fbc 100644 --- a/examples/supervision_tree.exs +++ b/examples/supervision_tree.exs @@ -59,12 +59,14 @@ IO.puts(" Pool status: #{inspect(status)}\n") # ── 5. Concurrent pool usage ────────────────────────────────────────────── IO.puts("Concurrent pool calls:") -tasks = for i <- 1..10 do - Task.async(fn -> - [result] = Firebird.Pool.call!(:math_pool, :fibonacci, [20]) - result - end) -end + +tasks = + for i <- 1..10 do + Task.async(fn -> + [result] = Firebird.Pool.call!(:math_pool, :fibonacci, [20]) + result + end) + end results = Task.await_many(tasks) IO.puts(" 10 concurrent fibonacci(20) calls all returned: #{hd(results)}\n") diff --git a/examples/testing_wasm.exs b/examples/testing_wasm.exs index e133c9d..b7cdc27 100644 --- a/examples/testing_wasm.exs +++ b/examples/testing_wasm.exs @@ -97,6 +97,7 @@ add_table = [ for {args, expected} <- add_table do {:ok, ^expected} = Firebird.call_one(wasm, :add, args) end + IO.puts("✓ assert_wasm_table wasm, :add, #{inspect(add_table)}") fib_table = [ @@ -109,6 +110,7 @@ fib_table = [ for {args, expected} <- fib_table do {:ok, ^expected} = Firebird.call_one(wasm, :fibonacci, args) end + IO.puts("✓ assert_wasm_table wasm, :fibonacci, #{inspect(fib_table)}") # ============================================================ @@ -120,9 +122,11 @@ IO.puts("✓ assert_wasm_table wasm, :fibonacci, #{inspect(fib_table)}") IO.puts("\n─── Pattern 5: Export assertions ───") exports = Firebird.exports(wasm) + for func <- [:add, :multiply, :fibonacci] do true = func in exports end + IO.puts("✓ assert_wasm_exports wasm, [:add, :multiply, :fibonacci]") # ============================================================ @@ -177,9 +181,11 @@ IO.puts("✓ assert_wasm_error wasm, :nonexistent_function, [1, 2]") IO.puts("\n─── Pattern 9: Negative export assertions ───") exports = Firebird.exports(wasm) + for func <- [:nonexistent, :missing] do false = func in exports end + IO.puts("✓ refute_wasm_exports wasm, [:nonexistent, :missing]") # ============================================================ diff --git a/examples/thirty_seconds.exs b/examples/thirty_seconds.exs index a76b49e..42bb559 100644 --- a/examples/thirty_seconds.exs +++ b/examples/thirty_seconds.exs @@ -11,18 +11,19 @@ IO.puts("✅ Setup verified\n") IO.puts("5 + 3 = 8 ✅") # Step 3: Write WASM inline with WAT -wasm = Firebird.from_wat!(""" -(module - (func (export "square") (param i32) (result i32) - local.get 0 - local.get 0 - i32.mul) - (func (export "double") (param i32) (result i32) - local.get 0 - i32.const 2 - i32.mul) -) -""") +wasm = + Firebird.from_wat!(""" + (module + (func (export "square") (param i32) (result i32) + local.get 0 + local.get 0 + i32.mul) + (func (export "double") (param i32) (result i32) + local.get 0 + i32.const 2 + i32.mul) + ) + """) [25] = Firebird.call!(wasm, :square, [5]) IO.puts("5² = 25 ✅") diff --git a/lib/firebird.ex b/lib/firebird.ex index 4f31b6d..75f8b16 100644 --- a/lib/firebird.ex +++ b/lib/firebird.ex @@ -160,12 +160,14 @@ defmodule Firebird do {:ok, [8]} = Firebird.call(instance, :add, [5, 3]) {:ok, [55]} = Firebird.call(instance, "fibonacci", [10]) """ - @spec call(pid(), atom() | String.t(), list()) :: {:ok, list()} | {:error, term()} - def call(instance, function, args) when is_list(args) do - Firebird.Runtime.call(instance, function, args) + @spec call(pid(), atom() | String.t(), list(), keyword()) :: {:ok, list()} | {:error, term()} + def call(instance, function, args, opts \\ []) + + def call(instance, function, args, opts) when is_list(args) do + Firebird.Runtime.call(instance, function, args, opts) end - def call(_instance, _function, args) when not is_list(args) do + def call(_instance, _function, args, _opts) when not is_list(args) do raise ArgumentError, """ Firebird.call/3 expects args as a list, got: #{inspect(args)} @@ -269,6 +271,7 @@ defmodule Firebird do case function_type(instance, func) do {:ok, {params, results}} -> %{name: func, params: params, results: results, arity: length(params)} + _ -> %{name: func, params: [], results: [], arity: 0} end @@ -282,7 +285,8 @@ defmodule Firebird do {:ok, {[:i32, :i32], [:i32]}} = Firebird.function_type(instance, :add) """ - @spec function_type(pid(), atom() | String.t()) :: {:ok, {list(), list()}} | {:error, :not_found} + @spec function_type(pid(), atom() | String.t()) :: + {:ok, {list(), list()}} | {:error, :not_found} def function_type(instance, function) do Firebird.Runtime.function_type(instance, function) end @@ -294,7 +298,8 @@ defmodule Firebird do {:ok, bytes} = Firebird.read_memory(instance, 0, 10) """ - @spec read_memory(pid(), non_neg_integer(), non_neg_integer()) :: {:ok, binary()} | {:error, term()} + @spec read_memory(pid(), non_neg_integer(), non_neg_integer()) :: + {:ok, binary()} | {:error, term()} def read_memory(instance, offset, length) do Firebird.Runtime.read_memory(instance, offset, length) end @@ -347,6 +352,7 @@ defmodule Firebird do # SyncNif references are garbage collected, no explicit stop needed :ok end + def stop(instance) do Firebird.Runtime.stop(instance) end @@ -391,10 +397,11 @@ defmodule Firebird do %{ alive: true, exports: exports(instance), - memory_bytes: case memory_size(instance) do - {:ok, size} -> size - _ -> nil - end + memory_bytes: + case memory_size(instance) do + {:ok, size} -> size + _ -> nil + end } else %{alive: false, exports: [], memory_bytes: nil} @@ -420,12 +427,13 @@ defmodule Firebird do {:error, {:file_error, :enoent, path}} -> searched = Firebird.Runtime.search_paths(path) - searched_str = searched |> Enum.map(&(" • #{&1}")) |> Enum.join("\n") + searched_str = searched |> Enum.map(&" • #{&1}") |> Enum.join("\n") + raise """ WASM file not found: #{path} Searched locations: -#{searched_str} + #{searched_str} To initialize WASM in your project: mix firebird.init To use the bundled sample: Firebird.sample_wasm_path() @@ -463,7 +471,7 @@ defmodule Firebird do {:ok, [55]} """ @spec run(binary() | String.t(), atom() | String.t(), list(), keyword()) :: - {:ok, list()} | {:error, term()} + {:ok, list()} | {:error, term()} def run(wasm_source, function, args, opts \\ []) do with {:ok, pid} <- load(wasm_source, opts) do try do @@ -506,7 +514,7 @@ defmodule Firebird do {:ok, 16} """ @spec with_instance(binary() | String.t(), (pid() -> term()), keyword()) :: - {:ok, term()} | {:error, term()} + {:ok, term()} | {:error, term()} def with_instance(wasm_source, fun, opts \\ []) when is_function(fun, 1) do case load(wasm_source, opts) do {:ok, pid} -> @@ -515,6 +523,7 @@ defmodule Firebird do after stop(pid) end + {:error, _} = error -> error end @@ -642,7 +651,8 @@ defmodule Firebird do {:ok, [8]} = Firebird.pool_call(pool, :add, [5, 3]) """ - @spec pool_call(GenServer.server(), atom() | String.t(), list()) :: {:ok, list()} | {:error, term()} + @spec pool_call(GenServer.server(), atom() | String.t(), list()) :: + {:ok, list()} | {:error, term()} def pool_call(pool, function, args) do Firebird.Pool.call(pool, function, args) end @@ -666,7 +676,8 @@ defmodule Firebird do {:ok, 8} = Firebird.pool_call_one(pool, :add, [5, 3]) """ - @spec pool_call_one(GenServer.server(), atom() | String.t(), list()) :: {:ok, term()} | {:error, term()} + @spec pool_call_one(GenServer.server(), atom() | String.t(), list()) :: + {:ok, term()} | {:error, term()} def pool_call_one(pool, function, args) do Firebird.Pool.call_one(pool, function, args) end @@ -739,9 +750,12 @@ defmodule Firebird do @spec from_wat!(String.t(), keyword()) :: pid() def from_wat!(wat_source, opts \\ []) do case from_wat(wat_source, opts) do - {:ok, pid} -> pid + {:ok, pid} -> + pid + {:error, {:wat_compile_error, msg}} -> raise "WAT compilation failed:\n#{msg}" + {:error, {:wat2wasm_not_found, _}} -> raise """ wat2wasm not found. Install it: @@ -749,6 +763,7 @@ defmodule Firebird do • Ubuntu: apt install wabt • Or download from: https://github.com/WebAssembly/wabt/releases """ + {:error, reason} -> raise "Failed to load WAT module: #{inspect(reason)}" end @@ -765,18 +780,22 @@ defmodule Firebird do def compile_wat(wat_source) do case System.find_executable("wat2wasm") do nil -> - {:error, {:wat2wasm_not_found, + {:error, + {:wat2wasm_not_found, "Install wabt: brew install wabt (macOS) or apt install wabt (Ubuntu)"}} + wat2wasm -> tmp_wat = Path.join(System.tmp_dir!(), "firebird_#{:erlang.phash2(wat_source)}.wat") tmp_wasm = Path.rootname(tmp_wat) <> ".wasm" try do File.write!(tmp_wat, wat_source) + case System.cmd(wat2wasm, [tmp_wat, "-o", tmp_wasm], stderr_to_stdout: true) do {_, 0} -> wasm_bytes = File.read!(tmp_wasm) {:ok, wasm_bytes} + {error_output, _} -> {:error, {:wat_compile_error, error_output}} end @@ -807,28 +826,39 @@ defmodule Firebird do IO.puts("🔥 WASM Instance (#{if info.alive, do: "alive", else: "stopped"})") IO.puts(" Memory: #{format_bytes(info.memory_bytes || 0)}") IO.puts(" Exports (#{length(info.exports)}):") + for func <- info.exports do case function_type(instance, func) do {:ok, {params, results}} -> - IO.puts(" #{func}(#{Enum.join(Enum.map(params, &inspect/1), ", ")}) → #{inspect(results)}") + IO.puts( + " #{func}(#{Enum.join(Enum.map(params, &inspect/1), ", ")}) → #{inspect(results)}" + ) + _ -> IO.puts(" #{func}") end end + :ok end def describe(path) when is_binary(path) do # Use smart path resolution for bare filenames resolved = Firebird.Runtime.resolve_wasm_path(path) + case inspect_file(resolved) do {:ok, info} -> IO.puts("🔥 #{Path.basename(resolved)} (#{format_bytes(info.size_bytes)})") IO.puts(" Functions (#{length(info.functions)}):") + for f <- info.functions do - IO.puts(" #{f.name}(#{Enum.join(Enum.map(f.params, &inspect/1), ", ")}) → #{inspect(f.results)}") + IO.puts( + " #{f.name}(#{Enum.join(Enum.map(f.params, &inspect/1), ", ")}) → #{inspect(f.results)}" + ) end + :ok + {:error, reason} -> IO.puts("❌ Cannot inspect #{path}: #{inspect(reason)}") :ok @@ -902,6 +932,7 @@ defmodule Firebird do {:ok, pid} -> stop(pid) :ok + {:error, _} = error -> error end @@ -1099,27 +1130,32 @@ defmodule Firebird do result {:error, {:call_error, func_name, _reason}} -> - suggestion = if Process.alive?(instance) do - available = exports(instance) |> Enum.map(&to_string/1) - func_str = to_string(func_name) - - if func_str not in available do - closest = available - |> Enum.map(fn a -> {a, String.jaro_distance(func_str, a)} end) - |> Enum.sort_by(fn {_, d} -> d end, :desc) - |> Enum.take(1) - |> Enum.filter(fn {_, d} -> d > 0.6 end) - - case closest do - [{match, _}] -> "\n\nDid you mean: #{match}?\nAvailable: #{Enum.join(available, ", ")}" - _ -> "\nAvailable functions: #{Enum.join(available, ", ")}" + suggestion = + if Process.alive?(instance) do + available = exports(instance) |> Enum.map(&to_string/1) + func_str = to_string(func_name) + + if func_str not in available do + closest = + available + |> Enum.map(fn a -> {a, String.jaro_distance(func_str, a)} end) + |> Enum.sort_by(fn {_, d} -> d end, :desc) + |> Enum.take(1) + |> Enum.filter(fn {_, d} -> d > 0.6 end) + + case closest do + [{match, _}] -> + "\n\nDid you mean: #{match}?\nAvailable: #{Enum.join(available, ", ")}" + + _ -> + "\nAvailable functions: #{Enum.join(available, ", ")}" + end + else + "\n\nThe function exists but the call failed. Check argument types/count." end else - "\n\nThe function exists but the call failed. Check argument types/count." + "" end - else - "" - end raise "WASM function '#{func_name}' not found.#{suggestion}" @@ -1142,7 +1178,7 @@ defmodule Firebird do {:ok, 55} """ @spec run_one(binary() | String.t(), atom() | String.t(), list(), keyword()) :: - {:ok, term()} | {:error, term()} + {:ok, term()} | {:error, term()} def run_one(wasm_source, function, args, opts \\ []) do case run(wasm_source, function, args, opts) do {:ok, [single]} -> {:ok, single} @@ -1259,7 +1295,8 @@ defmodule Firebird do {:ok, inst1} = Firebird.instantiate(compiled) {:ok, inst2} = Firebird.instantiate(compiled) """ - @spec precompile(binary() | String.t(), keyword()) :: {:ok, Firebird.Preloader.compiled()} | {:error, term()} + @spec precompile(binary() | String.t(), keyword()) :: + {:ok, Firebird.Preloader.compiled()} | {:error, term()} defdelegate precompile(wasm_source, opts \\ []), to: Firebird.Preloader, as: :compile @doc """ @@ -1358,8 +1395,10 @@ defmodule Firebird do def quick(source, function, args) when is_binary(source) do # Check if it looks like WAT (starts with parenthesis) trimmed = String.trim(source) + if String.starts_with?(trimmed, "(") do {:ok, instance} = from_wat(source) + try do call_one!(instance, function, args) after @@ -1427,6 +1466,7 @@ defmodule Firebird do # Check tools tools = Firebird.Builder.check_tools() IO.puts(" 🔧 Tools:") + for {name, version} <- tools do status = if version, do: "✅ #{version}", else: "❌ not found" IO.puts(" #{name}: #{status}") @@ -1434,9 +1474,11 @@ defmodule Firebird do # Check WASM directories IO.puts(" 📁 WASM files:") + for dir <- ["priv/wasm", "wasm", "fixtures"] do if File.dir?(dir) do wasm_files = Path.wildcard(Path.join(dir, "*.wasm")) + if length(wasm_files) > 0 do IO.puts(" #{dir}/: #{length(wasm_files)} file(s)") end diff --git a/lib/firebird/batch.ex b/lib/firebird/batch.ex index 0f62cb9..950cef2 100644 --- a/lib/firebird/batch.ex +++ b/lib/firebird/batch.ex @@ -40,7 +40,9 @@ defmodule Firebird.Batch do - `:timeout` - Maximum time for entire batch (default: 30s) - `:max_concurrency` - Max concurrent tasks (default: pool_size * 2) """ - @spec run(GenServer.server(), [{atom() | String.t(), list()}], keyword()) :: [{:ok, list()} | {:error, term()}] + @spec run(GenServer.server(), [{atom() | String.t(), list()}], keyword()) :: [ + {:ok, list()} | {:error, term()} + ] def run(pool, calls, opts \\ []) do timeout = Keyword.get(opts, :timeout, @default_timeout) max_concurrency = Keyword.get(opts, :max_concurrency, System.schedulers_online() * 2) @@ -88,7 +90,7 @@ defmodule Firebird.Batch do {:ok, results} = Firebird.Batch.pmap(pool, :fibonacci, Enum.map(1..10, &[&1])) """ @spec pmap(GenServer.server(), atom() | String.t(), [list()], keyword()) :: - {:ok, [list()]} | {:error, [{integer(), term()}]} + {:ok, [list()]} | {:error, [{integer(), term()}]} def pmap(pool, function, args_list, opts \\ []) do calls = Enum.map(args_list, fn args -> {function, args} end) results = run(pool, calls, opts) @@ -112,7 +114,7 @@ defmodule Firebird.Batch do Returns `{results, stats}` where stats includes timing info. """ @spec run_with_stats(GenServer.server(), [{atom() | String.t(), list()}], keyword()) :: - {[{:ok, list()} | {:error, term()}], map()} + {[{:ok, list()} | {:error, term()}], map()} def run_with_stats(pool, calls, opts \\ []) do start = System.monotonic_time(:microsecond) results = run(pool, calls, opts) @@ -127,7 +129,8 @@ defmodule Firebird.Batch do errors: errors, elapsed_us: elapsed, avg_us: if(length(results) > 0, do: Float.round(elapsed / length(results), 1), else: 0.0), - calls_per_second: if(elapsed > 0, do: Float.round(length(results) * 1_000_000 / elapsed, 1), else: 0.0) + calls_per_second: + if(elapsed > 0, do: Float.round(length(results) * 1_000_000 / elapsed, 1), else: 0.0) } {results, stats} diff --git a/lib/firebird/benchmark.ex b/lib/firebird/benchmark.ex index bc1bd12..600bc42 100644 --- a/lib/firebird/benchmark.ex +++ b/lib/firebird/benchmark.ex @@ -129,8 +129,7 @@ defmodule Firebird.Benchmark do wasm_stats = compute_stats(wasm_times, function, iterations) beam_stats = compute_stats(beam_times, function, iterations) - speedup = - if wasm_stats.avg_us > 0, do: beam_stats.avg_us / wasm_stats.avg_us, else: 0.0 + speedup = if wasm_stats.avg_us > 0, do: beam_stats.avg_us / wasm_stats.avg_us, else: 0.0 %{ function: function, @@ -206,7 +205,7 @@ defmodule Firebird.Benchmark do """ @spec percentile([number()], number()) :: number() def percentile(sorted, p) when is_list(sorted) and length(sorted) > 0 do - k = (p / 100) * (length(sorted) - 1) + k = p / 100 * (length(sorted) - 1) f = trunc(k) c = Float.ceil(k) |> trunc() @@ -254,7 +253,9 @@ defmodule Firebird.Benchmark do rows = Enum.map(results, fn r -> - name = (r[:name] || to_string(r.function)) |> String.pad_trailing(37) |> String.slice(0, 37) + name = + (r[:name] || to_string(r.function)) |> String.pad_trailing(37) |> String.slice(0, 37) + wasm = r.wasm.avg_us |> Float.round(1) |> to_string() |> String.pad_leading(9) beam = r.beam.avg_us |> Float.round(1) |> to_string() |> String.pad_leading(9) @@ -289,7 +290,8 @@ defmodule Firebird.Benchmark do """ @spec format_csv([map()]) :: String.t() def format_csv(results) do - header = "name,function,wasm_avg_us,beam_avg_us,speedup,wasm_p50,wasm_p95,wasm_p99,beam_p50,beam_p95,beam_p99" + header = + "name,function,wasm_avg_us,beam_avg_us,speedup,wasm_p50,wasm_p95,wasm_p99,beam_p50,beam_p95,beam_p99" rows = Enum.map(results, fn r -> @@ -325,7 +327,11 @@ defmodule Firebird.Benchmark do rows = Enum.map(results, fn r -> name = r[:name] || to_string(r.function) - speedup = if r.speedup >= 1.0, do: "#{Float.round(r.speedup, 2)}x", else: "#{Float.round(1.0 / max(r.speedup, 0.001), 2)}x slower" + + speedup = + if r.speedup >= 1.0, + do: "#{Float.round(r.speedup, 2)}x", + else: "#{Float.round(1.0 / max(r.speedup, 0.001), 2)}x slower" "| #{name} | #{Float.round(r.wasm.avg_us, 1)} | #{Float.round(r.beam.avg_us, 1)} | #{speedup} | #{Float.round(r.wasm.p95_us, 1)} | #{Float.round(r.beam.p95_us, 1)} |" end) @@ -333,4 +339,3 @@ defmodule Firebird.Benchmark do Enum.join([header, separator | rows], "\n") end end - diff --git a/lib/firebird/builder.ex b/lib/firebird/builder.ex index 7f55420..4fa88cc 100644 --- a/lib/firebird/builder.ex +++ b/lib/firebird/builder.ex @@ -50,6 +50,7 @@ defmodule Firebird.Builder do true -> # Check for standalone .go files go_files = Path.wildcard(Path.join(source_dir, "*.go")) + if length(go_files) > 0 do build_go(source_dir, opts) else @@ -102,10 +103,14 @@ defmodule Firebird.Builder do args = [ "build", - "-o", output, - "-target", target, - "-scheduler", scheduler, - "-gc", gc + "-o", + output, + "-target", + target, + "-scheduler", + scheduler, + "-gc", + gc ] args = if debug, do: args, else: args ++ ["-no-debug"] @@ -123,12 +128,14 @@ defmodule Firebird.Builder do defp find_go_main(source_dir) do # Look for main.go first, then any .go file main = Path.join(source_dir, "main.go") + if File.exists?(main) do "main.go" else case Path.wildcard(Path.join(source_dir, "*.go")) do [first | _] -> Path.basename(first) - [] -> "main.go" # Let tinygo give the error + # Let tinygo give the error + [] -> "main.go" end end end @@ -174,17 +181,17 @@ defmodule Firebird.Builder do {_output, 0} -> # Find the output wasm file profile = if release, do: "release", else: "debug" - wasm_files = Path.wildcard( - Path.join([source_dir, "target", target, profile, "*.wasm"]) - ) + wasm_files = Path.wildcard(Path.join([source_dir, "target", target, profile, "*.wasm"])) case wasm_files do [wasm_file | _] -> output = Keyword.get(opts, :output, wasm_file) + if output != wasm_file do File.mkdir_p!(Path.dirname(output)) File.cp!(wasm_file, output) end + {:ok, Path.expand(output)} [] -> @@ -215,14 +222,16 @@ defmodule Firebird.Builder do {load_opts, build_opts} = Keyword.split(opts, [:wasi, :memory_limit, :timeout]) # Auto-detect WASI for Go projects - is_go = File.exists?(Path.join(Path.expand(source_dir), "go.mod")) or - length(Path.wildcard(Path.join(Path.expand(source_dir), "*.go"))) > 0 - - load_opts = if is_go and not Keyword.has_key?(load_opts, :wasi) do - Keyword.put(load_opts, :wasi, true) - else - load_opts - end + is_go = + File.exists?(Path.join(Path.expand(source_dir), "go.mod")) or + length(Path.wildcard(Path.join(Path.expand(source_dir), "*.go"))) > 0 + + load_opts = + if is_go and not Keyword.has_key?(load_opts, :wasi) do + Keyword.put(load_opts, :wasi, true) + else + load_opts + end with {:ok, wasm_path} <- build(source_dir, build_opts), {:ok, instance} <- Firebird.load(wasm_path, load_opts) do @@ -245,7 +254,9 @@ defmodule Firebird.Builder do {:error, {:not_found, "WAT file not found: #{wat_file}"}} else case System.find_executable("wat2wasm") do - nil -> {:error, {:tool_missing, "wat2wasm not found. Install wabt."}} + nil -> + {:error, {:tool_missing, "wat2wasm not found. Install wabt."}} + _wat2wasm -> basename = Path.basename(wat_file, ".wat") default_output = Path.join(Path.dirname(wat_file), "#{basename}.wasm") @@ -281,7 +292,9 @@ defmodule Firebird.Builder do defp get_tool_version(tool, args) do case System.find_executable(tool) do - nil -> nil + nil -> + nil + _path -> case System.cmd(tool, args, stderr_to_stdout: true) do {output, 0} -> output |> String.trim() |> String.split("\n") |> hd() @@ -313,14 +326,15 @@ defmodule Firebird.Builder do """) # main.go - func_code = Enum.map_join(functions, "\n\n", fn func -> - """ - //export #{func} - func #{func}(a, b int32) int32 { - \treturn a + b // TODO: implement - } - """ - end) + func_code = + Enum.map_join(functions, "\n\n", fn func -> + """ + //export #{func} + func #{func}(a, b int32) int32 { + \treturn a + b // TODO: implement + } + """ + end) File.write!(Path.join(dir, "main.go"), """ package main @@ -388,14 +402,15 @@ defmodule Firebird.Builder do """) # src/lib.rs - func_code = Enum.map_join(functions, "\n\n", fn func -> - """ - #[unsafe(no_mangle)] - pub extern "C" fn #{func}(a: i32, b: i32) -> i32 { - a + b // TODO: implement - } - """ - end) + func_code = + Enum.map_join(functions, "\n\n", fn func -> + """ + #[unsafe(no_mangle)] + pub extern "C" fn #{func}(a: i32, b: i32) -> i32 { + a + b // TODO: implement + } + """ + end) File.write!(Path.join(dir, "src/lib.rs"), """ //! #{module_name} - WASM functions for Firebird diff --git a/lib/firebird/cache.ex b/lib/firebird/cache.ex index 320a823..9cc99cf 100644 --- a/lib/firebird/cache.ex +++ b/lib/firebird/cache.ex @@ -141,6 +141,7 @@ defmodule Firebird.Cache do # Also persist to disk disk_cache_file = Path.join(cache_dir, key) + unless cache_valid?(wasm_path, disk_cache_file) do persist_to_disk_async(wasm_path, disk_cache_file) end @@ -198,6 +199,7 @@ defmodule Firebird.Cache do if File.dir?(cache_dir) do File.rm_rf!(cache_dir) end + :ok end @@ -226,17 +228,19 @@ defmodule Firebird.Cache do case read_meta(meta_path) do {:ok, meta} -> - cache_size = case File.stat(cache_path) do - {:ok, %{size: size}} -> size - _ -> 0 - end + cache_size = + case File.stat(cache_path) do + {:ok, %{size: size}} -> size + _ -> 0 + end Map.merge(meta, %{ cache_file: cache_path, cache_size: cache_size }) - _ -> nil + _ -> + nil end end) |> Enum.reject(&is_nil/1) @@ -371,17 +375,20 @@ defmodule Firebird.Cache do File.write!(cache_file, bytes) {:ok, %{mtime: mtime}} = File.stat(expanded, time: :posix) + write_meta(cache_file <> ".meta", %{ source: wasm_path, source_mtime: mtime, cached_at: DateTime.utc_now() |> DateTime.to_iso8601(), source_size: byte_size(bytes) }) + _ -> :ok end rescue - _ -> :ok # Don't fail on cache errors + # Don't fail on cache errors + _ -> :ok end end) end @@ -397,7 +404,9 @@ defmodule Firebird.Cache do {:ok, meta} -> {:ok, meta} error -> error end - error -> error + + error -> + error end end end diff --git a/lib/firebird/compiler.ex b/lib/firebird/compiler.ex index 9d1305b..ac7a50a 100644 --- a/lib/firebird/compiler.ex +++ b/lib/firebird/compiler.ex @@ -50,7 +50,19 @@ defmodule Firebird.Compiler do {:ok, wasm_binary} = Firebird.Compiler.to_wasm(wat) """ - alias Firebird.Compiler.{IRGen, WATGen, TypeInference, Validator, Optimizer, TCO, Inliner, CSE, ConstantPropagation, IfChainToCase, LICM} + alias Firebird.Compiler.{ + IRGen, + WATGen, + TypeInference, + Validator, + Optimizer, + TCO, + Inliner, + CSE, + ConstantPropagation, + IfChainToCase, + LICM + } @type compile_opts :: [ output_dir: String.t(), @@ -228,9 +240,12 @@ defmodule Firebird.Compiler do {:docs_v1, _, _, _, _, _, _} -> # Try to find the source file from module info case module.module_info(:compile)[:source] do - nil -> {:error, {:no_source, module}} + nil -> + {:error, {:no_source, module}} + source_charlist -> source_path = List.to_string(source_charlist) + case File.read(source_path) do {:ok, source} -> {:ok, source} {:error, _} -> {:error, {:source_not_found, source_path}} diff --git a/lib/firebird/compiler/analyzer.ex b/lib/firebird/compiler/analyzer.ex index 99a0fb6..ceff89d 100644 --- a/lib/firebird/compiler/analyzer.ex +++ b/lib/firebird/compiler/analyzer.ex @@ -16,22 +16,22 @@ defmodule Firebird.Compiler.Analyzer do alias Firebird.Compiler.IR @type analysis :: %{ - module: atom(), - functions: [function_analysis()], - compilable: boolean(), - issues: [String.t()], - suggestions: [String.t()] - } + module: atom(), + functions: [function_analysis()], + compilable: boolean(), + issues: [String.t()], + suggestions: [String.t()] + } @type function_analysis :: %{ - name: atom(), - arity: non_neg_integer(), - recursive: boolean(), - tail_recursive: boolean(), - estimated_complexity: String.t(), - compilable: boolean(), - issues: [String.t()] - } + name: atom(), + arity: non_neg_integer(), + recursive: boolean(), + tail_recursive: boolean(), + estimated_complexity: String.t(), + compilable: boolean(), + issues: [String.t()] + } @doc """ Analyze an Elixir source string for WASM compilability. @@ -44,6 +44,7 @@ defmodule Firebird.Compiler.Analyzer do else {:error, {meta, msg, token}} -> {:error, {:parse_error, meta, msg, token}} + {:error, reason} -> {:error, reason} end @@ -68,9 +69,11 @@ defmodule Firebird.Compiler.Analyzer do func_analyses = Enum.map(module.functions, &analyze_function/1) all_compilable = Enum.all?(func_analyses, & &1.compilable) - all_issues = Enum.flat_map(func_analyses, fn fa -> - Enum.map(fa.issues, fn issue -> "#{fa.name}/#{fa.arity}: #{issue}" end) - end) + + all_issues = + Enum.flat_map(func_analyses, fn fa -> + Enum.map(fa.issues, fn issue -> "#{fa.name}/#{fa.arity}: #{issue}" end) + end) suggestions = generate_suggestions(func_analyses) @@ -125,41 +128,51 @@ defmodule Firebird.Compiler.Analyzer do defp has_nested_loops?({:call, _, args}) do Enum.any?(args, &has_nested_loops?/1) end + defp has_nested_loops?({:binop, _, left, right}) do has_nested_loops?(left) or has_nested_loops?(right) end + defp has_nested_loops?({:if, c, t, e}) do has_nested_loops?(c) or has_nested_loops?(t) or has_nested_loops?(e) end + defp has_nested_loops?(_), do: false defp count_recursive_calls({:call, name, args}, name) do 1 + Enum.sum(Enum.map(args, &count_recursive_calls(&1, name))) end + defp count_recursive_calls({:call, _, args}, name) do Enum.sum(Enum.map(args, &count_recursive_calls(&1, name))) end + defp count_recursive_calls({:binop, _, left, right}, name) do count_recursive_calls(left, name) + count_recursive_calls(right, name) end + defp count_recursive_calls({:if, c, t, e}, name) do - max(count_recursive_calls(c, name), - max(count_recursive_calls(t, name), count_recursive_calls(e, name))) + max( + count_recursive_calls(c, name), + max(count_recursive_calls(t, name), count_recursive_calls(e, name)) + ) end + defp count_recursive_calls({:unaryop, _, expr}, name) do count_recursive_calls(expr, name) end + defp count_recursive_calls({:block, exprs}, name) do Enum.sum(Enum.map(exprs, &count_recursive_calls(&1, name))) end + defp count_recursive_calls(_, _), do: 0 defp generate_suggestions(func_analyses) do suggestions = [] # Suggest TCO for recursive but not tail-recursive functions - non_tail = - Enum.filter(func_analyses, fn fa -> fa.recursive and not fa.tail_recursive end) + non_tail = Enum.filter(func_analyses, fn fa -> fa.recursive and not fa.tail_recursive end) suggestions = if Enum.any?(non_tail) do diff --git a/lib/firebird/compiler/combiner.ex b/lib/firebird/compiler/combiner.ex index c0734a4..299b808 100644 --- a/lib/firebird/compiler/combiner.ex +++ b/lib/firebird/compiler/combiner.ex @@ -176,7 +176,9 @@ defmodule Firebird.Compiler.Combiner do defp maybe_write(result, opts) do case Keyword.get(opts, :output_dir) do - nil -> :ok + nil -> + :ok + dir -> File.mkdir_p!(dir) base = Atom.to_string(result.module) |> String.replace(".", "_") diff --git a/lib/firebird/compiler/constant_propagation.ex b/lib/firebird/compiler/constant_propagation.ex index 5a6aa88..11a9a70 100644 --- a/lib/firebird/compiler/constant_propagation.ex +++ b/lib/firebird/compiler/constant_propagation.ex @@ -141,8 +141,7 @@ defmodule Firebird.Compiler.ConstantPropagation do # If expressions — propagate into all branches with the same env # (branches don't introduce new bindings visible outside) def propagate_expr({:if, cond_expr, then_b, else_b}, env, params) do - {:if, propagate_expr(cond_expr, env, params), - propagate_expr(then_b, env, params), + {:if, propagate_expr(cond_expr, env, params), propagate_expr(then_b, env, params), propagate_expr(else_b, env, params)} end diff --git a/lib/firebird/compiler/cse.ex b/lib/firebird/compiler/cse.ex index 843b543..e47d1cb 100644 --- a/lib/firebird/compiler/cse.ex +++ b/lib/firebird/compiler/cse.ex @@ -140,6 +140,7 @@ defmodule Firebird.Compiler.CSE do defp count_subexpressions({:case, subject, clauses}, acc) do acc = count_subexpressions(subject, acc) + Enum.reduce(clauses, acc, fn {_pat, _guard, body}, a -> count_subexpressions(body, a) end) @@ -249,8 +250,7 @@ defmodule Firebird.Compiler.CSE do end defp replace_expr({:if, cond_e, then_e, else_e}, target, replacement) do - {:if, replace_expr(cond_e, target, replacement), - replace_expr(then_e, target, replacement), + {:if, replace_expr(cond_e, target, replacement), replace_expr(then_e, target, replacement), replace_expr(else_e, target, replacement)} end diff --git a/lib/firebird/compiler/dependency_tracker.ex b/lib/firebird/compiler/dependency_tracker.ex index 1706a80..ffae648 100644 --- a/lib/firebird/compiler/dependency_tracker.ex +++ b/lib/firebird/compiler/dependency_tracker.ex @@ -165,7 +165,8 @@ defmodule Firebird.Compiler.DependencyTracker do # Match remote function calls: Module.function(args) # The AST is: {{:., _, [{:__aliases__, _, parts}, func]}, _, args} - defp collect_calls({{:., _, [{:__aliases__, _, parts}, _func]}, _, args}, acc) when is_list(args) do + defp collect_calls({{:., _, [{:__aliases__, _, parts}, _func]}, _, args}, acc) + when is_list(args) do module = parts |> Enum.map(&to_string/1) |> Enum.join(".") |> String.to_atom() # Also collect from args acc = [module | acc] @@ -274,6 +275,7 @@ defmodule Firebird.Compiler.DependencyTracker do in_progress: MapSet.delete(state.in_progress, node), result: [node | state.result] } + {:ok, state} {:error, _} = err -> @@ -304,9 +306,3 @@ defmodule Firebird.Compiler.DependencyTracker do end) end end - - - - - - diff --git a/lib/firebird/compiler/if_chain_to_case.ex b/lib/firebird/compiler/if_chain_to_case.ex index 82b1ffb..80ad6ba 100644 --- a/lib/firebird/compiler/if_chain_to_case.ex +++ b/lib/firebird/compiler/if_chain_to_case.ex @@ -54,7 +54,9 @@ defmodule Firebird.Compiler.IfChainToCase do Convert if-eq chains to case expressions in a single expression. """ @spec convert_expr(term()) :: term() - def convert_expr({:if, {:binop, :eq, {:var, var_name}, {:literal, _}} = _cond, _then, _else} = expr) do + def convert_expr( + {:if, {:binop, :eq, {:var, var_name}, {:literal, _}} = _cond, _then, _else} = expr + ) do case extract_if_eq_chain(expr, var_name) do {:ok, clauses, default_body} when length(clauses) >= @min_chain_length -> # Convert to case expression; recursively convert bodies @@ -125,7 +127,8 @@ defmodule Firebird.Compiler.IfChainToCase do defp extract_if_eq_chain( {:if, {:binop, :eq, {:var, var_name}, {:literal, val}}, then_body, else_body}, var_name - ) when is_integer(val) do + ) + when is_integer(val) do case extract_if_eq_chain(else_body, var_name) do {:ok, rest_clauses, default} -> {:ok, [{val, then_body} | rest_clauses], default} diff --git a/lib/firebird/compiler/inliner.ex b/lib/firebird/compiler/inliner.ex index d9ce534..ce57386 100644 --- a/lib/firebird/compiler/inliner.ex +++ b/lib/firebird/compiler/inliner.ex @@ -88,7 +88,10 @@ defmodule Firebird.Compiler.Inliner do def ir_size({:unaryop, _, expr}), do: 1 + ir_size(expr) def ir_size({:call, _, args}), do: 1 + Enum.sum(Enum.map(args, &ir_size/1)) def ir_size({:if, c, t, e}), do: 1 + ir_size(c) + ir_size(t) + ir_size(e) - def ir_size({:case, s, clauses}), do: 1 + ir_size(s) + Enum.sum(Enum.map(clauses, fn {_, _, b} -> ir_size(b) end)) + + def ir_size({:case, s, clauses}), + do: 1 + ir_size(s) + Enum.sum(Enum.map(clauses, fn {_, _, b} -> ir_size(b) end)) + def ir_size({:let, _, v}), do: 1 + ir_size(v) def ir_size({:block, exprs}), do: Enum.sum(Enum.map(exprs, &ir_size/1)) def ir_size({:tail_loop, _, body}), do: 1 + ir_size(body) @@ -124,7 +127,8 @@ defmodule Firebird.Compiler.Inliner do end defp inline_calls({:if, c, t, e}, inline_map, cf) do - {:if, inline_calls(c, inline_map, cf), inline_calls(t, inline_map, cf), inline_calls(e, inline_map, cf)} + {:if, inline_calls(c, inline_map, cf), inline_calls(t, inline_map, cf), + inline_calls(e, inline_map, cf)} end defp inline_calls({:case, s, clauses}, inline_map, cf) do @@ -160,7 +164,8 @@ defmodule Firebird.Compiler.Inliner do end defp substitute_inline({:if, c, t, e}, var_map) do - {:if, substitute_inline(c, var_map), substitute_inline(t, var_map), substitute_inline(e, var_map)} + {:if, substitute_inline(c, var_map), substitute_inline(t, var_map), + substitute_inline(e, var_map)} end defp substitute_inline({:let, name, value}, var_map) do diff --git a/lib/firebird/compiler/ir.ex b/lib/firebird/compiler/ir.ex index 97bc311..f140bd7 100644 --- a/lib/firebird/compiler/ir.ex +++ b/lib/firebird/compiler/ir.ex @@ -81,9 +81,19 @@ defmodule Firebird.Compiler.IR do | {:let, atom(), expr()} @type op :: - :add | :sub | :mul | :div_s | :rem_s - | :eq | :ne | :lt_s | :gt_s | :le_s | :ge_s - | :and_ | :or_ + :add + | :sub + | :mul + | :div_s + | :rem_s + | :eq + | :ne + | :lt_s + | :gt_s + | :le_s + | :ge_s + | :and_ + | :or_ @type pattern :: {:literal_pat, integer()} diff --git a/lib/firebird/compiler/ir_gen.ex b/lib/firebird/compiler/ir_gen.ex index d79fb77..dd2ac29 100644 --- a/lib/firebird/compiler/ir_gen.ex +++ b/lib/firebird/compiler/ir_gen.ex @@ -51,19 +51,20 @@ defmodule Firebird.Compiler.IRGen do exports = Enum.map(wasm_functions, fn f -> f.name end) |> Enum.uniq() - {:ok, %IR.Module{ - name: module_name, - functions: wasm_functions, - exports: exports - }} + {:ok, + %IR.Module{ + name: module_name, + functions: wasm_functions, + exports: exports + }} end # Handle {:__block__, _, _} wrapper defp extract_module({:__block__, _, stmts}) do case Enum.find(stmts, fn - {:defmodule, _, _} -> true - _ -> false - end) do + {:defmodule, _, _} -> true + _ -> false + end) do nil -> {:error, :no_module_found} module_ast -> extract_module(module_ast) end @@ -107,8 +108,10 @@ defmodule Firebird.Compiler.IRGen do |> Enum.flat_map(fn [{:@, _, [{:wasm, _, [true]}]}, {:def, _, [{:when, _, [{name, _, _} | _]} | _]}] -> [name] + [{:@, _, [{:wasm, _, [true]}]}, {:def, _, [{name, _, _} | _]}] when name != :when -> [name] + _ -> [] end) @@ -122,13 +125,14 @@ defmodule Firebird.Compiler.IRGen do |> Enum.map(&parse_def/1) |> Enum.group_by(fn {name, arity, _, _, _} -> {name, arity} end) |> Enum.map(fn {{name, arity}, clause_defs} -> - clauses = Enum.map(clause_defs, fn {_, _, patterns, guard, body} -> - %IR.Clause{ - patterns: Enum.map(patterns, &pattern_to_ir/1), - guard: guard_to_ir(guard), - body: expr_to_ir(body) - } - end) + clauses = + Enum.map(clause_defs, fn {_, _, patterns, guard, body} -> + %IR.Clause{ + patterns: Enum.map(patterns, &pattern_to_ir/1), + guard: guard_to_ir(guard), + body: expr_to_ir(body) + } + end) # Generate parameter names from the first clause's arity params = generate_params(arity) @@ -184,7 +188,11 @@ defmodule Firebird.Compiler.IRGen do if all_variable_patterns?(single.patterns) do # Single clause with all variable patterns - just use the body directly # But we need to substitute pattern var names with param names - substitute_pattern_vars(single.body, single.patterns, generate_params(length(single.patterns))) + substitute_pattern_vars( + single.body, + single.patterns, + generate_params(length(single.patterns)) + ) else build_clause_chain([single], generate_params(length(single.patterns))) end @@ -242,21 +250,30 @@ defmodule Firebird.Compiler.IRGen do |> Enum.flat_map(fn {{:literal_pat, val}, param} -> [{:binop, :eq, {:var, param}, {:literal, val}}] + _ -> [] end) - base_condition = case conditions do - [] -> {:literal, 1} # always true - [single] -> single - [first | rest] -> - Enum.reduce(rest, first, fn cond, acc -> - {:binop, :and_, acc, cond} - end) - end + base_condition = + case conditions do + # always true + [] -> + {:literal, 1} + + [single] -> + single + + [first | rest] -> + Enum.reduce(rest, first, fn cond, acc -> + {:binop, :and_, acc, cond} + end) + end case guard do - nil -> base_condition + nil -> + base_condition + guard_expr -> if base_condition == {:literal, 1} do guard_expr @@ -306,8 +323,7 @@ defmodule Firebird.Compiler.IRGen do end defp substitute_vars({:if, cond, then_b, else_b}, var_map) do - {:if, substitute_vars(cond, var_map), - substitute_vars(then_b, var_map), + {:if, substitute_vars(cond, var_map), substitute_vars(then_b, var_map), substitute_vars(else_b, var_map)} end @@ -342,8 +358,12 @@ defmodule Firebird.Compiler.IRGen do def expr_to_ir({:+, _, [left, right]}), do: {:binop, :add, expr_to_ir(left), expr_to_ir(right)} def expr_to_ir({:-, _, [left, right]}), do: {:binop, :sub, expr_to_ir(left), expr_to_ir(right)} def expr_to_ir({:*, _, [left, right]}), do: {:binop, :mul, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:div, _, [left, right]}), do: {:binop, :div_s, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:rem, _, [left, right]}), do: {:binop, :rem_s, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:div, _, [left, right]}), + do: {:binop, :div_s, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:rem, _, [left, right]}), + do: {:binop, :rem_s, expr_to_ir(left), expr_to_ir(right)} # Unary minus def expr_to_ir({:-, _, [expr]}), do: {:binop, :sub, {:literal, 0}, expr_to_ir(expr)} @@ -353,28 +373,54 @@ defmodule Firebird.Compiler.IRGen do def expr_to_ir({:!=, _, [left, right]}), do: {:binop, :ne, expr_to_ir(left), expr_to_ir(right)} def expr_to_ir({:<, _, [left, right]}), do: {:binop, :lt_s, expr_to_ir(left), expr_to_ir(right)} def expr_to_ir({:>, _, [left, right]}), do: {:binop, :gt_s, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:<=, _, [left, right]}), do: {:binop, :le_s, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:>=, _, [left, right]}), do: {:binop, :ge_s, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:<=, _, [left, right]}), + do: {:binop, :le_s, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:>=, _, [left, right]}), + do: {:binop, :ge_s, expr_to_ir(left), expr_to_ir(right)} # Boolean operations - def expr_to_ir({:and, _, [left, right]}), do: {:binop, :and_, expr_to_ir(left), expr_to_ir(right)} + def expr_to_ir({:and, _, [left, right]}), + do: {:binop, :and_, expr_to_ir(left), expr_to_ir(right)} + def expr_to_ir({:or, _, [left, right]}), do: {:binop, :or_, expr_to_ir(left), expr_to_ir(right)} def expr_to_ir({:not, _, [expr]}), do: {:unaryop, :not, expr_to_ir(expr)} # Bitwise operations - operator syntax (&&&, |||, ^^^, <<<, >>>) - def expr_to_ir({:&&&, _, [left, right]}), do: {:binop, :band, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:|||, _, [left, right]}), do: {:binop, :bor, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:"^^^", _, [left, right]}), do: {:binop, :bxor, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:<<<, _, [left, right]}), do: {:binop, :shl, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:>>>, _, [left, right]}), do: {:binop, :shr_s, expr_to_ir(left), expr_to_ir(right)} + def expr_to_ir({:&&&, _, [left, right]}), + do: {:binop, :band, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:|||, _, [left, right]}), + do: {:binop, :bor, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:"^^^", _, [left, right]}), + do: {:binop, :bxor, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:<<<, _, [left, right]}), + do: {:binop, :shl, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:>>>, _, [left, right]}), + do: {:binop, :shr_s, expr_to_ir(left), expr_to_ir(right)} + def expr_to_ir({:"~~~", _, [expr]}), do: {:unaryop, :bnot, expr_to_ir(expr)} # Bitwise operations - function syntax (band, bor, bxor, bsl, bsr, bnot) - def expr_to_ir({:band, _, [left, right]}), do: {:binop, :band, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:bor, _, [left, right]}), do: {:binop, :bor, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:bxor, _, [left, right]}), do: {:binop, :bxor, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:bsl, _, [left, right]}), do: {:binop, :shl, expr_to_ir(left), expr_to_ir(right)} - def expr_to_ir({:bsr, _, [left, right]}), do: {:binop, :shr_s, expr_to_ir(left), expr_to_ir(right)} + def expr_to_ir({:band, _, [left, right]}), + do: {:binop, :band, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:bor, _, [left, right]}), + do: {:binop, :bor, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:bxor, _, [left, right]}), + do: {:binop, :bxor, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:bsl, _, [left, right]}), + do: {:binop, :shl, expr_to_ir(left), expr_to_ir(right)} + + def expr_to_ir({:bsr, _, [left, right]}), + do: {:binop, :shr_s, expr_to_ir(left), expr_to_ir(right)} + def expr_to_ir({:bnot, _, [expr]}), do: {:unaryop, :bnot, expr_to_ir(expr)} # if/else (must be before the generic function call catch-all) @@ -406,9 +452,11 @@ defmodule Firebird.Compiler.IRGen do # case expression def expr_to_ir({:case, _, [subject, [do: clauses]]}) do - ir_clauses = Enum.map(clauses, fn {:->, _, [[pattern], body]} -> - {pattern_to_ir(pattern), nil, expr_to_ir(body)} - end) + ir_clauses = + Enum.map(clauses, fn {:->, _, [[pattern], body]} -> + {pattern_to_ir(pattern), nil, expr_to_ir(body)} + end) + {:case, expr_to_ir(subject), ir_clauses} end @@ -439,11 +487,11 @@ defmodule Firebird.Compiler.IRGen do def expr_to_ir({{:., _, [{:__aliases__, _, [:Bitwise]}, func]}, _, args}) do case {func, args} do {:band, [left, right]} -> {:binop, :band, expr_to_ir(left), expr_to_ir(right)} - {:bor, [left, right]} -> {:binop, :bor, expr_to_ir(left), expr_to_ir(right)} + {:bor, [left, right]} -> {:binop, :bor, expr_to_ir(left), expr_to_ir(right)} {:bxor, [left, right]} -> {:binop, :bxor, expr_to_ir(left), expr_to_ir(right)} - {:bsl, [left, right]} -> {:binop, :shl, expr_to_ir(left), expr_to_ir(right)} - {:bsr, [left, right]} -> {:binop, :shr_s, expr_to_ir(left), expr_to_ir(right)} - {:bnot, [expr]} -> {:unaryop, :bnot, expr_to_ir(expr)} + {:bsl, [left, right]} -> {:binop, :shl, expr_to_ir(left), expr_to_ir(right)} + {:bsr, [left, right]} -> {:binop, :shr_s, expr_to_ir(left), expr_to_ir(right)} + {:bnot, [expr]} -> {:unaryop, :bnot, expr_to_ir(expr)} _ -> {:error_node, {:unsupported_bitwise, func}} end end diff --git a/lib/firebird/compiler/licm.ex b/lib/firebird/compiler/licm.ex index b85eb07..6fd454c 100644 --- a/lib/firebird/compiler/licm.ex +++ b/lib/firebird/compiler/licm.ex @@ -131,8 +131,7 @@ defmodule Firebird.Compiler.LICM do end def licm_expr({:if, c, t, e}, func_params, opts) do - {:if, licm_expr(c, func_params, opts), - licm_expr(t, func_params, opts), + {:if, licm_expr(c, func_params, opts), licm_expr(t, func_params, opts), licm_expr(e, func_params, opts)} end @@ -213,6 +212,7 @@ defmodule Firebird.Compiler.LICM do defp do_collect({:case, subject, clauses}, ni, ms, acc) do acc = do_collect(subject, ni, ms, acc) + Enum.reduce(clauses, acc, fn {_pat, _guard, body}, a -> do_collect(body, ni, ms, a) end) @@ -279,20 +279,30 @@ defmodule Firebird.Compiler.LICM do end defp collect_let_names({:if, c, t, e}) do - [c, t, e] |> Enum.reduce(MapSet.new(), fn e, acc -> MapSet.union(acc, collect_let_names(e)) end) + [c, t, e] + |> Enum.reduce(MapSet.new(), fn e, acc -> MapSet.union(acc, collect_let_names(e)) end) end defp collect_let_names({:case, subject, clauses}) do - clause_lets = Enum.reduce(clauses, MapSet.new(), fn {_, _, body}, acc -> - MapSet.union(acc, collect_let_names(body)) - end) + clause_lets = + Enum.reduce(clauses, MapSet.new(), fn {_, _, body}, acc -> + MapSet.union(acc, collect_let_names(body)) + end) + MapSet.union(collect_let_names(subject), clause_lets) end - defp collect_let_names({:binop, _, l, r}), do: MapSet.union(collect_let_names(l), collect_let_names(r)) + defp collect_let_names({:binop, _, l, r}), + do: MapSet.union(collect_let_names(l), collect_let_names(r)) + defp collect_let_names({:unaryop, _, e}), do: collect_let_names(e) - defp collect_let_names({:call, _, args}), do: Enum.reduce(args, MapSet.new(), fn a, acc -> MapSet.union(acc, collect_let_names(a)) end) - defp collect_let_names({:tail_call, _, args}), do: Enum.reduce(args, MapSet.new(), fn a, acc -> MapSet.union(acc, collect_let_names(a)) end) + + defp collect_let_names({:call, _, args}), + do: Enum.reduce(args, MapSet.new(), fn a, acc -> MapSet.union(acc, collect_let_names(a)) end) + + defp collect_let_names({:tail_call, _, args}), + do: Enum.reduce(args, MapSet.new(), fn a, acc -> MapSet.union(acc, collect_let_names(a)) end) + defp collect_let_names(_), do: MapSet.new() # Check if an expression tree contains a specific subexpression @@ -314,7 +324,8 @@ defmodule Firebird.Compiler.LICM do end defp contains_expr?({:case, s, clauses}, target) do - contains_expr?(s, target) or Enum.any?(clauses, fn {_, _, body} -> contains_expr?(body, target) end) + contains_expr?(s, target) or + Enum.any?(clauses, fn {_, _, body} -> contains_expr?(body, target) end) end defp contains_expr?({:let, _, v}, target), do: contains_expr?(v, target) @@ -336,22 +347,26 @@ defmodule Firebird.Compiler.LICM do defp replace_expr(expr, target, replacement) when expr == target, do: replacement defp replace_expr({:binop, op, l, r} = expr, target, repl) do - if expr == target, do: repl, - else: {:binop, op, replace_expr(l, target, repl), replace_expr(r, target, repl)} + if expr == target, + do: repl, + else: {:binop, op, replace_expr(l, target, repl), replace_expr(r, target, repl)} end defp replace_expr({:unaryop, op, e} = expr, target, repl) do - if expr == target, do: repl, - else: {:unaryop, op, replace_expr(e, target, repl)} + if expr == target, + do: repl, + else: {:unaryop, op, replace_expr(e, target, repl)} end defp replace_expr({:call, name, args} = expr, target, repl) do - if expr == target, do: repl, - else: {:call, name, Enum.map(args, &replace_expr(&1, target, repl))} + if expr == target, + do: repl, + else: {:call, name, Enum.map(args, &replace_expr(&1, target, repl))} end defp replace_expr({:if, c, t, e}, target, repl) do - {:if, replace_expr(c, target, repl), replace_expr(t, target, repl), replace_expr(e, target, repl)} + {:if, replace_expr(c, target, repl), replace_expr(t, target, repl), + replace_expr(e, target, repl)} end defp replace_expr({:case, s, clauses}, target, repl) do diff --git a/lib/firebird/compiler/optimizer.ex b/lib/firebird/compiler/optimizer.ex index 8cdc952..dc4b43e 100644 --- a/lib/firebird/compiler/optimizer.ex +++ b/lib/firebird/compiler/optimizer.ex @@ -35,10 +35,24 @@ defmodule Firebird.Compiler.Optimizer do alias Firebird.Compiler.IR - @type pass :: :constant_fold | :dead_code | :strength_reduce | :identity_elim | :dead_let_elim | :algebraic_simplify | :cse + @type pass :: + :constant_fold + | :dead_code + | :strength_reduce + | :identity_elim + | :dead_let_elim + | :algebraic_simplify + | :cse @type opts :: [passes: [pass()]] - @all_passes [:constant_fold, :dead_code, :strength_reduce, :identity_elim, :algebraic_simplify, :dead_let_elim] + @all_passes [ + :constant_fold, + :dead_code, + :strength_reduce, + :identity_elim, + :algebraic_simplify, + :dead_let_elim + ] @doc """ Run optimization passes on a module IR. @@ -74,9 +88,10 @@ defmodule Firebird.Compiler.Optimizer do # Run all requested passes, iterating until fixpoint defp run_passes(expr, passes) do - optimized = Enum.reduce(passes, expr, fn pass, acc -> - apply_pass(acc, pass) - end) + optimized = + Enum.reduce(passes, expr, fn pass, acc -> + apply_pass(acc, pass) + end) # Run again if something changed (fixpoint iteration) if optimized != expr do @@ -121,8 +136,12 @@ defmodule Firebird.Compiler.Optimizer do end def constant_fold({:unaryop, :not, {:literal, 0}}), do: {:literal, 1} - def constant_fold({:unaryop, :not, {:literal, n}}) when is_integer(n) and n != 0, do: {:literal, 0} - def constant_fold({:unaryop, :bnot, {:literal, n}}) when is_integer(n), do: {:literal, Bitwise.bnot(n)} + + def constant_fold({:unaryop, :not, {:literal, n}}) when is_integer(n) and n != 0, + do: {:literal, 0} + + def constant_fold({:unaryop, :bnot, {:literal, n}}) when is_integer(n), + do: {:literal, Bitwise.bnot(n)} def constant_fold({:unaryop, op, expr}) do {:unaryop, op, constant_fold(expr)} @@ -165,9 +184,11 @@ defmodule Firebird.Compiler.Optimizer do defp eval_binop(:add, a, b), do: {:ok, a + b} defp eval_binop(:sub, a, b), do: {:ok, a - b} defp eval_binop(:mul, a, b), do: {:ok, a * b} - defp eval_binop(:div_s, _, 0), do: :error # Don't fold division by zero + # Don't fold division by zero + defp eval_binop(:div_s, _, 0), do: :error defp eval_binop(:div_s, a, b), do: {:ok, div(a, b)} - defp eval_binop(:rem_s, _, 0), do: :error # Don't fold rem by zero + # Don't fold rem by zero + defp eval_binop(:rem_s, _, 0), do: :error defp eval_binop(:rem_s, a, b), do: {:ok, rem(a, b)} defp eval_binop(:eq, a, b), do: {:ok, if(a == b, do: 1, else: 0)} defp eval_binop(:ne, a, b), do: {:ok, if(a != b, do: 1, else: 0)} @@ -256,6 +277,7 @@ defmodule Firebird.Compiler.Optimizer do # x * power_of_2 → x << log2(power_of_2) def strength_reduce({:binop, :mul, left, {:literal, n}}) when is_integer(n) and n > 1 do left = strength_reduce(left) + case power_of_two?(n) do {:ok, shift} -> {:binop, :shl, left, {:literal, shift}} :no -> {:binop, :mul, left, {:literal, n}} @@ -264,6 +286,7 @@ defmodule Firebird.Compiler.Optimizer do def strength_reduce({:binop, :mul, {:literal, n}, right}) when is_integer(n) and n > 1 do right = strength_reduce(right) + case power_of_two?(n) do {:ok, shift} -> {:binop, :shl, right, {:literal, shift}} :no -> {:binop, :mul, {:literal, n}, right} @@ -276,6 +299,7 @@ defmodule Firebird.Compiler.Optimizer do # cases (e.g. collatz: div(n, 2) where n is always positive). def strength_reduce({:binop, :div_s, left, {:literal, n}}) when is_integer(n) and n > 1 do left = strength_reduce(left) + case power_of_two?(n) do {:ok, shift} -> {:binop, :shr_s, left, {:literal, shift}} :no -> {:binop, :div_s, left, {:literal, n}} @@ -287,6 +311,7 @@ defmodule Firebird.Compiler.Optimizer do # in parity checks (rem(n, 2) == 0) and modular arithmetic. def strength_reduce({:binop, :rem_s, left, {:literal, n}}) when is_integer(n) and n > 1 do left = strength_reduce(left) + case power_of_two?(n) do {:ok, _shift} -> {:binop, :band, left, {:literal, n - 1}} :no -> {:binop, :rem_s, left, {:literal, n}} @@ -630,13 +655,21 @@ defmodule Firebird.Compiler.Optimizer do # x and false → 0 def algebraic_simplify({:binop, :and_, _, {:literal, 0}}), do: {:literal, 0} # true and x → x (identity for truthiness conversion via i32) - def algebraic_simplify({:binop, :and_, {:literal, n}, expr}) when is_integer(n) and n != 0, do: algebraic_simplify(expr) + def algebraic_simplify({:binop, :and_, {:literal, n}, expr}) when is_integer(n) and n != 0, + do: algebraic_simplify(expr) + # x and true → x - def algebraic_simplify({:binop, :and_, expr, {:literal, n}}) when is_integer(n) and n != 0, do: algebraic_simplify(expr) + def algebraic_simplify({:binop, :and_, expr, {:literal, n}}) when is_integer(n) and n != 0, + do: algebraic_simplify(expr) + # true or x → 1 (short-circuit) - def algebraic_simplify({:binop, :or_, {:literal, n}, _}) when is_integer(n) and n != 0, do: {:literal, 1} + def algebraic_simplify({:binop, :or_, {:literal, n}, _}) when is_integer(n) and n != 0, + do: {:literal, 1} + # x or true → 1 - def algebraic_simplify({:binop, :or_, _, {:literal, n}}) when is_integer(n) and n != 0, do: {:literal, 1} + def algebraic_simplify({:binop, :or_, _, {:literal, n}}) when is_integer(n) and n != 0, + do: {:literal, 1} + # false or x → x def algebraic_simplify({:binop, :or_, {:literal, 0}, expr}), do: algebraic_simplify(expr) # x or false → x @@ -702,7 +735,7 @@ defmodule Firebird.Compiler.Optimizer do # Check if n is a power of 2, return the exponent defp power_of_two?(n) when n > 0 do - if (n &&& (n - 1)) == 0 do + if (n &&& n - 1) == 0 do {:ok, round(:math.log2(n))} else :no diff --git a/lib/firebird/compiler/source_map.ex b/lib/firebird/compiler/source_map.ex index 9b419b2..147282e 100644 --- a/lib/firebird/compiler/source_map.ex +++ b/lib/firebird/compiler/source_map.ex @@ -95,15 +95,16 @@ defmodule Firebird.Compiler.SourceMap do "module" => Atom.to_string(sm.module), "source" => sm.source, "compiled_at" => DateTime.to_iso8601(sm.compiled_at), - "exports" => Enum.map(sm.entries, fn e -> - %{ - "function" => Atom.to_string(e.function), - "arity" => e.arity, - "export_name" => e.export_name, - "params" => Enum.map(e.params, &Atom.to_string/1), - "tail_recursive" => e.tail_recursive - } - end) + "exports" => + Enum.map(sm.entries, fn e -> + %{ + "function" => Atom.to_string(e.function), + "arity" => e.arity, + "export_name" => e.export_name, + "params" => Enum.map(e.params, &Atom.to_string/1), + "tail_recursive" => e.tail_recursive + } + end) } end diff --git a/lib/firebird/compiler/summary.ex b/lib/firebird/compiler/summary.ex index 8f384d8..55a0cda 100644 --- a/lib/firebird/compiler/summary.ex +++ b/lib/firebird/compiler/summary.ex @@ -56,8 +56,7 @@ defmodule Firebird.Compiler.Summary do else: nil ), modules: modules, - errors: - Enum.map(failures, fn {:error, reason} -> inspect(reason) end) + errors: Enum.map(failures, fn {:error, reason} -> inspect(reason) end) } end @@ -96,8 +95,7 @@ defmodule Firebird.Compiler.Summary do lines = if Enum.any?(summary.errors) do - error_lines = - Enum.map(summary.errors, fn e -> " ❌ #{e}" end) + error_lines = Enum.map(summary.errors, fn e -> " ❌ #{e}" end) lines ++ [" Errors:"] ++ error_lines else @@ -128,4 +126,3 @@ defmodule Firebird.Compiler.Summary do defp format_bytes(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)}KB" defp format_bytes(bytes), do: "#{Float.round(bytes / 1_048_576, 1)}MB" end - diff --git a/lib/firebird/compiler/tco.ex b/lib/firebird/compiler/tco.ex index 37f3c59..100f8f2 100644 --- a/lib/firebird/compiler/tco.ex +++ b/lib/firebird/compiler/tco.ex @@ -74,15 +74,24 @@ defmodule Firebird.Compiler.TCO do """ @spec has_self_calls?(term(), atom()) :: boolean() def has_self_calls?({:call, name, _args}, name), do: true - def has_self_calls?({:call, _other, args}, name), do: Enum.any?(args, &has_self_calls?(&1, name)) - def has_self_calls?({:binop, _, left, right}, name), do: has_self_calls?(left, name) or has_self_calls?(right, name) + + def has_self_calls?({:call, _other, args}, name), + do: Enum.any?(args, &has_self_calls?(&1, name)) + + def has_self_calls?({:binop, _, left, right}, name), + do: has_self_calls?(left, name) or has_self_calls?(right, name) + def has_self_calls?({:unaryop, _, expr}, name), do: has_self_calls?(expr, name) + def has_self_calls?({:if, cond, then_b, else_b}, name) do has_self_calls?(cond, name) or has_self_calls?(then_b, name) or has_self_calls?(else_b, name) end + def has_self_calls?({:case, subj, clauses}, name) do - has_self_calls?(subj, name) or Enum.any?(clauses, fn {_, _, body} -> has_self_calls?(body, name) end) + has_self_calls?(subj, name) or + Enum.any?(clauses, fn {_, _, body} -> has_self_calls?(body, name) end) end + def has_self_calls?({:let, _, value}, name), do: has_self_calls?(value, name) def has_self_calls?({:block, exprs}, name), do: Enum.any?(exprs, &has_self_calls?(&1, name)) def has_self_calls?(_, _), do: false @@ -102,10 +111,12 @@ defmodule Firebird.Compiler.TCO do defp has_non_tail_self_calls?({:call, name, args}, name, in_tail) do # The call itself: if not in tail position, it's a non-tail call args_have_non_tail = Enum.any?(args, &has_non_tail_self_calls?(&1, name, false)) + if in_tail do args_have_non_tail else - true # This self-call is not in tail position + # This self-call is not in tail position + true end end @@ -147,6 +158,7 @@ defmodule Firebird.Compiler.TCO do defp has_non_tail_self_calls?({:block, exprs}, name, in_tail) do {init, [last]} = Enum.split(exprs, -1) + Enum.any?(init, &has_non_tail_self_calls?(&1, name, false)) or has_non_tail_self_calls?(last, name, in_tail) end @@ -170,9 +182,11 @@ defmodule Firebird.Compiler.TCO do end defp mark_tail_calls({:case, subj, clauses}, name, params) do - clauses = Enum.map(clauses, fn {pat, guard, body} -> - {pat, guard, mark_tail_calls(body, name, params)} - end) + clauses = + Enum.map(clauses, fn {pat, guard, body} -> + {pat, guard, mark_tail_calls(body, name, params)} + end) + {:case, subj, clauses} end diff --git a/lib/firebird/compiler/type_inference.ex b/lib/firebird/compiler/type_inference.ex index bab20e5..be39593 100644 --- a/lib/firebird/compiler/type_inference.ex +++ b/lib/firebird/compiler/type_inference.ex @@ -37,10 +37,11 @@ defmodule Firebird.Compiler.TypeInference do def infer(%IR.Module{} = module) do typed_functions = Enum.map(module.functions, &infer_function/1) - errors = Enum.filter(typed_functions, fn - {:error, _} -> true - _ -> false - end) + errors = + Enum.filter(typed_functions, fn + {:error, _} -> true + _ -> false + end) if Enum.empty?(errors) do {:ok, %{module | functions: typed_functions}} @@ -57,24 +58,29 @@ defmodule Firebird.Compiler.TypeInference do # Check if any clause has float literals in patterns has_floats = check_for_floats(func.body) - param_types = if has_floats do - List.duplicate(:f64, func.arity) - else - List.duplicate(:i64, func.arity) - end + param_types = + if has_floats do + List.duplicate(:f64, func.arity) + else + List.duplicate(:i64, func.arity) + end - env = if has_floats do - Map.new(func.params, fn p -> {p, :f64} end) - else - env - end + env = + if has_floats do + Map.new(func.params, fn p -> {p, :f64} end) + else + env + end return_type = infer_expr_type(func.body, env) - %{func | type: %IR.FunctionType{ - params: param_types, - return: return_type - }} + %{ + func + | type: %IR.FunctionType{ + params: param_types, + return: return_type + } + } end @doc """ @@ -154,8 +160,13 @@ defmodule Firebird.Compiler.TypeInference do defp widen_type(_, _), do: :i64 defp check_for_floats({:literal, n}) when is_float(n), do: true - defp check_for_floats({:binop, _, left, right}), do: check_for_floats(left) or check_for_floats(right) - defp check_for_floats({:if, c, t, e}), do: check_for_floats(c) or check_for_floats(t) or check_for_floats(e) + + defp check_for_floats({:binop, _, left, right}), + do: check_for_floats(left) or check_for_floats(right) + + defp check_for_floats({:if, c, t, e}), + do: check_for_floats(c) or check_for_floats(t) or check_for_floats(e) + defp check_for_floats({:call, _, args}), do: Enum.any?(args, &check_for_floats/1) defp check_for_floats({:let, _, value}), do: check_for_floats(value) defp check_for_floats({:block, exprs}), do: Enum.any?(exprs, &check_for_floats/1) diff --git a/lib/firebird/compiler/validator.ex b/lib/firebird/compiler/validator.ex index be4bba8..c7718f3 100644 --- a/lib/firebird/compiler/validator.ex +++ b/lib/firebird/compiler/validator.ex @@ -67,9 +67,26 @@ defmodule Firebird.Compiler.Validator do def validate_expr({:var, _name}, _ctx), do: [] def validate_expr({:binop, op, left, right}, ctx) - when op in [:add, :sub, :mul, :div_s, :rem_s, - :eq, :ne, :lt_s, :gt_s, :le_s, :ge_s, - :and_, :or_, :shl, :shr_s, :band, :bor, :bxor] do + when op in [ + :add, + :sub, + :mul, + :div_s, + :rem_s, + :eq, + :ne, + :lt_s, + :gt_s, + :le_s, + :ge_s, + :and_, + :or_, + :shl, + :shr_s, + :band, + :bor, + :bxor + ] do validate_expr(left, ctx) ++ validate_expr(right, ctx) end @@ -87,9 +104,12 @@ defmodule Firebird.Compiler.Validator do def validate_expr({:case, subject, clauses}, ctx) do subject_errors = validate_expr(subject, ctx) - clause_errors = Enum.flat_map(clauses, fn {_pat, _guard, body} -> - validate_expr(body, ctx) - end) + + clause_errors = + Enum.flat_map(clauses, fn {_pat, _guard, body} -> + validate_expr(body, ctx) + end) + subject_errors ++ clause_errors end diff --git a/lib/firebird/compiler/wat_gen.ex b/lib/firebird/compiler/wat_gen.ex index 662e914..a5b8888 100644 --- a/lib/firebird/compiler/wat_gen.ex +++ b/lib/firebird/compiler/wat_gen.ex @@ -62,10 +62,13 @@ defmodule Firebird.Compiler.WATGen do # Build a map of function name → %IR.FunctionType{} for all module functions defp build_func_type_map(functions) do Map.new(functions, fn func -> - type = func.type || %IR.FunctionType{ - params: List.duplicate(:i64, func.arity), - return: :i64 - } + type = + func.type || + %IR.FunctionType{ + params: List.duplicate(:i64, func.arity), + return: :i64 + } + {func.name, type} end) end @@ -75,10 +78,12 @@ defmodule Firebird.Compiler.WATGen do """ @spec generate_function(IR.Function.t(), map()) :: String.t() def generate_function(%IR.Function{} = func, func_types \\ %{}) do - type = func.type || %IR.FunctionType{ - params: List.duplicate(:i64, func.arity), - return: :i64 - } + type = + func.type || + %IR.FunctionType{ + params: List.duplicate(:i64, func.arity), + return: :i64 + } # Build type environment: param name → wasm type type_env = Map.new(Enum.zip(func.params, type.params)) @@ -148,6 +153,7 @@ defmodule Firebird.Compiler.WATGen do defp collect_let_vars({:let, name, value}, params) do inner = collect_let_vars(value, params) + if name in params do inner else @@ -183,9 +189,12 @@ defmodule Firebird.Compiler.WATGen do defp collect_let_vars({:case, subject, clauses}, params) do subject_locals = collect_let_vars(subject, params) - clause_locals = Enum.reduce(clauses, MapSet.new(), fn {_, _, body}, acc -> - MapSet.union(acc, collect_let_vars(body, params)) - end) + + clause_locals = + Enum.reduce(clauses, MapSet.new(), fn {_, _, body}, acc -> + MapSet.union(acc, collect_let_vars(body, params)) + end) + # Also need __case_subject local MapSet.union(subject_locals, clause_locals) |> MapSet.put(:__case_subject) @@ -238,10 +247,13 @@ defmodule Firebird.Compiler.WATGen do defp collect_local_types_acc({:case, subject, clauses}, te, ftm, acc) do {_, acc} = collect_local_types_acc(subject, te, ftm, acc) - acc = Enum.reduce(clauses, acc, fn {_, _, body}, cur_acc -> - {_, cur_acc} = collect_local_types_acc(body, te, ftm, cur_acc) - cur_acc - end) + + acc = + Enum.reduce(clauses, acc, fn {_, _, body}, cur_acc -> + {_, cur_acc} = collect_local_types_acc(body, te, ftm, cur_acc) + cur_acc + end) + {te, acc} end @@ -251,10 +263,12 @@ defmodule Firebird.Compiler.WATGen do end defp collect_local_types_acc({:tail_call, _, args}, te, ftm, acc) do - acc = Enum.reduce(args, acc, fn arg, cur_acc -> - {_, cur_acc} = collect_local_types_acc(arg, te, ftm, cur_acc) - cur_acc - end) + acc = + Enum.reduce(args, acc, fn arg, cur_acc -> + {_, cur_acc} = collect_local_types_acc(arg, te, ftm, cur_acc) + cur_acc + end) + {te, acc} end @@ -269,10 +283,12 @@ defmodule Firebird.Compiler.WATGen do end defp collect_local_types_acc({:call, _, args}, te, ftm, acc) do - acc = Enum.reduce(args, acc, fn arg, cur_acc -> - {_, cur_acc} = collect_local_types_acc(arg, te, ftm, cur_acc) - cur_acc - end) + acc = + Enum.reduce(args, acc, fn arg, cur_acc -> + {_, cur_acc} = collect_local_types_acc(arg, te, ftm, cur_acc) + cur_acc + end) + {te, acc} end @@ -287,10 +303,8 @@ defmodule Firebird.Compiler.WATGen do # - type_env: map of variable name → WASM type (for locals/params) # - func_types: map of function name → %IR.FunctionType{} (for calls) - defp generate_expr(expr, target_type, ft) do - # Backward-compatible 3-arity entry point (no type_env/func_types) - generate_expr(expr, target_type, ft, %{}, %{}) - end + # NOTE: generate_expr/3 removed — use generate_expr/5 directly with + # explicit type_env and func_types maps. defp generate_expr({:literal, n}, target_type, _ft, _te, _ftm) when is_integer(n) do case target_type do @@ -347,6 +361,7 @@ defmodule Firebird.Compiler.WATGen do defp generate_expr({:unaryop, :negate, expr}, target_type, ft, te, ftm) do expr_type = infer_expr_wasm_type(expr, te, ftm) + if expr_type == :f64 do inner = generate_expr(expr, :f64, ft, te, ftm) result = "#{inner}\nf64.neg" @@ -384,11 +399,12 @@ defmodule Firebird.Compiler.WATGen do return_type = if callee_type, do: callee_type.return, else: :i64 - result = if args_wat == "" do - "call $#{name}" - else - "#{args_wat}\ncall $#{name}" - end + result = + if args_wat == "" do + "call $#{name}" + else + "#{args_wat}\ncall $#{name}" + end coerce(result, return_type, target_type) end @@ -430,6 +446,7 @@ defmodule Firebird.Compiler.WATGen do defp generate_expr({:tail_loop, _loop_params, body}, target_type, ft, te, ftm) do body_wat = generate_tco_body(body, target_type, ft, te, ftm) result_type = wasm_type(target_type) + """ (block $__tco_exit (result #{result_type}) (loop $__tco_loop @@ -534,7 +551,8 @@ defmodule Firebird.Compiler.WATGen do end # Blocks in TCO bodies: init exprs are normal, last expr may be a tail_call - defp generate_tco_body({:block, exprs}, target_type, ft, te, ftm) when is_list(exprs) and length(exprs) > 0 do + defp generate_tco_body({:block, exprs}, target_type, ft, te, ftm) + when is_list(exprs) and length(exprs) > 0 do {init, [last]} = Enum.split(exprs, -1) init_wat = @@ -584,6 +602,7 @@ defmodule Firebird.Compiler.WATGen do defp generate_tco_body(expr, target_type, ft, te, ftm) do result_wat = generate_expr(expr, target_type, ft, te, ftm) + """ #{result_wat} br $__tco_exit\ @@ -630,8 +649,19 @@ defmodule Firebird.Compiler.WATGen do range = max_val - min_val + 1 if range <= count * @br_table_density_factor and range <= 256 and min_val >= 0 do - {:ok, gen_br_table(literal_clauses, default_clause, min_val, range, - subject_local, target_type, ft, te, ftm, mode)} + {:ok, + gen_br_table( + literal_clauses, + default_clause, + min_val, + range, + subject_local, + target_type, + ft, + te, + ftm, + mode + )} else :fallback end @@ -686,8 +716,18 @@ defmodule Firebird.Compiler.WATGen do # ) # # br_table index 0 → innermost block ($__jt_0), index N → $__jt_default. - defp gen_br_table(literal_clauses, default_body, min_val, _range, - subject_local, target_type, ft, te, ftm, mode) do + defp gen_br_table( + literal_clauses, + default_body, + min_val, + _range, + subject_local, + target_type, + ft, + te, + ftm, + mode + ) do value_to_idx = literal_clauses |> Enum.with_index() @@ -701,7 +741,7 @@ defmodule Firebird.Compiler.WATGen do Enum.map(literal_clauses, fn {_val, body} -> case mode do :expr -> generate_expr(body, target_type, ft, te, ftm) - :tco -> generate_tco_body(body, target_type, ft, te, ftm) + :tco -> generate_tco_body(body, target_type, ft, te, ftm) end end) @@ -709,9 +749,9 @@ defmodule Firebird.Compiler.WATGen do default_body_wat = case {default_body, mode} do {nil, :expr} -> "#{wasm_type(target_type)}.const 0 ;; unreachable default" - {nil, :tco} -> "unreachable ;; no matching case clause" + {nil, :tco} -> "unreachable ;; no matching case clause" {body, :expr} -> generate_expr(body, target_type, ft, te, ftm) - {body, :tco} -> generate_tco_body(body, target_type, ft, te, ftm) + {body, :tco} -> generate_tco_body(body, target_type, ft, te, ftm) end # Build the br_table label list for each slot in [min_val .. max_val] @@ -722,6 +762,7 @@ defmodule Firebird.Compiler.WATGen do 0..(range - 1) |> Enum.map(fn offset -> val = min_val + offset + case Map.get(value_to_idx, val) do nil -> "$__jt_default" idx -> "$__jt_#{idx}" @@ -732,18 +773,20 @@ defmodule Firebird.Compiler.WATGen do # Dispatch: normalize subject to 0-based index, then br_table subtract_wat = if min_val == 0, do: "", else: "i64.const #{min_val}\ni64.sub\n" - dispatch = String.trim(""" - local.get #{subject_local} - #{subtract_wat}i32.wrap_i64 - br_table #{br_labels} $__jt_default\ - """) + dispatch = + String.trim(""" + local.get #{subject_local} + #{subtract_wat}i32.wrap_i64 + br_table #{br_labels} $__jt_default\ + """) # Build from innermost outward: start with dispatch, wrap in blocks, # interleave clause bodies after each block close. - exit_br = case mode do - :expr -> "\nbr $__jt_exit" - :tco -> "" - end + exit_br = + case mode do + :expr -> "\nbr $__jt_exit" + :tco -> "" + end result = Enum.reduce(0..(n - 1), dispatch, fn idx, code -> @@ -757,7 +800,7 @@ defmodule Firebird.Compiler.WATGen do # For expr mode, wrap in typed $__jt_exit block case mode do :expr -> "(block $__jt_exit (result #{result_type_str})\n#{result}\n)" - :tco -> result + :tco -> result end end @@ -771,6 +814,7 @@ defmodule Firebird.Compiler.WATGen do {:literal_pat, val} -> body_wat = generate_expr(body, target_type, ft, te, ftm) else_wat = generate_case_clauses(rest, subject, target_type, ft, te, ftm) + """ local.get #{subject} i64.const #{val} @@ -797,11 +841,19 @@ defmodule Firebird.Compiler.WATGen do "unreachable ;; no matching case clause" end - defp generate_tco_case_clauses([{pattern, _guard, body} | rest], subject, target_type, ft, te, ftm) do + defp generate_tco_case_clauses( + [{pattern, _guard, body} | rest], + subject, + target_type, + ft, + te, + ftm + ) do case pattern do {:literal_pat, val} -> body_wat = generate_tco_body(body, target_type, ft, te, ftm) else_wat = generate_tco_case_clauses(rest, subject, target_type, ft, te, ftm) + """ local.get #{subject} i64.const #{val} @@ -844,7 +896,9 @@ defmodule Firebird.Compiler.WATGen do defp infer_expr_wasm_type({:unaryop, :not, _}, _te, _ftm), do: :i32 defp infer_expr_wasm_type({:unaryop, :bnot, _}, _te, _ftm), do: :i64 - defp infer_expr_wasm_type({:unaryop, :negate, expr}, te, ftm), do: infer_expr_wasm_type(expr, te, ftm) + + defp infer_expr_wasm_type({:unaryop, :negate, expr}, te, ftm), + do: infer_expr_wasm_type(expr, te, ftm) defp infer_expr_wasm_type({:call, name, _args}, _te, ftm) do case Map.get(ftm, name) do @@ -853,15 +907,21 @@ defmodule Firebird.Compiler.WATGen do end end - defp infer_expr_wasm_type({:if, _, then_b, _}, te, ftm), do: infer_expr_wasm_type(then_b, te, ftm) - defp infer_expr_wasm_type({:case, _, [{_, _, body} | _]}, te, ftm), do: infer_expr_wasm_type(body, te, ftm) + defp infer_expr_wasm_type({:if, _, then_b, _}, te, ftm), + do: infer_expr_wasm_type(then_b, te, ftm) + + defp infer_expr_wasm_type({:case, _, [{_, _, body} | _]}, te, ftm), + do: infer_expr_wasm_type(body, te, ftm) + defp infer_expr_wasm_type({:let, _, value}, te, ftm), do: infer_expr_wasm_type(value, te, ftm) defp infer_expr_wasm_type({:block, exprs}, te, ftm) when is_list(exprs) and length(exprs) > 0 do infer_expr_wasm_type(List.last(exprs), te, ftm) end - defp infer_expr_wasm_type({:tail_loop, _, body}, te, ftm), do: infer_expr_wasm_type(body, te, ftm) + defp infer_expr_wasm_type({:tail_loop, _, body}, te, ftm), + do: infer_expr_wasm_type(body, te, ftm) + defp infer_expr_wasm_type({:tail_call, name, _}, _te, ftm) do case Map.get(ftm, name) do %{return: ret} -> ret @@ -934,6 +994,7 @@ defmodule Firebird.Compiler.WATGen do defp indent(text, spaces) do pad = String.duplicate(" ", spaces) + text |> String.split("\n") |> Enum.map(fn line -> diff --git a/lib/firebird/errors.ex b/lib/firebird/errors.ex index a7dfbb0..8a1be3a 100644 --- a/lib/firebird/errors.ex +++ b/lib/firebird/errors.ex @@ -18,11 +18,11 @@ defmodule Firebird.WasmError do defexception [:message, :reason, :function, :wasm_path] @type t :: %__MODULE__{ - message: String.t(), - reason: term(), - function: String.t() | atom() | nil, - wasm_path: String.t() | nil - } + message: String.t(), + reason: term(), + function: String.t() | atom() | nil, + wasm_path: String.t() | nil + } @impl true def message(%{message: message}), do: message diff --git a/lib/firebird/fast_nif.ex b/lib/firebird/fast_nif.ex index 04fc8c0..bf7312a 100644 --- a/lib/firebird/fast_nif.ex +++ b/lib/firebird/fast_nif.ex @@ -18,33 +18,39 @@ defmodule Firebird.FastNif do @on_load :load_nif def load_nif do - nif_path = Path.join([ - Application.app_dir(:firebird, "priv"), - "native", - "libfirebird_nif" - ]) + nif_path = + Path.join([ + Application.app_dir(:firebird, "priv"), + "native", + "libfirebird_nif" + ]) # Also check the build directory - build_path = Path.join([ - File.cwd!(), - "native", - "firebird_nif", - "target", - "release", - "libfirebird_nif" - ]) - - path = if File.exists?(nif_path <> ".so") or File.exists?(nif_path <> ".dylib") do - nif_path - else - build_path - end + build_path = + Path.join([ + File.cwd!(), + "native", + "firebird_nif", + "target", + "release", + "libfirebird_nif" + ]) + + path = + if File.exists?(nif_path <> ".so") or File.exists?(nif_path <> ".dylib") do + nif_path + else + build_path + end case :erlang.load_nif(String.to_charlist(path), 0) do - :ok -> :ok + :ok -> + :ok + {:error, reason} -> IO.warn("Failed to load Firebird FastNif: #{inspect(reason)}") - :ok # Don't crash - fall back to Wasmex + # Don't crash - fall back to Wasmex + :ok end end @@ -79,10 +85,12 @@ defmodule Firebird.FastNif do load_wasm(<<>>) false rescue - ErlangError -> false # NIF loaded but bad args + # NIF loaded but bad args + ErlangError -> false catch :error, :nif_not_loaded -> false - :error, _ -> true # NIF loaded + # NIF loaded + :error, _ -> true end end end diff --git a/lib/firebird/hot_reload.ex b/lib/firebird/hot_reload.ex index 30f4b52..4df3108 100644 --- a/lib/firebird/hot_reload.ex +++ b/lib/firebird/hot_reload.ex @@ -37,7 +37,8 @@ defmodule Firebird.HotReload do defstruct [:path, :opts, :callback, :instance, :last_modified, :interval] - @default_interval 2_000 # 2 seconds + # 2 seconds + @default_interval 2_000 @doc """ Start a hot-reload watcher for a WASM file. @@ -52,6 +53,7 @@ defmodule Firebird.HotReload do """ def start_link(opts) do path = Keyword.fetch!(opts, :path) + unless File.exists?(path) do raise ArgumentError, "WASM file not found: #{path}" end @@ -117,14 +119,15 @@ defmodule Firebird.HotReload do {:ok, instance, mtime} -> schedule_check(interval) - {:ok, %__MODULE__{ - path: path, - opts: load_opts, - callback: callback, - instance: instance, - last_modified: mtime, - interval: interval - }} + {:ok, + %__MODULE__{ + path: path, + opts: load_opts, + callback: callback, + instance: instance, + last_modified: mtime, + interval: interval + }} {:error, reason} -> {:stop, reason} @@ -154,6 +157,7 @@ defmodule Firebird.HotReload do instance_alive: state.instance != nil && Process.alive?(state.instance), interval: state.interval } + {:reply, stats, state} end @@ -166,6 +170,7 @@ defmodule Firebird.HotReload do def handle_info({:DOWN, _ref, :process, pid, _reason}, %{instance: pid} = state) do Logger.warning("[Firebird.HotReload] WASM instance crashed, reloading...") + case do_reload(state) do {:ok, new_state} -> {:noreply, new_state} {:error, _} -> {:noreply, %{state | instance: nil}} @@ -179,6 +184,7 @@ defmodule Firebird.HotReload do if state.instance && Process.alive?(state.instance) do Firebird.stop(state.instance) end + :ok end @@ -201,8 +207,11 @@ defmodule Firebird.HotReload do case File.stat(state.path) do {:ok, %{mtime: mtime}} when mtime != state.last_modified -> Logger.info("[Firebird.HotReload] #{state.path} changed, reloading...") + case do_reload(state) do - {:ok, new_state} -> new_state + {:ok, new_state} -> + new_state + {:error, reason} -> Logger.error("[Firebird.HotReload] Reload failed: #{inspect(reason)}") state diff --git a/lib/firebird/inspector.ex b/lib/firebird/inspector.ex index 77ef71d..f1916f8 100644 --- a/lib/firebird/inspector.ex +++ b/lib/firebird/inspector.ex @@ -122,8 +122,14 @@ defmodule Firebird.Inspector do } _ -> - %{functions: [], memories: [], globals: [], size_bytes: byte_size(bytes), - export_count: 0, function_count: 0} + %{ + functions: [], + memories: [], + globals: [], + size_bytes: byte_size(bytes), + export_count: 0, + function_count: 0 + } end end diff --git a/lib/firebird/lazy.ex b/lib/firebird/lazy.ex index 97ed1d8..c34ec4b 100644 --- a/lib/firebird/lazy.ex +++ b/lib/firebird/lazy.ex @@ -67,6 +67,7 @@ defmodule Firebird.Lazy do @spec child_spec([option()]) :: Supervisor.child_spec() def child_spec(opts) do name = Keyword.get(opts, :name, __MODULE__) + %{ id: name, start: {__MODULE__, :start_link, [opts]}, @@ -91,7 +92,8 @@ defmodule Firebird.Lazy do end @doc "Call a WASM function and return an unwrapped single value." - @spec call_one(GenServer.server(), atom() | String.t(), list()) :: {:ok, term()} | {:error, term()} + @spec call_one(GenServer.server(), atom() | String.t(), list()) :: + {:ok, term()} | {:error, term()} def call_one(server, function, args) do case call(server, function, args) do {:ok, [single]} -> {:ok, single} diff --git a/lib/firebird/memory.ex b/lib/firebird/memory.ex index fe7ce19..78ff829 100644 --- a/lib/firebird/memory.ex +++ b/lib/firebird/memory.ex @@ -95,15 +95,15 @@ defmodule Firebird.Memory do @type allocation :: {non_neg_integer(), non_neg_integer()} @type t :: %__MODULE__{ - instance: pid(), - alignment: pos_integer(), - base_offset: non_neg_integer(), - cursor: non_neg_integer(), - known_memory_size: non_neg_integer() | nil, - memory_ref: term() | nil, - store_ref: term() | nil, - allocations: [allocation()] - } + instance: pid(), + alignment: pos_integer(), + base_offset: non_neg_integer(), + cursor: non_neg_integer(), + known_memory_size: non_neg_integer() | nil, + memory_ref: term() | nil, + store_ref: term() | nil, + allocations: [allocation()] + } # ── Constructor ────────────────────────────────────────────────────────── @@ -139,6 +139,7 @@ defmodule Firebird.Memory do {:ok, %{memory: mem, store: store}} -> size = Wasmex.Memory.size(store, mem) {mem, store, size} + _ -> {nil, nil, nil} end @@ -178,10 +179,7 @@ defmodule Firebird.Memory do mem = ensure_capacity(mem, new_cursor) entry = {offset, byte_size} - mem = %{mem | - cursor: new_cursor, - allocations: [entry | mem.allocations] - } + mem = %{mem | cursor: new_cursor, allocations: [entry | mem.allocations]} {mem, offset} end @@ -205,7 +203,8 @@ defmodule Firebird.Memory do This is a thin convenience over `Firebird.read_memory/3` that uses the allocator's bound instance. """ - @spec read_bytes(t(), non_neg_integer(), non_neg_integer()) :: {:ok, binary()} | {:error, term()} + @spec read_bytes(t(), non_neg_integer(), non_neg_integer()) :: + {:ok, binary()} | {:error, term()} def read_bytes(%__MODULE__{} = mem, offset, length) do read_memory_direct(mem, offset, length) end @@ -232,7 +231,8 @@ defmodule Firebird.Memory do def write_string(%__MODULE__{} = mem, string, opts \\ []) when is_binary(string) do null_term = Keyword.get(opts, :null_terminated, false) data = if null_term, do: string <> <<0>>, else: string - logical_len = byte_size(string) # length without NUL terminator + # length without NUL terminator + logical_len = byte_size(string) {mem, offset, _raw_size} = write_bytes(mem, data) {mem, offset, logical_len} end @@ -244,7 +244,8 @@ defmodule Firebird.Memory do {:ok, str} = Firebird.Memory.read_string(alloc, ptr, len) """ - @spec read_string(t(), non_neg_integer(), non_neg_integer()) :: {:ok, String.t()} | {:error, term()} + @spec read_string(t(), non_neg_integer(), non_neg_integer()) :: + {:ok, String.t()} | {:error, term()} def read_string(%__MODULE__{} = mem, offset, length) do case read_bytes(mem, offset, length) do {:ok, bytes} -> @@ -253,7 +254,9 @@ defmodule Firebird.Memory do else {:error, :invalid_utf8} end - error -> error + + error -> + error end end @@ -283,13 +286,16 @@ defmodule Firebird.Memory do {:ok, [1, 2, 3]} = Firebird.Memory.read_i32_array(alloc, ptr, 3) """ - @spec read_i32_array(t(), non_neg_integer(), non_neg_integer()) :: {:ok, [integer()]} | {:error, term()} + @spec read_i32_array(t(), non_neg_integer(), non_neg_integer()) :: + {:ok, [integer()]} | {:error, term()} def read_i32_array(%__MODULE__{} = mem, offset, count) when is_integer(count) and count >= 0 do case read_bytes(mem, offset, count * 4) do {:ok, bytes} -> values = for <