@@ -188,7 +188,15 @@ defmodule Emily.IR do
188188 # so the IR routes Nx :quotient through it (cast both to out.type,
189189 # floor_divide, same bits as the eager Backend).
190190 arctan2: 111 ,
191- floor_divide: 112
191+ floor_divide: 112 ,
192+ # Constant-aware pad (operands [input, pad_value]; iattrs [axes, lows,
193+ # highs]); MLX has no interior dilation so the lowerer rejects
194+ # interior > 0, same as Emily.Backend.pad/4.
195+ pad: 113 ,
196+ # CPU-only triangular solve (operands [a, b]; iattrs [[upper]]); the
197+ # lowerer handles transform_a/left_side by transposing a/b/output
198+ # around this bare kernel call, mirroring Emily.Backend.triangular_solve/4.
199+ linalg_solve_triangular: 114
192200 }
193201
194202 # Quant mode string -> code; decoded by qmode_from_code in
@@ -829,6 +837,16 @@ defmodule Emily.IR do
829837 |> materialize_const ( t . shape , t . type , state )
830838 end
831839
840+ # eye: same shape as iota — a pure creation op with all-static shape /
841+ # type. Materialize as a captured constant via Nx.eye on the host
842+ # backend (which already handles the rank > 2 batch case, identity on
843+ # the trailing two axes — matching Emily.Backend.eye/2's broadcast).
844+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :eye , args: [ ] } } = t , state ) do
845+ t . shape
846+ |> Nx . eye ( type: t . type , backend: Nx.BinaryBackend )
847+ |> materialize_const ( t . shape , t . type , state )
848+ end
849+
832850 # Nx.block node: args [struct, in_args, expr, callback]. Known fused
833851 # structs lower to their fused opcode (matching Emily.Backend.block/4,
834852 # which the Evaluator dispatches through); unknown structs lower by
@@ -945,6 +963,72 @@ defmodule Emily.IR do
945963 emit_coerced ( state , op , [ rt , rs , ri ] , [ window , strides , pad_lo , pad_hi ] , t . type )
946964 end
947965
966+ # pad(input, pad_value, padding_config): constant-pad each axis by
967+ # `padding_config = [{lo, hi, interior}, ...]`. Mirrors
968+ # Emily.Backend.pad/4 — MLX has no interior dilation, so interior > 0
969+ # raises (no fallback to interior expansion). The pad_value is an Expr
970+ # scalar tensor and lowers as an operand (input scalars baked at lower
971+ # time end up as a const ref; runtime scalars as inputs — both routes
972+ # work because the pad opcode takes the value as operand[1]).
973+ defp lower_op (
974+ % T { data: % Nx.Defn.Expr { op: :pad , args: [ a , pad_value , padding_config ] } } = t ,
975+ state
976+ ) do
977+ lows = Enum . map ( padding_config , fn { lo , _ , _ } -> lo end )
978+ highs = Enum . map ( padding_config , fn { _ , hi , _ } -> hi end )
979+ interiors = Enum . map ( padding_config , fn { _ , _ , interior } -> interior end )
980+
981+ if Enum . any? ( interiors , & ( & 1 > 0 ) ) do
982+ raise ArgumentError ,
983+ "Emily Expr compiler does not lower :pad with interior > 0 " <>
984+ "(MLX has no primitive; Emily.Backend.pad/4 also raises)."
985+ end
986+
987+ axes = Enum . to_list ( 0 .. ( length ( lows ) - 1 ) // 1 )
988+ { ra , state } = lower_node ( a , state )
989+ { rp , state } = lower_node ( pad_value , state )
990+ emit_coerced ( state , :pad , [ ra , rp ] , [ axes , lows , highs ] , t . type )
991+ end
992+
993+ # triangular_solve(a, b, opts): solve A x = b (or x A = b) with A
994+ # triangular. Mirrors Emily.Backend.triangular_solve/4 — the bare kernel
995+ # is operands [a, b] + [[upper]], and the four `transform_a` /
996+ # `left_side` combinations are decomposed here into transposes around
997+ # the kernel call (same Native.transpose sequence the eager Backend
998+ # uses). MLX's mx::linalg::solve_triangular runs on the CPU stream,
999+ # which the C++ dispatcher overrides per call.
1000+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :triangular_solve , args: [ a , b , opts ] } } = t , state ) do
1001+ { ra , state } = lower_node ( a , state )
1002+ { rb , state } = lower_node ( b , state )
1003+ lower = opts [ :lower ]
1004+
1005+ { r , state } =
1006+ case { opts [ :transform_a ] , opts [ :left_side ] } do
1007+ { :none , true } ->
1008+ emit ( state , :linalg_solve_triangular , [ ra , rb ] , [ [ bool_int ( not lower ) ] ] )
1009+
1010+ { :transpose , true } ->
1011+ { ra_t , state } = emit ( state , :transpose , [ ra ] , [ mat_transpose_axes ( a . shape ) ] )
1012+ emit ( state , :linalg_solve_triangular , [ ra_t , rb ] , [ [ bool_int ( lower ) ] ] )
1013+
1014+ { :none , false } ->
1015+ { ra_t , state } = emit ( state , :transpose , [ ra ] , [ mat_transpose_axes ( a . shape ) ] )
1016+ { rb_t , state } = emit ( state , :transpose , [ rb ] , [ mat_transpose_axes ( b . shape ) ] )
1017+ { xt , state } = emit ( state , :linalg_solve_triangular , [ ra_t , rb_t ] , [ [ bool_int ( lower ) ] ] )
1018+ emit ( state , :transpose , [ xt ] , [ mat_transpose_axes ( t . shape ) ] )
1019+
1020+ { :transpose , false } ->
1021+ { rb_t , state } = emit ( state , :transpose , [ rb ] , [ mat_transpose_axes ( b . shape ) ] )
1022+
1023+ { xt , state } =
1024+ emit ( state , :linalg_solve_triangular , [ ra , rb_t ] , [ [ bool_int ( not lower ) ] ] )
1025+
1026+ emit ( state , :transpose , [ xt ] , [ mat_transpose_axes ( t . shape ) ] )
1027+ end
1028+
1029+ coerce ( r , t . type , state )
1030+ end
1031+
9481032 # Nx.reverse along one or more axes (the conv backward flips the kernel).
9491033 # Reversing is order-independent across axes, so chain a single-axis
9501034 # `flip` (mx negative-stride slice) per axis. Empty axes => identity.
@@ -1364,6 +1448,13 @@ defmodule Emily.IR do
13641448
13651449 defp dim_product ( axes , shape ) , do: Enum . reduce ( axes , 1 , & ( elem ( shape , & 1 ) * & 2 ) )
13661450
1451+ # The "matrix transpose" axes permutation: keep leading batch axes, swap
1452+ # the trailing two. Mirrors Emily.Backend.mat_transpose_axes/1.
1453+ defp mat_transpose_axes ( shape ) do
1454+ rank = tuple_size ( shape )
1455+ Enum . to_list ( 0 .. ( rank - 3 ) // 1 ) ++ [ rank - 1 , rank - 2 ]
1456+ end
1457+
13671458 # Coerce a ref to `type` (emit an astype). MLX astype to the same dtype
13681459 # is a no-op, so this is safe to apply unconditionally — it mirrors
13691460 # Emily.Backend.wrap/3's coerce and keeps the node's dtype exact.
0 commit comments