Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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<T></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 })!;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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:
|
||||
+16
@@ -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
|
||||
}
|
||||
+7
@@ -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:
|
||||
Reference in New Issue
Block a user