Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 14:21:07 +08:00
co-authored by Cursor
commit f3f66ab063
931 changed files with 112366 additions and 0 deletions
@@ -0,0 +1,17 @@
using System.Runtime.CompilerServices;
// The IlInterpreter VM is an implementation detail of eval/code reload, not a public scripting API:
// only the Script* exceptions are public (user code catches them from eval/reload failures).
// Everything else is internal, visible to the package
// assemblies that drive the VM and to the Unity test assemblies. The standalone dotnet suites
// (IlInterpreter/tests/IlInterpreter.*) compile these sources directly into their own assembly, so they need no
// grant here.
[assembly: InternalsVisibleTo("Unity.Pipeline")]
[assembly: InternalsVisibleTo("Unity.Pipeline.Editor")]
[assembly: InternalsVisibleTo("Unity.Pipeline.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Pipeline.Tests.Runtime")]
// IlInterpreter/tests/IlInterpreter/IlInterpreter.csproj compiles these same sources (this file included) into a
// standalone IlInterpreter.dll so the fuzzer can instrument the VM in isolation; the fuzz and bench
// executables consume that DLL cross-assembly.
[assembly: InternalsVisibleTo("IlInterpreter.Fuzz")]
[assembly: InternalsVisibleTo("IlInterpreter.Bench")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56c0e5c2691d76d46a9000322eec91dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 47a7b8492a07c4348a96952a3f0ea322
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
#nullable enable
namespace IlInterpreter.Interpreter
{
// Supplies the Vm engine's unmanaged numeric frame. Injected so the engine stays
// UnityEngine-free: the default is Marshal-based (works everywhere incl. IL2CPP), while a
// Unity host can provide a NativeArray/UnsafeUtility-backed implementation.
//
// Contract: Alloc returns memory aligned to at least 8 bytes (both AllocHGlobal and
// UnsafeUtility.Malloc(...,8,...) satisfy this), so a Value* frame and inline structs up to
// 8-byte field alignment are safe to address. The returned block is owned by the caller until
// passed back to Free; implementations must not move or GC it.
unsafe interface IFrameAllocator
{
void* Alloc(int byteCount);
void Free(void* ptr);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4d4b0ac718cfb5d4e9ed7c0fe4803d74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace IlInterpreter
{
/// <summary>
/// Represents a compiled, validated script that can be executed by the interpreter.
/// Instances are produced by the host compilation layer (Unity.Pipeline.Compilation) and
/// consumed by <c>IlInterpreter.Interpreter</c>.
/// </summary>
interface IScript
{
/// <summary>Name of the script class (as declared in source).</summary>
string Name { get; }
/// <summary>
/// The compiled IL bytes. These have been validated by the IL validator and
/// are safe to interpret.
/// </summary>
ReadOnlyMemory<byte> Il { get; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 285de7e71faedf64da123cc7351300c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,106 @@
#nullable enable
using System;
using System.Text;
namespace IlInterpreter.Interpreter
{
// Debug tool: render a lowered method's IR (slot types, token/string tables, decoded ops) as text
// — the missing "see what the lowerer produced" view for chasing slot-typing/marshalling bugs.
// Internal, for tests and probe harnesses. Op widths come from OpWidthForCoalesce (never drifts).
partial class ScriptInterpreter
{
/// <summary>Disassemble one loaded method by simple name (first match). Null if not loaded/found.</summary>
internal string? DisassembleMethod(string name)
{
if (_parsed == null || !_parsed.ByName.TryGetValue(name, out var m)) return null;
return DisassembleParsed(m);
}
/// <summary>Disassemble every loaded method (declaration order by token).</summary>
internal string DisassembleAll()
{
var sb = new StringBuilder();
if (_parsed == null) return "(no script loaded)";
foreach (var m in _parsed.ByToken.Values)
sb.Append(DisassembleParsed(m)).Append('\n');
return sb.ToString();
}
string DisassembleParsed(ParsedMethod m)
{
var sb = new StringBuilder();
sb.Append($"=== {m.Name} (static={m.IsStatic}, args={m.ArgCount}, locals={m.LocalCount}, ret={m.ReturnSType}, argSTypes=[{string.Join(",", m.ArgSTypes)}]");
var lo = m.Lowered;
if (lo == null)
{
sb.Append($") NOT LOWERED: {m.LoweringSkipReason ?? "(no reason)"}\n");
return sb.ToString();
}
sb.Append($", frameSize={lo.FrameSize})\n");
// Slot type table (slot -> SType), compact.
sb.Append(" slots: ");
for (int s = 0; s < lo.SlotTypes.Length; s++)
sb.Append($"{s}:{lo.SlotTypes[s]} ");
sb.Append('\n');
if (lo.Strings.Length > 0)
{
sb.Append(" strings: ");
for (int i = 0; i < lo.Strings.Length; i++)
sb.Append($"[{i}]=\"{lo.Strings[i]}\" ");
sb.Append('\n');
}
// Instruction stream.
var ir = lo.Ir;
int ip = 0;
while (ip < ir.Length)
{
var op = (Op)ir[ip];
int w = IrLowerer.OpWidthForCoalesce(op, ir, ip);
if (w <= 0) { sb.Append($" {ip,4}: <bad width for {op}={(uint)ir[ip]}>\n"); break; }
int il = ip < lo.IrToIlOffset.Length ? lo.IrToIlOffset[ip] : -1;
sb.Append($" {ip,4} (IL 0x{il:X4}): {op,-16}");
for (int k = 1; k < w; k++) sb.Append($" {(int)ir[ip + k]}");
sb.Append(Annotate(op, ir, ip, w, lo));
sb.Append('\n');
ip += w;
}
return sb.ToString();
}
// Human-readable suffix for the operands we can resolve: the ldstr literal, the target of a
// host/script call, and the field behind a ld/stsfld/ld/stfld.
string Annotate(Op op, uint[] ir, int ip, int w, LoweredMethod lo)
{
switch (op)
{
case Op.ldstr:
// [op, dst, strIdx]
int si = (int)ir[ip + 2];
return si < lo.Strings.Length ? $" ; dst=s{(int)ir[ip + 1]} \"{lo.Strings[si]}\"" : " ; <bad str idx>";
case Op.call_host:
// [op, dst, recvSlot, tokIdx, argc, args...]
return $" ; dst=s{(int)ir[ip + 1]} recv=s{(int)ir[ip + 2]} -> {TokName(lo, (int)ir[ip + 3])}";
case Op.call_script:
case Op.newobj_script:
case Op.newobj_host:
// [op, dst, tokIdx, argc, args...]
return $" ; dst=s{(int)ir[ip + 1]} -> {TokName(lo, (int)ir[ip + 2])}";
case Op.ldsfld_i4: case Op.ldsfld_r4: case Op.ldsfld_o:
case Op.stsfld_i4: case Op.stsfld_r4: case Op.stsfld_o:
return $" ; {TokName(lo, (int)ir[ip + 2])}";
default:
return "";
}
}
string TokName(LoweredMethod lo, int tokIdx)
{
if (_parsed == null || lo.Tokens == null || (uint)tokIdx >= (uint)lo.Tokens.Length) return $"tok#{tokIdx}";
int tok = lo.Tokens[tokIdx];
return _parsed.TokenNames.TryGetValue(tok, out var n) ? n : $"tok 0x{tok:X8}";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52ef5529fb77441e29e1687a62aeaf5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace IlInterpreter.Interpreter
{
// Default frame allocator used outside Unity (and anywhere a host doesn't inject its own).
// Marshal.AllocHGlobal is platform-neutral and AOT/IL2CPP-safe, and keeps the engine free of
// any Unity.Collections dependency. Returns 8-byte-aligned memory on all supported runtimes.
sealed unsafe class MarshalFrameAllocator : IFrameAllocator
{
public static readonly MarshalFrameAllocator Instance = new();
public void* Alloc(int byteCount) => (void*)Marshal.AllocHGlobal(byteCount);
public void Free(void* ptr)
{
if (ptr != null) Marshal.FreeHGlobal((nint)ptr);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34bdbc5d5dc71af4ababeaa8abc6be1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,387 @@
#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]
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd7e60fa09b64c9458e396035345a26c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,105 @@
#nullable enable
using System;
using System.Reflection;
namespace IlInterpreter.Interpreter
{
partial class ScriptInterpreter
{
/// <summary>
/// The object behind a delegate whose target is an INTERPRETED method (a method group over a
/// script method, or a lambda's display-class method). The host holds an ordinary CLR delegate
/// of the exact requested type — <c>System.Action</c>, <c>EventCallback&lt;T&gt;</c>, a custom
/// delegate — and each invocation re-enters the VM, like <see cref="ScriptEnumerator"/> does
/// for iterators.
///
/// No Reflection.Emit: the delegate is built with <c>Delegate.CreateDelegate</c> over a
/// shape-matched generic forwarder (<c>V0..V4</c> for void, <c>F0..F4</c> otherwise) closed
/// over the delegate's Invoke signature. Same AOT posture as HostBinding's typed fast paths;
/// on IL2CPP a value-typed signature relies on full generic sharing.
///
/// Lifetime: valid until the interpreter is re-<c>Load</c>ed or disposed — the same contract
/// as every other object that crosses the boundary.
/// </summary>
internal sealed class ScriptDelegateAdapter
{
readonly ScriptInterpreter _owner;
readonly ParsedMethod _target;
readonly object? _receiver; // null for static targets; prepended as arg 0 otherwise
readonly object _generation; // the ParsedAssembly this target was lowered with
ScriptDelegateAdapter(ScriptInterpreter owner, ParsedMethod target, object? receiver)
{
_owner = owner;
_target = target;
_receiver = receiver;
_generation = owner._parsed!;
}
public static Delegate Create(
ScriptInterpreter owner, Type delegateType, ParsedMethod target, object? receiver)
{
var invoke = delegateType.GetMethod("Invoke")
?? throw new ScriptRuntimeException($"'{delegateType}' has no Invoke — not a delegate type");
var ps = invoke.GetParameters();
bool isVoid = invoke.ReturnType == typeof(void);
if (ps.Length > MaxArity)
throw new ScriptRuntimeException(
$"Delegate '{delegateType.Name}' has {ps.Length} parameters — method groups over " +
$"interpreted methods support up to {MaxArity}");
foreach (var p in ps)
if (p.ParameterType.IsByRef)
throw new ScriptRuntimeException(
$"Delegate '{delegateType.Name}' has a ref/out parameter — not supported " +
"for method groups over interpreted methods");
var adapter = new ScriptDelegateAdapter(owner, target, receiver);
var shape = typeof(ScriptDelegateAdapter).GetMethod(
(isVoid ? "V" : "F") + ps.Length, BindingFlags.Public | BindingFlags.Instance)!;
int typeArgCount = ps.Length + (isVoid ? 0 : 1);
if (typeArgCount > 0)
{
var typeArgs = new Type[typeArgCount];
for (int i = 0; i < ps.Length; i++) typeArgs[i] = ps[i].ParameterType;
if (!isVoid) typeArgs[ps.Length] = invoke.ReturnType;
shape = shape.MakeGenericMethod(typeArgs);
}
try
{
return Delegate.CreateDelegate(delegateType, adapter, shape);
}
catch (ArgumentException ex)
{
throw new ScriptRuntimeException(
$"Could not bind '{target.Name}' to delegate type '{delegateType.Name}': {ex.Message}");
}
}
const int MaxArity = 4;
object? Call(object?[] args)
{
if (_receiver == null)
return _owner.InvokeDelegateTarget(_target, args, _generation);
var withReceiver = new object?[args.Length + 1];
withReceiver[0] = _receiver;
Array.Copy(args, 0, withReceiver, 1, args.Length);
return _owner.InvokeDelegateTarget(_target, withReceiver, _generation);
}
static readonly object?[] s_NoArgs = Array.Empty<object?>();
// Shape-matched forwarders — one per (void?, arity). Public so CreateDelegate binds them.
public void V0() => Call(s_NoArgs);
public void V1<A>(A a) => Call(new object?[] { a });
public void V2<A, B>(A a, B b) => Call(new object?[] { a, b });
public void V3<A, B, C>(A a, B b, C c) => Call(new object?[] { a, b, c });
public void V4<A, B, C, D>(A a, B b, C c, D d) => Call(new object?[] { a, b, c, d });
public R F0<R>() => (R)Call(s_NoArgs)!;
public R F1<A, R>(A a) => (R)Call(new object?[] { a })!;
public R F2<A, B, R>(A a, B b) => (R)Call(new object?[] { a, b })!;
public R F3<A, B, C, R>(A a, B b, C c) => (R)Call(new object?[] { a, b, c })!;
public R F4<A, B, C, D, R>(A a, B b, C c, D d) => (R)Call(new object?[] { a, b, c, d })!;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 73f82ba15825e4cb38b92f41cecbb73c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,113 @@
#nullable enable
using System;
using System.Collections;
namespace IlInterpreter.Interpreter
{
sealed partial class ScriptInterpreter
{
// Host-side IEnumerator over an interpreted iterator state machine. The host (Unity's
// coroutine scheduler, foreach, a manual MoveNext loop) drives the interface; each call
// re-enters the VM with the state machine ScriptObject as the receiver. Produced by
// WrapForHost whenever a script enumerator crosses the host boundary.
//
// Lifetime: valid until the owning interpreter is Unload()ed/Dispose()d or reLoad()ed —
// the state machine's methods belong to the loaded assembly. Driving a bridge after that
// ends the iterator like any other script error (see the guard below).
internal sealed class ScriptEnumerator : IEnumerator, IDisposable
{
readonly ScriptInterpreter _owner;
readonly EnumeratorMembers _members;
// Set when a pump threw: the iterator is over. Without this guard an exception from
// an interpreted body propagates out of MoveNext into Unity's coroutine scheduler and
// kills the ENTIRE host coroutine chain that yielded on this bridge (a code-reload
// error froze the whole scene loop). A compiled body has the registry's
// fall-back-to-original safety net at dispatch; a coroutine body runs after dispatch,
// so this is its equivalent: log through the interpreter's sink and end the iterator —
// the outer coroutine resumes as if it completed.
bool _faulted;
// Exposed for tests/diagnostics: the interpreted state machine instance.
internal ScriptObject StateMachine { get; }
// Exposed for tests/diagnostics: the exception that ended this iterator, if any. The
// bridge deliberately swallows it (see _faulted) — but a differential harness driving
// oracle and interpreter side by side still needs to SEE the fault to check parity.
internal Exception? FaultException { get; private set; }
internal ScriptEnumerator(ScriptInterpreter owner, ScriptObject stateMachine,
EnumeratorMembers members)
{
_owner = owner;
StateMachine = stateMachine;
_members = members;
}
public bool MoveNext()
{
if (_faulted) return false;
try
{
return _owner.InvokeEnumeratorMember(_members.MoveNext, StateMachine) is bool b && b;
}
catch (Exception ex)
{
Fault("MoveNext", ex);
return false;
}
}
// get_Current is a plain field read on the state machine — cheap and side-effect
// free, so no caching. A nested interpreted enumerator yielded as Current comes back
// wrapped (InvokeEnumeratorMember runs WrapForHost on its result).
public object? Current
{
get
{
if (_faulted) return null;
try
{
return _owner.InvokeEnumeratorMember(_members.GetCurrent, StateMachine);
}
catch (Exception ex)
{
Fault("Current", ex);
return null;
}
}
}
// Iterator state machines don't support Reset (their own Reset throws
// NotSupportedException — the exact body the interpreter skips as a cold stub).
public void Reset() => throw new NotSupportedException();
public void Dispose()
{
if (_faulted || _members.Dispose == null) return;
try
{
_owner.InvokeEnumeratorMember(_members.Dispose, StateMachine);
}
catch (Exception ex)
{
Fault("Dispose", ex);
}
}
void Fault(string where, Exception ex)
{
_faulted = true;
FaultException = ex;
// Full exception (type + stack), not just the message: a ScriptRuntimeException
// carries its location in the message, but a raw exception escaping the VM (an
// interpreter bug or a fast-path host throw) is only diagnosable from its stack.
_owner._logSink(
$"CodeReload: interpreted coroutine '{StateMachine.Type.Name}' threw in {where} — " +
$"ending this iterator so the host coroutine chain survives: {ex}");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bc4e4aa52414750ab81e831065292cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace IlInterpreter
{
/// <summary>Base exception for all IlInterpreter errors.</summary>
public class ScriptException : Exception
{
/// <summary>Create the exception with a message.</summary>
/// <param name="message">The exception message.</param>
public ScriptException(string message) : base(message) { }
/// <summary>Create the exception with a message and an inner cause.</summary>
/// <param name="message">The exception message.</param>
/// <param name="inner">The exception that caused this one, if any.</param>
public ScriptException(string message, Exception? inner) : base(message, inner) { }
}
/// <summary>Raised when script source fails to compile.</summary>
public class ScriptCompileException : ScriptException
{
/// <summary>The compiler diagnostics that failed the compilation.</summary>
public IReadOnlyList<string> Diagnostics { get; }
/// <summary>Create the exception from the diagnostics that failed the compilation.</summary>
/// <param name="diagnostics">The compiler diagnostics that failed the compilation.</param>
public ScriptCompileException(IReadOnlyList<string> diagnostics)
: base($"Script compilation failed with {diagnostics.Count} error(s).")
{
Diagnostics = diagnostics;
}
}
/// <summary>Raised when compiled IL fails validation.</summary>
public class ScriptValidationException : ScriptException
{
/// <summary>Create the exception with a message.</summary>
/// <param name="message">The exception message.</param>
public ScriptValidationException(string message) : base(message) { }
}
/// <summary>Raised during interpreter execution.</summary>
public class ScriptRuntimeException : ScriptException
{
/// <summary>Create the exception with a message.</summary>
/// <param name="message">The exception message.</param>
public ScriptRuntimeException(string message) : base(message) { }
/// <summary>Create the exception with a message and an inner cause.</summary>
/// <param name="message">The exception message.</param>
/// <param name="inner">The exception that caused this one, if any.</param>
public ScriptRuntimeException(string message, Exception? inner) : base(message, inner) { }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32382bdf2a41c4141a26fb433b9395f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 726e649c2ce1b774ca646a1e82fa5471
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,123 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace IlInterpreter.Interpreter
{
// Describes a script-defined class: maps field tokens to typed offsets.
internal sealed class ScriptTypeDescriptor
{
// Non-nullable fields are always set by the descriptor-building pass in Parse;
// `= null!` marks that invariant.
public string Name = null!;
public int FieldCount;
// FieldDef metadata token → slot index in ScriptObject.Fields
// (kept for write-back path in call_host_byref; slot index = field index)
public Dictionary<int, int> FieldSlots = null!;
// Per-field type tag (parallel to field declaration order 0..FieldCount-1)
public SType[] FieldTypes = null!;
// Per-field storage offset:
// I4/R4 fields: byte offset into ScriptObject.PrimBytes
// O fields: index into ScriptObject.RefSlots
// Vt fields: byte offset into ScriptObject.PrimBytes (inline storage)
public int[] FieldOffsets = null!;
public int PrimByteSize; // total size of PrimBytes array
public int RefSlotCount; // total length of RefSlots array
// Per-field struct layout, non-null only for SType.Vt fields.
// Length = FieldCount. Parallel to FieldTypes/FieldOffsets.
public HostBinding.StructLayout?[]? VtFieldLayouts;
// Per-field flag: true when field i is a SCRIPT-defined struct stored in an O slot (a value type
// masquerading as a ScriptObject reference). Non-null only when at least one such field exists.
// Parallel to FieldTypes/FieldOffsets. Used by Clone() to deep-copy nested value-type fields so a
// struct copy does not alias the source's nested structs (genuine reference fields stay shared).
public bool[]? FieldIsScriptStruct;
// Per-field descriptor of a script-struct field's own type (parallel to FieldTypes). Non-null entry
// only for a resolved script-struct field; used by ScriptObject.Create to recursively allocate the
// nested struct so a value-type field is never null (matching C#'s zero-init of value fields).
public ScriptTypeDescriptor?[]? FieldStructDescriptors;
// True when this type is a script-defined STRUCT (value type, not an enum, not a class). Used to
// zero-init the elements of `new T[n]` — a value-type array holds usable structs, not nulls.
public bool IsScriptStructValue;
// Non-null when this script struct is BLITTABLE (no reference fields, transitively) and is
// represented FLAT: locals/args/returns live as raw bytes in Vt frame slots (the flat image is
// identical to the PrimBytes image), and only O-boundary crossings materialize a boxed
// ScriptObject via the layout's marshallers. Synthesized by the parser's flat-resolution pass.
public HostBinding.StructLayout? FlatLayout;
}
// An instance of a script-defined class.
// Primitives (I4, R4) live in PrimBytes at their FieldOffsets[i] byte offset.
// References (O, boxed Vt) live in RefSlots at their FieldOffsets[i] ref index.
internal sealed class ScriptObject
{
// Create and Clone (the only construction sites) always set all three fields;
// `= null!` marks that invariant.
public ScriptTypeDescriptor Type = null!;
public byte[] PrimBytes = null!; // typed primitive storage (I4/R4/bool)
public object?[] RefSlots = null!; // reference fields only (O slots)
// Allocate an instance of a script class/struct, recursively initializing nested SCRIPT-struct
// fields to fresh zeroed instances. A value-type field is never null in C# (`new Outer()` gives a
// usable `inner`), so `a.inner.v = x` must not dereference null. Recursion terminates because
// value-type containment is acyclic (a struct cannot contain itself by value).
public static ScriptObject Create(ScriptTypeDescriptor desc)
{
var o = new ScriptObject
{
Type = desc,
PrimBytes = new byte[desc.PrimByteSize],
RefSlots = new object?[desc.RefSlotCount],
};
var subs = desc.FieldStructDescriptors;
if (subs != null)
{
var offs = desc.FieldOffsets;
for (int i = 0; i < subs.Length; i++)
if (subs[i] != null)
o.RefSlots[offs[i]] = Create(subs[i]!);
}
return o;
}
// Value-copy for script-struct copy semantics. Genuine reference fields keep their reference
// shared (a struct copy copies the reference, not its target) — but a NESTED SCRIPT STRUCT is
// itself a value type stored in an O slot, so it must be deep-copied recursively, or
// `b = a; b.inner.v = x` would alias a.inner. Recursion terminates because struct nesting
// is acyclic.
public ScriptObject Clone()
{
var copy = new ScriptObject
{
Type = Type,
PrimBytes = PrimBytes == null ? null! : (byte[])PrimBytes.Clone(),
RefSlots = RefSlots == null ? null! : (object?[])RefSlots.Clone(),
};
var isSc = Type?.FieldIsScriptStruct;
if (isSc != null && copy.RefSlots != null)
{
var offs = Type!.FieldOffsets;
for (int i = 0; i < isSc.Length; i++)
{
if (!isSc[i]) continue;
int ri = offs[i];
if ((uint)ri < (uint)copy.RefSlots.Length && copy.RefSlots[ri] is ScriptObject so)
copy.RefSlots[ri] = so.Clone();
}
}
return copy;
}
}
// A script-created array of FLAT structs: one byte[] backing for all elements instead of one
// ScriptObject (3+ allocations) per element. Elements are memcpy'd in/out of Vt frame slots by
// ldelem_vt/stelem_vt. Host-ABI note: like the old object?[] representation, this cannot be
// passed to a host T[] parameter — unchanged limitation.
internal sealed class ScriptVtArray
{
public HostBinding.StructLayout Layout = null!;
public byte[] Bytes = null!;
public int Length;
public int Stride; // element size rounded up to 4
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88d8c601d0ce53c42953bc95b8b0af6c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
{
"name": "Unity.Pipeline.IlInterpreter",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [
"UNITY_EDITOR || DEVELOPMENT_BUILD || ENABLE_RUNTIME_PIPELINE"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3352b3594156b1b49bc408b732c23df5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace IlInterpreter.Interpreter
{
// A single 4-byte slot of the Vm engine's unmanaged numeric frame. The slot's static SType
// (LoweredMethod.SlotTypes) tells the executor which field is live — there is no runtime tag.
// The dispatch loop reinterprets slots via byte* casts, so the backing memory stays native (no GC).
[StructLayout(LayoutKind.Explicit, Size = 4)]
internal struct Value
{
[FieldOffset(0)] public int I4; // int, bool (0/1), char, short — sign/zero-extended
[FieldOffset(0)] public float R4;
[FieldOffset(0)] public uint U4; // bit-exact copies (mov, Vt span words)
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 617b9a71d914e1842b16719af056ad77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace IlInterpreter
{
// VM intrinsics — always present, host-independent. Like syscalls / WASI imports.
// The interpreter intercepts calls to these methods; bodies exist only so the
// methods can be referenced from script source at compile time.
static class Vm
{
public static void Log(string message) { }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 95648017851331b429ece1d370bdee8a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b14fe6feb7a4ab04e913f44be3fef0a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: