#nullable enable using System; using System.Collections.Generic; namespace IlInterpreter.Interpreter { // Typed IR opcodes for the lowered-IR interpreter. // // Type suffixes: // _i4 32-bit int (includes bool as 0/1) // _r4 32-bit float // _o object reference (string, ScriptObject, host ref, null) // _struct boxed value type (flat blittable structs use the _vt ops instead) // // Encoding (all in uint[]): // [op, dst, src1, src2] arithmetic, compare // [op, dst, imm] ldc_i4, ldc_r4, ldnull, ldstr // [op, target_ip] br // [op, cond, target_ip] brtrue_i4, brfalse_i4, brtrue_o, brfalse_o // [op, dst, recv, argc, arg0, ...] call_static, call_virtual, call_host, newobj // [op, dst, src] mov, conv_*, ldfld_*, ldsfld_*, ldlen, newarr, ldelem_*, neg_* // [op, recv, fldIdx/tok, src] stfld_*, stsfld_* // [op, dst, arr, idx] ldelem_* (arr=slot, idx=slot) // [op, arr, idx, src] stelem_* // [op] ret_void // [op, src] ret_i4, ret_r4, ret_o enum Op : uint { // --- No-op (usually elided during lowering) --- nop, // --- Move (type-agnostic; routes between frames by slot type) --- mov, // [op, dst, src] // --- Constants --- ldc_i4, // [op, dst, imm_value] imm is the int value cast to uint ldc_r4, // [op, dst, imm_bits] imm is BitConverter.SingleToUInt32Bits(f) ldnull, // [op, dst] ldstr, // [op, dst, str_idx] str_idx indexes into LoweredMethod.Strings // --- Integer arithmetic (i4) --- add_i4, // [op, dst, src1, src2] add_i4_nn, // both srcs in numFrame as I4 sub_i4, sub_i4_nn, mul_i4, mul_i4_nn, div_i4, div_i4_nn, rem_i4, rem_i4_nn, neg_i4, // [op, dst, src] neg_i4_n, // src in numFrame as I4 // --- Float arithmetic (r4) --- add_r4, // [op, dst, src1, src2] add_r4_nn, // both srcs in numFrame as R4 sub_r4, sub_r4_nn, mul_r4, mul_r4_nn, div_r4, div_r4_nn, rem_r4, rem_r4_nn, neg_r4, // [op, dst, src] neg_r4_n, // src in numFrame as R4 // --- Bitwise (i4 only; float has no bitwise) --- and_i4, // [op, dst, src1, src2] and_i4_nn, or_i4, or_i4_nn, xor_i4, xor_i4_nn, not_i4, // [op, dst, src] not_i4_n, shl_i4, // [op, dst, src1, src2] shl_i4_nn, shr_i4, shr_i4_nn, shr_un_i4, shr_un_i4_nn, div_un_i4, // unsigned div/rem: uint is analyzer-legal, so `u / 2u` must run div_un_i4_nn, rem_un_i4, rem_un_i4_nn, // --- Integer comparisons (produce 1/0 int) --- ceq_i4, // [op, dst, src1, src2] ceq_i4_nn, cgt_i4, cgt_i4_nn, clt_i4, clt_i4_nn, cgt_un_i4, cgt_un_i4_nn, clt_un_i4, clt_un_i4_nn, // --- Float comparisons --- ceq_r4, // [op, dst, src1, src2] ceq_r4_nn, cgt_r4, cgt_r4_nn, clt_r4, clt_r4_nn, // Unordered float comparisons — what C# emits for float `<=`/`>=` (via cgt.un/clt.un). // cgt_un_r4 = !(a <= b) (true when a > b OR either is NaN); clt_un_r4 = !(a >= b). cgt_un_r4, // [op, dst, src1, src2] clt_un_r4, // [op, dst, src1, src2] // --- Object/reference comparisons --- ceq_o, // [op, dst, src1, src2] — reference equality cgt_un_o, // [op, dst, src1, src2] — cgt.un on object (null check pattern) // --- Branches --- br, // [op, target_ip] brtrue_i4, // [op, cond, target_ip] brfalse_i4, // [op, cond, target_ip] brtrue_o, // [op, cond, target_ip] brfalse_o, // [op, cond, target_ip] // --- Conversions --- conv_i4_r4, // [op, dst, src] int → float conv_r4_i4, // [op, dst, src] float → int conv_i4_i1, // [op, dst, src] int → sbyte (sign-extend) conv_i4_u1, // [op, dst, src] int → byte (zero-extend) conv_i4_i2, // [op, dst, src] int → short conv_i4_u2, // [op, dst, src] int → ushort // --- Host field access (via token + reflection-style lookup) --- // dst = frame slot; obj = frame slot holding receiver; tok = field token index ldfld_i4, // [op, dst, obj, tok_idx] ldfld_r4, ldfld_o, ldfld_struct, ldflda, // [op, dst, obj, tok_idx] — load field value (same as ldfld_o) stfld_i4, // [op, obj, tok_idx, src] stfld_r4, stfld_o, stfld_struct, // --- Script-class field access (flat typed storage) --- // For script-defined classes, fields are stored in ScriptObject.PrimBytes (primitives) // or ScriptObject.RefSlots (references). Byte offset is baked at lowering time. // ldfld_sc_i4/r4: [op, dst, obj, byteOffset] — read from PrimBytes // ldfld_sc_o: [op, dst, obj, refIdx] — read from RefSlots // stfld_sc_i4/r4: [op, obj, byteOffset, src] — write to PrimBytes // stfld_sc_o: [op, obj, refIdx, src] — write to RefSlots // ldfld_sc_vt: [op, dst, obj, byteOffset] — copy N bytes from PrimBytes into Vt slot // stfld_sc_vt: [op, obj, byteOffset, src] — copy N bytes from Vt slot into PrimBytes ldfld_sc_i4, ldfld_sc_r4, ldfld_sc_o, ldfld_sc_vt, stfld_sc_i4, stfld_sc_r4, stfld_sc_o, stfld_sc_vt, // --- Static field access --- ldsfld_i4, // [op, dst, tok_idx] ldsfld_r4, ldsfld_o, ldsfld_struct, stsfld_i4, // [op, tok_idx, src] stsfld_r4, stsfld_o, stsfld_struct, // --- Arrays --- newarr, // [op, dst, len_src, elem_tok_idx] // Typed-backing variants (same 5-word encoding as newarr): when the element type is // statically System.Int32 / System.Single the executor allocates a real int[]/float[] // instead of object?[], and ldelem/stelem take non-boxing direct paths — an int store // into an object?[] element boxes (GC pressure in hot Update bodies) and Mono pays ~3x // CoreCLR on that path. newarr_i4, newarr_r4, // Flat-struct element arrays (ScriptVtArray backing): zero-init bytes = C# array semantics, // no per-element allocation. Same 5-word encoding as newarr; elem layout resolved at lowering // into LoweredMethod.LayoutByTokIdx. newarr_vt, ldlen, // [op, dst, arr_src] ldelem_i4, // [op, dst, arr, idx] ldelem_r4, ldelem_o, ldelem_struct, stelem_i4, // [op, arr, idx, src] stelem_r4, stelem_o, stelem_struct, // Flat-struct element access: memcpy between the array's byte backing and a Vt frame slot // (value semantics without clone_sc). Fall back to boxed forms for object?[]/host T[] arrays. ldelem_vt, // [op, dst(Vt), arr, idx] stelem_vt, // [op, arr, idx, src(Vt)] // --- Calls --- // [op, dst, recv, argc, arg0, arg1, ...] // dst = 0xFFFFFFFF for void calls (ret discarded) // recv = 0xFFFFFFFF for static calls call_script, // callee is a script-defined method; tok_idx in recv slot (reuse) call_host, // callee is a host entry // call_host with byref args. After arg0..argN comes a writeback table: // wbCount, [argIdx, frameSlot, kind] × wbCount // kind: 0 = frame slot, 1 = field (frameSlot=objSlot, plus tokIdx in next word), // 2 = array element (frameSlot=arrSlot, plus idxSlot in next word). // The slow Invoke path is always used (Fast skipped) so MethodInfo.Invoke writes // back into the boxed args[] array, which we then copy into the right destination. call_host_byref, // --- Object construction --- newobj_script, // [op, dst, tok_idx, argc, arg0, ...] script-defined type ctor newobj_host, // [op, dst, tok_idx, argc, arg0, ...] host type ctor box_enum, // [op, dst, src, tok_idx] box an I4 value as its HOST ENUM type (resolved // via ParsedAssembly.TokenTypes) so ToString/Format render the member name new_delegate, // [op, dst, tok_idx, recv] delegate creation (ldftn/ldvirtftn + newobj); // tok_idx indexes LoweredMethod.DelegateSiteByTokIdx (pre-resolved target + // delegate type); recv is the receiver's O slot (holds null for statics) // --- Misc --- box, // [op, dst, src] — read slot as object into refFrame box_prim, // [op, dst, src, typecode] — box a primitive as its TRUE type (bool/char/… not int) unbox_any, // [op, dst, src, tok_idx] castclass, // [op, dst, src, tok_idx] isinst, // [op, dst, src, tok_idx] ldtoken, // [op, dst, tok_idx] initobj, // [op, dst] — zero-initialize slot (for script struct locals) switch_i4, // [op, val, n, ip0, ip1, ...] variable length; ip_default follows // --- Return --- ret_void, // [op] ret_i4, // [op, src] ret_r4, // [op, src] ret_o, // [op, src] throw_o, // [op, src] — `throw` of a script-constructed exception; ends the block like ret // Flat-struct return: reads the callee's Vt src bytes, memcpys them into the caller's // (Vt) RetDst slot on frame pop; boxes once via the layout at the top-level boundary. ret_vt, // [op, src(Vt)] // Zero-initialize a script-defined struct (O slot) without invoking a ctor. // Used when the IL emits ldloca + initobj for a parameterless default ctor. initobj_script, // [op, dst, typeDefTokIdx] // Ensure a script-defined struct O slot is non-null. Allocates a fresh zero-initialized // ScriptObject only if the slot is currently null; leaves it untouched otherwise. // Emitted by the stfld lowerer before field writes on struct locals that Roslyn considers // "definitely assigned by field stores" (so no initobj in the IL) — our O slots start null. ensure_script, // [op, dst, typeDefTokIdx] // Value-copy a script-defined struct: dst = shallow clone of the ScriptObject in src // (copies PrimBytes + RefSlots). Emitted when a script struct is loaded as a VALUE // (ldloc/ldarg/ldfld/ldelem) so each named location owns its instance — script structs // are represented as ScriptObject references, so without this a copy would alias. A null // or non-ScriptObject src is passed through unchanged. clone_sc, // [op, dst, src] // --- Flat-struct (Vt slots): read/write primitive fields by byte offset, copy/box at boundary --- // Vt slots reserve ceil(size/4) consecutive logical slots in numFrame. // ldfld_vt_*/stfld_vt_* take a byte offset (within the Vt slot) instead of a token, // since the field's offset is resolved at lowering time. // box_vt/unbox_vt/mov_vt look up the StructLayout via lm.StructLayouts[src or dst]. ldfld_vt_i4, // [op, dst, vtObj, byteOffset] ldfld_vt_r4, // [op, dst, vtObj, byteOffset] stfld_vt_i4, // [op, vtObj, byteOffset, src] stfld_vt_r4, // [op, vtObj, byteOffset, src] mov_vt, // [op, dst, src] — copy struct bytes (size from layout) // Nested flat-struct field access: copy a SUB-RANGE of a Vt slot (a blittable struct field // inlined inside another flat struct). Offsets are byte offsets within the receiver slot; // the sub-struct's size comes from the dst/src slot's own layout. ldfld_vt_vt, // [op, dst(Vt), vtObj, byteOffset] — memcpy size(dst) bytes out stfld_vt_vt, // [op, vtObj, byteOffset, src(Vt)] — memcpy size(src) bytes in // Sub-4-byte fields of flat host structs (Color32.r-class): loads widen into an I4 slot // (zero- or sign-extended); stores truncate. Script structs never need these — the parser // gives every script field its own 4-byte cell. ldfld_vt_u1, // [op, dst(I4), vtObj, byteOffset] ldfld_vt_i1, ldfld_vt_u2, ldfld_vt_i2, stfld_vt_b1, // [op, vtObj, byteOffset, src] — writes low byte stfld_vt_b2, // [op, vtObj, byteOffset, src] — writes low 2 bytes box_vt, // [op, dst, src] — read Vt bytes, box as T into refFrame[dst] unbox_vt, // [op, dst, src] — unbox refFrame[src] into Vt dst bytes // Reuse existing newobj_host / call_host opcodes; executor branches on dst's slot type. // --- Operand-immediate forms: RHS literal embedded in the IR word --- // Encoding: [op, dst, src, k_imm] (4 words). k_imm is the constant value cast to uint // (BitConverter.SingleToUInt32Bits for floats). Executor reads it directly; no separate // K-table lookup. Emitted by the lowerer when an `ldc_X tmp; binop_X_nn dst, src, tmp` // pattern has tmp single-use. Subset is corpus-driven — only ops that show up in bench // loops; expand on demand. Compare ops cover RHS-constant only (the canonical loop-bound // pattern); LHS-constant doesn't appear in the bench corpus. add_i4_nk, // [op, dst, src, k_int] add_r4_nk, // [op, dst, src, k_bits] sub_i4_nk, sub_r4_nk, mul_r4_nk, clt_i4_nk, clt_r4_nk, cgt_i4_nk, cgt_r4_nk, ceq_i4_nk, // Second _nk batch (profiling-driven): masks (`i & 255`), shifts (`acc >> 5`), scaling // (`i * 3`) and float division/equality against a literal all showed up as hot // `ldc; binop_nn` pairs in the bench op mix. Same [op, dst, src, k] encoding. mul_i4_nk, div_i4_nk, // k is the divisor (RHS only); k == 0 still faults like the slot form rem_i4_nk, and_i4_nk, or_i4_nk, xor_i4_nk, shl_i4_nk, shr_i4_nk, shr_un_i4_nk, div_r4_nk, ceq_r4_nk, // --- Fused compare-and-branch --- // Encoding: [op, s1, s2_or_k, target_ip] (4 words). _nn forms read both // operands as slots; _nk forms read s1 as a slot and s2_or_k as an inline // immediate. Always test "branch if cmp true" — for compare-then-brfalse // patterns the lowerer fuses ceq → bne. Subset is corpus-driven (only the // compare→branch patterns observed in bench loop bodies; no bge/ble, no // LHS-constant `_kn` forms); add more on demand. blt_i4_nn, // [op, s1, s2, target_ip] if (slot[s1] < slot[s2]) goto target blt_i4_nk, // [op, s1, k, target_ip] if (slot[s1] < k) goto target bgt_r4_nn, bgt_r4_nk, beq_i4_nn, beq_i4_nk, bne_i4_nn, bne_i4_nk, // --- Numeric for-loop super-instruction --- // Encoding: [op, induction_slot, limit_imm, target_ip] (4 words). // Executor body: slot[N] += 1; if (slot[N] < limit) goto target. Three // semantic steps (increment + bound check + back-edge) in one dispatch — // analogous to Luau's LOP_FORNLOOP. Step is hardcoded to 1 (Roslyn-emitted // C# `for (int i = ...; i < L; i++)` pattern). Variants for step != 1, // reverse loops, and r4 induction are deferred until the bench corpus // demands them. // // Lowerer pass detects `add_i4_nk slot[N], slot[N], 1; blt_i4_nk slot[N], // K_limit, body_top` plus a preceding `ldc_i4 slot[N] = K_init; br -> // blt_ip` with K_init statically less than K_limit. Replaces add+blt with // for_i4_nk and redirects the entry br to body_top so the first body // iteration isn't skipped. for_i4_nk, // [op, induction_slot, limit_int, target_ip] // --- try/finally (non-exceptional path only) --- // IL `leave T` lowers to push_cont(T), push_cont(outer handlers)…, br(innermost handler); // IL `endfinally` lowers to br_cont, which pops the next continuation and jumps to it — // chaining inner finally → outer finally → leave target. Exceptions thrown inside a try // still propagate without running the finally (the whole dispatch falls back to the // original compiled body via CodeReloadRegistry.TryInvokeCodeReload). push_cont, // [op, target_ip] — push an IR continuation address br_cont, // [op] — pop a continuation address and jump to it // --- 64-bit family (I8 = long/ulong, R8 = double) --- // Wide slots: two consecutive 4-byte frame cells, filler cell typed O/null like Vt // continuation cells. Methods whose frames contain a wide slot SKIP the optimizer // passes wholesale (see LowerMethod), so no _nn/_nk fast forms exist and none of // these ops appear in the pass tables. ldc_i8, // [op, dst, lo, hi] ldc_r8, // [op, dst, lo, hi] — IEEE754 double bits add_i8, sub_i8, mul_i8, div_i8, rem_i8, // [op, dst, s1, s2] div_un_i8, rem_un_i8, and_i8, or_i8, xor_i8, shl_i8, shr_i8, shr_un_i8, // [op, dst, value(I8), count(I4)] neg_i8, not_i8, // [op, dst, src] add_r8, sub_r8, mul_r8, div_r8, rem_r8, // [op, dst, s1, s2] neg_r8, // [op, dst, src] ceq_i8, cgt_i8, clt_i8, cgt_un_i8, clt_un_i8, // [op, dst(I4), s1, s2] ceq_r8, cgt_r8, clt_r8, cgt_un_r8, clt_un_r8, // unordered variants for float semantics conv_i8_i4, // [op, dst, src] — sign-extend i4 → i8 conv_i8_u4, // zero-extend u4 → i8 conv_i4_i8, // truncate i8 → i4 (also serves conv.u4) conv_i8_r4, conv_r4_i8, conv_i8_r8, conv_r8_i8, conv_r8_r4, conv_r4_r8, conv_r8_i4, conv_i4_r8, conv_r8_u8, // ulong bits → double (conv.r.un) brtrue_i8, brfalse_i8, // [op, src, target_ip] ret_i8, ret_r8, // [op, src] } }