@@ -158,7 +158,31 @@ defmodule Emily.IR do
158158 # idx0, ...] (one s32 index array per scattered axis); iattrs [[axes...]].
159159 # scatter overwrites (last-write on duplicates); scatter_add accumulates.
160160 scatter: 92 ,
161- scatter_add: 93
161+ scatter_add: 93 ,
162+ # Unary elementwise (round 2 — the missing Nx ops alongside the
163+ # original @unary_ops set). Names mirror the eager unary NIF names
164+ # (c_src/ops/unary.cpp) so the IR opcode atom matches the MLX entry
165+ # point — Nx renaming (e.g. acos -> arccos, is_nan -> isnan) happens
166+ # in @unary_ops below, just like the eager Backend's @renamed_unary.
167+ expm1: 94 ,
168+ tan: 95 ,
169+ sinh: 96 ,
170+ cosh: 97 ,
171+ arccos: 98 ,
172+ arcsin: 99 ,
173+ arctan: 100 ,
174+ arccosh: 101 ,
175+ arcsinh: 102 ,
176+ arctanh: 103 ,
177+ # round/0 — Backend hard-codes mx::round's decimals to 0
178+ # (Nx.round/1 takes no decimals arg). Dispatcher does the same.
179+ round: 104 ,
180+ bitwise_invert: 105 ,
181+ isnan: 106 ,
182+ isinf: 107 ,
183+ conjugate: 108 ,
184+ real: 109 ,
185+ imag: 110
162186 }
163187
164188 # Quant mode string -> code; decoded by qmode_from_code in
@@ -287,23 +311,44 @@ defmodule Emily.IR do
287311 }
288312
289313 # Unary elementwise: no coercion (MLX preserves the dtype Nx expects).
314+ # The post-emit `coerce/3` then astype-casts to out.type so MLX ops
315+ # whose dtype rule differs from Nx (e.g. `is_nan`/`is_infinity` returning
316+ # a bool that Nx wants as {:u, 8}, or `real`/`imag` whose Nx out.type is
317+ # the real component) line up — same machinery as the original 16 ops.
290318 @ unary_ops % {
291319 negate: :negative ,
292320 abs: :abs ,
293321 sign: :sign ,
294322 sqrt: :sqrt ,
295323 rsqrt: :rsqrt ,
296324 exp: :exp ,
325+ expm1: :expm1 ,
297326 log: :log ,
298327 log1p: :log1p ,
299328 sin: :sin ,
300329 cos: :cos ,
330+ tan: :tan ,
301331 tanh: :tanh ,
332+ sinh: :sinh ,
333+ cosh: :cosh ,
334+ acos: :arccos ,
335+ asin: :arcsin ,
336+ atan: :arctan ,
337+ acosh: :arccosh ,
338+ asinh: :arcsinh ,
339+ atanh: :arctanh ,
302340 sigmoid: :sigmoid ,
303341 floor: :floor ,
304342 ceil: :ceil ,
343+ round: :round ,
305344 erf: :erf ,
306- erf_inv: :erf_inv
345+ erf_inv: :erf_inv ,
346+ bitwise_not: :bitwise_invert ,
347+ is_nan: :isnan ,
348+ is_infinity: :isinf ,
349+ conjugate: :conjugate ,
350+ real: :real ,
351+ imag: :imag
307352 }
308353
309354 @ doc """
@@ -405,6 +450,30 @@ defmodule Emily.IR do
405450 emit ( state , :astype , [ ra ] , [ [ dtype_code ( t . type ) ] ] )
406451 end
407452
453+ # erfc(x) := 1 - erf(x). Mirrors Emily.Backend.erfc/2 — MLX has no
454+ # erfc primitive, so the eager path also composes from erf + subtract.
455+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :erfc , args: [ a ] } } = t , state ) do
456+ { ra , state } = lower_node ( a , state )
457+ { erf_r , state } = emit ( state , :erf , [ ra ] )
458+ { one_ref , state } = scalar_const ( 1.0 , t . type , state )
459+ { r , state } = emit ( state , :subtract , [ one_ref , erf_r ] )
460+ coerce ( r , t . type , state )
461+ end
462+
463+ # cbrt(x) := sign(x) * abs(x)^(1/3). Mirrors Emily.Backend.cbrt/2 —
464+ # MLX has no cbrt primitive. Splitting via sign+abs keeps the negative
465+ # branch correct (`x^(1/3)` over negatives lands in complex), matching
466+ # the eager path's bit pattern.
467+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :cbrt , args: [ a ] } } = t , state ) do
468+ { ra , state } = lower_node ( a , state )
469+ { sign_r , state } = emit ( state , :sign , [ ra ] )
470+ { abs_r , state } = emit ( state , :abs , [ ra ] )
471+ { third , state } = scalar_const ( 1.0 / 3.0 , t . type , state )
472+ { pow_r , state } = emit ( state , :power , [ abs_r , third ] )
473+ { r , state } = emit ( state , :multiply , [ sign_r , pow_r ] )
474+ coerce ( r , t . type , state )
475+ end
476+
408477 # bitcast: reinterpret the bytes as out.type (mirrors Emily.Backend.bitcast/2,
409478 # which calls mx::view). Used by the RNG path to turn random bits into floats.
410479 defp lower_op ( % T { data: % Nx.Defn.Expr { op: :bitcast , args: [ a ] } } = t , state ) do
@@ -1343,6 +1412,13 @@ defmodule Emily.IR do
13431412 { { :const , idx } , % { state | consts: [ ref | state . consts ] , n_consts: idx + 1 } }
13441413 end
13451414
1415+ # Bake a `{}` scalar of `type` as a captured const operand. Used by the
1416+ # composite lowerers (erfc, cbrt) whose Backend mirrors build the same
1417+ # scalar through `scalar_ref/2`.
1418+ defp scalar_const ( value , type , state ) do
1419+ materialize_const ( Nx . tensor ( value , type: type , backend: Nx.BinaryBackend ) , { } , type , state )
1420+ end
1421+
13461422 defp materialize_capture ( tensor , shape , type , state ) do
13471423 ref = Emily.Native . from_binary ( Nx . to_binary ( tensor ) , Tuple . to_list ( shape ) , type )
13481424 idx = state . n_captures
0 commit comments