Skip to content

Commit c2d8979

Browse files
committed
feat: clear errors for unsupported control flow (reduce fn / while)
- reduce / window_reduce with an arbitrary BEAM reducer raise a clear error (no silent fallback); the fixed-identity aggregates (sum/product/ reduce_max/reduce_min) lower natively as their own ops. - while and its :elem tuple projection raise a clear "deferred" error — the single-NIF replay has no loop construct, so a data-dependent while needs static-trip unrolling or a worker-side synced loop (a focused follow-up). defn while is not used by the core transformer forwards; decode/generation loops run in Elixir. Tests assert both raise clearly under the native compiler.
1 parent 4afbc0b commit c2d8979

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

lib/emily/ir.ex

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ defmodule Emily.IR do
195195

196196
alias Emily.Fast.Block, as: FB
197197
alias Emily.Quantization.Block, as: QB
198+
alias Nx.Defn.Tree
198199
alias Nx.Tensor, as: T
199200

200201
# Nx Expr op -> IR opcode. Arithmetic/bitwise cast both operands to the
@@ -547,7 +548,7 @@ defmodule Emily.IR do
547548
# hook the token is a no-op, so pass through to the inner expr. Hooks
548549
# would need a callback into Elixir mid-graph (program-split) — deferred.
549550
defp lower_op(%T{data: %Nx.Defn.Expr{op: :attach_token, args: [token, expr]}}, state) do
550-
if Nx.Defn.Tree.has_hooks?(token, %{}) do
551+
if Tree.has_hooks?(token, %{}) do
551552
raise ArgumentError,
552553
"Emily Expr compiler does not support hooks under native compilation " <>
553554
"(they require a mid-graph callback into Elixir)."
@@ -556,6 +557,27 @@ defmodule Emily.IR do
556557
lower_node(expr, state)
557558
end
558559

560+
# reduce / window_reduce with a user-supplied BEAM reducer cannot be
561+
# compiled — the reducer would have to run on the host mid-graph. The
562+
# fixed-identity aggregates (sum/product/max/min) are separate ops and
563+
# already lower natively; only an arbitrary reducer reaches here.
564+
defp lower_op(%T{data: %Nx.Defn.Expr{op: op}}, _state) when op in [:reduce, :window_reduce] do
565+
raise ArgumentError,
566+
"Emily Expr compiler cannot lower #{inspect(op)} with an arbitrary " <>
567+
"reducer function (it would require a host callback mid-graph; no " <>
568+
"fallback). Use the native aggregates (sum/product/reduce_max/" <>
569+
"reduce_min) where possible."
570+
end
571+
572+
# while / its tuple projection are deferred to a follow-up: the
573+
# single-NIF replay has no loop construct, so a data-dependent while
574+
# needs either static-trip unrolling or a worker-side synced loop.
575+
defp lower_op(%T{data: %Nx.Defn.Expr{op: op}}, _state) when op in [:while, :elem] do
576+
raise ArgumentError,
577+
"Emily Expr compiler does not yet lower #{inspect(op)} (defn `while` " <>
578+
"loops; deferred — the decode/generation loops run in Elixir today)."
579+
end
580+
559581
defp lower_op(%T{data: %Nx.Defn.Expr{op: op}}, _state) do
560582
raise ArgumentError,
561583
"Emily Expr compiler does not yet lower op #{inspect(op)} " <>

test/emily/compiler_control_flow_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ defmodule Emily.CompilerControlFlowTest do
3333
if Nx.greater(Nx.reduce_max(y), 5), do: Nx.divide(y, 2), else: y
3434
end
3535

36+
defn while_fn(x) do
37+
{_i, acc} =
38+
while {i = 0, acc = x}, i < 5 do
39+
{i + 1, Nx.multiply(acc, 2)}
40+
end
41+
42+
acc
43+
end
44+
3645
defp equiv(fun, x) do
3746
native = Nx.Defn.jit(fun, @native).(x)
3847
eval = Nx.Defn.jit(fun, @eval).(x)
@@ -60,4 +69,20 @@ defmodule Emily.CompilerControlFlowTest do
6069
end
6170
end
6271
end
72+
73+
describe "unsupported control flow raises (no silent fallback)" do
74+
test "arbitrary reduce/2 fn raises a clear error" do
75+
f = fn x -> Nx.reduce(x, 0.0, fn a, b -> Nx.add(a, b) end) end
76+
77+
assert_raise ArgumentError, ~r/arbitrary reducer/, fn ->
78+
Nx.Defn.jit(f, @native).(Nx.tensor([1.0, 2.0, 3.0], backend: Emily.Backend))
79+
end
80+
end
81+
82+
test "defn while raises (deferred)" do
83+
assert_raise ArgumentError, ~r/while/, fn ->
84+
Nx.Defn.jit(&while_fn/1, @native).(Nx.tensor([1.0, 2.0], backend: Emily.Backend))
85+
end
86+
end
87+
end
6388
end

0 commit comments

Comments
 (0)