diff --git a/lib/firebird/compiler/wat_gen.ex b/lib/firebird/compiler/wat_gen.ex index 57b2be5..3c0d741 100644 --- a/lib/firebird/compiler/wat_gen.ex +++ b/lib/firebird/compiler/wat_gen.ex @@ -364,29 +364,43 @@ defmodule Firebird.Compiler.WATGen do coerce(wat, var_type, target_type) end - defp generate_expr({:binop, op, left, right}, target_type, ft, te, ftm) do - if op in [:and_, :or_] do - # Boolean ops always use i32 - {operand_type, result_type, wasm_op} = binop_info(op) - left_wat = generate_expr(left, operand_type, ft, te, ftm) - right_wat = generate_expr(right, operand_type, ft, te, ftm) - result = "#{left_wat}\n#{right_wat}\n#{wasm_op}" - coerce(result, result_type, target_type) + # --- Peephole: i64.eqz for zero-equality checks --- + # `expr == 0` → `expr; i64.eqz` (saves one i64.const 0 instruction) + # `0 == expr` → `expr; i64.eqz` (commutative) + # Only applies when the non-zero operand is i64 (not f64). + defp generate_expr({:binop, :eq, expr, {:literal, 0}}, target_type, ft, te, ftm) + when not is_float(0) do + expr_type = infer_expr_wasm_type(expr, te, ftm) + + if expr_type in [:i64, :i32] do + inner = generate_expr(expr, :i64, ft, te, ftm) + result = "#{inner}\ni64.eqz" + coerce(result, :i32, target_type) else - # Determine the natural type of each operand to select i64 vs f64 ops - left_type = infer_expr_wasm_type(left, te, ftm) - right_type = infer_expr_wasm_type(right, te, ftm) - effective_type = widen_numeric(left_type, right_type) + generate_binop_expr(:eq, expr, {:literal, 0}, target_type, ft, te, ftm) + end + end - {operand_type, result_type, wasm_op} = binop_info(op, effective_type) + defp generate_expr({:binop, :eq, {:literal, 0}, expr}, target_type, ft, te, ftm) + when not is_float(0) do + expr_type = infer_expr_wasm_type(expr, te, ftm) - left_wat = generate_expr(left, operand_type, ft, te, ftm) - right_wat = generate_expr(right, operand_type, ft, te, ftm) + if expr_type in [:i64, :i32] do + inner = generate_expr(expr, :i64, ft, te, ftm) + result = "#{inner}\ni64.eqz" + coerce(result, :i32, target_type) + else + generate_binop_expr(:eq, {:literal, 0}, expr, target_type, ft, te, ftm) + end + end - result = "#{left_wat}\n#{right_wat}\n#{wasm_op}" + # --- Peephole: i64.eqz + i32.eqz for nonzero checks --- + # `expr != 0` → `expr; i64.eqz; i32.eqz` isn't better (same instruction count). + # However, for i32 target type, `expr; i64.eqz; i32.eqz` can avoid a coerce. + # We skip this pattern and let the normal path handle it. - coerce(result, result_type, target_type) - end + defp generate_expr({:binop, op, left, right}, target_type, ft, te, ftm) do + generate_binop_expr(op, left, right, target_type, ft, te, ftm) end defp generate_expr({:unaryop, :not, expr}, target_type, ft, te, ftm) do @@ -1013,25 +1027,22 @@ defmodule Firebird.Compiler.WATGen do body_wat = generate_expr(body, target_type, ft, te, ftm) else_wat = generate_case_clauses(rest, subject, target_type, ft, te, ftm) + # Peephole: use i64.eqz for pattern matching against 0 + eq_wat = + if val == 0 do + "local.get #{subject}\ni64.eqz" + else + "local.get #{subject}\ni64.const #{val}\ni64.eq" + end + cond_wat = case guard do nil -> - """ - local.get #{subject} - i64.const #{val} - i64.eq\ - """ + eq_wat guard_expr -> guard_wat = generate_expr(guard_expr, :i32, ft, te, ftm) - - """ - local.get #{subject} - i64.const #{val} - i64.eq - #{guard_wat} - i32.and\ - """ + "#{eq_wat}\n#{guard_wat}\ni32.and" end """ @@ -1111,25 +1122,22 @@ defmodule Firebird.Compiler.WATGen do body_wat = generate_tco_body(body, target_type, ft, te, ftm) else_wat = generate_tco_case_clauses(rest, subject, target_type, ft, te, ftm) + # Peephole: use i64.eqz for pattern matching against 0 + eq_wat = + if val == 0 do + "local.get #{subject}\ni64.eqz" + else + "local.get #{subject}\ni64.const #{val}\ni64.eq" + end + cond_wat = case guard do nil -> - """ - local.get #{subject} - i64.const #{val} - i64.eq\ - """ + eq_wat guard_expr -> guard_wat = generate_expr(guard_expr, :i32, ft, te, ftm) - - """ - local.get #{subject} - i64.const #{val} - i64.eq - #{guard_wat} - i32.and\ - """ + "#{eq_wat}\n#{guard_wat}\ni32.and" end """ @@ -1273,6 +1281,32 @@ defmodule Firebird.Compiler.WATGen do defp widen_numeric(_, :f64), do: :f64 defp widen_numeric(a, _), do: a + # General binary operation code generation (extracted for peephole fallback) + defp generate_binop_expr(op, left, right, target_type, ft, te, ftm) do + if op in [:and_, :or_] do + # Boolean ops always use i32 + {operand_type, result_type, wasm_op} = binop_info(op) + left_wat = generate_expr(left, operand_type, ft, te, ftm) + right_wat = generate_expr(right, operand_type, ft, te, ftm) + result = "#{left_wat}\n#{right_wat}\n#{wasm_op}" + coerce(result, result_type, target_type) + else + # Determine the natural type of each operand to select i64 vs f64 ops + left_type = infer_expr_wasm_type(left, te, ftm) + right_type = infer_expr_wasm_type(right, te, ftm) + effective_type = widen_numeric(left_type, right_type) + + {operand_type, result_type, wasm_op} = binop_info(op, effective_type) + + left_wat = generate_expr(left, operand_type, ft, te, ftm) + right_wat = generate_expr(right, operand_type, ft, te, ftm) + + result = "#{left_wat}\n#{right_wat}\n#{wasm_op}" + + coerce(result, result_type, target_type) + end + end + # Binary operation info: {operand_type, result_type, wasm_instruction} # Type-aware version: selects i64 or f64 instructions based on effective_type defp binop_info(op, effective_type \\ :i64) diff --git a/test/compiler/eqz_peephole_test.exs b/test/compiler/eqz_peephole_test.exs new file mode 100644 index 0000000..c9472f0 --- /dev/null +++ b/test/compiler/eqz_peephole_test.exs @@ -0,0 +1,134 @@ +defmodule Firebird.Compiler.EqzPeepholeTest do + @moduledoc """ + Tests for i64.eqz peephole optimization in WAT generation. + + When comparing an i64 expression against literal 0 for equality, + the code generator should emit `i64.eqz` instead of `i64.const 0; i64.eq`, + saving one instruction. + """ + use ExUnit.Case, async: true + + alias Firebird.Compiler + + describe "i64.eqz peephole" do + test "eq 0 emits i64.eqz instead of i64.const 0 + i64.eq" do + source = """ + defmodule EqzTest1 do + @wasm true + def is_zero(x), do: if(x == 0, do: 1, else: 0) + end + """ + + {:ok, result} = Compiler.compile_source(source, wat_only: true) + wat = result.wat + + # Should contain i64.eqz + assert wat =~ "i64.eqz" + # Should NOT contain `i64.const 0` followed by `i64.eq` for the comparison + # (the function body should use eqz instead of const 0 + eq) + refute Regex.match?(~r/i64\.const 0\n\s*i64\.eq/, wat) + end + + test "0 == expr also emits i64.eqz (commutative)" do + source = """ + defmodule EqzTest2 do + @wasm true + def is_zero(x), do: if(0 == x, do: 1, else: 0) + end + """ + + {:ok, result} = Compiler.compile_source(source, wat_only: true) + wat = result.wat + + assert wat =~ "i64.eqz" + refute Regex.match?(~r/i64\.const 0\n\s*i64\.eq/, wat) + end + + test "eq with non-zero literal uses normal comparison" do + source = """ + defmodule EqzTest3 do + @wasm true + def is_five(x), do: if(x == 5, do: 1, else: 0) + end + """ + + {:ok, result} = Compiler.compile_source(source, wat_only: true) + wat = result.wat + + # Should NOT use eqz for non-zero comparisons + assert wat =~ "i64.const 5" + assert wat =~ "i64.eq" + end + + test "eqz peephole works in case expressions" do + source = """ + defmodule EqzTest4 do + @wasm true + def check(x) do + case x do + 0 -> 100 + _ -> 200 + end + end + end + """ + + {:ok, result} = Compiler.compile_source(source, wat_only: true) + wat = result.wat + + # Case dispatch for pattern 0 should use i64.eqz + assert wat =~ "i64.eqz" + end + + test "eqz peephole with optimizations enabled" do + source = """ + defmodule EqzTest5 do + @wasm true + def is_even(n), do: if(rem(n, 2) == 0, do: 1, else: 0) + end + """ + + {:ok, result} = Compiler.compile_source(source, wat_only: true, optimize: true) + wat = result.wat + + # After optimization rem(n,2) becomes band(n,1), then == 0 should use eqz + assert wat =~ "i64.eqz" + end + + test "eqz produces valid WASM binary" do + source = """ + defmodule EqzTest6 do + @wasm true + def is_zero(x), do: if(x == 0, do: 1, else: 0) + + @wasm true + def check_zero_left(x), do: if(0 == x, do: 1, else: 0) + end + """ + + {:ok, result} = Compiler.compile_source(source) + # The WAT should compile to valid WASM binary + assert result.wasm != nil + assert is_binary(result.wasm) + assert byte_size(result.wasm) > 0 + + # WAT should use eqz + assert result.wat =~ "i64.eqz" + end + + test "ne 0 does not use eqz (no benefit)" do + source = """ + defmodule EqzTest7 do + @wasm true + def is_nonzero(x), do: if(x != 0, do: 1, else: 0) + end + """ + + {:ok, result} = Compiler.compile_source(source, wat_only: true) + wat = result.wat + + # ne 0 should use the normal path (i64.ne), not eqz + assert wat =~ "i64.ne" or wat =~ "i64.eqz" + end + end +end diff --git a/test/compiler/wat_gen_edge_cases_test.exs b/test/compiler/wat_gen_edge_cases_test.exs index 1fa4962..fd4fbab 100644 --- a/test/compiler/wat_gen_edge_cases_test.exs +++ b/test/compiler/wat_gen_edge_cases_test.exs @@ -50,8 +50,8 @@ defmodule Firebird.Compiler.WATGenEdgeCasesTest do assert {:ok, wat} = WATGen.generate(module) assert wat =~ "local.set $__case_subject" assert wat =~ "local.get $__case_subject" - assert wat =~ "i64.const 0" - assert wat =~ "i64.eq" + # Pattern 0 uses i64.eqz peephole instead of i64.const 0 + i64.eq + assert wat =~ "i64.eqz" assert wat =~ "i64.const 100" assert wat =~ "i64.const 200" assert wat =~ "i64.const 300"