Context
Emily.Compiler with native: true lowers a traced Nx.Defn.Expr to flat IR and replays it in one NIF call. Anything the IR can't lower falls back to Nx.Defn.Evaluator (the default native_fallback: :eval) or raises (:raise, used by the conformance suite and scripts/test-livebooks-native.sh).
This issue inventories the ops that don't lower yet. Each one is reachable only if a user model uses it: none of the bundled livebooks (scripts/test-livebooks-native.sh passes with native_fallback: :raise) and none of the conformance suites hit any of these. Adding a lowering removes a silent-fallback cliff for user code.
How this list was produced
scripts/expr_op_coverage.exs builds a tiny Nx.Defn.jit over each Nx op (via the public Nx API), runs it through compiler: Emily.Compiler, native: true, native_fallback: :raise, and reports whether it lowered. Re-run after each PR to keep this list honest:
mix run scripts/expr_op_coverage.exs
The script's final section prints a markdown checklist in the same format used below.
Checklist
✅ All clusters complete. Probe is down to 2 misses, both in "Unlowerable by design" below.
Easy wins (unary elementwise — pattern is a single line in @unary_ops + opcode)
The IR's @unary_ops map (lib/emily/ir.ex:290) covers 16 ops. These are the missing peers — each has a Backend implementation already (Emily.Backend.@renamed_unary / @direct_unary), so the lowering is one map entry plus an opcode if MLX has the primitive:
Binary / compare gaps
Top-level Expr ops
Nx.Block structs
Linalg blocks (heavier — multi-output, mostly via mx::linalg::*)
Emily-internal blocks (gpt-oss attention sinks)
These don't appear in the probe script (it walks Nx-public ops only) but are reachable from Emily.Fast.attention_with_sinks:
Unlowerable by design (don't add to the checklist)
These will always raise under the native path — they need a host bridge mid-graph that the single-NIF replay can't provide. They are correctly handled today: :eval mode routes the whole defn through the Evaluator (which has its own per-op via_binary fallback), and :raise mode surfaces the limitation loudly.
Nx.reduce / Nx.window_reduce with a user-supplied BEAM reducer (lib/emily/ir.ex:950)
Nx.Defn.Kernel.hook / :attach_token with active hooks (lib/emily/ir.ex:936)
Nx.count_leading_zeros, Nx.population_count — MLX has no primitive (Backend also raises)
Out of scope for this probe
The script doesn't probe control flow (cond, while) or window_scatter_max/min directly because they need defn macro context, not bare Nx.Defn.jit. All three are already lowered (while via CM10/CM11, tuple cond via CM21, window_scatter_max/min via the CNN-training milestone) — coverage is verified by test/emily/compiler_while_test.exs, test/emily/compiler_control_flow_test.exs, and test/emily/backend_window_scatter_test.exs.
Prioritisation suggestion
The unary block is by far the largest cluster and individually the cheapest — each is a one-line addition to @unary_ops plus an opcode + C++ dispatch. Knocking those out first shrinks the list dramatically and removes the silent-fallback cliff for the most common user code (trig / numeric stability ops). The LinAlg blocks are the highest-value remaining gap for scientific-computing users but each needs more thought (multi-output, dtype matrix, MLX kernel coverage).
Context
Emily.Compilerwithnative: truelowers a tracedNx.Defn.Exprto flat IR and replays it in one NIF call. Anything the IR can't lower falls back toNx.Defn.Evaluator(the defaultnative_fallback: :eval) or raises (:raise, used by the conformance suite andscripts/test-livebooks-native.sh).This issue inventories the ops that don't lower yet. Each one is reachable only if a user model uses it: none of the bundled livebooks (
scripts/test-livebooks-native.shpasses withnative_fallback: :raise) and none of the conformance suites hit any of these. Adding a lowering removes a silent-fallback cliff for user code.How this list was produced
scripts/expr_op_coverage.exsbuilds a tinyNx.Defn.jitover each Nx op (via the public Nx API), runs it throughcompiler: Emily.Compiler, native: true, native_fallback: :raise, and reports whether it lowered. Re-run after each PR to keep this list honest:The script's final section prints a markdown checklist in the same format used below.
Checklist
Easy wins (unary elementwise — pattern is a single line in
@unary_ops+ opcode)The IR's
@unary_opsmap (lib/emily/ir.ex:290) covers 16 ops. These are the missing peers — each has a Backend implementation already (Emily.Backend.@renamed_unary/@direct_unary), so the lowering is one map entry plus an opcode if MLX has the primitive:expm1— landed in feat: lower 19 more unary ops in the native Expr compiler #189tan— landed in feat: lower 19 more unary ops in the native Expr compiler #189sinh— landed in feat: lower 19 more unary ops in the native Expr compiler #189cosh— landed in feat: lower 19 more unary ops in the native Expr compiler #189acos— landed in feat: lower 19 more unary ops in the native Expr compiler #189asin— landed in feat: lower 19 more unary ops in the native Expr compiler #189atan— landed in feat: lower 19 more unary ops in the native Expr compiler #189acosh— landed in feat: lower 19 more unary ops in the native Expr compiler #189asinh— landed in feat: lower 19 more unary ops in the native Expr compiler #189atanh— landed in feat: lower 19 more unary ops in the native Expr compiler #189cbrt(Backend composes:sign * abs^(1/3)) — landed in feat: lower 19 more unary ops in the native Expr compiler #189erfc(Backend composes:1 - erf) — landed in feat: lower 19 more unary ops in the native Expr compiler #189round— landed in feat: lower 19 more unary ops in the native Expr compiler #189bitwise_not— landed in feat: lower 19 more unary ops in the native Expr compiler #189is_nan— landed in feat: lower 19 more unary ops in the native Expr compiler #189is_infinity— landed in feat: lower 19 more unary ops in the native Expr compiler #189conjugate— landed in feat: lower 19 more unary ops in the native Expr compiler #189real— landed in feat: lower 19 more unary ops in the native Expr compiler #189imag— landed in feat: lower 19 more unary ops in the native Expr compiler #189Binary / compare gaps
atan2(Backend:arctan2) — landed in feat: lower atan2, quotient and logical_xor in the native Expr compiler #190quotient— landed in feat: lower atan2, quotient and logical_xor in the native Expr compiler #190logical_xor— landed in feat: lower atan2, quotient and logical_xor in the native Expr compiler #190Top-level Expr ops
pad— landed in feat: lower pad, eye and triangular_solve in the native Expr compiler #191eye— landed in feat: lower pad, eye and triangular_solve in the native Expr compiler #191triangular_solve— landed in feat: lower pad, eye and triangular_solve in the native Expr compiler #191Nx.Block structs
Nx.Block.LogicalNot— landed in feat: lower LogicalNot, AllClose and Phase blocks in the native Expr compiler #192Nx.Block.AllClose— landed in feat: lower LogicalNot, AllClose and Phase blocks in the native Expr compiler #192Nx.Block.Phase— landed in feat: lower LogicalNot, AllClose and Phase blocks in the native Expr compiler #192Linalg blocks (heavier — multi-output, mostly via
mx::linalg::*)Nx.Block.LinAlg.Cholesky— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Nx.Block.LinAlg.LU— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Nx.Block.LinAlg.QR— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Nx.Block.LinAlg.SVD— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Nx.Block.LinAlg.Eigh— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Nx.Block.LinAlg.Solve— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Nx.Block.LinAlg.Determinant— landed in feat: lower the Nx.Block.LinAlg.* family in the native Expr compiler #193Emily-internal blocks (gpt-oss attention sinks)
These don't appear in the probe script (it walks Nx-public ops only) but are reachable from
Emily.Fast.attention_with_sinks:Emily.Fast.Block.SDPAWithSinks— landed in feat: lower SDPA-with-sinks blocks in the native Expr compiler #194Emily.Fast.Block.SDPAWithMaskAndSinks— landed in feat: lower SDPA-with-sinks blocks in the native Expr compiler #194Unlowerable by design (don't add to the checklist)
These will always raise under the native path — they need a host bridge mid-graph that the single-NIF replay can't provide. They are correctly handled today:
:evalmode routes the whole defn through the Evaluator (which has its own per-opvia_binaryfallback), and:raisemode surfaces the limitation loudly.Nx.reduce/Nx.window_reducewith a user-supplied BEAM reducer (lib/emily/ir.ex:950)Nx.Defn.Kernel.hook/:attach_tokenwith active hooks (lib/emily/ir.ex:936)Nx.count_leading_zeros,Nx.population_count— MLX has no primitive (Backend also raises)Out of scope for this probe
The script doesn't probe control flow (
cond,while) orwindow_scatter_max/mindirectly because they needdefnmacro context, not bareNx.Defn.jit. All three are already lowered (whilevia CM10/CM11, tuplecondvia CM21,window_scatter_max/minvia the CNN-training milestone) — coverage is verified bytest/emily/compiler_while_test.exs,test/emily/compiler_control_flow_test.exs, andtest/emily/backend_window_scatter_test.exs.Prioritisation suggestion
The unary block is by far the largest cluster and individually the cheapest — each is a one-line addition to
@unary_opsplus an opcode + C++ dispatch. Knocking those out first shrinks the list dramatically and removes the silent-fallback cliff for the most common user code (trig / numeric stability ops). The LinAlg blocks are the highest-value remaining gap for scientific-computing users but each needs more thought (multi-output, dtype matrix, MLX kernel coverage).