@@ -120,7 +120,11 @@ defmodule Emily.IR do
120120 flip: 70 ,
121121 # control flow: operands = initial loop-carried state; iattrs [[arity]];
122122 # subprograms [condition, body]. Multi-output (produces `arity` values).
123- while: 71
123+ while: 71 ,
124+ # RNG / dynamic indexing primitives
125+ bitcast: 72 ,
126+ erf_inv: 73 ,
127+ dyn_slice: 74
124128 }
125129
126130 # Quant mode string -> code; decoded by qmode_from_code in
@@ -256,7 +260,8 @@ defmodule Emily.IR do
256260 sigmoid: :sigmoid ,
257261 floor: :floor ,
258262 ceil: :ceil ,
259- erf: :erf
263+ erf: :erf ,
264+ erf_inv: :erf_inv
260265 }
261266
262267 @ doc """
@@ -358,6 +363,13 @@ defmodule Emily.IR do
358363 emit ( state , :astype , [ ra ] , [ [ dtype_code ( t . type ) ] ] )
359364 end
360365
366+ # bitcast: reinterpret the bytes as out.type (mirrors Emily.Backend.bitcast/2,
367+ # which calls mx::view). Used by the RNG path to turn random bits into floats.
368+ defp lower_op ( % T { data: % Nx.Defn.Expr { op: :bitcast , args: [ a ] } } = t , state ) do
369+ { ra , state } = lower_node ( a , state )
370+ emit ( state , :bitcast , [ ra ] , [ [ dtype_code ( t . type ) ] ] )
371+ end
372+
361373 defp lower_op ( % T { data: % Nx.Defn.Expr { op: :reshape , args: [ a ] } } = t , state ) do
362374 { ra , state } = lower_node ( a , state )
363375 emit ( state , :reshape , [ ra ] , [ Tuple . to_list ( t . shape ) ] )
@@ -510,24 +522,50 @@ defmodule Emily.IR do
510522 coerce ( r , t . type , state )
511523 end
512524
513- # slice(t, starts, lengths, strides): static integer starts only. Nx
514- # passes scalar-tensor starts for dynamic slicing; those depend on
515- # runtime values and are deferred (the decode offset becomes a runtime
516- # input in CM3). Stops = starts + lengths (see Emily.Backend.slice/5).
525+ # slice(t, starts, lengths, strides). Nx passes starts as integers (static
526+ # slice) or scalar tensors (dynamic slice — e.g. threefry indexing a
527+ # rotation table by the loop counter). Static starts -> mx::slice with
528+ # integer bounds. Dynamic starts -> mx::slice's dynamic-start overload via
529+ # the `dyn_slice` opcode (stride 1 only; the eager backend materialises the
530+ # start to a host int, but the compiled replay can't, so it threads the
531+ # start as a runtime s32 array).
517532 defp lower_op (
518533 % T { data: % Nx.Defn.Expr { op: :slice , args: [ a , starts , lengths , strides ] } } = t ,
519534 state
520535 ) do
521- unless Enum . all? ( starts , & is_integer / 1 ) do
522- raise ArgumentError ,
523- "Emily Expr compiler: dynamic (tensor) slice start indices are not yet " <>
524- "supported (they require a runtime input). Got: #{ inspect ( starts ) } "
525- end
526-
527536 { ra , state } = lower_node ( a , state )
528- stops = Enum . zip_with ( starts , lengths , fn st , l -> st + l end )
529- { r , state } = emit ( state , :slice , [ ra ] , [ starts , stops , strides ] )
530- coerce ( r , t . type , state )
537+
538+ if Enum . all? ( starts , & is_integer / 1 ) do
539+ stops = Enum . zip_with ( starts , lengths , fn st , l -> st + l end )
540+ { r , state } = emit ( state , :slice , [ ra ] , [ starts , stops , strides ] )
541+ coerce ( r , t . type , state )
542+ else
543+ unless Enum . all? ( strides , & ( & 1 == 1 ) ) do
544+ raise ArgumentError ,
545+ "Emily Expr compiler: dynamic (tensor) slice start indices are only " <>
546+ "supported with unit strides. Got strides: #{ inspect ( strides ) } "
547+ end
548+
549+ # Build the [ndim] s32 start array from the mixed int / scalar-tensor
550+ # starts (same machinery as the dynamic put_slice write). Each runtime
551+ # start is clamped to `[0, dim - length]` — MLX's dynamic slice reads
552+ # out of bounds, whereas Nx (XLA semantics) clamps the start so the
553+ # window stays in range; clamp is a no-op for the in-bounds starts the
554+ # threefry/RNG path produces.
555+ dims = Tuple . to_list ( a . shape )
556+
557+ { start_refs , state } =
558+ [ starts , lengths , dims ]
559+ |> Enum . zip ( )
560+ |> Enum . map_reduce ( state , fn { start , length , dim } , state ->
561+ { r , state } = lower_start_index ( start , state )
562+ clamp_start ( r , dim - length , state )
563+ end )
564+
565+ { start_arr , state } = emit ( state , :concatenate , start_refs , [ [ 0 ] ] )
566+ axes = Enum . to_list ( 0 .. ( length ( starts ) - 1 ) // 1 )
567+ emit_coerced ( state , :dyn_slice , [ ra , start_arr ] , [ axes , lengths ] , t . type )
568+ end
531569 end
532570
533571 # put_slice(src, start_indices, slice): write `slice` into `src` at
@@ -945,4 +983,28 @@ defmodule Emily.IR do
945983 { r , state } = emit ( state , :astype , [ r ] , [ [ dtype_code ( { :s , 32 } ) ] ] )
946984 emit ( state , :reshape , [ r ] , [ [ 1 ] ] )
947985 end
986+
987+ # Clamp a runtime s32 `[1]` dynamic-slice start to `[0, hi]`
988+ # (hi = dim - length), matching Nx/XLA dynamic-slice semantics — MLX's
989+ # dynamic slice would otherwise read out of bounds. Both bounds are static.
990+ defp clamp_start ( ref , hi , state ) do
991+ { lo_c , state } =
992+ materialize_const (
993+ Nx . tensor ( [ 0 ] , type: :s32 , backend: Nx.BinaryBackend ) ,
994+ { 1 } ,
995+ { :s , 32 } ,
996+ state
997+ )
998+
999+ { hi_c , state } =
1000+ materialize_const (
1001+ Nx . tensor ( [ hi ] , type: :s32 , backend: Nx.BinaryBackend ) ,
1002+ { 1 } ,
1003+ { :s , 32 } ,
1004+ state
1005+ )
1006+
1007+ { r , state } = emit ( state , :maximum , [ ref , lo_c ] )
1008+ emit ( state , :minimum , [ r , hi_c ] )
1009+ end
9481010end
0 commit comments