Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 78 additions & 44 deletions lib/firebird/compiler/wat_gen.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

"""
Expand Down Expand Up @@ -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

"""
Expand Down Expand Up @@ -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)
Expand Down
134 changes: 134 additions & 0 deletions test/compiler/eqz_peephole_test.exs
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions test/compiler/wat_gen_edge_cases_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading