9105 lines
520 KiB
C#
9105 lines
520 KiB
C#
#nullable enable
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Reflection;
|
||
using System.Runtime.InteropServices;
|
||
// The package bundles System.Reflection.Metadata renamed with a "UnityPipeline." prefix (Runtime/Plugins/CodeAnalysis);
|
||
// the dotnet test projects reference the same DLLs (IlInterpreter/BundledRoslyn.props).
|
||
using UnityPipeline.System.Reflection.Metadata;
|
||
using UnityPipeline.System.Reflection.Metadata.Ecma335;
|
||
using UnityPipeline.System.Reflection.PortableExecutable;
|
||
using System.Runtime.CompilerServices;
|
||
|
||
namespace IlInterpreter.Interpreter
|
||
{
|
||
|
||
sealed partial class ScriptInterpreter : IDisposable
|
||
{
|
||
public const int DefaultStepLimit = 1_000_000;
|
||
|
||
// Per-invocation IL-opcode budget. Set higher for benchmarks or long-running
|
||
// workloads; the default catches accidental infinite loops in dev.
|
||
public int StepLimit = DefaultStepLimit;
|
||
|
||
readonly HostBinding? _binding;
|
||
readonly Action<string> _logSink;
|
||
readonly IFrameAllocator _allocator;
|
||
Vm? _vm; // lazily created on first Invoke
|
||
ParsedAssembly? _parsed;
|
||
ScriptObject? _instance; // the live "Script" instance (null for static-only scripts)
|
||
|
||
// Per-size thread-local buffers for host-call arguments (sizes 0-8).
|
||
// Delegates registered via AllowType use MethodInfo.Invoke which requires an exact-length array.
|
||
[ThreadStatic]
|
||
static object?[][]? _argBufs;
|
||
|
||
static object?[] ArgBuf(int count)
|
||
{
|
||
_argBufs ??= new object?[9][];
|
||
if (count < _argBufs.Length) return _argBufs[count] ??= new object?[count];
|
||
return new object?[count]; // rare: >8 args, just allocate
|
||
}
|
||
|
||
public ScriptInterpreter(HostBinding? binding = null, Action<string>? log = null,
|
||
IFrameAllocator? allocator = null)
|
||
{
|
||
_binding = binding;
|
||
_logSink = log ?? (_ => { });
|
||
_allocator = allocator ?? MarshalFrameAllocator.Instance;
|
||
}
|
||
|
||
// Parse the script IL; disposes any previously loaded assembly first (code reload).
|
||
// Throws ScriptValidationException if any method can't be lowered — failing at Load,
|
||
// where the user can act on it, rather than at Invoke time.
|
||
// lenient=true keeps loading when individual methods can't be lowered (each becomes a skipped
|
||
// stub queryable via TryGetLoweringSkip) instead of throwing for the whole assembly — see
|
||
// IrLowerer.LowerAll. Code reload passes true so one bad method doesn't drop a whole file's reload.
|
||
public void Load(IScript script, bool lenient = false)
|
||
{
|
||
_parsed?.PdbProvider?.Dispose();
|
||
_parsed?.Pe.Dispose();
|
||
// Parse reads attacker-reachable PE/metadata directly (System.Reflection.Metadata). Convert
|
||
// a malformed-image fault into a clean load failure rather than letting a raw
|
||
// BadImageFormatException/ArgumentException escape Load. ScriptExceptions (our own validation,
|
||
// e.g. a cyclic base chain) pass through unchanged.
|
||
try { _parsed = Parse(script.Il.ToArray(), _binding, _logSink); }
|
||
catch (ScriptException) { throw; }
|
||
catch (Exception ex) { throw new ScriptValidationException($"Failed to parse script assembly: {ex.Message}"); }
|
||
var failures = IrLowerer.LowerAll(_parsed, lenient);
|
||
if (failures.Count > 0)
|
||
{
|
||
var sb = new System.Text.StringBuilder("Script could not be lowered:");
|
||
foreach (var f in failures) sb.Append('\n').Append(" ").Append(f.Name).Append(": ").Append(f.Reason);
|
||
_parsed.PdbProvider?.Dispose();
|
||
_parsed.Pe.Dispose();
|
||
_parsed = null;
|
||
throw new ScriptValidationException(sb.ToString());
|
||
}
|
||
IrLowerer.BuildIrBlob(_parsed);
|
||
_instance = MakeInstance(_parsed);
|
||
// Run script static constructors eagerly (CLR runs them lazily; eager keeps the VM free of
|
||
// per-access init checks). The load-bearing case is the compiler-generated `<>c..cctor`
|
||
// that creates the singleton behind non-capturing lambdas — without it, `ldsfld <>9`
|
||
// reads null and a delegate over an instance method loses its receiver. A user static
|
||
// initializer that throws fails the Load, like everything else it validates.
|
||
foreach (var m in _parsed.ByToken.Values)
|
||
if (m.Name == ".cctor" && m.Lowered != null)
|
||
{
|
||
try { (_vm ??= new Vm(this)).Invoke(m, Array.Empty<object?>()); }
|
||
catch (ScriptException ex)
|
||
{
|
||
// Name the type whose static initializer faulted — a bare "array index out of
|
||
// bounds at IL+0x…" gives no clue which of an assembly's many .cctors blew up.
|
||
var typeName = _parsed.TypeDefToType.TryGetValue(m.DeclaringTypeDef, out var td)
|
||
? td.Name : $"typedef 0x{m.DeclaringTypeDef:X8}";
|
||
throw new ScriptRuntimeException(
|
||
$"static initializer of type '{typeName}' failed at load: {ex.Message}", ex);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Release the loaded assembly. Invoke() will throw until Load() is called again.
|
||
public void Unload()
|
||
{
|
||
_parsed?.PdbProvider?.Dispose();
|
||
_parsed?.Pe.Dispose();
|
||
_parsed = null;
|
||
_instance = null;
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
Unload();
|
||
_vm?.Dispose();
|
||
_vm = null;
|
||
}
|
||
|
||
// True if the loaded script defines a method with this (simple) name. Lets callers
|
||
// probe for an optional entry point (e.g. a code-reload override) without catching the
|
||
// "method not found" exception that Invoke throws.
|
||
public bool HasMethod(string name) => _parsed != null && _parsed.ByName.ContainsKey(name);
|
||
|
||
// Host member refs of the loaded script that resolved to throwing stubs (or missing host
|
||
// fields) — the script runs, but reaching one of these sites throws ScriptRuntimeException.
|
||
// Callers surface these as load-time warnings instead of exploding mid-play on first call.
|
||
public IReadOnlyList<string> UnboundHostMembers =>
|
||
(IReadOnlyList<string>?)_parsed?.UnboundHostMembers ?? Array.Empty<string>();
|
||
|
||
// A method that exists but was skipped at load — Lowered==null with a reason — because it
|
||
// couldn't be lowered under lenient loading (see Load). Its call sites throw the reason if
|
||
// reached, so the code-reload executor skips registering just that override instead of the file.
|
||
public bool TryGetLoweringSkip(string methodName, out string reason)
|
||
{
|
||
if (_parsed != null && _parsed.ByName.TryGetValue(methodName, out var m)
|
||
&& m.Lowered == null && m.LoweringSkipReason != null)
|
||
{ reason = m.LoweringSkipReason; return true; }
|
||
reason = null!;
|
||
return false;
|
||
}
|
||
|
||
// Resolve a host-invocable method by simple name, rejecting stubs that were skipped at
|
||
// load (cold enumerator members whose lowering failed).
|
||
ParsedMethod ResolveInvocable(string methodName)
|
||
{
|
||
if (_parsed == null) throw new ScriptRuntimeException("No script loaded — call Load() first");
|
||
if (!_parsed.ByName.TryGetValue(methodName, out var method))
|
||
throw new ScriptRuntimeException($"Method '{methodName}' not found");
|
||
if (method.Lowered == null)
|
||
throw new ScriptRuntimeException(
|
||
$"Method '{methodName}' was skipped at load: {method.LoweringSkipReason}");
|
||
return method;
|
||
}
|
||
|
||
// Invoke a named method.
|
||
// For instance methods: args[0] = script instance, args[1..] = extraArgs.
|
||
// For static methods: args[0..] = extraArgs.
|
||
public object? Invoke(string methodName, params object?[] extraArgs)
|
||
{
|
||
var method = ResolveInvocable(methodName);
|
||
|
||
// Hot no-arg path (OnUpdate/Run): the Vm places the receiver and runs with no object?[]
|
||
// allocation. Only the primitive return is boxed (at the API boundary).
|
||
if (extraArgs.Length == 0)
|
||
return WrapForHost((_vm ??= new Vm(this)).InvokeTyped(method));
|
||
|
||
var args = new object?[method.ArgCount];
|
||
int slot = 0;
|
||
if (!method.IsStatic && _instance != null) args[slot++] = _instance;
|
||
for (int i = 0; i < extraArgs.Length && slot < method.ArgCount; i++)
|
||
args[slot++] = extraArgs[i];
|
||
|
||
return WrapForHost((_vm ??= new Vm(this)).Invoke(method, args));
|
||
}
|
||
|
||
// Typed 1-arg overload: bypasses object?[] allocation for primitive args.
|
||
// RyuJIT specializes per value-type T1; typeof checks fold to constants in
|
||
// each specialization, so no boxing occurs for int/float/bool callers.
|
||
// Void-returning methods (the common Unity Update pattern) allocate zero bytes.
|
||
// Numeric-returning methods re-box the result once (24 B); ref-returning methods
|
||
// have no boxing on either side.
|
||
public object? Invoke<T1>(string methodName, T1 a1)
|
||
{
|
||
var method = ResolveInvocable(methodName);
|
||
return WrapForHost((_vm ??= new Vm(this)).InvokeTyped(method, a1));
|
||
}
|
||
|
||
public object? Invoke<T1, T2>(string methodName, T1 a1, T2 a2)
|
||
{
|
||
var method = ResolveInvocable(methodName);
|
||
return WrapForHost((_vm ??= new Vm(this)).InvokeTyped(method, a1, a2));
|
||
}
|
||
|
||
public object? Invoke<T1, T2, T3>(string methodName, T1 a1, T2 a2, T3 a3)
|
||
{
|
||
var method = ResolveInvocable(methodName);
|
||
return WrapForHost((_vm ??= new Vm(this)).InvokeTyped(method, a1, a2, a3));
|
||
}
|
||
|
||
// A script enumerator (iterator state machine) crossing to the host gets wrapped in a
|
||
// bridge the host can drive (Unity's StartCoroutine, foreach, LINQ over IEnumerator).
|
||
// Anything else passes through. ScriptObject is internal, so an unwrapped state machine
|
||
// would be useless to the caller anyway.
|
||
internal object? WrapForHost(object? result)
|
||
{
|
||
return result is ScriptObject so && _parsed != null &&
|
||
_parsed.EnumeratorTypes.TryGetValue(so.Type, out var em)
|
||
? new ScriptEnumerator(this, so, em)
|
||
: result;
|
||
}
|
||
|
||
// Re-enter the VM to run one member of a script enumerator with an explicit receiver
|
||
// (the state machine ScriptObject). Used by the ScriptEnumerator bridge; safe to call
|
||
// while another Invoke is live (the Vm's _base reentrancy handling covers it).
|
||
internal object? InvokeEnumeratorMember(ParsedMethod member, ScriptObject receiver)
|
||
{
|
||
if (_parsed == null) throw new ScriptRuntimeException("No script loaded — call Load() first");
|
||
if (member.Lowered == null)
|
||
throw new ScriptRuntimeException(
|
||
$"Method '{member.Name}' was skipped at load: {member.LoweringSkipReason}");
|
||
return WrapForHost((_vm ??= new Vm(this)).InvokeReceiver(member, receiver));
|
||
}
|
||
|
||
// Re-enter the VM from a delegate created over an interpreted method (button click, event
|
||
// callback). args includes the receiver at [0] for instance targets. Same reentrancy story
|
||
// as InvokeEnumeratorMember. The generation check makes staleness a CLEAN error: the target's
|
||
// IR offsets are blob-absolute into the assembly it was lowered with, so a delegate surviving
|
||
// a re-Load would execute arbitrary words of the NEW blob — silently wrong, not crashing.
|
||
internal object? InvokeDelegateTarget(ParsedMethod target, object?[] args, object generation)
|
||
{
|
||
if (_parsed == null) throw new ScriptRuntimeException("No script loaded — call Load() first");
|
||
if (!ReferenceEquals(_parsed, generation))
|
||
throw new ScriptRuntimeException(
|
||
$"Delegate over '{target.Name}' was created by a previous script load and is no " +
|
||
"longer valid — re-run the code that created it after a reload");
|
||
return WrapForHost((_vm ??= new Vm(this)).Invoke(target, args));
|
||
}
|
||
|
||
// Per-receiver delegate cache: `b.clicked += Click; b.clicked -= Click` must observe delegate
|
||
// equality, and script-adapter delegates only compare equal as the SAME instance.
|
||
readonly System.Runtime.CompilerServices.ConditionalWeakTable<object, Dictionary<DelegateSite, Delegate>>
|
||
_delegatesByReceiver = new();
|
||
|
||
internal Delegate GetOrCreateDelegate(DelegateSite site, object? receiver)
|
||
{
|
||
if (receiver == null)
|
||
return site.CachedStatic ??= CreateDelegateCore(site, null);
|
||
var perReceiver = _delegatesByReceiver.GetOrCreateValue(receiver);
|
||
if (!perReceiver.TryGetValue(site, out var d))
|
||
perReceiver[site] = d = CreateDelegateCore(site, receiver);
|
||
return d;
|
||
}
|
||
|
||
Delegate CreateDelegateCore(DelegateSite site, object? receiver)
|
||
{
|
||
if (site.HostMethod is { } hm)
|
||
{
|
||
if (hm.IsStatic)
|
||
return Delegate.CreateDelegate(site.DelegateType, hm);
|
||
if (receiver == null)
|
||
throw new ScriptRuntimeException(
|
||
$"Cannot create a {site.DelegateType.Name} over instance method '{hm.Name}' with a null receiver");
|
||
return Delegate.CreateDelegate(site.DelegateType, receiver, hm);
|
||
}
|
||
return ScriptDelegateAdapter.Create(this, site.DelegateType, site.ScriptMethod!, receiver);
|
||
}
|
||
|
||
public object? Execute(IScript script, string methodName = "Run")
|
||
{
|
||
Load(script);
|
||
return Invoke(methodName);
|
||
}
|
||
|
||
internal sealed class ParsedMethod
|
||
{
|
||
// Name/IlBytes/ArgSTypes are always set by Parse before the instance escapes;
|
||
// `= null!` marks that invariant (same idiom as Lowered below).
|
||
public string Name = null!;
|
||
public int Token;
|
||
public int ArgCount;
|
||
public int LocalCount;
|
||
public byte[] IlBytes = null!;
|
||
public bool IsStatic;
|
||
public bool IsVoid;
|
||
// True when the declared return type is bool (which stores as I4). The method-return boxing
|
||
// uses this to hand back a boxed bool rather than a boxed int, so a host caller casting the
|
||
// Invoke result to bool doesn't fault.
|
||
public bool ReturnIsBool;
|
||
// Return and parameter types for IR slot classification (set during Parse)
|
||
public SType ReturnSType;
|
||
public SType[] ArgSTypes = null!; // length = explicit param count (no `this` slot)
|
||
// Per-local struct layout. Non-null when the local is a registered host
|
||
// value type and should be allocated as a Vt slot in numFrame.
|
||
public HostBinding.StructLayout?[]? LocalStructLayouts;
|
||
// Per-local DECLARED slot type, from the LocalVarSig — the authoritative, stable backing:
|
||
// an IL local is single-typed, so its declared type (not last-wins store inference) says
|
||
// whether its value lives unboxed in the numeric frame (I4/R4) or boxed in refStack (O).
|
||
// Only the unambiguous, VM-consistent cases are captured (bool/char/sub-int/int -> I4,
|
||
// float -> R4, string/object/class/array/generic -> O); null entries (valuetype/enum, long,
|
||
// double, nint, byref, …) fall back to the inference pass. Non-null entries are FROZEN: the
|
||
// lowering neither infers nor retypes them, so a fast op is never emitted over a slot whose
|
||
// value is actually boxed. Null array when the sig gives nothing to freeze.
|
||
public SType?[]? LocalSTypes;
|
||
// Per-local flag: true when the local is a SCRIPT-defined struct (ELEMENT_TYPE_VALUETYPE
|
||
// referencing a TypeDef in this assembly). Such locals are O ScriptObject references, so a
|
||
// value load (ldloc) must clone to preserve struct copy semantics. Null when none are.
|
||
// The flat-resolution post-pass CLEARS entries whose struct turned out blittable — those
|
||
// locals get a Vt layout in LocalStructLayouts instead.
|
||
public bool[]? LocalIsScriptStruct;
|
||
// Per-local TypeDef token when the local is a script-defined struct (0 otherwise). Lets the
|
||
// post-pass resolve the local to a descriptor once all types are built. Null when none.
|
||
public int[]? LocalScriptStructTypeDefs;
|
||
// Per-explicit-param / return TypeDef token for script-struct types (0 = not one).
|
||
public int[]? ArgScriptStructTypeDefs;
|
||
public int ReturnScriptStructTypeDef;
|
||
// Per-explicit-param flat layout (parallel to ArgSTypes; entry non-null ⇔ ArgSTypes[i]==Vt).
|
||
public HostBinding.StructLayout?[]? ArgStructLayouts;
|
||
// Flat layout of a Vt return (ReturnSType == Vt). Null otherwise.
|
||
public HostBinding.StructLayout? ReturnStructLayout;
|
||
// Declaring TypeDef token (0 when unknown). Set at parse; lets the post-pass flatten
|
||
// `this` for instance methods declared on a blittable script struct.
|
||
public int DeclaringTypeDef;
|
||
// Flat layout for `this` (arg slot 0) when the declaring type is a flat script struct.
|
||
public HostBinding.StructLayout? ThisStructLayout;
|
||
// Doc is the source file name (no directory), "" when the PDB document had no name.
|
||
public (int IlOffset, int Line, string Doc)[]? SeqPoints;
|
||
// Finally clauses from the method body's exception-region table (offsets are IL byte
|
||
// offsets, End = exclusive). Null when the method has none. Consumed by the lowerer's
|
||
// leave/endfinally handling. Only the NON-exceptional path runs handlers.
|
||
public (int TryStart, int TryEnd, int HandlerStart, int HandlerEnd)[]? FinallyRegions;
|
||
// Fault-handler entry IL offsets. Fault handlers never run (the interpreter has no
|
||
// exception dispatch — see the region parse in Parse), but their bodies still lower as
|
||
// unreachable code; the lowerer treats each entry as a basic-block boundary.
|
||
public int[]? FaultHandlerStarts;
|
||
// True when the body has catch/filter clauses — those need real exception
|
||
// dispatch, which the interpreter doesn't do; lowering fails with a clear reason.
|
||
public bool HasUnsupportedEhClauses;
|
||
// True for enumerator members the host never drives (Reset — its body is
|
||
// `throw new NotSupportedException()`, which the interpreter can't lower). A lowering
|
||
// failure on a cold member becomes a skipped stub (LoweringSkipReason) instead of
|
||
// failing the whole Load.
|
||
public bool IsColdEnumeratorMember;
|
||
// Set when lowering this method failed but was tolerated (cold enumerator member).
|
||
// Lowered stays null; invoking the method throws this reason.
|
||
public string? LoweringSkipReason;
|
||
// Non-nullable post-Load for every invocable method: Load throws
|
||
// ScriptValidationException listing every method that didn't lower. The one exception
|
||
// is a cold enumerator member (IsColdEnumeratorMember) whose lowering failed — it keeps
|
||
// Lowered null and carries LoweringSkipReason; the Vm entry points guard against it.
|
||
public LoweredMethod Lowered = null!;
|
||
}
|
||
|
||
// Output of the lowering pass for one method.
|
||
internal sealed class LoweredMethod
|
||
{
|
||
// All non-nullable fields are set by the lowering pass before the instance escapes.
|
||
public uint[] Ir = null!; // flat instruction stream (variable-width words)
|
||
public int FrameSize; // number of logical slots (numFrame = FrameSize*4 bytes, refFrame = FrameSize refs)
|
||
public int[] IrToIlOffset = null!; // ir word index → IL offset (for source mapping)
|
||
public string[] Strings = null!; // string table, indexed by ldstr immediate
|
||
public int[] Tokens = null!; // token table (field/type/method tokens), indexed by tok_idx immediate
|
||
// Per-slot type: I4/R4 → numeric byte frame; O → reference frame; Vt → flat struct
|
||
// bytes occupying ceil(StructLayouts[s].Size/4) consecutive slots. Indexed by slot number.
|
||
public SType[] SlotTypes = null!;
|
||
// Parallel to SlotTypes; non-null entries describe the struct layout for Vt slots.
|
||
public HostBinding.StructLayout?[]? StructLayouts;
|
||
// Pre-resolved fast-delegate cache, parallel to Tokens. For each tokIdx whose
|
||
// entry has a Fast closure, HostFastByTokIdx[tokIdx] is that closure. Eliminates
|
||
// the per-call_host `asm.HostCalls[tok]` dictionary lookup on the hot path.
|
||
// Null entries fall through to the slow path (which still does the dict lookup).
|
||
public FastCallDelegate?[]? HostFastByTokIdx;
|
||
// Parallel to HostFastByTokIdx: true when the entry's Fast closure handles a FLAT
|
||
// (Vt) receiver itself (Entry.FastVtRecv) — the executor may keep the fast path
|
||
// instead of dropping to the boxing slow path.
|
||
public bool[]? HostFastVtRecvByTokIdx;
|
||
public bool[]? HostFastWideOkByTokIdx; // Fast closure handles I8/R8 slots itself
|
||
|
||
// Pre-resolved script callee per tokIdx. Eliminates the asm.ByToken[tok] dict
|
||
// lookup on the call_script / newobj_script hot path. Null for tokens that don't
|
||
// resolve to a script method (host calls, host ctors, fields, etc.).
|
||
public ParsedMethod?[]? CalleeByTokIdx;
|
||
|
||
// Highest O-typed slot index + 1 (0 when the frame is all-numeric). Frame setup only
|
||
// needs to null refFrame slots the method can actually read as references — clearing
|
||
// the numeric remainder is wasted work on every call_script into a math-y helper.
|
||
public int RefClearLen;
|
||
|
||
// This method's base offset inside ParsedAssembly.IrBlob. Execution ips are
|
||
// blob-absolute; diagnostics index IrToIlOffset with (ip - IrStart).
|
||
public int IrStart;
|
||
|
||
// Arg index → frame slot. Null when identity (no Vt args): args live at slots
|
||
// 0..ArgCount-1. Non-null when a flat-struct arg occupies multiple slots, shifting
|
||
// the args after it.
|
||
public int[]? ArgSlot;
|
||
|
||
// Sparse per-tokIdx flat layout for newarr_vt element types (parallel to Tokens).
|
||
public HostBinding.StructLayout?[]? LayoutByTokIdx;
|
||
|
||
// Sparse per-tokIdx element Type for primitive arrays that need a typed runtime backing
|
||
// (bool[]/char[]/byte[]/sbyte[]/short[]/ushort[]). Int32/Single use the dedicated
|
||
// newarr_i4/newarr_r4 ops; every other primitive was previously object?[]-backed, which
|
||
// lost the element type (element ToString/boxing-identity wrong). Null when no such array.
|
||
public Type?[]? PrimArrayElemTypeByTokIdx;
|
||
|
||
// Sparse per-tokIdx delegate-creation site (parallel to Tokens, keyed by the delegate
|
||
// ctor's tokIdx). Null when the method creates no delegates.
|
||
public DelegateSite?[]? DelegateSiteByTokIdx;
|
||
}
|
||
|
||
// One ldftn/ldvirtftn + newobj delegate-creation site, pre-resolved at lowering: the runtime
|
||
// delegate type plus the target — a host MethodInfo (bound via Delegate.CreateDelegate) or an
|
||
// interpreted method (bound via a ScriptDelegateAdapter that re-enters the VM).
|
||
internal sealed class DelegateSite
|
||
{
|
||
public Type DelegateType = null!;
|
||
public MethodInfo? HostMethod; // exactly one of HostMethod / ScriptMethod is set
|
||
public ParsedMethod? ScriptMethod;
|
||
// Receiverless creations (static target) share one delegate per site, so repeated
|
||
// event `+=`/`-=` over the same method group observe delegate equality.
|
||
public Delegate? CachedStatic;
|
||
}
|
||
|
||
sealed class HostEntry
|
||
{
|
||
public HostBinding.Entry Binding = null!; // the registered entry (handles int→bool coercion); always set at construction
|
||
public bool IsVoid; // true when return type is void — don't push result
|
||
// Per-arg flat-struct layouts (length = explicit param count, no receiver).
|
||
// Non-null entries indicate the parameter type was registered via AllowTypeStruct
|
||
// and its arg should be marshalled from a Vt slot at call time. null = boxed object.
|
||
public HostBinding.StructLayout?[]? ArgStructs;
|
||
// Receiver struct layout: non-null when the receiver is a flat struct (rare).
|
||
public HostBinding.StructLayout? ReceiverStruct;
|
||
// Return struct layout: non-null when the return type was registered via AllowTypeStruct.
|
||
public HostBinding.StructLayout? ReturnStruct;
|
||
// The EXACT overload this token resolved to. Aliased operator pairs (Quaternion's
|
||
// op_Multiply(Q,Q)->Q and op_Multiply(Q,V3)->V3) share one Entry whose Method is the
|
||
// FIRST overload — typing a call site from Entry.Method then mislabels the return
|
||
// (a Quaternion Vt dst for a Vector3 result => InvalidCastException at UnboxVt).
|
||
// Null when resolution didn't go through an exact MethodInfo (string fallback).
|
||
public MethodInfo? ResolvedMethod;
|
||
// Return SType from the call site's METADATA signature. The lowering types the result slot
|
||
// from ResolvedMethod when available; for string-keyed entries (hand-wired Allow/AllowBcl
|
||
// shims) there is no MethodInfo, and without this the slot defaulted to O — boxing every
|
||
// numeric host return, which broke `a.Length != a.Length` (reference-compared boxes) and
|
||
// any op that writes its result flat.
|
||
public SType SigRetSType = SType.O;
|
||
// How to box an I4-slot receiver for an instance host call. char and bool are stored in I4
|
||
// slots but their boxed identity differs from int: 'r' (114) boxed as int renders "114" not
|
||
// "r", and a bool boxed as int renders "1"/"0" not "True"/"False". char and bool are the only
|
||
// I4-mapped receivers whose boxed identity changes the result (byte/short/... render the same
|
||
// digits as int). None for every other receiver.
|
||
public ReceiverBoxKind ReceiverBox;
|
||
}
|
||
|
||
enum ReceiverBoxKind : byte { None, Char, Bool }
|
||
|
||
|
||
// Driver members of a script enumerator type (iterator state machine): what the host
|
||
// bridge invokes to pump it. Dispose is null when the type has no (parsed) Dispose.
|
||
internal readonly struct EnumeratorMembers
|
||
{
|
||
public readonly ParsedMethod MoveNext;
|
||
public readonly ParsedMethod GetCurrent;
|
||
public readonly ParsedMethod? Dispose;
|
||
public EnumeratorMembers(ParsedMethod moveNext, ParsedMethod getCurrent, ParsedMethod? dispose)
|
||
{
|
||
MoveNext = moveNext; GetCurrent = getCurrent; Dispose = dispose;
|
||
}
|
||
}
|
||
|
||
sealed class ParsedAssembly
|
||
{
|
||
// Every non-nullable field is set by Parse's object initializer before the instance escapes.
|
||
public Dictionary<int, ParsedMethod> ByToken = null!;
|
||
public Dictionary<string, ParsedMethod> ByName = null!;
|
||
public Dictionary<int, HostEntry> HostCalls = null!; // MemberRef token → host method
|
||
public Dictionary<int, HostEntry> HostCtors = null!; // MemberRef token → host constructor
|
||
public Dictionary<int, HostBinding.FieldEntry> HostFields = null!; // MemberRef token → host field
|
||
public Dictionary<int, int> HostFieldDeclTypeTok = null!; // host field MemberRef token → declaring TypeRef token (ensure-init of boxed struct locals)
|
||
public Dictionary<int, string> TokenNames = null!; // MemberRef token → "Type.Method/N" for diagnostics
|
||
public Dictionary<int, (ScriptTypeDescriptor, int)> FieldSlots = null!; // FieldDef token → (type, slot)
|
||
public Dictionary<int, SType> FieldSTypes = null!; // FieldDef token → numeric type (for IR lowering)
|
||
public HashSet<int> FieldIsScriptStruct = null!; // FieldDef tokens whose type is a script-defined struct (O storage, needs clone on value-load)
|
||
public Dictionary<int, int> FieldToTypeDef = null!; // FieldDef token → declaring TypeDef token
|
||
public Dictionary<int, ScriptTypeDescriptor> CtorToType = null!; // .ctor MethodDef token → type
|
||
public Dictionary<int, int> TypeDefToCtorTok = null!; // TypeDef token → default .ctor MethodDef token
|
||
public Dictionary<int, ScriptTypeDescriptor> TypeDefToType = null!; // TypeDef token → script type (for initobj)
|
||
public Dictionary<string, ScriptTypeDescriptor> TypesByName = null!;
|
||
// Script enumerator types (iterator state machines) → driver members for the host bridge.
|
||
public Dictionary<ScriptTypeDescriptor, EnumeratorMembers> EnumeratorTypes = null!;
|
||
public Dictionary<int, Type> TokenTypes = null!; // TypeRef/TypeSpec token → host Type (for castclass/isinst/ldtoken)
|
||
public MetadataReader Reader = null!;
|
||
public PEReader Pe = null!;
|
||
public MetadataReaderProvider? PdbProvider;
|
||
// Host member refs that resolved to throwing stubs (or missing host fields) — the script
|
||
// loaded, but reaching one of these call sites throws ScriptRuntimeException. Surfaced by
|
||
// ScriptInterpreter.UnboundHostMembers so callers can warn at load time.
|
||
public List<string> UnboundHostMembers = new();
|
||
|
||
// SCRIPT static fields (FieldDef tokens accessed by ldsfld/stsfld), stored boxed, lazily.
|
||
// Unwritten fields read as null — matching CLR zero-init for the one pattern that needs
|
||
// this: the C# 11+ method-group delegate cache (`ldsfld <>O.<n>__M; dup; brtrue …`),
|
||
// which null-checks before every use. Static ctors never run (pre-existing semantics),
|
||
// so a numeric static read before any write still fails, as it did before.
|
||
public Dictionary<int, object?> ScriptStatics = new();
|
||
// Primitive (I4/R4) script statics, UNBOXED as raw bits (written by stsfld_i4/r4, read by
|
||
// ldsfld_i4/r4) — avoids a heap box on every static write. Miss = 0 (CLR zero-init);
|
||
// reference/long/double statics stay in the boxed ScriptStatics above.
|
||
public Dictionary<int, long> ScriptStaticsNum = new();
|
||
|
||
// The host binding this assembly was parsed against — lets the LOWERER resolve
|
||
// flat layouts for host struct types (e.g. Vector3[] element layouts) by TypeRef name.
|
||
public HostBinding? HostSurface;
|
||
|
||
// All methods' lowered IR concatenated (branch targets blob-absolute, one ret_void
|
||
// sentinel after each method's region). Built by IrLowerer.BuildIrBlob after LowerAll.
|
||
// Lets the Vm run script-to-script calls iteratively under a single `fixed` — no
|
||
// recursive Run() invocation (measured ~120ns per nested call on Mono) and no per-call
|
||
// array pin.
|
||
public uint[]? IrBlob;
|
||
}
|
||
|
||
// CLR full name for a TypeRef — namespace-qualified, nested types joined with '+' — matching
|
||
// what Assembly.GetType expects. A nested TypeRef's ResolutionScope is its declaring TypeRef.
|
||
static string TypeRefFullName(MetadataReader reader, TypeReferenceHandle handle)
|
||
{
|
||
var tr = reader.GetTypeReference(handle);
|
||
var name = reader.GetString(tr.Name);
|
||
if (tr.ResolutionScope.Kind == HandleKind.TypeReference)
|
||
return TypeRefFullName(reader, (TypeReferenceHandle)tr.ResolutionScope) + "+" + name;
|
||
var ns = reader.GetString(tr.Namespace);
|
||
return ns.Length == 0 ? name : ns + "." + name;
|
||
}
|
||
|
||
static ParsedAssembly Parse(byte[] bytes, HostBinding? binding, Action<string> logSink)
|
||
{
|
||
var pe = new PEReader(new MemoryStream(bytes), PEStreamOptions.PrefetchEntireImage);
|
||
MetadataReader reader = pe.GetMetadataReader();
|
||
|
||
// --- Demand-time auto-bind: register this script's referenced host types on first use ---
|
||
// When the binding carries an AutoBindResolver policy, resolve every TypeRef the script
|
||
// references and AllowType the ones the policy accepts, BEFORE the MemberRef resolution
|
||
// below — every resolution site then sees the type as if it had been curated. What binds
|
||
// is entirely the resolver's decision (which types exist, which assemblies are policy-
|
||
// skipped); this pass just walks the refs. Type args of closed generics appear as their
|
||
// own TypeRef rows, so MethodSpec instantiation (AddComponent<T>) benefits too.
|
||
if (binding?.AutoBindResolver != null)
|
||
foreach (var trHandle in reader.TypeReferences)
|
||
binding.TryAutoBindType(TypeRefFullName(reader, trHandle));
|
||
|
||
// --- Script methods ---
|
||
var byToken = new Dictionary<int, ParsedMethod>();
|
||
var byName = new Dictionary<string, ParsedMethod>();
|
||
|
||
foreach (var handle in reader.MethodDefinitions)
|
||
{
|
||
var def = reader.GetMethodDefinition(handle);
|
||
var name = reader.GetString(def.Name);
|
||
var token = MetadataTokens.GetToken(handle);
|
||
|
||
if (def.RelativeVirtualAddress == 0) continue;
|
||
|
||
var body = pe.GetMethodBody(def.RelativeVirtualAddress);
|
||
var ilBytes = body.GetILBytes() ?? Array.Empty<byte>();
|
||
var localCount = ReadLocalCount(reader, body.LocalSignature);
|
||
var localStrs = ReadLocalStructLayouts(reader, body.LocalSignature, localCount, binding);
|
||
var localScSt = ReadLocalIsScriptStruct(reader, body.LocalSignature, localCount, out var localScTds);
|
||
var localSTps = ReadLocalSTypes(reader, body.LocalSignature, localCount);
|
||
var isStatic = (def.Attributes & MethodAttributes.Static) != 0;
|
||
var sigBytes = reader.GetBlobBytes(def.Signature);
|
||
var (isVoid, retSType, retIsBool) = SigReturn(sigBytes);
|
||
var argSTypes = SigParamSTypes(sigBytes);
|
||
// Use signature param count, not Param-table count: Roslyn omits Param table entries
|
||
// for synthetic parameters in compiler-generated methods (closures, state machines).
|
||
var argCount = (isStatic ? 0 : 1) + argSTypes.Length;
|
||
|
||
// Exception-region table: finally clauses are lowered (leave/endfinally continuation
|
||
// chain); catch/filter need real exception dispatch and fail lowering instead.
|
||
// FAULT clauses are IGNORED: a fault handler runs only under exception dispatch,
|
||
// which the interpreter never does — a script exception propagates straight to the
|
||
// host and the frame is abandoned — so skipping the region keeps normal-path
|
||
// semantics exact (fault must NOT run on normal exit). C# can't write a fault
|
||
// clause; Roslyn emits them in iterator/async MoveNext bodies, so this is what lets
|
||
// `yield return` state machines with try/finally lower. The handler body still
|
||
// lowers (as unreachable code); its start is recorded so the lowerer treats it as a
|
||
// basic-block boundary.
|
||
(int, int, int, int)[]? finRegions = null;
|
||
int[]? faultHandlerStarts = null;
|
||
bool unsupportedEh = false;
|
||
if (!body.ExceptionRegions.IsEmpty)
|
||
{
|
||
var fins = new List<(int, int, int, int)>();
|
||
List<int>? faults = null;
|
||
foreach (var er in body.ExceptionRegions)
|
||
{
|
||
if (er.Kind == ExceptionRegionKind.Finally)
|
||
fins.Add((er.TryOffset, er.TryOffset + er.TryLength,
|
||
er.HandlerOffset, er.HandlerOffset + er.HandlerLength));
|
||
else if (er.Kind == ExceptionRegionKind.Fault)
|
||
(faults ??= new List<int>()).Add(er.HandlerOffset);
|
||
else
|
||
unsupportedEh = true;
|
||
}
|
||
if (fins.Count > 0) finRegions = fins.ToArray();
|
||
if (faults != null) faultHandlerStarts = faults.ToArray();
|
||
}
|
||
|
||
var (retScTd, argScTds) = ReadSigScriptStructTokens(sigBytes);
|
||
int declTypeDef = 0;
|
||
try { declTypeDef = MetadataTokens.GetToken(def.GetDeclaringType()); } catch (Exception) { }
|
||
var m = new ParsedMethod { Name = name, Token = token, DeclaringTypeDef = declTypeDef, ArgCount = argCount, LocalCount = localCount, IlBytes = ilBytes, IsStatic = isStatic, IsVoid = isVoid, ReturnIsBool = retIsBool, ReturnSType = retSType, ArgSTypes = argSTypes, LocalStructLayouts = localStrs, LocalSTypes = localSTps, LocalIsScriptStruct = localScSt, LocalScriptStructTypeDefs = localScTds, ArgScriptStructTypeDefs = argScTds, ReturnScriptStructTypeDef = retScTd, FinallyRegions = finRegions, FaultHandlerStarts = faultHandlerStarts, HasUnsupportedEhClauses = unsupportedEh };
|
||
byToken[token] = m;
|
||
byName[name] = m;
|
||
}
|
||
|
||
// --- Script-defined types: field slots, .ctor → type mapping, and name → type ---
|
||
var fieldSlots = new Dictionary<int, (ScriptTypeDescriptor, int)>();
|
||
var fieldSTypes = new Dictionary<int, SType>();
|
||
var fieldIsScStruct = new HashSet<int>();
|
||
var fieldToTypeDef = new Dictionary<int, int>(); // FieldDef token → declaring TypeDef token
|
||
var ctorToType = new Dictionary<int, ScriptTypeDescriptor>();
|
||
var typeDefToCtorTok = new Dictionary<int, int>(); // TypeDef token → default .ctor MethodDef token
|
||
var typeDefToType = new Dictionary<int, ScriptTypeDescriptor>(); // TypeDef token → type
|
||
var typesByName = new Dictionary<string, ScriptTypeDescriptor>();
|
||
// Script types implementing System.Collections.IEnumerator → their driver members,
|
||
// so the host bridge (ScriptEnumerator) can pump an interpreted state machine.
|
||
var enumeratorTypes = new Dictionary<ScriptTypeDescriptor, EnumeratorMembers>();
|
||
// (descriptor, per-field nested-struct TypeDef token) pending resolution once all types exist.
|
||
var pendingStructFields = new List<(ScriptTypeDescriptor desc, int[] typeDefs)>();
|
||
|
||
// This type's own INSTANCE fields, declaration order. Statics are excluded: an enum's
|
||
// members are static literals typed as the enum itself, which made the descriptor
|
||
// self-referential and recursed ScriptObject.Create forever.
|
||
List<FieldDefinitionHandle> GetOwnInstanceFields(TypeDefinition td)
|
||
{
|
||
var handles = td.GetFields();
|
||
var result = new List<FieldDefinitionHandle>(handles.Count);
|
||
foreach (var fh in handles)
|
||
if ((reader.GetFieldDefinition(fh).Attributes & FieldAttributes.Static) == 0)
|
||
result.Add(fh);
|
||
return result;
|
||
}
|
||
|
||
// Base-first field layout ([root base]..[own]), walking in-assembly BaseTypes. ownStart =
|
||
// first own field. Invariant this fix relies on: a base field lands at the identical
|
||
// slot/offset in every descriptor that inherits it.
|
||
List<FieldDefinitionHandle> BuildBaseFirstFields(TypeDefinition td, out int ownStart, int depth = 0)
|
||
{
|
||
// Guard against a cyclic base chain (A : B, B : A) in hostile metadata — an unbounded
|
||
// recursion here is a StackOverflowException, which .NET cannot catch (whole-process
|
||
// kill). A real inheritance depth never approaches this.
|
||
if (depth > 256)
|
||
throw new ScriptValidationException(
|
||
"base-type chain is too deep or cyclic — malformed metadata");
|
||
List<FieldDefinitionHandle> fields;
|
||
var bt = td.BaseType;
|
||
if (bt.Kind == HandleKind.TypeDefinition)
|
||
{
|
||
var baseTd = reader.GetTypeDefinition((TypeDefinitionHandle)bt);
|
||
var baseName = reader.GetString(baseTd.Name);
|
||
fields = (baseName == "ValueType" || baseName == "Enum")
|
||
? new List<FieldDefinitionHandle>()
|
||
: BuildBaseFirstFields(baseTd, out _, depth + 1);
|
||
}
|
||
else
|
||
{
|
||
fields = new List<FieldDefinitionHandle>();
|
||
}
|
||
ownStart = fields.Count;
|
||
fields.AddRange(GetOwnInstanceFields(td));
|
||
return fields;
|
||
}
|
||
|
||
foreach (var typeHandle in reader.TypeDefinitions)
|
||
{
|
||
var typeDef = reader.GetTypeDefinition(typeHandle);
|
||
var typeName = reader.GetString(typeDef.Name);
|
||
if (typeName == "<Module>") continue;
|
||
|
||
// Detect a script-defined struct (base type System.ValueType, excluding enums whose base is
|
||
// System.Enum). Used to zero-init `new T[n]` elements to usable structs rather than nulls.
|
||
bool isStructType = false;
|
||
{
|
||
var bt = typeDef.BaseType;
|
||
string baseName = "";
|
||
if (bt.Kind == HandleKind.TypeReference)
|
||
baseName = reader.GetString(reader.GetTypeReference((TypeReferenceHandle)bt).Name);
|
||
else if (bt.Kind == HandleKind.TypeDefinition)
|
||
baseName = reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)bt).Name);
|
||
isStructType = baseName == "ValueType"; // "Enum" (also a value type) intentionally excluded
|
||
}
|
||
|
||
// Assign field slots in declaration order, BASE-FIRST: inherited fields occupy the front
|
||
// (same indices/offsets as in their own descriptor), then this type's own fields. See
|
||
// BuildBaseFirstFields for the invariant this depends on.
|
||
var typeFields = BuildBaseFirstFields(typeDef, out int ownFieldStart).ToArray();
|
||
var fieldSlotMap = new Dictionary<int, int>(typeFields.Length);
|
||
var fldTypes = new SType[typeFields.Length];
|
||
var fldOffsets = new int[typeFields.Length];
|
||
HostBinding.StructLayout?[]? fldVtLayouts = null; // allocated on demand
|
||
bool[]? fldIsScStruct = null; // allocated on demand (nested value-type fields)
|
||
int[]? fldScStructTypeDef = null; // nested script-struct field's TypeDef token (resolved to a descriptor after the loop)
|
||
int primByteOff = 0;
|
||
int refSlotOff = 0;
|
||
for (int i = 0; i < typeFields.Length; i++)
|
||
{
|
||
int fieldToken = MetadataTokens.GetToken(typeFields[i]);
|
||
bool isOwnField = i >= ownFieldStart;
|
||
fieldSlotMap[fieldToken] = i;
|
||
// Global field maps are keyed by declaring type only, so register OWN fields here;
|
||
// an inherited field is registered by its own type's pass (base-first makes the
|
||
// value identical anyway, and re-keying it here corrupts declaring-type bookkeeping).
|
||
if (isOwnField)
|
||
fieldSlots[fieldToken] = (null!, i); // type filled in below
|
||
var fDef = reader.GetFieldDefinition(typeFields[i]);
|
||
var fSig = reader.GetBlobBytes(fDef.Signature);
|
||
// Field sig: [calling_conv(0x06), type_byte, ...]
|
||
// VALUETYPE (0x11) followed by compressed TypeRef/TypeDef token — check
|
||
// if it's a registered host struct; if so, store inline in PrimBytes (SType.Vt).
|
||
SType fst = SType.O;
|
||
HostBinding.StructLayout? vtLayout = null;
|
||
if (fSig.Length >= 2)
|
||
{
|
||
byte tb = fSig[1];
|
||
// 0x03 = ELEMENT_TYPE_CHAR: char is I4-typed in the VM; omitting it typed a
|
||
// hoisted char field O (a ref slot), the mirror of the SigReturn char fix
|
||
// (found by fuzzing: a foreach-over-string char hoisted across a yield).
|
||
// nint/nuint (0x18/0x19) are NOT here: an I4 slot would truncate 64-bit
|
||
// handles (AndroidJNI-style IntPtrs); they ride O slots as boxed IntPtr.
|
||
if (tb is 0x02 or 0x03 or 0x04 or 0x05 or 0x06 or 0x07 or 0x08 or 0x09)
|
||
fst = SType.I4;
|
||
else if (tb == 0x0C)
|
||
fst = SType.R4;
|
||
else if (tb == 0x11 && fSig.Length >= 3)
|
||
{
|
||
// VALUETYPE: try to decode the TypeRef name and look up a struct layout
|
||
int codedIdx = 0, n = 2;
|
||
while (n < fSig.Length)
|
||
{
|
||
byte b = fSig[n++];
|
||
if ((b & 0x80) == 0) { codedIdx |= b; break; }
|
||
if ((b & 0x40) == 0) { codedIdx = ((b & 0x3F) << 8) | fSig[n++]; break; }
|
||
codedIdx = ((b & 0x1F) << 24) | (fSig[n] << 16) | (fSig[n+1] << 8) | fSig[n+2]; n += 3; break;
|
||
}
|
||
int table = codedIdx & 0x03; int row = codedIdx >> 2;
|
||
string vtName = "";
|
||
if (table == 1 && row > 0) // TypeRef
|
||
{
|
||
try { vtName = reader.GetString(reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(row)).Name); }
|
||
catch { }
|
||
}
|
||
else if (table == 0 && row > 0) // TypeDef
|
||
{
|
||
try { vtName = reader.GetString(reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(row)).Name); }
|
||
catch { }
|
||
}
|
||
if (vtName != "" && binding != null && binding.TryGetStructLayout(vtName, out vtLayout) && vtLayout != null)
|
||
fst = SType.Vt;
|
||
else if (table == 0 && IsEnumTypeDef(reader, row))
|
||
{
|
||
// VALUETYPE that is a script-defined ENUM: the value is a plain int,
|
||
// not a nested struct — classify I4 so loads/stores/compares treat it
|
||
// as a number and Create never allocates a ScriptObject for it.
|
||
fst = SType.I4;
|
||
}
|
||
else if (table == 0)
|
||
{
|
||
// VALUETYPE referencing a TypeDef in this assembly that isn't a registered
|
||
// host struct = a script-defined struct. Stored as O (boxed ScriptObject),
|
||
// so a value-load of this field must clone to preserve copy semantics.
|
||
if (isOwnField) fieldIsScStruct.Add(fieldToken);
|
||
(fldIsScStruct ??= new bool[typeFields.Length])[i] = true;
|
||
// Stash the field's own TypeDef token (0x02 table) to resolve to a descriptor
|
||
// after all types are built, so Create can allocate the nested struct.
|
||
(fldScStructTypeDef ??= new int[typeFields.Length])[i] =
|
||
MetadataTokens.GetToken(MetadataTokens.TypeDefinitionHandle(row));
|
||
}
|
||
}
|
||
}
|
||
if (isOwnField) fieldSTypes[fieldToken] = fst;
|
||
fldTypes[i] = fst;
|
||
if (fst == SType.I4 || fst == SType.R4)
|
||
{
|
||
fldOffsets[i] = primByteOff;
|
||
primByteOff += 4;
|
||
}
|
||
else if (fst == SType.Vt && vtLayout != null)
|
||
{
|
||
// Align to 4 bytes (structs are naturally aligned in Marshal layout)
|
||
int align = (4 - primByteOff % 4) % 4;
|
||
primByteOff += align;
|
||
fldOffsets[i] = primByteOff;
|
||
primByteOff += vtLayout.Size;
|
||
if (primByteOff % 4 != 0) primByteOff += 4 - primByteOff % 4;
|
||
if (fldVtLayouts == null) fldVtLayouts = new HostBinding.StructLayout?[typeFields.Length];
|
||
fldVtLayouts[i] = vtLayout;
|
||
}
|
||
else
|
||
{
|
||
fldOffsets[i] = refSlotOff;
|
||
refSlotOff++;
|
||
}
|
||
}
|
||
|
||
var desc = new ScriptTypeDescriptor
|
||
{
|
||
Name = typeName,
|
||
FieldCount = typeFields.Length,
|
||
FieldSlots = fieldSlotMap,
|
||
FieldTypes = fldTypes,
|
||
FieldOffsets = fldOffsets,
|
||
PrimByteSize = primByteOff,
|
||
RefSlotCount = refSlotOff,
|
||
VtFieldLayouts = fldVtLayouts,
|
||
FieldIsScriptStruct = fldIsScStruct,
|
||
IsScriptStructValue = isStructType,
|
||
};
|
||
|
||
// Patch back the type reference into fieldSlots — OWN fields only (see isOwnField above).
|
||
foreach (var ft in fieldSlotMap)
|
||
if (ft.Value >= ownFieldStart)
|
||
fieldSlots[ft.Key] = (desc, ft.Value);
|
||
|
||
typesByName[typeName] = desc;
|
||
if (fldScStructTypeDef != null) pendingStructFields.Add((desc, fldScStructTypeDef));
|
||
|
||
// Map TypeDef token → type descriptor (used by initobj lowering).
|
||
int typeDefTok = MetadataTokens.GetToken(typeHandle);
|
||
typeDefToType[typeDefTok] = desc;
|
||
// Map each OWN field → declaring TypeDef token (used by auto-initobj in stfld lowering).
|
||
// An inherited field's declaring TypeDef is registered when its own type is processed.
|
||
foreach (var ft in fieldSlotMap)
|
||
if (ft.Value >= ownFieldStart)
|
||
fieldToTypeDef[ft.Key] = typeDefTok;
|
||
|
||
// Map each .ctor to this type; also record TypeDef → default-ctor for initobj handling.
|
||
foreach (var methodHandle in typeDef.GetMethods())
|
||
{
|
||
var mDef = reader.GetMethodDefinition(methodHandle);
|
||
var mName = reader.GetString(mDef.Name);
|
||
if (mName != ".ctor") continue;
|
||
int ctorTok = MetadataTokens.GetToken(methodHandle);
|
||
ctorToType[ctorTok] = desc;
|
||
// Record the first ctor as the default; prefer the 0-arg ctor (ArgCount==1 = just `this`)
|
||
if (!typeDefToCtorTok.TryGetValue(typeDefTok, out _) ||
|
||
(byToken.TryGetValue(ctorTok, out var ctorM) && ctorM.ArgCount == 1))
|
||
typeDefToCtorTok[typeDefTok] = ctorTok;
|
||
}
|
||
|
||
// --- Enumerator (iterator state-machine) detection ---
|
||
// A script type implementing System.Collections.IEnumerator — in practice a Roslyn
|
||
// `yield return` state machine — gets its driver members resolved so the host
|
||
// bridge (ScriptEnumerator) can pump it from Unity's coroutine scheduler. Reset is
|
||
// marked cold: its body is `throw new NotSupportedException()`, which the
|
||
// interpreter can't lower, and no driver ever calls it.
|
||
bool isEnumerator = false;
|
||
foreach (var ih in typeDef.GetInterfaceImplementations())
|
||
{
|
||
var ii = reader.GetInterfaceImplementation(ih);
|
||
if (ii.Interface.Kind == HandleKind.TypeReference &&
|
||
TypeRefFullName(reader, (TypeReferenceHandle)ii.Interface) == "System.Collections.IEnumerator")
|
||
{
|
||
isEnumerator = true;
|
||
break;
|
||
}
|
||
}
|
||
if (isEnumerator)
|
||
{
|
||
ParsedMethod? moveNext = null, getCurrent = null, dispose = null;
|
||
foreach (var mh in typeDef.GetMethods())
|
||
{
|
||
if (!byToken.TryGetValue(MetadataTokens.GetToken(mh), out var pm)) continue;
|
||
switch (pm.Name)
|
||
{
|
||
case "MoveNext":
|
||
case "System.Collections.IEnumerator.MoveNext":
|
||
moveNext = pm; break;
|
||
// The non-generic getter — present in every flavor (a generic
|
||
// IEnumerator<T> state machine also implements it). Overwrites a plain
|
||
// get_Current seen earlier; the plain name only fills when nothing
|
||
// better exists (hand-written enumerator without explicit impls).
|
||
case "System.Collections.IEnumerator.get_Current":
|
||
getCurrent = pm; break;
|
||
case "get_Current":
|
||
getCurrent ??= pm; break;
|
||
case "Dispose":
|
||
case "System.IDisposable.Dispose":
|
||
dispose = pm; break;
|
||
case "Reset":
|
||
case "System.Collections.IEnumerator.Reset":
|
||
pm.IsColdEnumeratorMember = true; break;
|
||
}
|
||
}
|
||
if (moveNext != null && getCurrent != null)
|
||
enumeratorTypes[desc] = new EnumeratorMembers(moveNext, getCurrent, dispose);
|
||
}
|
||
}
|
||
|
||
// Resolve each script-struct field's nested TypeDef token to a descriptor now that every type
|
||
// is built (declaration order isn't guaranteed to be dependency order). Enables Create() to
|
||
// recursively allocate nested value-type fields.
|
||
foreach (var (desc, typeDefs) in pendingStructFields)
|
||
{
|
||
ScriptTypeDescriptor?[]? arr = null;
|
||
for (int i = 0; i < typeDefs.Length; i++)
|
||
if (typeDefs[i] != 0 && typeDefToType.TryGetValue(typeDefs[i], out var sub))
|
||
(arr ??= new ScriptTypeDescriptor?[typeDefs.Length])[i] = sub;
|
||
desc.FieldStructDescriptors = arr;
|
||
}
|
||
|
||
// --- Flat script-struct resolution (zero-GC structs) ---
|
||
// Recursively classify blittable script structs (no reference-frame fields, transitively),
|
||
// inline nested blittable struct FIELDS into the parent's PrimBytes (they were RefSlot
|
||
// ScriptObjects), and synthesize a FlatLayout so blittable structs live in Vt frame slots.
|
||
// Runs after FieldStructDescriptors resolution (needs nested descs) and before methods'
|
||
// flat metadata below.
|
||
{
|
||
var flatVisited = new HashSet<ScriptTypeDescriptor>();
|
||
foreach (var d in typesByName.Values)
|
||
ResolveFlatScriptStruct(d, flatVisited, fieldSTypes, fieldIsScStruct);
|
||
|
||
// Method metadata post-pass: locals/args/returns whose script-struct type resolved
|
||
// flat switch from the heap (O + clone_sc) representation to Vt.
|
||
foreach (var m in byToken.Values)
|
||
{
|
||
var ltds = m.LocalScriptStructTypeDefs;
|
||
if (ltds != null)
|
||
for (int j = 0; j < m.LocalCount && j < ltds.Length; j++)
|
||
if (ltds[j] != 0 && typeDefToType.TryGetValue(ltds[j], out var ld) && ld.FlatLayout != null)
|
||
{
|
||
(m.LocalStructLayouts ??= new HostBinding.StructLayout?[m.LocalCount])[j] = ld.FlatLayout;
|
||
if (m.LocalIsScriptStruct != null) m.LocalIsScriptStruct[j] = false;
|
||
}
|
||
var atds = m.ArgScriptStructTypeDefs;
|
||
if (atds != null)
|
||
for (int j = 0; j < m.ArgSTypes.Length && j < atds.Length; j++)
|
||
if (atds[j] != 0 && typeDefToType.TryGetValue(atds[j], out var ad) && ad.FlatLayout != null)
|
||
{
|
||
m.ArgSTypes[j] = SType.Vt;
|
||
(m.ArgStructLayouts ??= new HostBinding.StructLayout?[m.ArgSTypes.Length])[j] = ad.FlatLayout;
|
||
}
|
||
if (m.ReturnScriptStructTypeDef != 0
|
||
&& typeDefToType.TryGetValue(m.ReturnScriptStructTypeDef, out var rd) && rd.FlatLayout != null)
|
||
{
|
||
m.ReturnSType = SType.Vt;
|
||
m.ReturnStructLayout = rd.FlatLayout;
|
||
}
|
||
if (!m.IsStatic && m.DeclaringTypeDef != 0
|
||
&& typeDefToType.TryGetValue(m.DeclaringTypeDef, out var dd) && dd.FlatLayout != null)
|
||
m.ThisStructLayout = dd.FlatLayout;
|
||
}
|
||
}
|
||
|
||
// --- Host calls: resolve MemberRef tokens ---
|
||
var hostCalls = new Dictionary<int, HostEntry>();
|
||
var hostCtors = new Dictionary<int, HostEntry>();
|
||
var hostFields = new Dictionary<int, HostBinding.FieldEntry>();
|
||
var hostFieldDeclTypeTok = new Dictionary<int, int>();
|
||
var tokenNames = new Dictionary<int, string>();
|
||
var unboundHostMembers = new List<string>();
|
||
|
||
foreach (var memberRefHandle in reader.MemberReferences)
|
||
{
|
||
var memberRef = reader.GetMemberReference(memberRefHandle);
|
||
var memberName = reader.GetString(memberRef.Name);
|
||
int token = MetadataTokens.GetToken(memberRefHandle);
|
||
|
||
string typeName2 = "";
|
||
string typeNs2 = "";
|
||
if (memberRef.Parent.Kind == HandleKind.TypeReference)
|
||
{
|
||
var typeRef = reader.GetTypeReference((TypeReferenceHandle)memberRef.Parent);
|
||
typeName2 = reader.GetString(typeRef.Name);
|
||
typeNs2 = reader.GetString(typeRef.Namespace);
|
||
}
|
||
else if (memberRef.Parent.Kind == HandleKind.TypeSpecification)
|
||
{
|
||
typeName2 = ResolveTypeSpecName(reader, (TypeSpecificationHandle)memberRef.Parent);
|
||
}
|
||
|
||
// Field refs have signature byte 0x06 (FIELD calling convention)
|
||
var sigBytes = reader.GetBlobBytes(memberRef.Signature);
|
||
if (sigBytes.Length > 0 && sigBytes[0] == 0x06)
|
||
{
|
||
if (binding != null && binding.TryGetField($"{typeName2}.{memberName}", out var fe))
|
||
{
|
||
hostFields[token] = fe!;
|
||
if (memberRef.Parent.Kind == HandleKind.TypeReference)
|
||
hostFieldDeclTypeTok[token] = MetadataTokens.GetToken(memberRef.Parent);
|
||
// Classify the field type so the IR lowerer can emit ldfld_i4/ldfld_r4. Prefer the
|
||
// binding's FieldSt (derived from the CLR field type, so it knows an enum is I4); the
|
||
// signature-byte guess below can't distinguish an enum (0x11 VALUETYPE) from a struct
|
||
// and would mis-type it O. Fall back to the sig guess only when FieldSt is unset (O).
|
||
if (fe!.FieldSt != SType.O)
|
||
fieldSTypes[token] = fe.FieldSt;
|
||
else if (sigBytes.Length >= 2)
|
||
fieldSTypes[token] = sigBytes[1] switch
|
||
{
|
||
// nint/nuint (0x18/0x19) stay O: boxed IntPtr, no 64-bit truncation
|
||
0x02 or 0x04 or 0x05 or 0x06 or 0x07 or 0x08 or 0x09 => SType.I4,
|
||
0x0C => SType.R4,
|
||
_ => SType.O,
|
||
};
|
||
}
|
||
else if (binding != null && memberRef.Parent.Kind == HandleKind.TypeSpecification
|
||
&& ResolveClosedSpecType(reader, (TypeSpecificationHandle)memberRef.Parent, binding) is { } fldDecl
|
||
&& fldDecl.GetField(memberName) is { } specFi)
|
||
{
|
||
// Field reached through a closed generic instantiation (ValueTuple`2.Item1):
|
||
// synthesize per token — each instantiation's MemberRef row carries its own
|
||
// FieldInfo. Reads only on value types: writing through the boxed receiver
|
||
// would alias every copy of the struct (silent value-semantics break).
|
||
var specFe = HostBinding.BuildFieldEntry(specFi);
|
||
if (fldDecl.IsValueType)
|
||
{
|
||
var fname = $"{typeName2}.{memberName}";
|
||
specFe.Set = (_, _2) => throw new ScriptRuntimeException(
|
||
$"assigning {fname} through a boxed struct is not supported — construct a new value instead");
|
||
}
|
||
hostFields[token] = specFe;
|
||
if (specFe.FieldSt != SType.O)
|
||
fieldSTypes[token] = specFe.FieldSt;
|
||
}
|
||
else if (binding != null)
|
||
unboundHostMembers.Add($"{typeName2}.{memberName} (field)");
|
||
continue;
|
||
}
|
||
|
||
// Constructor — registered ones become host ctors; others are no-op base inits.
|
||
// System.Object::.ctor() is always the canonical base ctor (script chains to it
|
||
// via `ldarg.0; call .ctor`); force it to noop even if a name-only collision
|
||
// (e.g. UnityEngine.Object also being registered) would otherwise route it to
|
||
// hostCtors. Lookups in `_entries` are unqualified `TypeName..ctor/N`.
|
||
if (memberName == ".ctor")
|
||
{
|
||
int memberParamCount0 = DecompressInt(sigBytes, 1);
|
||
// new Nullable<T>(v) — including the implicit T→T? conversion and `T? x = v;` local
|
||
// inits (ldloca + call .ctor, routed through the Call lowering's hostCtor pattern).
|
||
// The interpreter stores nullables the way CLR boxing does — as the boxed T, or
|
||
// null — so construction is the identity on the single argument.
|
||
if (memberParamCount0 == 1 && typeName2 == "Nullable`1"
|
||
&& memberRef.Parent.Kind == HandleKind.TypeSpecification)
|
||
{
|
||
var nullableCtor = new HostBinding.Entry
|
||
{
|
||
Delegate = (_, args) => args[0],
|
||
ParamCount = 1,
|
||
HasThis = false,
|
||
};
|
||
hostCtors[token] = new HostEntry { Binding = nullableCtor, IsVoid = true };
|
||
continue;
|
||
}
|
||
// In-script `new List<T>()` / Dictionary / HashSet / Queue / Stack: the shimmed
|
||
// collections dispatch every member on the receiver's runtime type, so construction
|
||
// was the only missing piece. Resolve the TypeSpec to its closed type and bind the
|
||
// ctor whose params decode concretely — the 0-arg and int-capacity overloads; a
|
||
// VAR-typed param (e.g. IEnumerable<T>) fails the decode and stays loud-unsupported.
|
||
if (memberRef.Parent.Kind == HandleKind.TypeSpecification && binding != null
|
||
&& (typeName2 is "List`1" or "Dictionary`2" or "HashSet`1" or "Queue`1" or "Stack`1"
|
||
|| typeName2.StartsWith("ValueTuple`", StringComparison.Ordinal)))
|
||
{
|
||
var closedType = ResolveClosedSpecType(reader, (TypeSpecificationHandle)memberRef.Parent, binding);
|
||
ConstructorInfo? ci = null;
|
||
if (closedType != null)
|
||
{
|
||
if (memberParamCount0 == 0)
|
||
ci = closedType.GetConstructor(Type.EmptyTypes);
|
||
else if (DecodeMethodSigParamTypes(reader, sigBytes, binding) is { } ctorParams)
|
||
ci = closedType.GetConstructor(ctorParams);
|
||
// Tuple ctor params are VARs (!0, !1) — the sig decode can't name them, but
|
||
// for a CLOSED spec they are exactly the instantiation's type args.
|
||
if (ci == null && closedType.IsGenericType
|
||
&& closedType.GetGenericArguments() is { } ga && ga.Length == memberParamCount0)
|
||
ci = closedType.GetConstructor(ga);
|
||
}
|
||
if (ci != null)
|
||
{
|
||
var ciLocal = ci;
|
||
var ctorParamTypes = Array.ConvertAll(ci.GetParameters(), p => p.ParameterType);
|
||
hostCtors[token] = new HostEntry
|
||
{
|
||
Binding = new HostBinding.Entry
|
||
{
|
||
// I4-slot identity erasure: a char/bool/enum arg arrives boxed as
|
||
// int; Invoke is strict, so coerce to the declared param type.
|
||
Delegate = (_, args) =>
|
||
{
|
||
for (int ai = 0; ai < ctorParamTypes.Length; ai++)
|
||
{
|
||
var pt = ctorParamTypes[ai];
|
||
if (args[ai] is int iv && pt != typeof(int) && pt != typeof(object))
|
||
{
|
||
if (pt == typeof(char)) args[ai] = (char)iv;
|
||
else if (pt == typeof(bool)) args[ai] = iv != 0;
|
||
else if (pt.IsEnum) args[ai] = Enum.ToObject(pt, iv);
|
||
else if (pt == typeof(float)) args[ai] = (float)iv;
|
||
else args[ai] = HostBinding.ToFieldIntegral(iv, pt);
|
||
}
|
||
}
|
||
return ciLocal.Invoke(args);
|
||
},
|
||
ParamCount = memberParamCount0,
|
||
HasThis = false,
|
||
},
|
||
IsVoid = true,
|
||
};
|
||
continue;
|
||
}
|
||
}
|
||
bool isSystemObjectCtor = typeNs2 == "System" && typeName2 == "Object" && memberParamCount0 == 0;
|
||
if (!isSystemObjectCtor && binding != null && binding.TryGet($"{typeName2}..ctor", memberParamCount0, out var ctorEntry))
|
||
hostCtors[token] = new HostEntry { Binding = ctorEntry!, IsVoid = true };
|
||
else
|
||
{
|
||
var noop = new HostBinding.Entry { Delegate = (_, _) => null, ParamCount = 0, HasThis = true };
|
||
hostCalls[token] = new HostEntry { Binding = noop, IsVoid = true };
|
||
}
|
||
continue;
|
||
}
|
||
|
||
int memberParamCount = DecompressInt(sigBytes, 1);
|
||
var (sigIsVoid, sigRetSType, _) = SigReturn(sigBytes);
|
||
|
||
tokenNames[token] = $"{typeName2}.{memberName}/{memberParamCount}";
|
||
|
||
// System.Object's members are intrinsics dispatched virtually on the receiver.
|
||
// Resolving them through the binding by SHORT name is wrong: "Object" collides with
|
||
// UnityEngine.Object in the standard surface, so a `constrained.` callvirt (e.g.
|
||
// `"impact" + quaternion` → Object.ToString on the boxed struct) invoked
|
||
// UnityEngine.Object.ToString with a foreign receiver — a TargetException the call
|
||
// site remapped to a misleading "called Object.ToString/0 on a null object".
|
||
// The namespace check mirrors the System.Object ctor special case above.
|
||
if (typeNs2 == "System" && typeName2 == "Object"
|
||
&& memberRef.Parent.Kind == HandleKind.TypeReference)
|
||
{
|
||
bool objHasThis = sigBytes.Length > 0 && (sigBytes[0] & 0x20) != 0;
|
||
HostBinding.Entry objIntrinsic = memberName switch
|
||
{
|
||
"ToString" when objHasThis && memberParamCount == 0 => new HostBinding.Entry
|
||
{
|
||
Delegate = (recv, _) => recv != null ? recv.ToString()
|
||
: throw new ScriptRuntimeException("NullReferenceException: called Object.ToString on a null object"),
|
||
ParamCount = 0, HasThis = true,
|
||
},
|
||
"GetHashCode" when objHasThis && memberParamCount == 0 => new HostBinding.Entry
|
||
{
|
||
Delegate = (recv, _) => recv != null ? recv.GetHashCode()
|
||
: throw new ScriptRuntimeException("NullReferenceException: called Object.GetHashCode on a null object"),
|
||
ParamCount = 0, HasThis = true,
|
||
},
|
||
"GetType" when objHasThis && memberParamCount == 0 => new HostBinding.Entry
|
||
{
|
||
Delegate = (recv, _) => recv != null ? recv.GetType()
|
||
: throw new ScriptRuntimeException("NullReferenceException: called Object.GetType on a null object"),
|
||
ParamCount = 0, HasThis = true,
|
||
},
|
||
"Equals" when objHasThis && memberParamCount == 1 => new HostBinding.Entry
|
||
{
|
||
Delegate = (recv, args) => recv != null ? recv.Equals(args![0])
|
||
: throw new ScriptRuntimeException("NullReferenceException: called Object.Equals on a null object"),
|
||
ParamCount = 1, HasThis = true,
|
||
},
|
||
"Equals" when !objHasThis && memberParamCount == 2 => new HostBinding.Entry
|
||
{ Delegate = (_, args) => Equals(args![0], args[1]), ParamCount = 2, HasThis = false },
|
||
"ReferenceEquals" when !objHasThis && memberParamCount == 2 => new HostBinding.Entry
|
||
{ Delegate = (_, args) => ReferenceEquals(args![0], args[1]), ParamCount = 2, HasThis = false },
|
||
_ => null!,
|
||
};
|
||
if (objIntrinsic != null)
|
||
{
|
||
hostCalls[token] = new HostEntry
|
||
{ Binding = objIntrinsic, IsVoid = false, SigRetSType = sigRetSType };
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// VM intrinsic: IlInterpreter.Vm.Log → route to per-VM logSink.
|
||
if (typeNs2 == "IlInterpreter" && typeName2 == "Vm" && memberName == "Log" && memberParamCount == 1)
|
||
{
|
||
var entry = new HostBinding.Entry
|
||
{
|
||
Delegate = (_, args) => { logSink((string)args[0]!); return null; },
|
||
ParamCount = 1,
|
||
HasThis = false,
|
||
};
|
||
hostCalls[token] = new HostEntry { Binding = entry, IsVoid = true };
|
||
continue;
|
||
}
|
||
|
||
// System.Nullable<T> members are intrinsics. The interpreter stores nullables the way
|
||
// CLR boxing does — as the boxed T, or null — so HasValue/Value/GetValueOrDefault
|
||
// reduce to null checks on the receiver slot's boxed value (the ldloca/ldflda receiver
|
||
// phantom resolves to that boxed value before the call).
|
||
if (typeName2 == "Nullable`1" && memberRef.Parent.Kind == HandleKind.TypeSpecification)
|
||
{
|
||
var (_, nullableArg) = DecodeGenericInstHeader(reader, (TypeSpecificationHandle)memberRef.Parent);
|
||
var nullableEntry = MakeNullableIntrinsic(memberName, memberParamCount, nullableArg, binding);
|
||
if (nullableEntry != null)
|
||
{
|
||
// Type the result slot from the RESOLVED type arg (not the open sig, which says
|
||
// bool for HasValue but generic T for Value/GetValueOrDefault -> O). get_HasValue
|
||
// -> I4 (bool); Value/GetValueOrDefault -> the type arg's SType. Without this the
|
||
// slot defaulted to O (boxed) and numeric ops on the result used object compares
|
||
// (found by fuzzing: `q != null` boxes, and `0 != q.GetValueOrDefault()` too).
|
||
hostCalls[token] = new HostEntry
|
||
{
|
||
Binding = nullableEntry,
|
||
IsVoid = false,
|
||
SigRetSType = NullableIntrinsicResultSType(memberName, nullableArg),
|
||
};
|
||
continue;
|
||
}
|
||
// Unmodeled members (ToString, GetHashCode, Equals) fall through to the normal
|
||
// resolution/stub path.
|
||
}
|
||
|
||
// TypeSpec: attempt MethodHandle-based resolution for generic types
|
||
if (memberRef.Parent.Kind == HandleKind.TypeSpecification && binding != null)
|
||
{
|
||
var (outerName, arg0Name) = DecodeGenericInstHeader(
|
||
reader, (TypeSpecificationHandle)memberRef.Parent);
|
||
if (outerName != "" && arg0Name != "" &&
|
||
(binding.TryGetGenericType(outerName, arg0Name, out var genType)
|
||
|| binding.TryMakeClosedGenericType(outerName, arg0Name, out genType)))
|
||
{
|
||
var mi = FindMethod(genType!, memberName, memberParamCount);
|
||
if (mi != null && binding.TryGetByHandle(mi.MethodHandle.Value, out var he))
|
||
{
|
||
hostCalls[token] = new HostEntry { Binding = he!, IsVoid = sigIsVoid, ResolvedMethod = mi };
|
||
continue;
|
||
}
|
||
// Mono: MethodHandle.Value for closed generic instantiations may differ
|
||
// between registration time and load time, causing TryGetByHandle to miss.
|
||
// Synthesize an Entry directly from the MethodInfo so the call still works.
|
||
if (mi != null)
|
||
{
|
||
hostCalls[token] = FallbackEntry(mi, sigIsVoid,
|
||
$"[IlInterpreter] TypeSpec handle fallback: {typeName2}.{memberName}/{memberParamCount}", logSink);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Signature-based resolution: TypeRef parent + registered type → decode full param types → GetMethod
|
||
if (memberRef.Parent.Kind == HandleKind.TypeReference && binding != null &&
|
||
TryResolveReceiverTypeRef(reader, (TypeReferenceHandle)memberRef.Parent, typeName2,
|
||
binding, out var hostType2) && hostType2 != null)
|
||
{
|
||
var paramTypes = DecodeMethodSigParamTypes(reader, sigBytes, binding);
|
||
MethodInfo? mi2 = null;
|
||
if (paramTypes != null)
|
||
{
|
||
// Manual exact-signature scan rather than GetMethod: DefaultBinder ignores
|
||
// generic-ness, so a generic twin with the same parameter list (Go(string,
|
||
// object[]) vs Go<T>(string, object[])) throws AmbiguousMatchException, and
|
||
// `new`-hiding across the hierarchy under Public|NonPublic does too. A plain
|
||
// MemberRef never targets a generic method definition (those calls arrive via
|
||
// MethodSpec), so skip them outright; GetMethods lists most-derived first, so
|
||
// a `new`-hidden pair resolves to the hiding member like a normal call would.
|
||
foreach (var m in hostType2.GetMethods(
|
||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||
{
|
||
if (m.Name != memberName || m.IsGenericMethodDefinition) continue;
|
||
var ps = m.GetParameters();
|
||
if (ps.Length != paramTypes.Length) continue;
|
||
bool exact = true;
|
||
for (int i = 0; i < ps.Length; i++)
|
||
if (ps[i].ParameterType != paramTypes[i]) { exact = false; break; }
|
||
if (exact) { mi2 = m; break; }
|
||
}
|
||
}
|
||
// Fallback: if sig decode failed (e.g. unregistered VALUETYPE param) and EXACTLY ONE
|
||
// non-generic method with this name+arity exists on the registered type, use it
|
||
// unambiguously. Multiple candidates → bail; the string-based fallback or a runtime
|
||
// error is safer than a silent mispick. Generic method definitions never count:
|
||
// a plain MemberRef row can't be their call site (those go through MethodSpec).
|
||
if (mi2 == null)
|
||
{
|
||
MethodInfo? unique = null;
|
||
foreach (var m in hostType2.GetMethods(
|
||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||
{
|
||
if (m.Name != memberName || m.IsGenericMethodDefinition ||
|
||
m.GetParameters().Length != memberParamCount) continue;
|
||
if (unique != null) { unique = null; break; } // ambiguous
|
||
unique = m;
|
||
}
|
||
mi2 = unique;
|
||
}
|
||
if (mi2 != null && binding.TryGetByHandle(mi2.MethodHandle.Value, out var he2))
|
||
{
|
||
hostCalls[token] = new HostEntry { Binding = he2!, IsVoid = sigIsVoid, ResolvedMethod = mi2, ReceiverBox = ReceiverBoxFor(typeName2) };
|
||
continue;
|
||
}
|
||
// Mono/IL2CPP: MethodHandle.Value may differ between registration and load time.
|
||
// When TryGetByHandle misses but the MethodInfo is unambiguous, synthesize an
|
||
// Entry directly — the string-keyed fallback only covers properties/operators.
|
||
if (mi2 != null)
|
||
{
|
||
hostCalls[token] = FallbackEntry(mi2, sigIsVoid,
|
||
$"[IlInterpreter] TypeRef handle fallback: {typeName2}.{memberName}/{memberParamCount}", logSink);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// String-based fallback (properties, operators, AllowBcl, hand-wired Allow/AllowStatic)
|
||
if (binding != null && binding.TryGet($"{typeName2}.{memberName}", memberParamCount, out var entry2))
|
||
{
|
||
// A TypeSpec member returning the generic VAR `!n` (Func`N.Invoke's TResult,
|
||
// List<T>.get_Item, Enumerator.get_Current, KeyValuePair Key/Value, …) types O
|
||
// from the open signature — but `weights[0] + weights[2]` on a List<float> then
|
||
// lowered add_i4 over boxed floats (sum 0), and `switch (s)` over a List<Shape>
|
||
// element lowered brfalse_o on a boxed 0 (case silently missed). Recover the
|
||
// CLOSED return SType from the instantiation's n-th type argument — enums
|
||
// included (their values ride I4 like everywhere else).
|
||
var invRetSt = sigRetSType;
|
||
if (memberRef.Parent.Kind == HandleKind.TypeSpecification && invRetSt == SType.O)
|
||
{
|
||
var d = GenericVarReturnSType(reader,
|
||
(TypeSpecificationHandle)memberRef.Parent, sigBytes, binding);
|
||
if (d != SType.O) invRetSt = d;
|
||
}
|
||
hostCalls[token] = new HostEntry { Binding = entry2!, IsVoid = sigIsVoid, SigRetSType = invRetSt, ReceiverBox = ReceiverBoxFor(typeName2) };
|
||
}
|
||
else
|
||
{
|
||
// Unregistered external call: stub that throws at runtime, so the method still
|
||
// lowers and the gap is reported in UnboundHostMembers at load time.
|
||
//
|
||
// EXCEPT open generic rows (GENERIC bit set on the calling convention): IL calls
|
||
// generic methods through MethodSpec tokens, which resolve — and report real gaps
|
||
// as 'Type.Name<T>/N' — in the pass below. The open MemberRef row is only their
|
||
// metadata parent; reporting it too flagged every generic call as unbound
|
||
// ("Component.GetComponent/1") even when all instantiations resolved fine.
|
||
string msgKey = $"{typeName2}.{memberName}";
|
||
bool isOpenGenericRow = sigBytes.Length > 0 && (sigBytes[0] & 0x10) != 0;
|
||
if (!isOpenGenericRow)
|
||
unboundHostMembers.Add($"{msgKey}/{memberParamCount}");
|
||
var stub = new HostBinding.Entry
|
||
{
|
||
Delegate = (_, _) => throw new ScriptRuntimeException($"Host method '{msgKey}' is not registered in the binding"),
|
||
ParamCount = memberParamCount,
|
||
HasThis = sigBytes.Length > 0 && (sigBytes[0] & 0x20) != 0,
|
||
};
|
||
hostCalls[token] = new HostEntry { Binding = stub, IsVoid = sigIsVoid };
|
||
}
|
||
}
|
||
|
||
// --- MethodSpec resolution: closed generic method calls (e.g. AddComponent<Rigidbody>) ---
|
||
// For each MethodSpec token, decode (open def, type args), instantiate via MakeGenericMethod,
|
||
// and register the resulting closed method under the methodspec token.
|
||
int msCount = reader.GetTableRowCount(TableIndex.MethodSpec);
|
||
for (int msRow = 1; msRow <= msCount; msRow++)
|
||
{
|
||
var msHandle = MetadataTokens.MethodSpecificationHandle(msRow);
|
||
var ms = reader.GetMethodSpecification(msHandle);
|
||
int token = MetadataTokens.GetToken(msHandle);
|
||
|
||
// Parent for host generic methods is always a MemberRef (script-defined open generics
|
||
// are banned by the validator).
|
||
if (ms.Method.Kind != HandleKind.MemberReference) continue;
|
||
var parentRef = reader.GetMemberReference((MemberReferenceHandle)ms.Method);
|
||
if (parentRef.Parent.Kind != HandleKind.TypeReference) continue;
|
||
|
||
var parentTypeRef = reader.GetTypeReference((TypeReferenceHandle)parentRef.Parent);
|
||
var parentType = reader.GetString(parentTypeRef.Name);
|
||
var parentName = reader.GetString(parentRef.Name);
|
||
|
||
// Generic method signature: [conv-byte][genParamCount][paramCount]...
|
||
// The GENERIC bit (0x10) must be set on the calling-convention byte.
|
||
var parentSig = reader.GetBlobBytes(parentRef.Signature);
|
||
if (parentSig.Length < 3) continue;
|
||
int sigIdx = 0;
|
||
byte conv = parentSig[sigIdx++];
|
||
if ((conv & 0x10) == 0) continue;
|
||
DecompressIntAdv(parentSig, ref sigIdx); // skip genParamCount
|
||
int paramCount = DecompressIntAdv(parentSig, ref sigIdx);
|
||
|
||
if (binding == null) continue;
|
||
if (!binding.TryGetOpenGenerics(parentType, parentName, paramCount, out var openDefs))
|
||
{
|
||
// Unregistered open generic: insert a runtime stub so the lowerer can still lower
|
||
// the method. The stub throws at runtime if the call is actually reached.
|
||
string stubKey = $"{parentType}.{parentName}<T>/{paramCount}";
|
||
unboundHostMembers.Add(stubKey);
|
||
bool hasThisStub = (conv & 0x20) != 0;
|
||
var stub2 = new HostBinding.Entry
|
||
{
|
||
Delegate = (_, _) => throw new ScriptRuntimeException($"Host generic method '{stubKey}' is not in the binding allowlist"),
|
||
ParamCount = paramCount,
|
||
HasThis = hasThisStub,
|
||
};
|
||
hostCalls[token] = new HostEntry { Binding = stub2, IsVoid = false };
|
||
continue;
|
||
}
|
||
|
||
var typeArgs = DecodeMethodSpecArgs(reader, ms.Signature, binding, token);
|
||
// Several open overloads can share (type, name, arity) — Object.Instantiate<T> has both
|
||
// (T, Transform, bool) and (T, Vector3, Quaternion). Pick by matching the MemberRef's
|
||
// parameter signature (sigIdx points at the return type here); a mispick invokes the
|
||
// wrong host method with type-mismatched args.
|
||
var openDef = openDefs!.Count == 1
|
||
? openDefs[0]
|
||
: SelectOpenGenericOverload(reader, openDefs!, parentSig, sigIdx, typeArgs);
|
||
if (openDef == null)
|
||
{
|
||
string stubKey = $"{parentType}.{parentName}<T>/{paramCount}";
|
||
unboundHostMembers.Add(stubKey + " (ambiguous overloads)");
|
||
bool hasThisStub2 = (conv & 0x20) != 0;
|
||
var stub3 = new HostBinding.Entry
|
||
{
|
||
Delegate = (_, _) => throw new ScriptRuntimeException($"Host generic method '{stubKey}' has multiple overloads and none matched the call signature"),
|
||
ParamCount = paramCount,
|
||
HasThis = hasThisStub2,
|
||
};
|
||
hostCalls[token] = new HostEntry { Binding = stub3, IsVoid = false };
|
||
continue;
|
||
}
|
||
var closed = binding.GetOrMakeClosedMethod(openDef!, typeArgs);
|
||
bool hasThis = !openDef!.IsStatic;
|
||
var ps = closed.GetParameters();
|
||
var msEntry = new HostBinding.Entry
|
||
{
|
||
Delegate = (recv, args) => closed.Invoke(recv, args),
|
||
ParamCount = ps.Length,
|
||
HasThis = hasThis,
|
||
Params = ps,
|
||
};
|
||
// ResolvedMethod = the CLOSED generic method (e.g. Echo<int>): its ReturnType is the
|
||
// concrete instantiation (int), so the call-site lowering types the result slot I4/R4
|
||
// instead of defaulting to O. Without it a generic call returning int/float/bool boxed
|
||
// its result, and a numeric use (e.g. `Echo<int>(x) != Echo<int>(x)`) reference-compared
|
||
// two boxes (found by fuzzing the MethodSpec path).
|
||
hostCalls[token] = new HostEntry
|
||
{
|
||
Binding = msEntry,
|
||
IsVoid = closed.ReturnType == typeof(void),
|
||
ResolvedMethod = closed as MethodInfo,
|
||
};
|
||
}
|
||
|
||
// --- Token → Type map: resolve TypeRef/TypeSpec tokens for castclass/isinst/ldtoken ---
|
||
var tokenTypes = new Dictionary<int, Type>();
|
||
if (binding != null)
|
||
{
|
||
foreach (var trHandle in reader.TypeReferences)
|
||
{
|
||
var tr = reader.GetTypeReference(trHandle);
|
||
var name = reader.GetString(tr.Name);
|
||
int tok = MetadataTokens.GetToken(trHandle);
|
||
// System primitives, so unbox.any is type-exact (a boxed char can't unbox to int),
|
||
// and isinst/castclass/typeof work on boxed primitives (box_prim gives them the real
|
||
// runtime type). The binding doesn't register these as host types.
|
||
Type? primType = reader.GetString(tr.Namespace) == "System" ? name switch
|
||
{
|
||
"Int32" => typeof(int), "Boolean" => typeof(bool), "Char" => typeof(char),
|
||
"Byte" => typeof(byte), "SByte" => typeof(sbyte), "Int16" => typeof(short),
|
||
"UInt16" => typeof(ushort), "UInt32" => typeof(uint), "Int64" => typeof(long),
|
||
"UInt64" => typeof(ulong), "Single" => typeof(float), "Double" => typeof(double),
|
||
_ => null,
|
||
} : null;
|
||
if (primType != null)
|
||
tokenTypes[tok] = primType;
|
||
else if (binding.TryGetTypeByName(name, out var t) && t != null)
|
||
tokenTypes[tok] = t;
|
||
}
|
||
int typeSpecCount = reader.GetTableRowCount(TableIndex.TypeSpec);
|
||
for (int tsRow = 1; tsRow <= typeSpecCount; tsRow++)
|
||
{
|
||
var tsHandle = MetadataTokens.TypeSpecificationHandle(tsRow);
|
||
int tok = MetadataTokens.GetToken(tsHandle);
|
||
var name = ResolveTypeSpecName(reader, tsHandle);
|
||
if (name != "" && binding.TryGetTypeByName(name, out var t) && t != null)
|
||
tokenTypes[tok] = t;
|
||
}
|
||
|
||
// `new S { x = 1 }` / `default(S)` on a BOXED host struct local (AllowType, the
|
||
// auto-bind default) lowers to `ldloca; initobj S; ldloca; stfld x`. The O slot has
|
||
// no box yet, and stfld on null threw "Non-static field requires a target" (found
|
||
// live in CubeFlipr's Update). Give initobj a 0-arg "ctor" keyed by the TYPE token
|
||
// — disjoint from the MemberRef tokens real ctors use — so the lowerer can emit
|
||
// newobj_host to produce the default box for the following stores to mutate in
|
||
// place. Nullable<T> keeps the ldnull path (null IS its representation here);
|
||
// primitives/enums live in numeric slots; flat structs never reach the O-slot path.
|
||
foreach (var kv in tokenTypes)
|
||
{
|
||
var vt = kv.Value;
|
||
if (!vt.IsValueType || vt.IsPrimitive || vt.IsEnum || Nullable.GetUnderlyingType(vt) != null)
|
||
continue;
|
||
if (hostCtors.ContainsKey(kv.Key)) continue;
|
||
var vtLocal = vt;
|
||
hostCtors[kv.Key] = new HostEntry
|
||
{
|
||
Binding = new HostBinding.Entry
|
||
{
|
||
Delegate = (_, _) => Activator.CreateInstance(vtLocal),
|
||
ParamCount = 0, HasThis = false,
|
||
},
|
||
IsVoid = true,
|
||
};
|
||
}
|
||
}
|
||
|
||
// --- Embedded PDB: read sequence points for source-mapped errors ---
|
||
MetadataReaderProvider? pdbProvider = null;
|
||
foreach (var entry in pe.ReadDebugDirectory())
|
||
{
|
||
if (entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb)
|
||
{
|
||
pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(entry);
|
||
break;
|
||
}
|
||
}
|
||
if (pdbProvider != null)
|
||
{
|
||
var pdbReader = pdbProvider.GetMetadataReader();
|
||
// Document handle → short file name, resolved once per document. With #line directives
|
||
// in the compiled source (the code-reload transform emits them), documents are the USER's
|
||
// files, so the name makes error locations self-locating: " at line 394 (TankAI.cs)".
|
||
var docNames = new Dictionary<DocumentHandle, string>();
|
||
foreach (var handle in reader.MethodDefinitions)
|
||
{
|
||
int token = MetadataTokens.GetToken(handle);
|
||
if (!byToken.TryGetValue(token, out var pm)) continue;
|
||
int row = MetadataTokens.GetRowNumber(handle);
|
||
var debugInfo = pdbReader.GetMethodDebugInformation(MetadataTokens.MethodDebugInformationHandle(row));
|
||
if (debugInfo.SequencePointsBlob.IsNil) continue;
|
||
var points = new List<(int, int, string)>();
|
||
foreach (var sp in debugInfo.GetSequencePoints())
|
||
{
|
||
if (sp.IsHidden) continue;
|
||
if (!docNames.TryGetValue(sp.Document, out var doc))
|
||
{
|
||
var fullName = sp.Document.IsNil ? "" : pdbReader.GetString(pdbReader.GetDocument(sp.Document).Name);
|
||
doc = Path.GetFileName(fullName);
|
||
docNames[sp.Document] = doc;
|
||
}
|
||
points.Add((sp.Offset, sp.StartLine, doc));
|
||
}
|
||
if (points.Count > 0)
|
||
pm.SeqPoints = points.ToArray();
|
||
}
|
||
}
|
||
|
||
// Resolve flat-struct (Vt) layouts for every host call / host ctor whose params,
|
||
// return, or receiver are registered struct types. Done in one sweep at the end
|
||
// so the per-site HostEntry construction code stays unchanged.
|
||
foreach (var he in hostCalls.Values) FillStructInfo(he, binding, isCtor: false);
|
||
foreach (var he in hostCtors.Values) FillStructInfo(he, binding, isCtor: true);
|
||
|
||
return new ParsedAssembly
|
||
{
|
||
ByToken = byToken,
|
||
ByName = byName,
|
||
HostCalls = hostCalls,
|
||
HostCtors = hostCtors,
|
||
HostFieldDeclTypeTok = hostFieldDeclTypeTok,
|
||
HostFields = hostFields,
|
||
TokenNames = tokenNames,
|
||
FieldSlots = fieldSlots,
|
||
FieldSTypes = fieldSTypes,
|
||
FieldIsScriptStruct = fieldIsScStruct,
|
||
FieldToTypeDef = fieldToTypeDef,
|
||
CtorToType = ctorToType,
|
||
TypeDefToCtorTok = typeDefToCtorTok,
|
||
TypeDefToType = typeDefToType,
|
||
TypesByName = typesByName,
|
||
EnumeratorTypes = enumeratorTypes,
|
||
TokenTypes = tokenTypes,
|
||
Reader = reader,
|
||
Pe = pe,
|
||
PdbProvider = pdbProvider,
|
||
UnboundHostMembers = unboundHostMembers,
|
||
HostSurface = binding,
|
||
};
|
||
}
|
||
|
||
// Build an intrinsic Entry for a System.Nullable<T> member. The receiver arrives as the slot's
|
||
// boxed value — boxed T or null, the CLR boxed representation of a nullable — so the semantics
|
||
// are plain null checks. Returns null for members that aren't modeled (ToString, GetHashCode,
|
||
// Equals), which then resolve or stub through the normal path.
|
||
static HostBinding.Entry? MakeNullableIntrinsic(string memberName, int paramCount, string typeArgName, HostBinding? binding)
|
||
{
|
||
switch (memberName)
|
||
{
|
||
case "get_HasValue" when paramCount == 0:
|
||
return new HostBinding.Entry
|
||
{ Delegate = (recv, _) => recv != null, ParamCount = 0, HasThis = true };
|
||
|
||
case "get_Value" when paramCount == 0:
|
||
return new HostBinding.Entry
|
||
{
|
||
Delegate = (recv, _) => recv ?? throw new ScriptRuntimeException(
|
||
"InvalidOperationException: Nullable object must have a value"),
|
||
ParamCount = 0,
|
||
HasThis = true,
|
||
};
|
||
|
||
case "GetValueOrDefault" when paramCount == 0:
|
||
{
|
||
// default(T), boxed once at load time. Stays null when T can't be resolved — safe
|
||
// for the compiler-generated uses (`x ?? y`, `x?.M()` evaluate GetValueOrDefault
|
||
// only alongside a HasValue guard); a hand-written call on an unresolved T yields
|
||
// null, which downstream O-slot ops treat as default anyway.
|
||
object? boxedDefault = null;
|
||
var t = ResolveNullableTypeArg(typeArgName, binding);
|
||
if (t != null && t.IsValueType)
|
||
try { boxedDefault = HostBinding.NormalizeIntegralReturn(Activator.CreateInstance(t)); } catch { /* keep null */ }
|
||
return new HostBinding.Entry
|
||
{ Delegate = (recv, _) => recv ?? boxedDefault, ParamCount = 0, HasThis = true };
|
||
}
|
||
|
||
case "GetValueOrDefault" when paramCount == 1:
|
||
return new HostBinding.Entry
|
||
{ Delegate = (recv, args) => recv ?? args[0], ParamCount = 1, HasThis = true };
|
||
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Result SType for a Nullable<T> intrinsic. get_HasValue returns bool (I4); get_Value and
|
||
// GetValueOrDefault return T, whose SType comes from the RESOLVED type arg — the open signature
|
||
// says T (which SigReturn can only classify as O), so without this the result lands in a boxed
|
||
// O slot and a numeric comparison like `0 != n.GetValueOrDefault()` lowers to an OBJECT compare
|
||
// (reference-unequal) instead of an int compare (found by fuzzing).
|
||
static SType NullableIntrinsicResultSType(string memberName, string typeArgName)
|
||
{
|
||
if (memberName == "get_HasValue") return SType.I4; // bool
|
||
switch (typeArgName)
|
||
{
|
||
case "Boolean": case "Char": case "SByte": case "Byte":
|
||
case "Int16": case "UInt16": case "Int32": case "UInt32":
|
||
return SType.I4;
|
||
case "Single": case "Double":
|
||
return SType.R4;
|
||
default:
|
||
return SType.O; // long/ulong/enums/ref types stay boxed
|
||
}
|
||
}
|
||
|
||
// Resolve a Nullable<T> type-arg name (as produced by DecodeGenericInstHeader) to a Type:
|
||
// CLR primitive names first, then any binding-registered type.
|
||
static Type? ResolveNullableTypeArg(string name, HostBinding? binding)
|
||
{
|
||
switch (name)
|
||
{
|
||
case "Boolean": return typeof(bool);
|
||
case "Char": return typeof(char);
|
||
case "SByte": return typeof(sbyte);
|
||
case "Byte": return typeof(byte);
|
||
case "Int16": return typeof(short);
|
||
case "UInt16": return typeof(ushort);
|
||
case "Int32": return typeof(int);
|
||
case "UInt32": return typeof(uint);
|
||
case "Int64": return typeof(long);
|
||
case "UInt64": return typeof(ulong);
|
||
case "Single": return typeof(float);
|
||
case "Double": return typeof(double);
|
||
}
|
||
return binding != null && binding.TryGetTypeByName(name, out var t) ? t : null;
|
||
}
|
||
|
||
// Build a HostEntry from a MethodInfo that slipped through TryGetByHandle (Mono/IL2CPP handle instability).
|
||
static HostEntry FallbackEntry(MethodInfo mi, bool isVoid, string label, Action<string> logSink)
|
||
{
|
||
logSink(label);
|
||
var captured = mi;
|
||
var mp = mi.GetParameters();
|
||
var entry = new HostBinding.Entry
|
||
{
|
||
Delegate = mi.IsStatic
|
||
? (_, args) => captured.Invoke(null, args)
|
||
: (recv, args) => captured.Invoke(recv, args),
|
||
ParamCount = mp.Length,
|
||
HasThis = !mi.IsStatic,
|
||
Params = mp,
|
||
};
|
||
return new HostEntry { Binding = entry, IsVoid = isVoid };
|
||
}
|
||
|
||
// Create one instance of the script's "Script" class and run its default .ctor.
|
||
// The ctor runs on the Vm (the sole engine) via _parsed, which Load() has already set.
|
||
ScriptObject? MakeInstance(ParsedAssembly asm)
|
||
{
|
||
if (!asm.TypesByName.TryGetValue("Script", out var desc)) return null;
|
||
var obj = ScriptObject.Create(desc);
|
||
// Find the default .ctor (ArgCount == 1 means just 'this')
|
||
foreach (var (tok, type) in asm.CtorToType)
|
||
{
|
||
if (type == desc && asm.ByToken.TryGetValue(tok, out var ctor) && ctor.ArgCount == 1)
|
||
{
|
||
(_vm ??= new Vm(this)).Invoke(ctor, new object?[] { obj }); // result discarded (ctor)
|
||
break;
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
// Recursive flat classification for script-defined structs (see the Parse call site).
|
||
// Relayouts a type in place when nested blittable struct fields move inline, updates the
|
||
// assembly-level per-field-token maps, and synthesizes FlatLayout for blittable structs
|
||
// (Size = PrimByteSize; box form = a ScriptObject whose PrimBytes IS the flat image).
|
||
static unsafe void ResolveFlatScriptStruct(ScriptTypeDescriptor desc,
|
||
HashSet<ScriptTypeDescriptor> visited,
|
||
Dictionary<int, SType> fieldSTypes, HashSet<int> fieldIsScStruct)
|
||
{
|
||
if (!visited.Add(desc)) return;
|
||
var nested = desc.FieldStructDescriptors;
|
||
if (nested != null)
|
||
foreach (var nd in nested)
|
||
if (nd != null) ResolveFlatScriptStruct(nd, visited, fieldSTypes, fieldIsScStruct);
|
||
|
||
bool anyInline = false;
|
||
if (desc.FieldIsScriptStruct != null && nested != null)
|
||
for (int i = 0; i < desc.FieldCount; i++)
|
||
if (desc.FieldIsScriptStruct[i] && i < nested.Length && nested[i]?.FlatLayout != null)
|
||
{ anyInline = true; break; }
|
||
|
||
if (anyInline)
|
||
{
|
||
int prim = 0, refs = 0;
|
||
for (int i = 0; i < desc.FieldCount; i++)
|
||
{
|
||
bool inline = desc.FieldIsScriptStruct != null && desc.FieldIsScriptStruct[i]
|
||
&& nested != null && i < nested.Length && nested[i]?.FlatLayout != null;
|
||
var ft = desc.FieldTypes[i];
|
||
if (inline)
|
||
{
|
||
var flay = nested![i]!.FlatLayout!;
|
||
prim = (prim + 3) & ~3;
|
||
desc.FieldOffsets[i] = prim;
|
||
prim += (flay.Size + 3) & ~3;
|
||
desc.FieldTypes[i] = SType.Vt;
|
||
(desc.VtFieldLayouts ??= new HostBinding.StructLayout?[desc.FieldCount])[i] = flay;
|
||
desc.FieldIsScriptStruct![i] = false;
|
||
nested![i] = null;
|
||
}
|
||
else if (ft == SType.I4 || ft == SType.R4)
|
||
{
|
||
desc.FieldOffsets[i] = prim; prim += 4;
|
||
}
|
||
else if (ft == SType.Vt)
|
||
{
|
||
var lay = desc.VtFieldLayouts![i]!;
|
||
prim = (prim + 3) & ~3;
|
||
desc.FieldOffsets[i] = prim;
|
||
prim += (lay.Size + 3) & ~3;
|
||
}
|
||
else
|
||
{
|
||
desc.FieldOffsets[i] = refs++;
|
||
}
|
||
}
|
||
desc.PrimByteSize = prim;
|
||
desc.RefSlotCount = refs;
|
||
foreach (var kv in desc.FieldSlots)
|
||
{
|
||
fieldSTypes[kv.Key] = desc.FieldTypes[kv.Value];
|
||
if (desc.FieldTypes[kv.Value] == SType.Vt) fieldIsScStruct.Remove(kv.Key);
|
||
}
|
||
}
|
||
|
||
if (desc.IsScriptStructValue && desc.RefSlotCount == 0 && desc.FlatLayout == null
|
||
// A local function's captured environment is a display STRUCT the caller mutates
|
||
// in place and passes BYREF (`call Scale(x, ref V_0)`). Flattening it splits the
|
||
// representation — caller writes Vt frame bytes, callee reads a ScriptObject —
|
||
// so the environment must stay ScriptObject-backed and shared by reference.
|
||
&& !desc.Name.StartsWith("<>c__DisplayClass", StringComparison.Ordinal))
|
||
{
|
||
int copyLen = desc.PrimByteSize;
|
||
int size = copyLen == 0 ? 4 : copyLen; // empty struct still occupies one slot
|
||
var d = desc;
|
||
HostBinding.BoxFromPtrDelegate box = src =>
|
||
{
|
||
var so = ScriptObject.Create(d);
|
||
if (copyLen > 0) Marshal.Copy((IntPtr)src, so.PrimBytes, 0, copyLen);
|
||
return so;
|
||
};
|
||
HostBinding.CopyToPtrDelegate copy = (dst, boxed) =>
|
||
{
|
||
var so = (ScriptObject)boxed;
|
||
int n = so.PrimBytes.Length < copyLen ? so.PrimBytes.Length : copyLen;
|
||
if (n > 0) Marshal.Copy(so.PrimBytes, 0, (IntPtr)dst, n);
|
||
};
|
||
// Fields is scanned BY OFFSET (host-call receiver materialization for primitives
|
||
// inside flat struct slots — `v.f0.ToString()`); metadata field names aren't retained
|
||
// on the descriptor, so keys are synthetic. Nested flat structs contribute their
|
||
// primitives at composed offsets (their own layouts were built first, bottom-up).
|
||
var flatFields = new Dictionary<string, (int, SType)>(desc.FieldCount);
|
||
for (int i = 0; i < desc.FieldCount; i++)
|
||
{
|
||
if (desc.FieldTypes[i] is SType.I4 or SType.R4)
|
||
flatFields["#" + i] = (desc.FieldOffsets[i], desc.FieldTypes[i]);
|
||
else if (desc.FieldTypes[i] == SType.Vt && desc.VtFieldLayouts?[i] is { } nlay)
|
||
foreach (var kv in nlay.Fields)
|
||
flatFields[$"#{i}.{kv.Key}"] = (desc.FieldOffsets[i] + kv.Value.Offset, kv.Value.St);
|
||
}
|
||
desc.FlatLayout = new HostBinding.StructLayout
|
||
{
|
||
Type = typeof(ScriptObject),
|
||
Size = size,
|
||
TypeName = desc.Name,
|
||
Fields = flatFields,
|
||
BoxFromPtr = box,
|
||
CopyToPtr = copy,
|
||
};
|
||
}
|
||
}
|
||
|
||
static int ReadLocalCount(MetadataReader reader, StandaloneSignatureHandle sig)
|
||
{
|
||
if (sig.IsNil) return 0;
|
||
var blob = reader.GetBlobBytes(reader.GetStandaloneSignature(sig).Signature);
|
||
if (blob.Length < 2 || blob[0] != 0x07) return 0; // 0x07 = LOCAL_SIG marker
|
||
int count = DecompressInt(blob, 1);
|
||
// Every local needs at least one byte of type signature, so a well-formed LOCAL_SIG can't
|
||
// declare more locals than the blob has bytes. A larger count is a lying/hostile signature
|
||
// that would otherwise size a per-local array from it and OOM in Parse.
|
||
if ((uint)count > (uint)blob.Length)
|
||
throw new ScriptValidationException(
|
||
$"LOCAL_SIG declares {count} locals but the signature blob is only {blob.Length} bytes — malformed metadata");
|
||
return count;
|
||
}
|
||
|
||
// Decode each local's struct layout from the LOCAL_SIG blob. Returns null when
|
||
// none are structs registered with the binding (saves an array allocation per method).
|
||
// LOCAL_SIG = 0x07 [count] (type)*. Per-local: optional CMOD/PINNED/BYREF prefixes,
|
||
// then ELEMENT_TYPE_VALUETYPE (0x11) + coded TypeDefOrRef → look up via TypeRef name.
|
||
static HostBinding.StructLayout?[]? ReadLocalStructLayouts(
|
||
MetadataReader reader, StandaloneSignatureHandle sig, int localCount, HostBinding? binding)
|
||
{
|
||
if (sig.IsNil || binding == null || localCount == 0) return null;
|
||
var blob = reader.GetBlobBytes(reader.GetStandaloneSignature(sig).Signature);
|
||
if (blob.Length < 2 || blob[0] != 0x07) return null;
|
||
int idx = 1;
|
||
DecompressIntAdv(blob, ref idx); // count (already known)
|
||
var result = new HostBinding.StructLayout?[localCount];
|
||
bool any = false;
|
||
for (int i = 0; i < localCount && idx < blob.Length; i++)
|
||
{
|
||
// Skip CMOD_REQD (0x1F), CMOD_OPT (0x20), PINNED (0x45), BYREF (0x10)
|
||
while (idx < blob.Length && (blob[idx] == 0x1F || blob[idx] == 0x20 || blob[idx] == 0x45 || blob[idx] == 0x10))
|
||
{
|
||
byte tag = blob[idx++];
|
||
if (tag == 0x1F || tag == 0x20) DecompressIntAdv(blob, ref idx);
|
||
}
|
||
if (idx >= blob.Length) break;
|
||
byte t = blob[idx++];
|
||
if (t == 0x11) // VALUETYPE
|
||
{
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
int table = coded & 0x03; int row = coded >> 2;
|
||
if (table == 1) // TypeRef
|
||
{
|
||
var name = reader.GetString(reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(row)).Name);
|
||
if (binding.TryGetStructLayout(name, out var lay)) { result[i] = lay; any = true; }
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Skip nested types (SZARRAY 0x1D, GENERICINST 0x15) — none are structs we track.
|
||
// Primitive types (I4/R4/...) — just consume the single tag byte (already done).
|
||
if (t == 0x12) DecompressIntAdv(blob, ref idx); // CLASS — also consumes coded index
|
||
else if (t == 0x1D) SkipSigType(blob, ref idx); // SZARRAY → recurse
|
||
else if (t == 0x15)
|
||
{
|
||
// GENERICINST: 0x11/0x12, coded index, arity, args. Skip conservatively.
|
||
if (idx < blob.Length) idx++; // value/class flag
|
||
if (idx < blob.Length) DecompressIntAdv(blob, ref idx);
|
||
if (idx < blob.Length)
|
||
{
|
||
int n = DecompressIntAdv(blob, ref idx);
|
||
for (int j = 0; j < n; j++) SkipSigType(blob, ref idx);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return any ? result : null;
|
||
}
|
||
|
||
// Per-local DECLARED SType from the LocalVarSig (see ParsedMethod.LocalSTypes). Walks the blob
|
||
// exactly like ReadLocalStructLayouts to stay byte-aligned, but classifies each local into the
|
||
// FROZEN set — only the unambiguous, VM-consistent declared types. Anything else (valuetype/enum,
|
||
// long/ulong, double, nint/nuint, byref/pinned locals) is left null to fall back to inference,
|
||
// so this can only make typing MORE stable, never change a type the fuzzer hasn't validated.
|
||
static SType?[]? ReadLocalSTypes(MetadataReader reader, StandaloneSignatureHandle sig, int localCount)
|
||
{
|
||
if (sig.IsNil || localCount == 0) return null;
|
||
var blob = reader.GetBlobBytes(reader.GetStandaloneSignature(sig).Signature);
|
||
if (blob.Length < 2 || blob[0] != 0x07) return null;
|
||
int idx = 1;
|
||
DecompressIntAdv(blob, ref idx); // count (already known)
|
||
var result = new SType?[localCount];
|
||
bool any = false;
|
||
for (int i = 0; i < localCount && idx < blob.Length; i++)
|
||
{
|
||
bool byRef = false;
|
||
while (idx < blob.Length && (blob[idx] == 0x1F || blob[idx] == 0x20 || blob[idx] == 0x45 || blob[idx] == 0x10))
|
||
{
|
||
byte tag = blob[idx++];
|
||
if (tag == 0x10) byRef = true; // BYREF local — not a plain value; fall back
|
||
else if (tag == 0x1F || tag == 0x20) DecompressIntAdv(blob, ref idx); // CMOD token
|
||
}
|
||
if (idx >= blob.Length) break;
|
||
byte t = blob[idx++];
|
||
SType? st;
|
||
if (t == 0x11 || t == 0x12)
|
||
{
|
||
DecompressIntAdv(blob, ref idx); // TypeDefOrRef token (also the alignment skip)
|
||
// CLASS (0x12) is a reference -> O. VALUETYPE (0x11) is left to inference: a host
|
||
// struct is Vt / a script struct is O (classified elsewhere), and an ENUM is NOT
|
||
// safely I4 here — an enum local zero-inited via `initobj En` takes the script-object
|
||
// path, so freezing it I4 regressed real fuzzed programs (oracle [False] vs interp []).
|
||
st = t == 0x12 ? SType.O : (SType?)null;
|
||
}
|
||
else
|
||
{
|
||
st = t switch
|
||
{
|
||
0x02 or 0x03 or 0x04 or 0x05 or 0x06 or 0x07 or 0x08 or 0x09 => SType.I4, // bool,char,i1,u1,i2,u2,i4,u4
|
||
0x0C => SType.R4, // float
|
||
0x0A or 0x0B => SType.I8, // long/ulong — wide slot
|
||
0x0D => SType.R8, // double — wide slot
|
||
0x0E or 0x1C or 0x1D or 0x15 => SType.O, // string,object,array,generic
|
||
_ => (SType?)null, // nint/… left to inference (rare or validator-rejected)
|
||
};
|
||
// Keep idx aligned for the next local (mirror ReadLocalStructLayouts's skips).
|
||
if (t == 0x1D) SkipSigType(blob, ref idx);
|
||
else if (t == 0x15)
|
||
{
|
||
if (idx < blob.Length) idx++; // value/class flag
|
||
if (idx < blob.Length) DecompressIntAdv(blob, ref idx);
|
||
if (idx < blob.Length) { int n = DecompressIntAdv(blob, ref idx); for (int j = 0; j < n; j++) SkipSigType(blob, ref idx); }
|
||
}
|
||
}
|
||
if (byRef) st = null;
|
||
if (st is { }) { result[i] = st; any = true; }
|
||
}
|
||
return any ? result : null;
|
||
}
|
||
|
||
// True when TypeDef row `row` in this assembly is an enum (base type System.Enum). Enum values
|
||
// are I4 ints, not nested script structs — the distinction matters wherever a VALUETYPE TypeDef
|
||
// is classified (fields, locals): misclassifying an enum as a struct gives it object identity
|
||
// and, through its own static literal members, a self-referential descriptor.
|
||
static bool IsEnumTypeDef(MetadataReader reader, int row)
|
||
{
|
||
try
|
||
{
|
||
var bt = reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(row)).BaseType;
|
||
return bt.Kind switch
|
||
{
|
||
HandleKind.TypeReference => reader.GetString(reader.GetTypeReference((TypeReferenceHandle)bt).Name) == "Enum",
|
||
HandleKind.TypeDefinition => reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)bt).Name) == "Enum",
|
||
_ => false,
|
||
};
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
// Decode which locals are SCRIPT-defined structs: ELEMENT_TYPE_VALUETYPE (0x11) whose
|
||
// TypeDefOrRef coded token resolves to a TypeDef (table 0 — defined in THIS assembly), as opposed
|
||
// to a TypeRef (table 1 — a host struct, handled by ReadLocalStructLayouts). Such locals are stored
|
||
// as O ScriptObject references, so a value load must clone them. Returns null when none qualify.
|
||
static bool[]? ReadLocalIsScriptStruct(
|
||
MetadataReader reader, StandaloneSignatureHandle sig, int localCount, out int[]? typeDefs)
|
||
{
|
||
typeDefs = null;
|
||
if (sig.IsNil || localCount == 0) return null;
|
||
var blob = reader.GetBlobBytes(reader.GetStandaloneSignature(sig).Signature);
|
||
if (blob.Length < 2 || blob[0] != 0x07) return null;
|
||
int idx = 1;
|
||
DecompressIntAdv(blob, ref idx); // count (already known)
|
||
var result = new bool[localCount];
|
||
bool any = false;
|
||
for (int i = 0; i < localCount && idx < blob.Length; i++)
|
||
{
|
||
// Skip CMOD_REQD (0x1F), CMOD_OPT (0x20), PINNED (0x45), BYREF (0x10)
|
||
while (idx < blob.Length && (blob[idx] == 0x1F || blob[idx] == 0x20 || blob[idx] == 0x45 || blob[idx] == 0x10))
|
||
{
|
||
byte tag = blob[idx++];
|
||
if (tag == 0x1F || tag == 0x20) DecompressIntAdv(blob, ref idx);
|
||
}
|
||
if (idx >= blob.Length) break;
|
||
byte t = blob[idx++];
|
||
if (t == 0x11) // VALUETYPE
|
||
{
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
if ((coded & 0x03) == 0) // table 0 == TypeDef
|
||
{
|
||
result[i] = true; any = true;
|
||
(typeDefs ??= new int[localCount])[i] =
|
||
MetadataTokens.GetToken(MetadataTokens.TypeDefinitionHandle(coded >> 2));
|
||
}
|
||
}
|
||
else if (t == 0x12) DecompressIntAdv(blob, ref idx); // CLASS: consume coded index
|
||
else if (t == 0x1D) SkipSigType(blob, ref idx); // SZARRAY: recurse
|
||
else if (t == 0x15) // GENERICINST: skip
|
||
{
|
||
if (idx < blob.Length) idx++;
|
||
if (idx < blob.Length) DecompressIntAdv(blob, ref idx);
|
||
if (idx < blob.Length)
|
||
{
|
||
int n = DecompressIntAdv(blob, ref idx);
|
||
for (int j = 0; j < n; j++) SkipSigType(blob, ref idx);
|
||
}
|
||
}
|
||
}
|
||
return any ? result : null;
|
||
}
|
||
|
||
// Decode a TypeSpec signature to extract the simple type name.
|
||
// TypeSpec for a generic instantiation: 0x15 (CLASS|VALUETYPE) coded-index arg-count args...
|
||
static string ResolveTypeSpecName(MetadataReader reader, TypeSpecificationHandle handle)
|
||
{
|
||
var blob = reader.GetBlobBytes(reader.GetTypeSpecification(handle).Signature);
|
||
if (blob.Length < 3 || blob[0] != 0x15) return "";
|
||
int codedIndex = DecompressInt(blob, 2);
|
||
int table = codedIndex & 0x03;
|
||
int row = codedIndex >> 2;
|
||
if (table == 1) // TypeRef
|
||
return reader.GetString(reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(row)).Name);
|
||
if (table == 0) // TypeDef
|
||
return reader.GetString(reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(row)).Name);
|
||
return "";
|
||
}
|
||
|
||
// Parses the return info from a method signature blob.
|
||
// Sig layout: [callconv (1 byte)] [paramCount (compressed)] [CMOD_REQD/OPT*] [returnType] ...
|
||
// How an I4-slot receiver of an instance host call on this type must be boxed (see ReceiverBox).
|
||
static ReceiverBoxKind ReceiverBoxFor(string typeName) => typeName switch
|
||
{
|
||
"Char" => ReceiverBoxKind.Char,
|
||
"Boolean" => ReceiverBoxKind.Bool,
|
||
_ => ReceiverBoxKind.None,
|
||
};
|
||
|
||
// Returns (isVoid, retSType, isBool): retSType is O when isVoid is true. isBool is true only for
|
||
// ELEMENT_TYPE_BOOLEAN — bool collapses to I4 for storage, but the method-return boxing must
|
||
// still hand back a boxed bool (not int), so callers casting the result to bool don't fault.
|
||
static (bool isVoid, SType retSType, bool isBool) SigReturn(byte[] sig)
|
||
{
|
||
if (sig.Length < 3) return (false, SType.O, false);
|
||
int idx = 1; // skip calling convention byte
|
||
DecompressIntAdv(sig, ref idx); // skip paramCount
|
||
// skip custom modifiers (CMOD_REQD=0x1F, CMOD_OPT=0x20) each followed by a compressed type ref
|
||
while (idx < sig.Length && (sig[idx] == 0x1F || sig[idx] == 0x20))
|
||
{
|
||
idx++; // skip modifier tag
|
||
DecompressIntAdv(sig, ref idx); // skip encoded type reference
|
||
}
|
||
if (idx >= sig.Length) return (false, SType.O, false);
|
||
byte tb = sig[idx];
|
||
if (tb == 0x01) return (true, SType.O, false); // ELEMENT_TYPE_VOID
|
||
SType st = tb switch
|
||
{
|
||
// 0x03 = ELEMENT_TYPE_CHAR: char is I4-typed in the VM; omitting it left a char return
|
||
// (e.g. String.get_Chars) boxed in an O slot, read back as 0. (found by fuzzing.)
|
||
// nint/nuint (0x18/0x19) stay O: boxed IntPtr, no 64-bit truncation.
|
||
0x02 or 0x03 or 0x04 or 0x05 or 0x06 or 0x07 or 0x08 or 0x09 => SType.I4,
|
||
0x0C => SType.R4,
|
||
0x0A or 0x0B => SType.I8, // long/ulong — wide slot
|
||
0x0D => SType.R8, // double — wide slot
|
||
_ => SType.O,
|
||
};
|
||
return (false, st, tb == 0x02); // 0x02 = ELEMENT_TYPE_BOOLEAN
|
||
}
|
||
|
||
// Parse explicit parameter types from a method signature blob.
|
||
// Returns SType[] of length = explicit param count (not counting implicit `this`).
|
||
static SType[] SigParamSTypes(byte[] sig)
|
||
{
|
||
if (sig.Length < 2) return Array.Empty<SType>();
|
||
int idx = 1; // skip calling convention
|
||
int paramCount = DecompressIntAdv(sig, ref idx);
|
||
if (paramCount == 0) return Array.Empty<SType>();
|
||
// skip return type (with optional modopt/modreq then the whole type)
|
||
while (idx < sig.Length && (sig[idx] == 0x1F || sig[idx] == 0x20))
|
||
{ idx++; DecompressIntAdv(sig, ref idx); }
|
||
// Skip the ENTIRE return type via SkipSigType. Consuming only the leading tag left a
|
||
// compound return's inner tag (SZARRAY/GENERICINST) to be read as the first parameter's
|
||
// type, desyncing every following param by one signature byte.
|
||
if (idx < sig.Length) SkipSigType(sig, ref idx);
|
||
var result = new SType[paramCount];
|
||
for (int p = 0; p < paramCount && idx < sig.Length; p++)
|
||
{
|
||
// skip any modopt/modreq
|
||
while (idx < sig.Length && (sig[idx] == 0x1F || sig[idx] == 0x20))
|
||
{ idx++; DecompressIntAdv(sig, ref idx); }
|
||
if (idx >= sig.Length) break;
|
||
byte tb = sig[idx++];
|
||
result[p] = tb switch
|
||
{
|
||
// 0x03 = ELEMENT_TYPE_CHAR — I4-typed in the VM (see SigReturn).
|
||
// nint/nuint (0x18/0x19) stay O: boxed IntPtr, no 64-bit truncation.
|
||
0x02 or 0x03 or 0x04 or 0x05 or 0x06 or 0x07 or 0x08 or 0x09 => SType.I4,
|
||
0x0C => SType.R4,
|
||
0x0A or 0x0B => SType.I8, // long/ulong — wide slot
|
||
0x0D => SType.R8, // double — wide slot
|
||
_ => SType.O,
|
||
};
|
||
// VALUETYPE (0x11) and CLASS (0x12) are followed by a compressed type token — skip it.
|
||
if (tb == 0x11 || tb == 0x12) DecompressIntAdv(sig, ref idx);
|
||
// Array/generic params carry a nested type — skip it so the next param's tag is read
|
||
// from the right position (e.g. `(int[] a, MyStruct s)` would otherwise desync).
|
||
else if (tb == 0x1D || tb == 0x15) SkipSigType(sig, ref idx);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Walks a method signature like SigReturn/SigParamSTypes but captures the TypeDef token of
|
||
// every VALUETYPE (0x11, coded table 0) return/param — script-defined struct types. Returns
|
||
// (returnTypeDefTok, perParamTypeDefToks); 0 / null when not script structs.
|
||
static (int retTok, int[]? paramToks) ReadSigScriptStructTokens(byte[] sig)
|
||
{
|
||
if (sig.Length < 3) return (0, null);
|
||
int idx = 1;
|
||
int paramCount = DecompressIntAdv(sig, ref idx);
|
||
while (idx < sig.Length && (sig[idx] == 0x1F || sig[idx] == 0x20))
|
||
{ idx++; DecompressIntAdv(sig, ref idx); }
|
||
if (idx >= sig.Length) return (0, null);
|
||
|
||
int retTok = 0;
|
||
byte retTb = sig[idx++];
|
||
if (retTb == 0x11 || retTb == 0x12)
|
||
{
|
||
int coded = DecompressIntAdv(sig, ref idx);
|
||
if (retTb == 0x11 && (coded & 0x03) == 0)
|
||
retTok = MetadataTokens.GetToken(MetadataTokens.TypeDefinitionHandle(coded >> 2));
|
||
}
|
||
else if (retTb == 0x1D) SkipSigType(sig, ref idx); // SZARRAY return
|
||
|
||
int[]? paramToks = null;
|
||
for (int pi = 0; pi < paramCount && idx < sig.Length; pi++)
|
||
{
|
||
while (idx < sig.Length && (sig[idx] == 0x1F || sig[idx] == 0x20))
|
||
{ idx++; DecompressIntAdv(sig, ref idx); }
|
||
if (idx >= sig.Length) break;
|
||
byte tb = sig[idx++];
|
||
if (tb == 0x11 || tb == 0x12)
|
||
{
|
||
int coded = DecompressIntAdv(sig, ref idx);
|
||
if (tb == 0x11 && (coded & 0x03) == 0)
|
||
(paramToks ??= new int[paramCount])[pi] =
|
||
MetadataTokens.GetToken(MetadataTokens.TypeDefinitionHandle(coded >> 2));
|
||
}
|
||
else if (tb == 0x1D) SkipSigType(sig, ref idx); // SZARRAY param
|
||
else if (tb == 0x15) SkipSigType(sig, ref idx); // GENERICINST — conservative skip
|
||
}
|
||
return (retTok, paramToks);
|
||
}
|
||
|
||
static int DecompressInt(byte[] b, int i)
|
||
{
|
||
if ((b[i] & 0x80) == 0) return b[i];
|
||
if ((b[i] & 0xC0) == 0x80) return ((b[i] & 0x3F) << 8) | b[i + 1];
|
||
return ((b[i] & 0x1F) << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3];
|
||
}
|
||
|
||
// DecompressInt variant that advances the index past the compressed integer.
|
||
static int DecompressIntAdv(byte[] b, ref int idx)
|
||
{
|
||
byte first = b[idx];
|
||
if ((first & 0x80) == 0) { idx += 1; return first; }
|
||
if ((first & 0xC0) == 0x80) { int v = ((first & 0x3F) << 8) | b[idx + 1]; idx += 2; return v; }
|
||
int v4 = ((first & 0x1F) << 24) | (b[idx + 1] << 16) | (b[idx + 2] << 8) | b[idx + 3];
|
||
idx += 4; return v4;
|
||
}
|
||
|
||
// Decode a TypeSpec GENERICINST blob: returns (outerTypeName, firstArgTypeName).
|
||
/// <summary>SType of a Func`N TypeSpec's LAST generic arg — its TResult. Func`N is
|
||
/// <c>0x15 (CLASS) coded-outer argCount T1..Tn TResult</c>; the return is the final arg.
|
||
/// Skips the leading args, then maps the final arg's element-type tag to I4 (integral/bool/
|
||
/// char) or R4 (Single); anything else (String, object, another generic) stays O so only
|
||
/// primitive delegate results get numeric-frame typing. Returns O on any decode failure.</summary>
|
||
// Closed return SType for a TypeSpec member whose signature returns the generic VAR `!n`:
|
||
// decode n from the member sig, then read the instantiation's n-th type argument.
|
||
// Primitives map to their slot type; enums (host TypeRef or script TypeDef) map to I4;
|
||
// everything else — and any decode wobble — stays O, the pre-existing behavior.
|
||
static SType GenericVarReturnSType(
|
||
MetadataReader reader, TypeSpecificationHandle spec, byte[] memberSig, HostBinding? binding)
|
||
{
|
||
try
|
||
{
|
||
int idx = 0;
|
||
byte conv = memberSig[idx++];
|
||
if ((conv & 0x10) != 0) DecompressIntAdv(memberSig, ref idx); // genParamCount
|
||
DecompressIntAdv(memberSig, ref idx); // paramCount
|
||
if (idx >= memberSig.Length || memberSig[idx] != 0x13) return SType.O; // ELEMENT_TYPE_VAR
|
||
idx++;
|
||
int varIdx = DecompressIntAdv(memberSig, ref idx);
|
||
|
||
var blob = reader.GetBlobBytes(reader.GetTypeSpecification(spec).Signature);
|
||
if (blob.Length < 4 || blob[0] != 0x15) return SType.O; // GENERICINST
|
||
int b = 2; // skip 0x15 and the CLASS/VALUETYPE tag
|
||
DecompressIntAdv(blob, ref b); // generic definition coded index
|
||
int argc = DecompressIntAdv(blob, ref b);
|
||
for (int i = 0; i < argc && b < blob.Length; i++)
|
||
{
|
||
if (i != varIdx) { if (!SkipSigType(blob, ref b)) return SType.O; continue; }
|
||
byte tag = blob[b];
|
||
switch (tag)
|
||
{
|
||
case 0x02: case 0x03: case 0x04: case 0x05:
|
||
case 0x06: case 0x07: case 0x08: case 0x09:
|
||
return SType.I4;
|
||
case 0x0C:
|
||
return SType.R4;
|
||
case 0x11: // VALUETYPE coded index — enum type args ride I4
|
||
{
|
||
int c = b + 1;
|
||
int coded = DecompressIntAdv(blob, ref c);
|
||
if ((coded & 0x03) == 0 && IsEnumTypeDef(reader, coded >> 2)) return SType.I4;
|
||
if ((coded & 0x03) == 1 && binding != null
|
||
&& ResolveSigTypeRef(reader, MetadataTokens.TypeReferenceHandle(coded >> 2), binding)
|
||
is { IsEnum: true }) return SType.I4;
|
||
return SType.O;
|
||
}
|
||
default:
|
||
return SType.O;
|
||
}
|
||
}
|
||
return SType.O;
|
||
}
|
||
catch { return SType.O; }
|
||
}
|
||
|
||
static SType FuncReturnSType(MetadataReader reader, TypeSpecificationHandle handle)
|
||
{
|
||
try
|
||
{
|
||
var blob = reader.GetBlobBytes(reader.GetTypeSpecification(handle).Signature);
|
||
if (blob.Length < 3 || blob[0] != 0x15) return SType.O;
|
||
int idx = 2; // skip 0x15 (GENERICINST) and 0x11/0x12 (CLASS/VALUETYPE)
|
||
DecompressIntAdv(blob, ref idx); // coded index of the open type (Func`N)
|
||
int argCount = DecompressIntAdv(blob, ref idx);
|
||
if (argCount < 1) return SType.O;
|
||
for (int i = 0; i < argCount - 1; i++) // skip T1..Tn, leaving TResult
|
||
if (!SkipSigType(blob, ref idx)) return SType.O;
|
||
if (idx >= blob.Length) return SType.O;
|
||
return blob[idx] switch
|
||
{
|
||
0x02 or 0x03 or 0x04 or 0x05 or 0x06 or 0x07 or 0x08 or 0x09 => SType.I4, // bool/char/sub-int/int/uint
|
||
0x0C => SType.R4, // Single
|
||
_ => SType.O,
|
||
};
|
||
}
|
||
catch { return SType.O; }
|
||
}
|
||
|
||
static (string, string) DecodeGenericInstHeader(MetadataReader reader, TypeSpecificationHandle handle)
|
||
{
|
||
var blob = reader.GetBlobBytes(reader.GetTypeSpecification(handle).Signature);
|
||
if (blob.Length < 3 || blob[0] != 0x15) return ("", "");
|
||
|
||
int idx = 2; // skip 0x15 and 0x11/0x12
|
||
int codedIndex = DecompressIntAdv(blob, ref idx);
|
||
int table = codedIndex & 0x03;
|
||
int row = codedIndex >> 2;
|
||
|
||
string outerName;
|
||
if (table == 1)
|
||
outerName = reader.GetString(reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(row)).Name);
|
||
else if (table == 0)
|
||
outerName = reader.GetString(reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(row)).Name);
|
||
else return ("", "");
|
||
|
||
int argCount = DecompressIntAdv(blob, ref idx);
|
||
if (argCount < 1 || idx >= blob.Length) return (outerName, "");
|
||
|
||
byte argTag = blob[idx++];
|
||
string arg0Name = "";
|
||
switch (argTag)
|
||
{
|
||
case 0x02: arg0Name = "Boolean"; break;
|
||
case 0x03: arg0Name = "Char"; break;
|
||
case 0x04: arg0Name = "SByte"; break;
|
||
case 0x05: arg0Name = "Byte"; break;
|
||
case 0x06: arg0Name = "Int16"; break;
|
||
case 0x07: arg0Name = "UInt16"; break;
|
||
case 0x08: arg0Name = "Int32"; break;
|
||
case 0x09: arg0Name = "UInt32"; break;
|
||
case 0x0A: arg0Name = "Int64"; break;
|
||
case 0x0B: arg0Name = "UInt64"; break;
|
||
case 0x0C: arg0Name = "Single"; break;
|
||
case 0x0D: arg0Name = "Double"; break;
|
||
case 0x0E: arg0Name = "String"; break;
|
||
case 0x11: case 0x12:
|
||
{
|
||
int ac = DecompressIntAdv(blob, ref idx);
|
||
int at = ac & 0x03; int ar = ac >> 2;
|
||
if (at == 1) arg0Name = reader.GetString(reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(ar)).Name);
|
||
else if (at == 0) arg0Name = reader.GetString(reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(ar)).Name);
|
||
break;
|
||
}
|
||
}
|
||
return (outerName, arg0Name);
|
||
}
|
||
|
||
// Resolve a TypeSpecification handle to its closed runtime Type (List`1<Vec2> → List<Vec2>).
|
||
// Null when any piece of the signature is unsupported or unresolved.
|
||
static Type? ResolveClosedSpecType(MetadataReader reader, TypeSpecificationHandle handle, HostBinding binding)
|
||
{
|
||
try
|
||
{
|
||
var blob = reader.GetBlobBytes(reader.GetTypeSpecification(handle).Signature);
|
||
int idx = 0;
|
||
return DecodeSigType(reader, blob, ref idx, binding);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
// Decode a MethodSpec signature blob (II.23.2.15) into resolved host Types.
|
||
// Format: 0x0A [GenArgCount] [Type signatures...]
|
||
// Throws ScriptValidationException for unsupported tags or unresolved type-args —
|
||
// the caller should let it propagate so the user sees a friendly load-time error.
|
||
static Type[] DecodeMethodSpecArgs(MetadataReader reader, BlobHandle sigHandle, HostBinding binding, int msToken)
|
||
{
|
||
var blob = reader.GetBlobBytes(sigHandle);
|
||
if (blob.Length < 2 || blob[0] != 0x0A)
|
||
throw new ScriptValidationException($"MethodSpec at 0x{msToken:X8}: malformed signature");
|
||
int idx = 1;
|
||
int argCount = DecompressIntAdv(blob, ref idx);
|
||
var result = new Type[argCount];
|
||
for (int i = 0; i < argCount; i++)
|
||
result[i] = DecodeMethodSpecArg(reader, blob, ref idx, binding, msToken);
|
||
return result;
|
||
}
|
||
|
||
static Type DecodeMethodSpecArg(MetadataReader reader, byte[] blob, ref int idx, HostBinding binding, int msToken)
|
||
{
|
||
byte tag = blob[idx++];
|
||
switch (tag)
|
||
{
|
||
case 0x02: return typeof(bool);
|
||
case 0x08: return typeof(int);
|
||
case 0x09: return typeof(uint);
|
||
case 0x0A: return typeof(long);
|
||
case 0x0C: return typeof(float);
|
||
case 0x0D: return typeof(double);
|
||
case 0x0E: return typeof(string);
|
||
case 0x18: return typeof(IntPtr); // ELEMENT_TYPE_I — native int
|
||
case 0x19: return typeof(UIntPtr); // ELEMENT_TYPE_U
|
||
case 0x1C: return typeof(object);
|
||
case 0x11: case 0x12:
|
||
{
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
int table = coded & 0x03;
|
||
int row = coded >> 2;
|
||
if (table != 1) // only TypeRef supported (TypeDef / TypeSpec out of scope)
|
||
throw new ScriptValidationException(
|
||
$"MethodSpec at 0x{msToken:X8}: unsupported type-arg table {table}");
|
||
var trh = reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(row));
|
||
var name = reader.GetString(trh.Name);
|
||
if (binding.TryGetTypeByName(name, out var t))
|
||
return t!;
|
||
// Unregistered ENUM type-args resolve by full name (Enum.TryParse<DayOfWeek>):
|
||
// an enum carries no callable surface, so it doesn't widen the allowlist.
|
||
var full = trh.Namespace.IsNil ? name : reader.GetString(trh.Namespace) + "." + name;
|
||
if (FindTypeByFullName(full) is { IsEnum: true } et)
|
||
return et;
|
||
throw new ScriptValidationException(
|
||
$"MethodSpec at 0x{msToken:X8}: type argument '{name}' is not in the host binding allowlist (call AllowType<{name}>())");
|
||
}
|
||
default:
|
||
throw new ScriptValidationException(
|
||
$"MethodSpec at 0x{msToken:X8}: unsupported type-arg signature tag 0x{tag:X2}");
|
||
}
|
||
}
|
||
|
||
// Decode a standard method signature blob (II.23.2.1) into a param Type[].
|
||
// Format: [conv-byte] [paramCount] [returnType] [paramType * paramCount]
|
||
// Returns null if the blob is malformed or contains an unsupported tag — caller falls through.
|
||
static Type[]? DecodeMethodSigParamTypes(MetadataReader reader, byte[] blob, HostBinding binding)
|
||
{
|
||
if (blob.Length < 3) return null;
|
||
int idx = 0;
|
||
byte conv = blob[idx++];
|
||
if ((conv & 0x10) != 0) return null; // GENERIC — handled via MethodSpec path
|
||
int paramCount = DecompressIntAdv(blob, ref idx);
|
||
// skip return type
|
||
if (!SkipSigType(blob, ref idx)) return null;
|
||
var result = new Type[paramCount];
|
||
for (int i = 0; i < paramCount; i++)
|
||
{
|
||
var t = DecodeSigType(reader, blob, ref idx, binding);
|
||
if (t == null) return null;
|
||
result[i] = t;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Decode one type from a signature blob at idx, advancing idx. Returns null on unsupported tag.
|
||
static Type? DecodeSigType(MetadataReader reader, byte[] blob, ref int idx, HostBinding binding)
|
||
{
|
||
if (idx >= blob.Length) return null;
|
||
byte tag = blob[idx++];
|
||
switch (tag)
|
||
{
|
||
case 0x01: return typeof(void);
|
||
case 0x02: return typeof(bool);
|
||
case 0x03: return typeof(char);
|
||
case 0x04: return typeof(sbyte);
|
||
case 0x05: return typeof(byte);
|
||
case 0x06: return typeof(short);
|
||
case 0x07: return typeof(ushort);
|
||
case 0x08: return typeof(int);
|
||
case 0x09: return typeof(uint);
|
||
case 0x0A: return typeof(long);
|
||
case 0x0B: return typeof(ulong);
|
||
case 0x0C: return typeof(float);
|
||
case 0x0D: return typeof(double);
|
||
case 0x0E: return typeof(string);
|
||
case 0x18: return typeof(IntPtr); // ELEMENT_TYPE_I — native int
|
||
case 0x19: return typeof(UIntPtr); // ELEMENT_TYPE_U
|
||
case 0x1C: return typeof(object);
|
||
case 0x11: case 0x12: // VALUETYPE / CLASS + coded TypeDefOrRef
|
||
{
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
int table = coded & 0x03;
|
||
int row = coded >> 2;
|
||
if (table != 1) return null; // only TypeRef supported
|
||
return ResolveSigTypeRef(reader, MetadataTokens.TypeReferenceHandle(row), binding);
|
||
}
|
||
case 0x10: // BYREF — recurse then MakeByRefType
|
||
{
|
||
var inner = DecodeSigType(reader, blob, ref idx, binding);
|
||
return inner?.MakeByRefType();
|
||
}
|
||
case 0x15: // GENERICINST: (CLASS|VALUETYPE) coded-index argCount arg* — e.g. List<int>
|
||
{
|
||
if (idx >= blob.Length) return null;
|
||
idx++; // CLASS or VALUETYPE tag of the generic definition
|
||
int gcoded = DecompressIntAdv(blob, ref idx);
|
||
Type? def = (gcoded & 0x03) == 1
|
||
? ResolveSigTypeRef(reader, MetadataTokens.TypeReferenceHandle(gcoded >> 2), binding)
|
||
: null;
|
||
// A registered CLOSED instantiation (e.g. an AllowType'd List<float>) shares the
|
||
// metadata short name "List`1" — normalize to the open definition before re-closing.
|
||
if (def is { IsGenericType: true, IsGenericTypeDefinition: false })
|
||
def = def.GetGenericTypeDefinition();
|
||
int argc = DecompressIntAdv(blob, ref idx);
|
||
var args = new Type[argc];
|
||
for (int i = 0; i < argc; i++)
|
||
{
|
||
var a = DecodeSigType(reader, blob, ref idx, binding);
|
||
if (a == null) return null; // caller aborts the whole decode; position is moot
|
||
args[i] = a;
|
||
}
|
||
if (def == null || !def.IsGenericTypeDefinition ||
|
||
def.GetGenericArguments().Length != argc) return null;
|
||
try { return def.MakeGenericType(args); } catch { return null; }
|
||
}
|
||
case 0x1D: // SZARRAY — recurse then MakeArrayType
|
||
{
|
||
var inner = DecodeSigType(reader, blob, ref idx, binding);
|
||
return inner?.MakeArrayType();
|
||
}
|
||
default: return null; // unsupported tag — fall through
|
||
}
|
||
}
|
||
|
||
// Resolve the runtime Type a ctor MemberRef constructs — used by delegate-creation lowering,
|
||
// where the delegate type is only named by the newobj token's parent. TypeRef parents cover
|
||
// custom delegates and System.Action; TypeSpec parents cover generic instantiations
|
||
// (Func`2<int,int>, EventCallback`1<ClickEvent>) via the full recursive sig decode.
|
||
static Type? ResolveCtorParentType(ParsedAssembly asm, int ctorTok)
|
||
{
|
||
if (asm.HostSurface == null) return null;
|
||
try
|
||
{
|
||
var h = MetadataTokens.EntityHandle(ctorTok);
|
||
if (h.Kind != HandleKind.MemberReference) return null;
|
||
var mr = asm.Reader.GetMemberReference((MemberReferenceHandle)h);
|
||
switch (mr.Parent.Kind)
|
||
{
|
||
case HandleKind.TypeReference:
|
||
return ResolveSigTypeRef(asm.Reader, (TypeReferenceHandle)mr.Parent, asm.HostSurface);
|
||
case HandleKind.TypeSpecification:
|
||
{
|
||
var blob = asm.Reader.GetBlobBytes(
|
||
asm.Reader.GetTypeSpecification((TypeSpecificationHandle)mr.Parent).Signature);
|
||
int idx = 0;
|
||
return DecodeSigType(asm.Reader, blob, ref idx, asm.HostSurface);
|
||
}
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
// Resolve the receiver type of a MemberRef whose parent is a TypeRef. The namespace-qualified
|
||
// name goes through the (policy-gated) auto-bind FIRST: simple names collide across namespaces
|
||
// (UnityEngine.Application vs UnityEngine.WSA/Device.Application), and resolving against the
|
||
// wrong twin either loses the member (unbound) or — worse — silently binds a same-named member
|
||
// of the wrong type. Nested TypeRefs (nil namespace) and names the auto-bind policy declines
|
||
// (or bindings without a resolver) keep the registered simple-name behavior unchanged.
|
||
static bool TryResolveReceiverTypeRef(MetadataReader reader, TypeReferenceHandle handle,
|
||
string simpleName, HostBinding binding, out Type? type)
|
||
{
|
||
var tr = reader.GetTypeReference(handle);
|
||
if (!tr.Namespace.IsNil)
|
||
{
|
||
var fullName = reader.GetString(tr.Namespace) + "." + simpleName;
|
||
if (binding.TryAutoBindType(fullName) && binding.TryGetTypeByFullName(fullName, out type))
|
||
return true;
|
||
}
|
||
return binding.TryGetTypeByName(simpleName, out type);
|
||
}
|
||
|
||
// Resolve a sig-blob TypeRef to a host Type for SIGNATURE MATCHING only (no registration
|
||
// implied): registered simple names first, then the namespace-qualified name — covering types
|
||
// the auto-bind policy deliberately never registers (List`1, System.Uri) and generic
|
||
// definitions living outside corelib (InputFeatureUsage`1) — without a Type for them, every
|
||
// same-arity overload set that differs only by such a parameter resolves ambiguous and lands
|
||
// in UnboundHostMembers.
|
||
static Type? ResolveSigTypeRef(MetadataReader reader, TypeReferenceHandle handle, HostBinding binding)
|
||
{
|
||
var tr = reader.GetTypeReference(handle);
|
||
var name = reader.GetString(tr.Name);
|
||
if (binding.TryGetTypeByName(name, out var t) && t != null) return t;
|
||
return FindTypeByFullName(tr.Namespace.IsNil
|
||
? name : reader.GetString(tr.Namespace) + "." + name);
|
||
}
|
||
|
||
// Full-name type lookup across corelib and every loaded assembly, for signature matching only.
|
||
// Deliberately NOT the auto-bind resolver: no policy applies (registering nothing, we only need
|
||
// Type identity to pick the right overload), and results — including misses — are cached for
|
||
// the process (a type loaded later than its first sig miss stays unresolved; overload matching
|
||
// then degrades to the name+arity fallback exactly as before this lookup existed).
|
||
static readonly System.Collections.Concurrent.ConcurrentDictionary<string, Type?> _sigTypeByFullName = new();
|
||
|
||
static Type? FindTypeByFullName(string fullName) =>
|
||
_sigTypeByFullName.GetOrAdd(fullName, static fn =>
|
||
{
|
||
try { var t = Type.GetType(fn, throwOnError: false); if (t != null) return t; } catch { }
|
||
// UNITY_6000_5_OR_NEWER: AppDomain.GetAssemblies can return already-unloaded assemblies
|
||
// in the editor (UAC0005, an error on 6000.6+); the pure-dotnet builds keep AppDomain.
|
||
#if UNITY_6000_5_OR_NEWER
|
||
foreach (var asm in UnityEngine.Assemblies.CurrentAssemblies.GetLoadedAssemblies())
|
||
#else
|
||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||
#endif
|
||
{
|
||
try { var t = asm.GetType(fn, throwOnError: false); if (t != null) return t; } catch { }
|
||
}
|
||
return null;
|
||
});
|
||
|
||
// Advance idx past one type in a sig blob without returning a Type. Returns false on error.
|
||
static bool SkipSigType(byte[] blob, ref int idx)
|
||
{
|
||
if (idx >= blob.Length) return false;
|
||
byte tag = blob[idx++];
|
||
switch (tag)
|
||
{
|
||
case 0x01: case 0x02: case 0x03: case 0x04: case 0x05:
|
||
case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A:
|
||
case 0x0B: case 0x0C: case 0x0D: case 0x0E: case 0x18:
|
||
case 0x19: case 0x1C:
|
||
return true; // primitives/native int/string/object — no extra bytes
|
||
case 0x11: case 0x12:
|
||
DecompressIntAdv(blob, ref idx); // coded index
|
||
return true;
|
||
case 0x10: case 0x1D:
|
||
return SkipSigType(blob, ref idx); // wrapper: skip inner type
|
||
case 0x13: case 0x1E: // VAR / MVAR: generic parameter reference + index
|
||
DecompressIntAdv(blob, ref idx);
|
||
return true;
|
||
case 0x15: // GENERICINST: (CLASS|VALUETYPE) coded-index argCount arg*
|
||
{
|
||
// A generic RETURN type (e.g. AsyncOperationHandle<GameObject>) must be skippable
|
||
// even though DecodeSigType can't produce it — otherwise param decoding never runs
|
||
// and multi-overload members (Addressables.InstantiateAsync/5) resolve ambiguous.
|
||
if (idx >= blob.Length) return false;
|
||
idx++; // CLASS or VALUETYPE tag
|
||
DecompressIntAdv(blob, ref idx); // coded TypeDefOrRef
|
||
int n = DecompressIntAdv(blob, ref idx);
|
||
for (int i = 0; i < n; i++)
|
||
if (!SkipSigType(blob, ref idx)) return false;
|
||
return true;
|
||
}
|
||
default: return false; // unsupported
|
||
}
|
||
}
|
||
|
||
// Pick the open generic overload whose MemberRef parameter signature matches the call.
|
||
// sigIdx points at the RETURN type inside the GENERIC-calling-convention MemberRef blob
|
||
// (conv byte, genParamCount and paramCount already consumed). Each candidate is closed with
|
||
// typeArgs and its parameters compared structurally against the blob. Null = none matched.
|
||
static MethodInfo? SelectOpenGenericOverload(MetadataReader reader, List<MethodInfo> candidates,
|
||
byte[] sig, int sigIdx, Type[] typeArgs)
|
||
{
|
||
foreach (var cand in candidates)
|
||
{
|
||
MethodInfo closed;
|
||
try { closed = cand.MakeGenericMethod(typeArgs); }
|
||
catch { continue; }
|
||
var ps = closed.GetParameters();
|
||
int idx = sigIdx;
|
||
if (!SkipSigType(sig, ref idx)) continue; // return type
|
||
bool ok = true;
|
||
for (int i = 0; i < ps.Length && ok; i++)
|
||
ok = SigTypeMatches(reader, sig, ref idx, ps[i].ParameterType, typeArgs);
|
||
if (ok) return cand;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Structural comparison of one signature-blob type against a resolved CLR type, advancing idx.
|
||
// TypeRef comparisons use short names (good enough to split overloads; full-name resolution
|
||
// isn't available for unregistered refs). VAR/MVAR compare against the instantiation's args.
|
||
static bool SigTypeMatches(MetadataReader reader, byte[] blob, ref int idx, Type expected, Type[] typeArgs)
|
||
{
|
||
if (idx >= blob.Length) return false;
|
||
byte tag = blob[idx++];
|
||
switch (tag)
|
||
{
|
||
case 0x02: return expected == typeof(bool);
|
||
case 0x03: return expected == typeof(char);
|
||
case 0x04: return expected == typeof(sbyte);
|
||
case 0x05: return expected == typeof(byte);
|
||
case 0x06: return expected == typeof(short);
|
||
case 0x07: return expected == typeof(ushort);
|
||
case 0x08: return expected == typeof(int);
|
||
case 0x09: return expected == typeof(uint);
|
||
case 0x0A: return expected == typeof(long);
|
||
case 0x0B: return expected == typeof(ulong);
|
||
case 0x0C: return expected == typeof(float);
|
||
case 0x0D: return expected == typeof(double);
|
||
case 0x0E: return expected == typeof(string);
|
||
case 0x1C: return expected == typeof(object);
|
||
case 0x11: case 0x12: // VALUETYPE / CLASS + coded TypeDefOrRef
|
||
{
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
if ((coded & 0x03) != 1) return false; // only TypeRef comparable here
|
||
var name = reader.GetString(reader.GetTypeReference(
|
||
MetadataTokens.TypeReferenceHandle(coded >> 2)).Name);
|
||
return expected.Name == name;
|
||
}
|
||
case 0x13: case 0x1E: // VAR / MVAR n → n-th type argument of this instantiation
|
||
{
|
||
int n = DecompressIntAdv(blob, ref idx);
|
||
return n < typeArgs.Length && expected == typeArgs[n];
|
||
}
|
||
case 0x10: // BYREF
|
||
return expected.IsByRef && SigTypeMatches(reader, blob, ref idx, expected.GetElementType()!, typeArgs);
|
||
case 0x1D: // SZARRAY
|
||
return expected.IsArray && SigTypeMatches(reader, blob, ref idx, expected.GetElementType()!, typeArgs);
|
||
case 0x15: // GENERICINST: outer definition name + each type arg
|
||
{
|
||
if (idx >= blob.Length) return false;
|
||
idx++; // CLASS|VALUETYPE tag
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
string outer = (coded & 0x03) == 1
|
||
? reader.GetString(reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(coded >> 2)).Name)
|
||
: "";
|
||
int n = DecompressIntAdv(blob, ref idx);
|
||
if (!expected.IsGenericType || expected.Name != outer) return false;
|
||
var eargs = expected.GetGenericArguments();
|
||
if (eargs.Length != n) return false;
|
||
for (int i = 0; i < n; i++)
|
||
if (!SigTypeMatches(reader, blob, ref idx, eargs[i], typeArgs)) return false;
|
||
return true;
|
||
}
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
static MethodInfo? FindMethod(Type type, string name, int paramCount)
|
||
{
|
||
foreach (var m in type.GetMethods(
|
||
BindingFlags.Public | BindingFlags.Instance |
|
||
BindingFlags.Static | BindingFlags.DeclaredOnly))
|
||
if (m.Name == name && m.GetParameters().Length == paramCount)
|
||
return m;
|
||
return null;
|
||
}
|
||
|
||
// Resolve flat-struct (Vt) layouts for a HostEntry's params/return/receiver, given
|
||
// the binding. Called for hostCalls and hostCtors after construction in Parse.
|
||
static void FillStructInfo(HostEntry he, HostBinding? binding, bool isCtor)
|
||
{
|
||
if (binding == null) return;
|
||
var ps = he.Binding.Params;
|
||
if (ps != null && ps.Length > 0)
|
||
{
|
||
var arr = new HostBinding.StructLayout?[ps.Length];
|
||
bool any = false;
|
||
for (int i = 0; i < ps.Length; i++)
|
||
{
|
||
var pt = ps[i].ParameterType;
|
||
if (pt.IsByRef) pt = pt.GetElementType()!;
|
||
if (pt.IsValueType && binding.TryGetStructLayout(pt, out var lay))
|
||
{ arr[i] = lay; any = true; }
|
||
}
|
||
if (any) he.ArgStructs = arr;
|
||
}
|
||
var m = (MethodBase?)he.ResolvedMethod ?? he.Binding.Method;
|
||
if (isCtor)
|
||
{
|
||
// Ctor's "return" type is its declaring type.
|
||
var declT = he.Binding.DeclaringType;
|
||
if (declT != null && declT.IsValueType && binding.TryGetStructLayout(declT, out var declLay))
|
||
he.ReturnStruct = declLay;
|
||
}
|
||
else if (m is MethodInfo mi)
|
||
{
|
||
var rt = mi.ReturnType;
|
||
if (rt.IsValueType && rt != typeof(void) && binding.TryGetStructLayout(rt, out var retLay))
|
||
he.ReturnStruct = retLay;
|
||
// Receiver: for a value-type instance method, the receiver is a struct
|
||
// (rarely useful in our subset but tracked for completeness).
|
||
if (he.Binding.HasThis && mi.DeclaringType != null && mi.DeclaringType.IsValueType
|
||
&& binding.TryGetStructLayout(mi.DeclaringType, out var recvLay))
|
||
he.ReceiverStruct = recvLay;
|
||
}
|
||
}
|
||
|
||
|
||
// Walks one ParsedMethod's IL, simulates the eval stack symbolically to determine
|
||
// operand types, allocates frame slots, and emits uint[] IR. All methods in the
|
||
// assembly are lowered at Load time and cached on ParsedMethod.Lowered.
|
||
static class IrLowerer
|
||
{
|
||
// Returns a list of (methodName, reason) for any method that failed to lower.
|
||
// Empty list means full success. Load throws when this is non-empty.
|
||
// lenient=true demotes EVERY per-method lowering failure to a skipped stub (as cold
|
||
// enumerator members already are) instead of failing the whole Load — a method that can't
|
||
// lower is left with Lowered==null + a LoweringSkipReason, and the rest load normally. Hot
|
||
// reload uses this so one unsupported method (e.g. SetupUI referencing a member missing on
|
||
// the running build) doesn't poison every other reloaded method in the same file. Strict
|
||
// (default) stays all-or-nothing for the fuzzer/tests, where a failure is a bug to surface.
|
||
public static List<(string Name, string Reason)> LowerAll(ParsedAssembly asm, bool lenient = false)
|
||
{
|
||
var failures = new List<(string, string)>();
|
||
foreach (var m in asm.ByToken.Values)
|
||
{
|
||
try { m.Lowered = LowerMethod(m, asm); }
|
||
catch (NotSupportedException ex)
|
||
{
|
||
// A cold enumerator member (iterator Reset — body is `throw new
|
||
// NotSupportedException()`) becomes a skipped stub instead of failing the
|
||
// whole Load: no driver ever calls it. Invoking it anyway throws the reason.
|
||
if (m.IsColdEnumeratorMember || lenient) { m.LoweringSkipReason = ex.Message; continue; }
|
||
failures.Add((m.Name, ex.Message));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// A lowering crash must still name the METHOD: a raw IndexOutOfRangeException
|
||
// escaping Load reads as "Index was outside the bounds of the array" with
|
||
// nothing to act on. The stack rides along so a bug report is actionable.
|
||
var reason = $"internal error while lowering (an unsupported construct — please report): {ex}";
|
||
if (m.IsColdEnumeratorMember || lenient) { m.LoweringSkipReason = reason; continue; }
|
||
failures.Add((m.Name, reason));
|
||
}
|
||
}
|
||
|
||
// A lowered method whose call sites target a skipped stub would jump through a null
|
||
// Lowered at runtime — demote such callers to failures too (cascading; defensive:
|
||
// in practice cold members are leaf Reset bodies nothing calls).
|
||
bool changed = true;
|
||
while (changed)
|
||
{
|
||
changed = false;
|
||
foreach (var m in asm.ByToken.Values)
|
||
{
|
||
var callees = m.Lowered?.CalleeByTokIdx;
|
||
if (callees == null) continue;
|
||
foreach (var callee in callees)
|
||
{
|
||
if (callee == null || callee.Lowered != null) continue;
|
||
var reason = $"calls '{callee.Name}', which could not be lowered: {callee.LoweringSkipReason}";
|
||
m.Lowered = null!;
|
||
if (m.IsColdEnumeratorMember || lenient) m.LoweringSkipReason = reason;
|
||
else failures.Add((m.Name, reason));
|
||
changed = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return failures;
|
||
}
|
||
|
||
// Concatenates every lowered method's IR into ParsedAssembly.IrBlob with branch targets
|
||
// rewritten blob-absolute, so the Vm can execute script-to-script calls iteratively (frame
|
||
// push + ip jump) under one `fixed` instead of recursing into Run(). Each method's region
|
||
// gets a trailing ret_void sentinel: it preserves both the old "fell off the end" return
|
||
// (void methods) and the lowerer's past-end fall-through branch targets, and stops
|
||
// execution from running into the next method's code.
|
||
public static void BuildIrBlob(ParsedAssembly asm)
|
||
{
|
||
int total = 0;
|
||
foreach (var m in asm.ByToken.Values)
|
||
if (m.Lowered != null) total += m.Lowered.Ir.Length + 1;
|
||
var blob = new uint[total];
|
||
int off = 0;
|
||
foreach (var m in asm.ByToken.Values)
|
||
{
|
||
var lm = m.Lowered;
|
||
if (lm == null) continue; // skipped stub (cold enumerator member)
|
||
var ir = lm.Ir;
|
||
int n = ir.Length;
|
||
lm.IrStart = off;
|
||
Array.Copy(ir, 0, blob, off, n);
|
||
|
||
// PatchInsnBranchTargets computes newTarget = t - shift[t]; a constant -off shift
|
||
// relocates every target (including past-end t == n, which lands on the sentinel).
|
||
var shift = new int[n + 1];
|
||
for (int i = 0; i <= n; i++) shift[i] = -off;
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0)
|
||
throw new NotSupportedException($"IR blob: unknown width for op {op} in '{m.Name}'");
|
||
PatchInsnBranchTargets(blob, off + pos, op, shift, ir, pos);
|
||
pos += w;
|
||
}
|
||
blob[off + n] = (uint)Op.ret_void;
|
||
off += n + 1;
|
||
}
|
||
asm.IrBlob = blob;
|
||
}
|
||
|
||
// Simple (unqualified) type name of a TypeRef/TypeDef token, for matching a
|
||
// `constrained.` prefix's type against a flat struct layout's TypeName. Null for
|
||
// TypeSpecs (generic instantiations — never a registered flat struct) or bad tokens.
|
||
static string TokenSimpleTypeName(ParsedAssembly asm, int token)
|
||
{
|
||
try
|
||
{
|
||
var handle = MetadataTokens.Handle(token);
|
||
switch (handle.Kind)
|
||
{
|
||
case HandleKind.TypeReference:
|
||
return asm.Reader.GetString(
|
||
asm.Reader.GetTypeReference((TypeReferenceHandle)handle).Name);
|
||
case HandleKind.TypeDefinition:
|
||
return asm.Reader.GetString(
|
||
asm.Reader.GetTypeDefinition((TypeDefinitionHandle)handle).Name);
|
||
default:
|
||
return null!;
|
||
}
|
||
}
|
||
catch (Exception)
|
||
{
|
||
return null!;
|
||
}
|
||
}
|
||
|
||
public static LoweredMethod LowerMethod(ParsedMethod method, ParsedAssembly asm)
|
||
{
|
||
var il = method.IlBytes;
|
||
var ir = new List<uint>(il.Length * 2);
|
||
var irToIl = new List<int>(il.Length * 2);
|
||
var strings = new List<string>();
|
||
var tokens = new List<int>();
|
||
|
||
// Frame layout: [args (one slot each, 0..ArgCount-1),
|
||
// locals (variable slot count — struct locals occupy ceil(size/4) slots),
|
||
// eval-stack slots (above)]
|
||
int frameSize = method.ArgCount;
|
||
// Local slot index → first frame-slot index. Filler slots (struct continuation) are
|
||
// not directly addressable but exist in slotTypes/structLayouts to keep AllocSlot monotonic.
|
||
var localFrameSlot = new int[method.LocalCount];
|
||
|
||
// Per-slot type table — grows alongside frameSize.
|
||
// Arg slots: slot 0 is `this` for instance methods; then explicit params from ArgSTypes.
|
||
// A flat-struct arg (Vt) occupies ceil(size/4) slots, so args after it shift —
|
||
// argFrameSlot maps arg index → frame slot (identity when no Vt args).
|
||
var slotTypes = new List<SType>(frameSize + 32);
|
||
// Parallel list of per-Vt-slot struct layouts. null for non-Vt slots and filler slots.
|
||
var slotStructs = new List<HostBinding.StructLayout?>(frameSize + 32);
|
||
var argFrameSlot = new int[method.ArgCount];
|
||
bool argIndirect = false;
|
||
{
|
||
int sigParamBase = method.IsStatic ? 0 : 1; // slot 0 is `this` for instance methods
|
||
for (int i = 0; i < method.ArgCount; i++)
|
||
{
|
||
int sigIdx = i - sigParamBase;
|
||
SType t = (sigIdx >= 0 && sigIdx < method.ArgSTypes.Length) ? method.ArgSTypes[sigIdx] : SType.O;
|
||
// `this` of a flat script struct is ALSO Vt (instance methods on flat structs).
|
||
HostBinding.StructLayout? alay = null;
|
||
if (sigIdx >= 0 && method.ArgStructLayouts != null && sigIdx < method.ArgStructLayouts.Length)
|
||
alay = method.ArgStructLayouts[sigIdx];
|
||
else if (sigIdx < 0 && method.ThisStructLayout != null) { alay = method.ThisStructLayout; t = SType.Vt; }
|
||
argFrameSlot[i] = slotTypes.Count;
|
||
if (t == SType.Vt && alay != null)
|
||
{
|
||
int slotsNeeded = (alay.Size + 3) / 4;
|
||
slotTypes.Add(SType.Vt);
|
||
slotStructs.Add(alay);
|
||
for (int k = 1; k < slotsNeeded; k++) { slotTypes.Add(SType.O); slotStructs.Add(null); }
|
||
argIndirect = true;
|
||
}
|
||
else
|
||
{
|
||
var at = t == SType.Vt ? SType.O : t; // Vt without layout: defensive O
|
||
slotTypes.Add(at);
|
||
slotStructs.Add(null);
|
||
if (at is SType.I8 or SType.R8)
|
||
{
|
||
// Wide arg: continuation cell, and later args shift — argFrameSlot maps.
|
||
slotTypes.Add(SType.O);
|
||
slotStructs.Add(null);
|
||
argIndirect = true;
|
||
}
|
||
}
|
||
}
|
||
frameSize = slotTypes.Count;
|
||
}
|
||
// Allocate locals — struct locals get Vt + filler slots whenever a layout exists for
|
||
// the local's type. Vt-typed operator dispatchers chain through Vt slots without
|
||
// boxing; only the crossing into a boxed host API pays a single box_vt.
|
||
var localStrs = method.LocalStructLayouts;
|
||
// A local with a FROZEN declared type (LocalSTypes) is typed authoritatively and never
|
||
// re-inferred/retyped — this restores the "one slot ⇒ one type ⇒ one backing store"
|
||
// invariant that last-wins inference broke, so a fast op is never emitted over a slot
|
||
// whose value is actually boxed. Null entries fall through to inference as before.
|
||
var localDeclared = new bool[method.LocalCount];
|
||
for (int i = 0; i < method.LocalCount; i++)
|
||
{
|
||
var lay = localStrs?[i];
|
||
bool useVt = lay != null;
|
||
localFrameSlot[i] = frameSize;
|
||
if (useVt)
|
||
{
|
||
int slotsNeeded = (lay!.Size + 3) / 4;
|
||
slotTypes.Add(SType.Vt);
|
||
slotStructs.Add(lay);
|
||
for (int k = 1; k < slotsNeeded; k++)
|
||
{
|
||
slotTypes.Add(SType.O); // filler slot: never addressed directly
|
||
slotStructs.Add(null);
|
||
}
|
||
frameSize += slotsNeeded;
|
||
}
|
||
else if (method.LocalSTypes?[i] is { } declSt)
|
||
{
|
||
slotTypes.Add(declSt); // frozen: declared type, not inferred
|
||
slotStructs.Add(null);
|
||
localDeclared[i] = true;
|
||
frameSize++;
|
||
if (declSt is SType.I8 or SType.R8)
|
||
{
|
||
slotTypes.Add(SType.O); // wide local: continuation cell
|
||
slotStructs.Add(null);
|
||
frameSize++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
slotTypes.Add(SType.O); // patched by pre-scan
|
||
slotStructs.Add(null);
|
||
frameSize++;
|
||
}
|
||
}
|
||
// Pre-scan: determine local types from first stloc in each slot.
|
||
// Skips struct-typed and declared-typed locals (already classified above).
|
||
PreClassifyLocals(il, method, slotTypes, localFrameSlot, localStrs, localDeclared);
|
||
|
||
// Exception clauses: finally is lowered via the push_cont/br_cont continuation chain;
|
||
// catch/filter would need real exception dispatch — fail with the reason instead
|
||
// of a bare opcode number. (Fault clauses were dropped at Parse: their handlers only
|
||
// run under exception dispatch, which the interpreter never does.)
|
||
if (method.HasUnsupportedEhClauses)
|
||
throw new NotSupportedException(
|
||
$"IR lowering: '{method.Name}' contains a try/catch (or filter) clause — " +
|
||
"only try/finally is supported by the interpreter");
|
||
|
||
// Pre-scan: collect all IL offsets that are branch targets.
|
||
// Used to clear ensuredScriptSlots at basic-block boundaries.
|
||
var ilBranchTargets = CollectIlBranchTargets(il);
|
||
// Finally handler entries are reached via the lowered leave's br — treat them as
|
||
// basic-block boundaries too.
|
||
if (method.FinallyRegions != null)
|
||
foreach (var r in method.FinallyRegions)
|
||
ilBranchTargets.Add(r.HandlerStart);
|
||
// Fault handlers are never entered (no exception dispatch) but their bodies still
|
||
// lower — mark each entry a basic-block boundary so the linear scan doesn't carry
|
||
// stale eval-stack slot state into the dead region.
|
||
if (method.FaultHandlerStarts != null)
|
||
foreach (var h in method.FaultHandlerStarts)
|
||
ilBranchTargets.Add(h);
|
||
|
||
// Symbolic eval stack — each entry is (frameSlot, SType)
|
||
var evalStack = new (int slot, SType type)[64];
|
||
int sp = 0;
|
||
|
||
// Address tracking: indexed by frame slot number (slots are allocated monotonically).
|
||
// addrTagBySlot[s]: -2 = normal slot; >= 0 = "frame-slot address" (points to that frame slot);
|
||
// -1 = "field address" (use addrFldObjBySlot[s] + addrFldTokBySlot[s]);
|
||
// -3 = "array element address" (addrFldObjBySlot[s] = arr slot, addrFldTokBySlot[s] = idx slot).
|
||
// Size 512 is a generous upper bound; slots never exceed a few dozen in practice.
|
||
var addrTagBySlot = new int[512];
|
||
var addrFldObjBySlot = new int[512];
|
||
var addrFldTokBySlot = new int[512];
|
||
// Byte-offset accumulation for addresses that point INSIDE flat struct bytes:
|
||
// tag >= 0 → addrByteOffBySlot = offset within the frame Vt slot (0 = whole slot);
|
||
// tag == -1 → addrFldOffBySlot >= 0 = composed byte offset into the parent
|
||
// ScriptObject's PrimBytes (-1 = classic token-addressed field phantom).
|
||
// Composition happens along INLINE segments (nested blittable structs); at a
|
||
// reference boundary (O field / array element) the chain REBASES onto the loaded ref.
|
||
var addrByteOffBySlot = new int[512];
|
||
var addrFldOffBySlot = new int[512];
|
||
// For -3 (array element) phantoms whose ARRAY has flat struct elements: the element
|
||
// layout (consumers materialize via ldelem_vt + write back via stelem_vt).
|
||
var addrElemLayoutBySlot = new HostBinding.StructLayout?[512];
|
||
for (int i = 0; i < 512; i++) { addrTagBySlot[i] = -2; addrFldOffBySlot[i] = -1; }
|
||
// newarr_vt element layouts, resolved at lowering (sparse, keyed by tokIdx).
|
||
Dictionary<int, HostBinding.StructLayout>? layoutByTokIdx = null;
|
||
// Primitive typed-array element types (bool/char/byte/…), resolved at lowering.
|
||
Dictionary<int, Type>? primArrayElemTypeByTokIdx = null;
|
||
// Delegate-creation sites (ldftn + newobj), resolved at lowering (sparse, by tokIdx).
|
||
Dictionary<int, DelegateSite>? delegateSiteByTokIdx = null;
|
||
|
||
// Per-local and per-arg type tracking (updated on store, read on load)
|
||
var localTypes = new SType[method.LocalCount]; // synced from slotTypes
|
||
var argTypes = new SType[method.ArgCount]; // synced from slotTypes
|
||
|
||
for (int i = 0; i < method.ArgCount; i++) argTypes[i] = slotTypes[argFrameSlot[i]];
|
||
for (int i = 0; i < method.LocalCount; i++) localTypes[i] = slotTypes[localFrameSlot[i]];
|
||
|
||
// A script-defined struct local is always an O ScriptObject. PreClassifyLocals infers a
|
||
// local's type from the first value stored into it, and a generic `ldelem <T>` (from
|
||
// `s = arr[i]`) can be mis-inferred as I4 — which would then suppress clone-on-load and
|
||
// corrupt struct value semantics. Force these locals back to O.
|
||
if (method.LocalIsScriptStruct != null)
|
||
for (int i = 0; i < method.LocalCount; i++)
|
||
if (method.LocalIsScriptStruct[i])
|
||
{
|
||
localTypes[i] = SType.O;
|
||
if (localFrameSlot[i] < slotTypes.Count) slotTypes[localFrameSlot[i]] = SType.O;
|
||
|
||
// CLR localsinit: a struct local is usable without any initobj — Roslyn
|
||
// relies on it for local-function DISPLAY STRUCTS (`V_0.mult = 3` straight
|
||
// into the uninitialized local, then `call Scale(ref V_0)`). The O slot
|
||
// starts null here, so create the ScriptObject in the prologue.
|
||
if (method.LocalScriptStructTypeDefs != null
|
||
&& method.LocalScriptStructTypeDefs[i] is var lsTok && lsTok != 0)
|
||
{
|
||
int lsTokIdx = tokens.Count; tokens.Add(lsTok);
|
||
ir.Add((uint)Op.initobj_script); irToIl.Add(0);
|
||
ir.Add((uint)localFrameSlot[i]); irToIl.Add(0);
|
||
ir.Add((uint)lsTokIdx); irToIl.Add(0);
|
||
}
|
||
}
|
||
|
||
// IL offset → IR word index (for branch patching)
|
||
var ilToIrIp = new Dictionary<int, int>();
|
||
// Forward branches that need patching: (ir_word_index_of_target, il_target_offset)
|
||
var patchList = new List<(int irIdx, int ilTarget)>();
|
||
// Branch-target → authoritative eval-stack depth delivered by its incoming edge(s).
|
||
// Recorded at every FORWARD branch (the delivered depth after the branch's own stack
|
||
// effect). In structured IL all edges into a join agree on depth, so when the linear
|
||
// walk reaches a target this value is authoritative: it drops any dead carry left on the
|
||
// stack after a preceding unconditional branch (e.g. a ternary then-arm result at the
|
||
// ELSE label) while preserving values that live *beneath* a short-circuit region (e.g.
|
||
// the `==` left operand while its right side is a `&&`/`||` chain).
|
||
var branchTargetDepth = new Dictionary<int, int>();
|
||
// Record a forward branch's delivered depth at its target (backward edges — loop headers —
|
||
// are already past in the linear walk, so they never need reconciling here).
|
||
void RecordJoinDepth(int src, int target, int depth)
|
||
{
|
||
if (target > src) branchTargetDepth[target] = depth;
|
||
}
|
||
// Ternary / short-circuit merge: END offset -> the fresh slot BOTH branches converge into.
|
||
// Each branch moves its result into this slot at its own tail; the join reads it. Using a
|
||
// fresh slot (never a branch's own value slot) is essential — the value slot may be a real
|
||
// local, or the two branches may leave their results in different slots.
|
||
var mergeSlotForEnd = new Dictionary<int, (int slot, SType type)>();
|
||
|
||
// O slots known to hold a non-null ScriptObject in the current basic block.
|
||
// Cleared at branch targets and after any branch/call instruction.
|
||
// Allows the Stfld lowerer to skip redundant ensure_script ops.
|
||
var ensuredScriptSlots = new HashSet<int>();
|
||
|
||
// Tracks whether the immediately preceding instruction was a `dup`. Roslyn emits
|
||
// `dup; brtrue`/`dup; brfalse` only for the null-coalescing / null-conditional idioms
|
||
// (`a ?? b`, `a?.b`), where the value left under the popped condition is a genuine
|
||
// forward-join carry. A plain conditional branch with no preceding dup (if/while/&&/||)
|
||
// leaves only base operands beneath, which must NOT be merged.
|
||
bool lastWasDup = false;
|
||
|
||
// Type token of a pending `constrained.` prefix, consumed by the next call lowering.
|
||
// Disambiguates a Vt receiver: constrained to the slot's OWN struct type → box the
|
||
// whole struct; constrained to a primitive → extract the field at the byte offset.
|
||
int constrainedTok = 0;
|
||
|
||
// Method token of a pending ldftn/ldvirtftn, consumed by the delegate-ctor Newobj that
|
||
// C# always emits immediately after (`ldnull|recv[; dup]; ld(virt)ftn M; newobj D`).
|
||
// Anything else between them is unsupported IL for this lowerer.
|
||
int pendingFtnTok = 0;
|
||
|
||
// Byref locals carry an ADDRESS. Roslyn spills a managed pointer (&x.f from ldflda,
|
||
// &a[i] from ldelema, &local from ldloca) into a local when control flow sits between
|
||
// the address and its use — e.g. `x.f op= (cond switch {...})` or `a[i].Mut((cond)
|
||
// switch {...})`, where the switch-expr's branches force the spill. That address is a
|
||
// lowering-time PHANTOM (per-slot addr* metadata, no runtime value), so a plain
|
||
// stloc/ldloc dropped it and the later stind/call saw a non-address operand. Carry the
|
||
// phantom through the local by stashing its metadata ON THE LOCAL'S FRAME SLOT (the
|
||
// addr* arrays are per-slot and stable) and restoring a fresh phantom on load.
|
||
// (found by fuzzing: switch-expr in a compound-assign RHS / struct-array Mut arg.)
|
||
bool TryStoreLocalPhantom(int localIdx)
|
||
{
|
||
int src = evalStack[sp - 1].Item1;
|
||
if (addrTagBySlot[src] == -2) return false; // ordinary value → normal stloc
|
||
int dst = localFrameSlot[localIdx];
|
||
addrTagBySlot[dst] = addrTagBySlot[src];
|
||
addrFldObjBySlot[dst] = addrFldObjBySlot[src];
|
||
addrFldTokBySlot[dst] = addrFldTokBySlot[src];
|
||
addrByteOffBySlot[dst] = addrByteOffBySlot[src];
|
||
addrFldOffBySlot[dst] = addrFldOffBySlot[src];
|
||
addrElemLayoutBySlot[dst] = addrElemLayoutBySlot[src];
|
||
sp--; // consume; the phantom is metadata-only, no runtime mov
|
||
return true;
|
||
}
|
||
bool TryLoadLocalPhantom(int localIdx)
|
||
{
|
||
int srcSlot = localFrameSlot[localIdx];
|
||
if (addrTagBySlot[srcSlot] == -2) return false;
|
||
int ph = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
addrTagBySlot[ph] = addrTagBySlot[srcSlot];
|
||
addrFldObjBySlot[ph] = addrFldObjBySlot[srcSlot];
|
||
addrFldTokBySlot[ph] = addrFldTokBySlot[srcSlot];
|
||
addrByteOffBySlot[ph] = addrByteOffBySlot[srcSlot];
|
||
addrFldOffBySlot[ph] = addrFldOffBySlot[srcSlot];
|
||
addrElemLayoutBySlot[ph] = addrElemLayoutBySlot[srcSlot];
|
||
evalStack[sp++] = (ph, SType.O);
|
||
return true;
|
||
}
|
||
|
||
int ip = 0;
|
||
while (ip < il.Length)
|
||
{
|
||
// The addr phantom tables are indexed by frame slot and evalStack by stack depth;
|
||
// both start at a size that covers ordinary methods, but generated/pathological
|
||
// code can allocate thousands of temp slots (found by fuzzing: IndexOutOfRange on
|
||
// addrTagBySlot). Grow here, once per instruction — no single instruction
|
||
// allocates anywhere near 256 slots, and the locals are re-read by every helper
|
||
// call below, so Array.Resize's new arrays propagate.
|
||
if (frameSize + 256 > addrTagBySlot.Length)
|
||
{
|
||
int oldCap = addrTagBySlot.Length;
|
||
int newCap = Math.Max(oldCap * 2, frameSize + 512);
|
||
Array.Resize(ref addrTagBySlot, newCap);
|
||
Array.Resize(ref addrFldObjBySlot, newCap);
|
||
Array.Resize(ref addrFldTokBySlot, newCap);
|
||
Array.Resize(ref addrByteOffBySlot, newCap);
|
||
Array.Resize(ref addrFldOffBySlot, newCap);
|
||
Array.Resize(ref addrElemLayoutBySlot, newCap);
|
||
// Same defaults as the initial fill: -2 = normal slot, -1 = token-addressed.
|
||
for (int i = oldCap; i < newCap; i++) { addrTagBySlot[i] = -2; addrFldOffBySlot[i] = -1; }
|
||
}
|
||
if (sp + 8 > evalStack.Length)
|
||
Array.Resize(ref evalStack, evalStack.Length * 2);
|
||
|
||
int instrStart = ip;
|
||
// Basic-block boundary: clear ensuredScriptSlots when the current instruction
|
||
// is a branch target (we may have arrived via a backward edge or a jump).
|
||
if (ilBranchTargets.Contains(instrStart)) ensuredScriptSlots.Clear();
|
||
// Reconcile the abstract eval-stack depth with the depth the branch edges deliver.
|
||
// The base slots are identical on every edge (they were computed before the diamond),
|
||
// so the current walk's evalStack entries remain valid — only the depth needs fixing.
|
||
if (branchTargetDepth.TryGetValue(instrStart, out var joinDepth))
|
||
sp = joinDepth;
|
||
// Ternary/merge join: the second branch's result is on top; move it into the merge
|
||
// slot M so both paths converge on M. Done BEFORE recording ilToIrIp[ip] so the first
|
||
// branch's `br` (patched to this offset) lands AFTER the mov, not on top of it.
|
||
if (mergeSlotForEnd.TryGetValue(instrStart, out var mergeJoin))
|
||
{
|
||
if (sp > 0)
|
||
{
|
||
var (joinTop, _) = evalStack[sp - 1];
|
||
if (joinTop != mergeJoin.slot)
|
||
Emit3(ir, irToIl, mergeJoin.type == SType.Vt ? Op.mov_vt : Op.mov,
|
||
mergeJoin.slot, joinTop, instrStart);
|
||
evalStack[sp - 1] = (mergeJoin.slot, mergeJoin.type);
|
||
}
|
||
mergeSlotForEnd.Remove(instrStart);
|
||
}
|
||
// Record the mapping BEFORE emitting IR words
|
||
int irIp = ir.Count;
|
||
ilToIrIp[ip] = irIp;
|
||
|
||
byte b = il[ip++];
|
||
ILOpCode op = b == 0xFE ? (ILOpCode)(0xFE00 | il[ip++]) : (ILOpCode)b;
|
||
|
||
// A loaded function pointer is only modeled as the immediate operand of a
|
||
// delegate ctor; any other consumer would see a placeholder slot.
|
||
if (pendingFtnTok != 0 && op != ILOpCode.Newobj)
|
||
throw new NotSupportedException(
|
||
$"IR lowering: ldftn in '{method.Name}' at IL+0x{instrStart:X4} is not part of a delegate construction");
|
||
|
||
bool afterDup = lastWasDup;
|
||
lastWasDup = false;
|
||
|
||
switch (op)
|
||
{
|
||
case ILOpCode.Nop: break;
|
||
|
||
case ILOpCode.Pop:
|
||
sp--;
|
||
break;
|
||
|
||
case ILOpCode.Dup:
|
||
{
|
||
lastWasDup = true;
|
||
var (src, t) = evalStack[sp - 1];
|
||
// Phantom-address slots aren't real values — duplicate by sharing the
|
||
// tag without emitting a mov. Required for `arr[i] += v` (ldelema; dup;
|
||
// ldind; add; stind) so both ldind and stind see the same array address.
|
||
if (addrTagBySlot[src] != -2)
|
||
{
|
||
int phDup = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
addrTagBySlot[phDup] = addrTagBySlot[src];
|
||
addrFldObjBySlot[phDup] = addrFldObjBySlot[src];
|
||
addrFldTokBySlot[phDup] = addrFldTokBySlot[src];
|
||
addrByteOffBySlot[phDup] = addrByteOffBySlot[src];
|
||
addrFldOffBySlot[phDup] = addrFldOffBySlot[src];
|
||
addrElemLayoutBySlot[phDup] = addrElemLayoutBySlot[src];
|
||
evalStack[sp++] = (phDup, SType.O);
|
||
break;
|
||
}
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, t, null);
|
||
evalStack[sp++] = (dst, t);
|
||
Emit3(ir, irToIl, Op.mov, dst, src, instrStart);
|
||
break;
|
||
}
|
||
|
||
// --- Constants ---
|
||
case ILOpCode.Ldc_i4_m1: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, -1, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_0: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 0, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_1: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 1, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_2: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 2, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_3: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 3, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_4: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 4, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_5: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 5, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_6: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 6, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_7: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 7, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_8: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, 8, SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4_s: PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, (int)(sbyte)il[ip++], SType.I4, Op.ldc_i4, instrStart); break;
|
||
case ILOpCode.Ldc_i4:
|
||
{
|
||
int imm = BitConverter.ToInt32(il, ip); ip += 4;
|
||
PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, imm, SType.I4, Op.ldc_i4, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldc_r4:
|
||
{
|
||
// Store float bits as int (bit-identical, read back with UInt32BitsToSingle in executor)
|
||
int imm = (int)BitConverter.ToUInt32(il, ip); ip += 4;
|
||
PushConst(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, imm, SType.R4, Op.ldc_r4, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldc_r8:
|
||
{
|
||
// Real double constant into a wide R8 slot (bits split across two IR words).
|
||
ulong bits = BitConverter.ToUInt64(il, ip); ip += 8;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.R8, null);
|
||
evalStack[sp++] = (dst, SType.R8);
|
||
ir.Add((uint)Op.ldc_r8); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)(bits & 0xFFFFFFFF)); irToIl.Add(instrStart);
|
||
ir.Add((uint)(bits >> 32)); irToIl.Add(instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldc_i8:
|
||
{
|
||
ulong bits = BitConverter.ToUInt64(il, ip); ip += 8;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I8, null);
|
||
evalStack[sp++] = (dst, SType.I8);
|
||
ir.Add((uint)Op.ldc_i8); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)(bits & 0xFFFFFFFF)); irToIl.Add(instrStart);
|
||
ir.Add((uint)(bits >> 32)); irToIl.Add(instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldnull:
|
||
{
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst, SType.O);
|
||
Emit2(ir, irToIl, Op.ldnull, dst, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldstr:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
string s = asm.Reader.GetUserString(MetadataTokens.UserStringHandle(tok & 0x00FFFFFF));
|
||
int strIdx = strings.Count; strings.Add(s);
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst, SType.O);
|
||
Emit3(ir, irToIl, Op.ldstr, dst, strIdx, instrStart);
|
||
break;
|
||
}
|
||
|
||
// --- Locals ---
|
||
case ILOpCode.Ldloc_0: if (!TryLoadLocalPhantom(0)) PushLocal(ir, irToIl, evalStack, ref sp, ref frameSize, 0, localFrameSlot, localTypes, slotTypes, slotStructs, method.LocalIsScriptStruct, instrStart); break;
|
||
case ILOpCode.Ldloc_1: if (!TryLoadLocalPhantom(1)) PushLocal(ir, irToIl, evalStack, ref sp, ref frameSize, 1, localFrameSlot, localTypes, slotTypes, slotStructs, method.LocalIsScriptStruct, instrStart); break;
|
||
case ILOpCode.Ldloc_2: if (!TryLoadLocalPhantom(2)) PushLocal(ir, irToIl, evalStack, ref sp, ref frameSize, 2, localFrameSlot, localTypes, slotTypes, slotStructs, method.LocalIsScriptStruct, instrStart); break;
|
||
case ILOpCode.Ldloc_3: if (!TryLoadLocalPhantom(3)) PushLocal(ir, irToIl, evalStack, ref sp, ref frameSize, 3, localFrameSlot, localTypes, slotTypes, slotStructs, method.LocalIsScriptStruct, instrStart); break;
|
||
case ILOpCode.Ldloc_s: { int li = il[ip++]; if (!TryLoadLocalPhantom(li)) PushLocal(ir, irToIl, evalStack, ref sp, ref frameSize, li, localFrameSlot, localTypes, slotTypes, slotStructs, method.LocalIsScriptStruct, instrStart); break; }
|
||
case ILOpCode.Ldloc: { int li = ReadU16(il, ref ip); if (!TryLoadLocalPhantom(li)) PushLocal(ir, irToIl, evalStack, ref sp, ref frameSize, li, localFrameSlot, localTypes, slotTypes, slotStructs, method.LocalIsScriptStruct, instrStart); break; }
|
||
case ILOpCode.Stloc_0: if (!TryStoreLocalPhantom(0)) { addrTagBySlot[localFrameSlot[0]] = -2; StoreLocal(ir, irToIl, evalStack, ref sp, 0, localFrameSlot, localTypes, slotTypes, slotStructs, localDeclared, ref frameSize, instrStart); } break;
|
||
case ILOpCode.Stloc_1: if (!TryStoreLocalPhantom(1)) { addrTagBySlot[localFrameSlot[1]] = -2; StoreLocal(ir, irToIl, evalStack, ref sp, 1, localFrameSlot, localTypes, slotTypes, slotStructs, localDeclared, ref frameSize, instrStart); } break;
|
||
case ILOpCode.Stloc_2: if (!TryStoreLocalPhantom(2)) { addrTagBySlot[localFrameSlot[2]] = -2; StoreLocal(ir, irToIl, evalStack, ref sp, 2, localFrameSlot, localTypes, slotTypes, slotStructs, localDeclared, ref frameSize, instrStart); } break;
|
||
case ILOpCode.Stloc_3: if (!TryStoreLocalPhantom(3)) { addrTagBySlot[localFrameSlot[3]] = -2; StoreLocal(ir, irToIl, evalStack, ref sp, 3, localFrameSlot, localTypes, slotTypes, slotStructs, localDeclared, ref frameSize, instrStart); } break;
|
||
case ILOpCode.Stloc_s: { int li = il[ip++]; if (!TryStoreLocalPhantom(li)) { addrTagBySlot[localFrameSlot[li]] = -2; StoreLocal(ir, irToIl, evalStack, ref sp, li, localFrameSlot, localTypes, slotTypes, slotStructs, localDeclared, ref frameSize, instrStart); } break; }
|
||
case ILOpCode.Stloc: { int li = ReadU16(il, ref ip); if (!TryStoreLocalPhantom(li)) { addrTagBySlot[localFrameSlot[li]] = -2; StoreLocal(ir, irToIl, evalStack, ref sp, li, localFrameSlot, localTypes, slotTypes, slotStructs, localDeclared, ref frameSize, instrStart); } break; }
|
||
|
||
// ldloca / ldloca.s — push a phantom address of the local's frame slot.
|
||
// The consuming instruction (ldind.*, stind.*, call .ctor) resolves it.
|
||
case ILOpCode.Ldloca_s:
|
||
case ILOpCode.Ldloca:
|
||
{
|
||
int localIdx = op == ILOpCode.Ldloca_s ? il[ip++] : ReadU16(il, ref ip);
|
||
int frameSlot = localFrameSlot[localIdx];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null); // phantom O slot
|
||
addrTagBySlot[dst] = frameSlot; // frame-slot address
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
// ldarga / ldarga.s — push a phantom address of the arg's frame slot.
|
||
case ILOpCode.Ldarga_s:
|
||
case ILOpCode.Ldarga:
|
||
{
|
||
int argIdx = op == ILOpCode.Ldarga_s ? il[ip++] : ReadU16(il, ref ip);
|
||
int frameSlot = argFrameSlot[argIdx];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null); // phantom O slot
|
||
addrTagBySlot[dst] = frameSlot; // frame-slot address
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
|
||
// --- Args ---
|
||
case ILOpCode.Ldarg_0: PushArg(evalStack, ref sp, 0, argTypes, argFrameSlot); break;
|
||
case ILOpCode.Ldarg_1: PushArg(evalStack, ref sp, 1, argTypes, argFrameSlot); break;
|
||
case ILOpCode.Ldarg_2: PushArg(evalStack, ref sp, 2, argTypes, argFrameSlot); break;
|
||
case ILOpCode.Ldarg_3: PushArg(evalStack, ref sp, 3, argTypes, argFrameSlot); break;
|
||
case ILOpCode.Ldarg_s: PushArg(evalStack, ref sp, il[ip++], argTypes, argFrameSlot); break;
|
||
case ILOpCode.Ldarg: PushArg(evalStack, ref sp, ReadU16(il, ref ip), argTypes, argFrameSlot); break;
|
||
case ILOpCode.Starg_s: StoreArg(ir, irToIl, evalStack, ref sp, il[ip++], argTypes, argFrameSlot, slotTypes, slotStructs, ref frameSize, instrStart); break;
|
||
case ILOpCode.Starg: StoreArg(ir, irToIl, evalStack, ref sp, ReadU16(il, ref ip), argTypes, argFrameSlot, slotTypes, slotStructs, ref frameSize, instrStart); break;
|
||
|
||
// --- Arithmetic ---
|
||
case ILOpCode.Add: EmitBinop(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.add_i4, Op.add_r4, Op.add_i4_nn, Op.add_r4_nn, instrStart); break;
|
||
case ILOpCode.Sub: EmitBinop(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.sub_i4, Op.sub_r4, Op.sub_i4_nn, Op.sub_r4_nn, instrStart); break;
|
||
case ILOpCode.Mul: EmitBinop(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.mul_i4, Op.mul_r4, Op.mul_i4_nn, Op.mul_r4_nn, instrStart); break;
|
||
case ILOpCode.Div: EmitBinop(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.div_i4, Op.div_r4, Op.div_i4_nn, Op.div_r4_nn, instrStart); break;
|
||
case ILOpCode.Rem: EmitBinop(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.rem_i4, Op.rem_r4, Op.rem_i4_nn, Op.rem_r4_nn, instrStart); break;
|
||
case ILOpCode.Div_un: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.div_un_i4, Op.div_un_i4_nn, instrStart); break;
|
||
case ILOpCode.Rem_un: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.rem_un_i4, Op.rem_un_i4_nn, instrStart); break;
|
||
case ILOpCode.Neg: EmitUnop(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.neg_i4, Op.neg_r4, Op.neg_i4_n, Op.neg_r4_n, instrStart); break;
|
||
case ILOpCode.And: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.and_i4, Op.and_i4_nn, instrStart); break;
|
||
case ILOpCode.Or: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.or_i4, Op.or_i4_nn, instrStart); break;
|
||
case ILOpCode.Xor: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.xor_i4, Op.xor_i4_nn, instrStart); break;
|
||
case ILOpCode.Not: EmitUnopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.not_i4, Op.not_i4_n, instrStart); break;
|
||
case ILOpCode.Shl: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.shl_i4, Op.shl_i4_nn, instrStart); break;
|
||
case ILOpCode.Shr: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.shr_i4, Op.shr_i4_nn, instrStart); break;
|
||
case ILOpCode.Shr_un: EmitBinopI4(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.shr_un_i4, Op.shr_un_i4_nn, instrStart); break;
|
||
|
||
// --- Comparisons ---
|
||
case ILOpCode.Ceq:
|
||
{
|
||
var (s2, t2) = evalStack[--sp];
|
||
var (s1, t1) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
Op cop;
|
||
if (t1 == SType.R8 || t2 == SType.R8)
|
||
cop = Op.ceq_r8;
|
||
else if (t1 == SType.I8 || t2 == SType.I8)
|
||
cop = Op.ceq_i8;
|
||
else if (t1 == SType.R4 || t2 == SType.R4)
|
||
cop = (t1 == SType.R4 && t2 == SType.R4 && slotTypes[s1] == SType.R4 && slotTypes[s2] == SType.R4) ? Op.ceq_r4_nn : Op.ceq_r4;
|
||
else if (t1 == SType.I4 || t2 == SType.I4)
|
||
// ceq_i4 reads each operand from numStack or refStack based on slotT at runtime,
|
||
// so it handles mixed O/I4 cases (e.g. boxed int from a non-flat host call vs. ldc.i4).
|
||
cop = (slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4) ? Op.ceq_i4_nn : Op.ceq_i4;
|
||
else
|
||
cop = Op.ceq_o;
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
Emit4(ir, irToIl, cop, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Cgt:
|
||
{
|
||
var (s2, t2) = evalStack[--sp];
|
||
var (s1, t1) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
Op cop;
|
||
if (t1 == SType.R8 || t2 == SType.R8)
|
||
cop = Op.cgt_r8;
|
||
else if (t1 == SType.I8 || t2 == SType.I8)
|
||
cop = Op.cgt_i8;
|
||
else if (t1 == SType.R4 || t2 == SType.R4)
|
||
cop = (t1 == SType.R4 && t2 == SType.R4 && slotTypes[s1] == SType.R4 && slotTypes[s2] == SType.R4) ? Op.cgt_r4_nn : Op.cgt_r4;
|
||
else
|
||
cop = (slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4) ? Op.cgt_i4_nn : Op.cgt_i4;
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
Emit4(ir, irToIl, cop, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Cgt_un:
|
||
{
|
||
var (s2, t2) = evalStack[--sp];
|
||
var (s1, t1) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
Op cop;
|
||
if (t1 == SType.R8 || t2 == SType.R8)
|
||
cop = Op.cgt_un_r8;
|
||
else if (t1 == SType.I8 || t2 == SType.I8)
|
||
cop = Op.cgt_un_i8;
|
||
else if (t1 == SType.R4 || t2 == SType.R4)
|
||
cop = Op.cgt_un_r4; // float `<=` lowers to `cgt.un` — unordered greater-than
|
||
else if (t1 == SType.O)
|
||
cop = Op.cgt_un_o;
|
||
else
|
||
cop = (slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4) ? Op.cgt_un_i4_nn : Op.cgt_un_i4;
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
Emit4(ir, irToIl, cop, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Clt:
|
||
{
|
||
var (s2, t2) = evalStack[--sp];
|
||
var (s1, t1) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
Op cop;
|
||
if (t1 == SType.R8 || t2 == SType.R8)
|
||
cop = Op.clt_r8;
|
||
else if (t1 == SType.I8 || t2 == SType.I8)
|
||
cop = Op.clt_i8;
|
||
else if (t1 == SType.R4 || t2 == SType.R4)
|
||
cop = (t1 == SType.R4 && t2 == SType.R4 && slotTypes[s1] == SType.R4 && slotTypes[s2] == SType.R4) ? Op.clt_r4_nn : Op.clt_r4;
|
||
else
|
||
cop = (slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4) ? Op.clt_i4_nn : Op.clt_i4;
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
Emit4(ir, irToIl, cop, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Clt_un:
|
||
{
|
||
var (s2, t2) = evalStack[--sp];
|
||
var (s1, t1) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
if (t1 == SType.R8 || t2 == SType.R8)
|
||
{
|
||
Emit4(ir, irToIl, Op.clt_un_r8, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
if (t1 == SType.I8 || t2 == SType.I8)
|
||
{
|
||
Emit4(ir, irToIl, Op.clt_un_i8, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
if (t1 == SType.R4 || t2 == SType.R4)
|
||
{
|
||
Emit4(ir, irToIl, Op.clt_un_r4, dst, s1, s2, instrStart); // float `>=` lowers to `clt.un`
|
||
break;
|
||
}
|
||
bool both_i4 = t1 == SType.I4 && t2 == SType.I4 && slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4;
|
||
Emit4(ir, irToIl, both_i4 ? Op.clt_un_i4_nn : Op.clt_un_i4, dst, s1, s2, instrStart);
|
||
break;
|
||
}
|
||
|
||
// Box is a no-op: values already live unboxed in the frame; boxing
|
||
// happens at host-call boundaries.
|
||
case ILOpCode.Box:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
// Materialize a REAL, correctly-typed boxed object for primitive value types.
|
||
// Leaving box a no-op kept the value in its I4/R4 slot: the target `object`
|
||
// local was then classified I4 (so `object a=5; object b=5; a==b` compared by
|
||
// value, not reference) and bool/char boxed as int (Debug.Log(true) -> "1").
|
||
// Reference types are already O (box is identity); flat structs box on store.
|
||
int tc = BoxPrimTypeCode(asm, tok);
|
||
if (tc >= 0)
|
||
{
|
||
var (src, _) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp - 1] = (dst, SType.O);
|
||
Emit4(ir, irToIl, Op.box_prim, dst, src, tc, instrStart);
|
||
}
|
||
// HOST enum: box as the real enum type, not the underlying int —
|
||
// `$"state={m}"` / Debug.Log(m) must render the member name. Script-
|
||
// declared enums (TypeDefs) have no runtime Type and keep the int form.
|
||
else if (ResolveHostEnumType(asm, tok) is { } enumType)
|
||
{
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
asm.TokenTypes[tok] = enumType; // executor resolves through TokenTypes
|
||
var (src, _) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp - 1] = (dst, SType.O);
|
||
Emit4(ir, irToIl, Op.box_enum, dst, src, tokIdx, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
|
||
// --- Castclass / isinst / unbox.any / ldtoken ---
|
||
case ILOpCode.Castclass:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (src, _) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp - 1] = (dst, SType.O);
|
||
Emit4(ir, irToIl, Op.castclass, dst, src, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Isinst:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (src, _) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp - 1] = (dst, SType.O);
|
||
Emit4(ir, irToIl, Op.isinst, dst, src, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Unbox_any:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (src, _) = evalStack[sp - 1];
|
||
// Type the result by the TARGET, not the boxed source: `(int)(object)x` must
|
||
// yield an I4 value, not an O. Keeping the source's O tag reclassified the
|
||
// destination local as a ref slot (e.g. `int acc2 = (int)(object)k` made acc2
|
||
// O-typed, so later int reads saw 0). Primitives -> I4/R4; ref/struct/enum -> O.
|
||
var targetSt = PrimitiveSTypeForTypeToken(asm, tok) ?? SType.O;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, targetSt, null);
|
||
evalStack[sp - 1] = (dst, targetSt);
|
||
Emit4(ir, irToIl, Op.unbox_any, dst, src, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldtoken:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst, SType.O);
|
||
Emit3(ir, irToIl, Op.ldtoken, dst, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
|
||
// --- Conversions ---
|
||
case ILOpCode.Conv_i4:
|
||
case ILOpCode.Conv_u4:
|
||
{
|
||
var (src, st) = evalStack[sp - 1];
|
||
if (st == SType.I4) break; // already int
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
var cvOp = st == SType.I8 ? Op.conv_i4_i8
|
||
: st == SType.R8 ? Op.conv_i4_r8
|
||
: Op.conv_r4_i4;
|
||
Emit3(ir, irToIl, cvOp, dst, src, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_i8:
|
||
case ILOpCode.Conv_u8:
|
||
{
|
||
var (src, st) = evalStack[sp - 1];
|
||
if (st == SType.I8) break; // already 64-bit
|
||
// conv.i8 sign-extends an i4; conv.u8 zero-extends it. From float sources
|
||
// only the SIGNED form is supported — (ulong)someFloat stays loud.
|
||
bool zext = op == ILOpCode.Conv_u8;
|
||
Op cvOp;
|
||
if (st == SType.R4) { if (zext) goto notSupported; cvOp = Op.conv_i8_r4; }
|
||
else if (st == SType.R8) { if (zext) goto notSupported; cvOp = Op.conv_i8_r8; }
|
||
else cvOp = zext ? Op.conv_i8_u4 : Op.conv_i8_i4;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I8, null);
|
||
evalStack[sp - 1] = (dst, SType.I8);
|
||
Emit3(ir, irToIl, cvOp, dst, src, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_r_un:
|
||
{
|
||
// Unsigned int → floating: ulong/uint reinterpreted unsigned, result R8
|
||
// (a following conv.r4 narrows it).
|
||
var (src, st) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.R8, null);
|
||
evalStack[sp - 1] = (dst, SType.R8);
|
||
if (st == SType.I8)
|
||
Emit3(ir, irToIl, Op.conv_r8_u8, dst, src, instrStart);
|
||
else
|
||
{
|
||
// u4 source: zero-extend to I8 first, then unsigned-convert.
|
||
int wide = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I8, null);
|
||
Emit3(ir, irToIl, Op.conv_i8_u4, wide, src, instrStart);
|
||
Emit3(ir, irToIl, Op.conv_r8_u8, dst, wide, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_i1:
|
||
{
|
||
var (src2, _) = evalStack[sp - 1];
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst2, SType.I4);
|
||
Emit3(ir, irToIl, Op.conv_i4_i1, dst2, src2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_u1:
|
||
{
|
||
var (src2, _) = evalStack[sp - 1];
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst2, SType.I4);
|
||
Emit3(ir, irToIl, Op.conv_i4_u1, dst2, src2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_i2:
|
||
{
|
||
var (src2, _) = evalStack[sp - 1];
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst2, SType.I4);
|
||
Emit3(ir, irToIl, Op.conv_i4_i2, dst2, src2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_u2:
|
||
{
|
||
var (src2, _) = evalStack[sp - 1];
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst2, SType.I4);
|
||
Emit3(ir, irToIl, Op.conv_i4_u2, dst2, src2, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_r4:
|
||
{
|
||
var (src, st) = evalStack[sp - 1];
|
||
if (st == SType.R4) break;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.R4, null);
|
||
evalStack[sp - 1] = (dst, SType.R4);
|
||
var cvOp = st == SType.I8 ? Op.conv_r4_i8
|
||
: st == SType.R8 ? Op.conv_r4_r8
|
||
: Op.conv_i4_r4;
|
||
Emit3(ir, irToIl, cvOp, dst, src, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Conv_r8:
|
||
{
|
||
// Honest double now that R8 slots exist (this retires the double-as-float
|
||
// divergence the analyzer guarded with MS002).
|
||
var (src, st) = evalStack[sp - 1];
|
||
if (st == SType.R8) break;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.R8, null);
|
||
evalStack[sp - 1] = (dst, SType.R8);
|
||
var cvOp = st == SType.I8 ? Op.conv_r8_i8
|
||
: st == SType.R4 ? Op.conv_r8_r4
|
||
: Op.conv_r8_i4;
|
||
Emit3(ir, irToIl, cvOp, dst, src, instrStart);
|
||
break;
|
||
}
|
||
|
||
// --- Branches ---
|
||
case ILOpCode.Br_s:
|
||
case ILOpCode.Br:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Br_s);
|
||
// Ternary / short-circuit value pattern: a FORWARD br that is the tail of a
|
||
// value-producing arm — `<then>; br END; ELSE: <else>; END:`. The top-of-stack
|
||
// is this arm's result; move it into a fresh merge slot M so the other arm can
|
||
// converge on the same M at END.
|
||
//
|
||
// The arm is genuine when the label that follows the br (`ip`) is the ELSE
|
||
// label — i.e. an earlier branch already delivered depth `sp-1` there, so ELSE
|
||
// drops exactly this one result. When the recorded depth is `sp` instead, the
|
||
// value on top is NOT an arm result but a live operand carried beneath a
|
||
// short-circuit region (the `v0 == (… || …)` case): merging/dropping it would
|
||
// corrupt the stack, so we leave it alone and let RecordJoinDepth carry it.
|
||
//
|
||
// It is ALSO genuine — regardless of the ELSE-adjacency heuristic above — when
|
||
// `target` is already a known merge join (an earlier sibling arm already
|
||
// converged there): every further forward edge into an established merge point
|
||
// must converge its own top-of-stack into the same slot too. This matters for a
|
||
// degenerate zero-displacement `br` (target == ip, e.g. when Roslyn collapses two
|
||
// constant-valued ternary arms into one shared tail): such a br still carries a
|
||
// value that MUST be merged, but its "label right after it" is the merge point
|
||
// itself rather than a distinct ELSE label, so the ELSE-adjacency check alone
|
||
// (which additionally requires target > ip) never fires for it. Without this,
|
||
// the arm's value is silently dropped and the merge slot keeps a stale/default
|
||
// value when that arm is the one actually taken at runtime.
|
||
if (target >= ip && sp > 0 &&
|
||
(mergeSlotForEnd.ContainsKey(target) ||
|
||
(branchTargetDepth.TryGetValue(ip, out var elseDepth) && elseDepth == sp - 1)))
|
||
{
|
||
MergeTopIntoJoinSlot(ir, irToIl, evalStack, sp, ref frameSize, slotTypes, slotStructs, mergeSlotForEnd, target, instrStart);
|
||
}
|
||
RecordJoinDepth(instrStart, target, sp); // br leaves the stack unchanged
|
||
int patchIdx = ir.Count + 1;
|
||
Emit2(ir, irToIl, Op.br, -1, instrStart);
|
||
patchList.Add((patchIdx, target));
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
// leave empties the eval stack, then runs every enclosing finally whose try
|
||
// region contains this instruction but not the target (innermost first),
|
||
// then continues at the target. Lowered as a continuation chain: push the
|
||
// final target and the outer handlers' entries, branch to the innermost
|
||
// handler; each handler's endfinally (br_cont) pops the next address.
|
||
case ILOpCode.Leave_s:
|
||
case ILOpCode.Leave:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Leave_s);
|
||
sp = 0; // leave semantics: the eval stack is emptied
|
||
var chain = FinallyChain(method.FinallyRegions, instrStart, target);
|
||
RecordJoinDepth(instrStart, target, 0);
|
||
if (chain.Count == 0)
|
||
{
|
||
int patchIdx = ir.Count + 1;
|
||
Emit2(ir, irToIl, Op.br, -1, instrStart);
|
||
patchList.Add((patchIdx, target));
|
||
}
|
||
else
|
||
{
|
||
// Push order: final target first, then outer→inner-but-one handlers,
|
||
// so pops run inner handler → … → outer handler → target.
|
||
int pi = ir.Count + 1;
|
||
Emit2(ir, irToIl, Op.push_cont, -1, instrStart);
|
||
patchList.Add((pi, target));
|
||
for (int k = chain.Count - 1; k >= 1; k--)
|
||
{
|
||
pi = ir.Count + 1;
|
||
Emit2(ir, irToIl, Op.push_cont, -1, instrStart);
|
||
patchList.Add((pi, chain[k]));
|
||
}
|
||
RecordJoinDepth(instrStart, chain[0], 0);
|
||
pi = ir.Count + 1;
|
||
Emit2(ir, irToIl, Op.br, -1, instrStart);
|
||
patchList.Add((pi, chain[0]));
|
||
}
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Endfinally: // also ends the handler when reached by fall-through
|
||
{
|
||
ir.Add((uint)Op.br_cont); irToIl.Add(instrStart);
|
||
sp = 0;
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
case ILOpCode.Brtrue_s:
|
||
case ILOpCode.Brtrue:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Brtrue_s);
|
||
var (cond, ct) = evalStack[--sp];
|
||
// Value left on the stack after the condition is a forward-join carry ONLY in
|
||
// the `dup; brtrue L` idiom (`a ?? b`, `a?.b`): converge it with the
|
||
// fall-through's value into one slot; the join reconciles the other edge. With
|
||
// no preceding dup the remaining top is just a base operand (e.g. the `==` left
|
||
// operand under a `&&`/`||` chain) and must be left untouched.
|
||
if (afterDup && sp > 0 && target > ip)
|
||
MergeTopIntoJoinSlot(ir, irToIl, evalStack, sp, ref frameSize, slotTypes, slotStructs, mergeSlotForEnd, target, instrStart);
|
||
RecordJoinDepth(instrStart, target, sp); // condition already popped
|
||
var bop = ct == SType.O ? Op.brtrue_o : ct == SType.I8 ? Op.brtrue_i8 : Op.brtrue_i4;
|
||
if (ct == SType.R8) goto notSupported; // no IL brtrue on floats
|
||
int patchIdx = ir.Count + 2;
|
||
Emit3(ir, irToIl, bop, cond, -1, instrStart);
|
||
patchList.Add((patchIdx, target));
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Brfalse_s:
|
||
case ILOpCode.Brfalse:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Brfalse_s);
|
||
var (cond, ct) = evalStack[--sp];
|
||
if (afterDup && sp > 0 && target > ip)
|
||
MergeTopIntoJoinSlot(ir, irToIl, evalStack, sp, ref frameSize, slotTypes, slotStructs, mergeSlotForEnd, target, instrStart);
|
||
RecordJoinDepth(instrStart, target, sp); // condition already popped
|
||
var bop = ct == SType.O ? Op.brfalse_o : ct == SType.I8 ? Op.brfalse_i8 : Op.brfalse_i4;
|
||
if (ct == SType.R8) goto notSupported;
|
||
int patchIdx = ir.Count + 2;
|
||
Emit3(ir, irToIl, bop, cond, -1, instrStart);
|
||
patchList.Add((patchIdx, target));
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
// Compare-and-branch (lower to ceq+brtrue pattern)
|
||
case ILOpCode.Beq_s: case ILOpCode.Beq:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Beq_s);
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.ceq_i4, Op.ceq_r4, Op.ceq_o, Op.ceq_i4_nn, Op.ceq_r4_nn, Op.brtrue_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Bne_un_s: case ILOpCode.Bne_un:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Bne_un_s);
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.ceq_i4, Op.ceq_r4, Op.ceq_o, Op.ceq_i4_nn, Op.ceq_r4_nn, Op.brfalse_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Blt_s: case ILOpCode.Blt:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Blt_s);
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.clt_i4, Op.clt_r4, Op.clt_i4, Op.clt_i4_nn, Op.clt_r4_nn, Op.brtrue_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Blt_un_s: case ILOpCode.Blt_un:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Blt_un_s);
|
||
// Unordered: branch when a < b OR either is NaN, so the float compare must be the
|
||
// unordered clt_un_r4 (used for both the typed and _nn slots — it reads operand types itself).
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.clt_un_i4, Op.clt_un_r4, Op.clt_un_i4, Op.clt_un_i4_nn, Op.clt_un_r4, Op.brtrue_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Bgt_s: case ILOpCode.Bgt:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Bgt_s);
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.cgt_i4, Op.cgt_r4, Op.cgt_i4, Op.cgt_i4_nn, Op.cgt_r4_nn, Op.brtrue_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Bgt_un_s: case ILOpCode.Bgt_un:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Bgt_un_s);
|
||
// Unordered: branch when a > b OR either is NaN, so the float compare must be the
|
||
// unordered cgt_un_r4 (used for both the typed and _nn slots — it reads operand types itself).
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.cgt_un_i4, Op.cgt_un_r4, Op.cgt_un_o, Op.cgt_un_i4_nn, Op.cgt_un_r4, Op.brtrue_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Ble_s: case ILOpCode.Ble:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Ble_s);
|
||
// !(a > b) — negating through brfalse swaps ordered/unordered, so the ORDERED ble
|
||
// (no branch on NaN) needs the UNORDERED cgt_un_r4: !(a > b OR NaN) is false for NaN.
|
||
// (cgt_un_r4 serves both the typed and _nn slots — it reads operand types itself.)
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.cgt_i4, Op.cgt_un_r4, Op.cgt_i4, Op.cgt_i4_nn, Op.cgt_un_r4, Op.brfalse_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Ble_un_s: case ILOpCode.Ble_un:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Ble_un_s);
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.cgt_un_i4, Op.cgt_r4, Op.cgt_un_o, Op.cgt_un_i4_nn, Op.cgt_r4_nn, Op.brfalse_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Bge_s: case ILOpCode.Bge:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Bge_s);
|
||
// !(a < b) — negating through brfalse swaps ordered/unordered, so the ORDERED bge
|
||
// (no branch on NaN) needs the UNORDERED clt_un_r4: !(a < b OR NaN) is false for NaN.
|
||
// (clt_un_r4 serves both the typed and _nn slots — it reads operand types itself.)
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.clt_i4, Op.clt_un_r4, Op.clt_i4, Op.clt_i4_nn, Op.clt_un_r4, Op.brfalse_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
case ILOpCode.Bge_un_s: case ILOpCode.Bge_un:
|
||
{
|
||
int target = ReadBranchTarget(il, ref ip, op == ILOpCode.Bge_un_s);
|
||
EmitCmpBranch(ir, irToIl, evalStack, ref sp, ref frameSize, slotTypes, slotStructs, Op.clt_un_i4, Op.clt_r4, Op.clt_un_i4, Op.clt_un_i4_nn, Op.clt_r4_nn, Op.brfalse_i4, patchList, target, instrStart, branchTargetDepth);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
// --- Switch ---
|
||
case ILOpCode.Switch:
|
||
{
|
||
int n = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tableStart = ip;
|
||
ip += 4 * n; // skip offset table
|
||
int afterSwitch = ip; // base for relative offsets
|
||
|
||
var (valSlot, _) = evalStack[--sp];
|
||
// Emit: [switch_i4, val, n, default_patch_idx, ip0, ip1, ...]; targets patched afterwards.
|
||
int irSwitchStart = ir.Count;
|
||
ir.Add((uint)Op.switch_i4);
|
||
irToIl.Add(instrStart);
|
||
ir.Add((uint)valSlot);
|
||
irToIl.Add(instrStart);
|
||
ir.Add((uint)n);
|
||
irToIl.Add(instrStart);
|
||
// placeholder for default (fall-through = afterSwitch → patched via patchList)
|
||
int defaultPatchIdx = ir.Count;
|
||
ir.Add(unchecked((uint)-1));
|
||
irToIl.Add(instrStart);
|
||
patchList.Add((defaultPatchIdx, afterSwitch));
|
||
// per-case targets — the switch value is already popped, so every edge
|
||
// (cases + default fall-through) delivers the current base depth.
|
||
RecordJoinDepth(instrStart, afterSwitch, sp);
|
||
for (int ci = 0; ci < n; ci++)
|
||
{
|
||
int off = BitConverter.ToInt32(il, tableStart + ci * 4);
|
||
int target = afterSwitch + off;
|
||
int patchIdx2 = ir.Count;
|
||
ir.Add(unchecked((uint)-1));
|
||
irToIl.Add(instrStart);
|
||
patchList.Add((patchIdx2, target));
|
||
RecordJoinDepth(instrStart, target, sp);
|
||
}
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
// --- Ret ---
|
||
case ILOpCode.Throw:
|
||
{
|
||
var (thrSrc, thrSt) = evalStack[--sp];
|
||
if (thrSt != SType.O) goto notSupported; // exceptions are always O refs
|
||
Emit2(ir, irToIl, Op.throw_o, thrSrc, instrStart);
|
||
// Unreachable code follows until the next branch target; the linear walk's
|
||
// join-depth bookkeeping handles it like the code after an unconditional br.
|
||
sp = 0;
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
case ILOpCode.Ret:
|
||
{
|
||
if (sp > 0)
|
||
{
|
||
var (src, st) = evalStack[--sp];
|
||
if (method.ReturnSType == SType.Vt && method.ReturnStructLayout != null)
|
||
{
|
||
// Flat-struct return. A boxed (O) value — e.g. from a path the
|
||
// lowerer left boxed — is unboxed into a Vt temp first so ret_vt
|
||
// always reads real frame bytes.
|
||
if (st != SType.Vt)
|
||
{
|
||
int vtmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, method.ReturnStructLayout);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmp, src, instrStart);
|
||
src = vtmp;
|
||
}
|
||
Emit2(ir, irToIl, Op.ret_vt, src, instrStart);
|
||
}
|
||
else
|
||
{
|
||
// A Vt value returned through a non-Vt signature (defensive): box it.
|
||
if (st == SType.Vt)
|
||
{
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, src, instrStart);
|
||
src = boxed;
|
||
}
|
||
var retOp = st == SType.R4 ? Op.ret_r4 : st == SType.I4 ? Op.ret_i4
|
||
: st == SType.I8 ? Op.ret_i8 : st == SType.R8 ? Op.ret_r8 : Op.ret_o;
|
||
Emit2(ir, irToIl, retOp, src, instrStart);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ir.Add((uint)Op.ret_void);
|
||
irToIl.Add(instrStart);
|
||
}
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
// --- Method calls ---
|
||
// `constrained. T` prefixes a callvirt on a byref receiver. Boxed (O-slot)
|
||
// struct receivers already give constrained semantics through plain virtual
|
||
// dispatch. FLAT (Vt-slot) receivers need the type token: constrained to the
|
||
// slot's own struct type, the whole struct must box before the call — the
|
||
// receiver resolution below consumes the remembered token for that.
|
||
case ILOpCode.Constrained:
|
||
constrainedTok = BitConverter.ToInt32(il, ip);
|
||
ip += 4;
|
||
break;
|
||
|
||
case ILOpCode.Call:
|
||
case ILOpCode.Callvirt:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
int ctok = constrainedTok; constrainedTok = 0; // consume the prefix, if any
|
||
|
||
if (asm.ByToken.TryGetValue(tok, out var callee))
|
||
{
|
||
// Detect ldloca.s + call .ctor (script struct ctor) pattern:
|
||
// if this is a .ctor and the first arg slot is a frame-addr phantom,
|
||
// emit newobj_script writing into the existing local slot.
|
||
int argc = callee.ArgCount;
|
||
int spBase = sp - argc;
|
||
if (callee.Name == ".ctor" && argc >= 1 && addrTagBySlot[evalStack[spBase].slot] >= 0)
|
||
{
|
||
int destSlot = addrTagBySlot[evalStack[spBase].slot];
|
||
int destOff = addrByteOffBySlot[evalStack[spBase].slot];
|
||
addrTagBySlot[evalStack[spBase].slot] = -2; // consume the addr tag
|
||
int explicitArgc = argc - 1; // exclude `this`
|
||
if (slotTypes[destSlot] == SType.Vt && destOff == 0)
|
||
{
|
||
// FLAT struct ctor: zero the destination and run the ctor body
|
||
// with `this` = the Vt slot itself. The VM copies the bytes into
|
||
// the callee's Vt arg0 and copies them back on ret (VtThisWb).
|
||
Emit2(ir, irToIl, Op.initobj, destSlot, instrStart);
|
||
ir.Add((uint)Op.call_script); irToIl.Add(instrStart);
|
||
ir.Add(unchecked((uint)-1)); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)argc); irToIl.Add(instrStart);
|
||
ir.Add((uint)destSlot); irToIl.Add(instrStart); // `this`
|
||
for (int k = 0; k < explicitArgc; k++)
|
||
{ ir.Add((uint)evalStack[spBase + 1 + k].slot); irToIl.Add(instrStart); }
|
||
sp = spBase; // ctor is void, no push
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
ir.Add((uint)Op.newobj_script); irToIl.Add(instrStart);
|
||
ir.Add((uint)destSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)explicitArgc); irToIl.Add(instrStart);
|
||
for (int k = 0; k < explicitArgc; k++)
|
||
{ ir.Add((uint)evalStack[spBase + 1 + k].slot); irToIl.Add(instrStart); }
|
||
sp = spBase; // ctor is void, no push
|
||
}
|
||
else
|
||
{
|
||
// Instance callee: resolve a phantom receiver (ldloca/ldflda/ldelema)
|
||
// to a real slot — otherwise `this` arrives null. Flat receivers with
|
||
// a composed offset (or field/element receivers) are materialized into
|
||
// a temp with a write-back after the call (copy-in/copy-out byref
|
||
// semantics).
|
||
int wbKind = 0; // 0=none, 1=stfld_vt_vt, 2=stfld_sc_vt
|
||
int wbA = 0, wbB = 0, wbTemp = 0;
|
||
if (!callee.IsStatic && argc >= 1)
|
||
{
|
||
var (rSlot, _) = evalStack[spBase];
|
||
int rTag = addrTagBySlot[rSlot];
|
||
if (rTag >= 0)
|
||
{
|
||
int rOff = addrByteOffBySlot[rSlot];
|
||
addrTagBySlot[rSlot] = -2;
|
||
if (rOff == 0 || slotTypes[rTag] != SType.Vt)
|
||
{
|
||
evalStack[spBase] = (rTag, slotTypes[rTag]);
|
||
}
|
||
else if (callee.ThisStructLayout != null)
|
||
{
|
||
int tmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, callee.ThisStructLayout);
|
||
Emit4(ir, irToIl, Op.ldfld_vt_vt, tmp, rTag, rOff, instrStart);
|
||
evalStack[spBase] = (tmp, SType.Vt);
|
||
wbKind = 1; wbA = rTag; wbB = rOff; wbTemp = tmp;
|
||
}
|
||
}
|
||
else if (rTag == -1 && addrFldOffBySlot[rSlot] >= 0 && callee.ThisStructLayout != null)
|
||
{
|
||
int parentR = addrFldObjBySlot[rSlot];
|
||
int offR = addrFldOffBySlot[rSlot];
|
||
addrTagBySlot[rSlot] = -2;
|
||
int tmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, callee.ThisStructLayout);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, tmp, parentR, offR, instrStart);
|
||
evalStack[spBase] = (tmp, SType.Vt);
|
||
wbKind = 2; wbA = parentR; wbB = offR; wbTemp = tmp;
|
||
}
|
||
else if (rTag == -1)
|
||
{
|
||
// Heap script-struct field receiver: load the ScriptObject
|
||
// reference — mutations through it propagate, no write-back.
|
||
int parentR = addrFldObjBySlot[rSlot];
|
||
int fTokIdx = addrFldTokBySlot[rSlot];
|
||
int fTok = tokens[fTokIdx];
|
||
addrTagBySlot[rSlot] = -2;
|
||
int tmp = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
if (asm.FieldSlots.TryGetValue(fTok, out var rfs))
|
||
Emit4(ir, irToIl, Op.ldfld_sc_o, tmp, parentR, rfs.Item1.FieldOffsets[rfs.Item2], instrStart);
|
||
else
|
||
Emit4(ir, irToIl, Op.ldfld_o, tmp, parentR, fTokIdx, instrStart);
|
||
evalStack[spBase] = (tmp, SType.O);
|
||
}
|
||
else if (rTag == -3 && addrElemLayoutBySlot[rSlot] != null
|
||
&& callee.ThisStructLayout != null)
|
||
{
|
||
// arr[i].Method(): materialize the flat element, call with
|
||
// `this` = the temp, memcpy the element back after (wbKind 4).
|
||
int arrS = addrFldObjBySlot[rSlot];
|
||
int idxS = addrFldTokBySlot[rSlot];
|
||
addrTagBySlot[rSlot] = -2;
|
||
int tmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, addrElemLayoutBySlot[rSlot]!);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, tmp, arrS, idxS, instrStart);
|
||
evalStack[spBase] = (tmp, SType.Vt);
|
||
wbKind = 4; wbA = arrS; wbB = idxS; wbTemp = tmp;
|
||
}
|
||
else if (rTag == -3)
|
||
{
|
||
int arrS = addrFldObjBySlot[rSlot];
|
||
int idxS = addrFldTokBySlot[rSlot];
|
||
addrTagBySlot[rSlot] = -2;
|
||
int tmp = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit4(ir, irToIl, Op.ldelem_o, tmp, arrS, idxS, instrStart);
|
||
evalStack[spBase] = (tmp, SType.O);
|
||
}
|
||
}
|
||
// Normal script → script call; return type comes from callee's signature
|
||
bool isVoid = callee.IsVoid;
|
||
SType retSType = callee.ReturnSType;
|
||
int dst = isVoid ? -1
|
||
: retSType == SType.Vt && callee.ReturnStructLayout != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, callee.ReturnStructLayout)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, retSType == SType.Vt ? SType.O : retSType, null);
|
||
// Resolve ldloca-phantom args BEFORE emitting: a local function's
|
||
// captured environment arrives as `ref <>c__DisplayClass` — pass the
|
||
// ScriptObject reference itself, so callee field writes propagate.
|
||
// Any other byref arg shape stays loud (emitting the phantom slot
|
||
// raw would read null/garbage at runtime — silent wrong).
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
int aSlot = evalStack[spBase + k].slot;
|
||
if (aSlot < 0 || addrTagBySlot[aSlot] == -2) continue;
|
||
int aTag = addrTagBySlot[aSlot];
|
||
if (aTag >= 0)
|
||
{
|
||
// Frame-local environment (ldloca in a plain method).
|
||
if (slotTypes[aTag] != SType.O || addrByteOffBySlot[aSlot] != 0)
|
||
goto notSupported;
|
||
addrTagBySlot[aSlot] = -2;
|
||
evalStack[spBase + k] = (aTag, SType.O);
|
||
}
|
||
else if (aTag == -1 && addrFldOffBySlot[aSlot] < 0)
|
||
{
|
||
// Token mode — the addr names a whole O field; a composed
|
||
// byte offset (>= 0) would point INSIDE a nested struct,
|
||
// which falls through to the loud path below.
|
||
// State-machine-hoisted environment (ldflda in MoveNext):
|
||
// load the ScriptObject field — same reference, mutations
|
||
// propagate — exactly like the heap receiver path above.
|
||
int aParent = addrFldObjBySlot[aSlot];
|
||
int aTokIdx = addrFldTokBySlot[aSlot];
|
||
int aTok = tokens[aTokIdx];
|
||
addrTagBySlot[aSlot] = -2;
|
||
int tmp = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
if (asm.FieldSlots.TryGetValue(aTok, out var afs))
|
||
Emit4(ir, irToIl, Op.ldfld_sc_o, tmp, aParent, afs.Item1.FieldOffsets[afs.Item2], instrStart);
|
||
else
|
||
Emit4(ir, irToIl, Op.ldfld_o, tmp, aParent, aTokIdx, instrStart);
|
||
evalStack[spBase + k] = (tmp, SType.O);
|
||
}
|
||
else
|
||
goto notSupported;
|
||
}
|
||
// [call_script, dst, tok_idx, argc, arg0, arg1, ...]
|
||
ir.Add((uint)Op.call_script); irToIl.Add(instrStart);
|
||
ir.Add((uint)(isVoid ? -1 : dst)); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)argc); irToIl.Add(instrStart);
|
||
for (int k = 0; k < argc; k++)
|
||
{ ir.Add((uint)evalStack[spBase + k].slot); irToIl.Add(instrStart); }
|
||
if (wbKind == 1) Emit4(ir, irToIl, Op.stfld_vt_vt, wbA, wbB, wbTemp, instrStart);
|
||
else if (wbKind == 2) Emit4(ir, irToIl, Op.stfld_sc_vt, wbA, wbB, wbTemp, instrStart);
|
||
else if (wbKind == 4) Emit4(ir, irToIl, Op.stelem_vt, wbA, wbB, wbTemp, instrStart);
|
||
sp = spBase;
|
||
if (!isVoid) evalStack[sp++] = (dst, retSType);
|
||
}
|
||
}
|
||
else if (asm.HostCalls.TryGetValue(tok, out var hostEntry))
|
||
{
|
||
// Detect byref params — these go through the slow boxed path with
|
||
// post-call write-back. The Fast / FastFlat shortcuts are bypassed
|
||
// (none of the registered Fast templates take byref args today).
|
||
var ps = hostEntry.Binding.Params;
|
||
int byRefArgCount = 0;
|
||
int explicitArgc = hostEntry.Binding.ParamCount;
|
||
if (ps != null)
|
||
{
|
||
int nByref = ps.Length;
|
||
if (nByref > explicitArgc) nByref = explicitArgc;
|
||
for (int k = 0; k < nByref; k++)
|
||
if (ps[k].ParameterType.IsByRef) byRefArgCount++;
|
||
}
|
||
if (byRefArgCount > 0)
|
||
{
|
||
EmitCallHostByref(ir, irToIl, evalStack, ref sp, ref frameSize,
|
||
slotTypes, slotStructs, addrTagBySlot, addrFldObjBySlot, addrFldTokBySlot, addrByteOffBySlot,
|
||
asm, tokens, hostEntry, tokIdx, ps!, instrStart);
|
||
break;
|
||
}
|
||
|
||
int argc = hostEntry.Binding.ParamCount;
|
||
bool isVoid2 = hostEntry.IsVoid;
|
||
// Dst-slot picker. Three flat paths (all gated on
|
||
// Binding.FastIsFlat — when true the entry's Fast closure writes
|
||
// results to numFrame, so dst must be a numFrame-resident SType):
|
||
// - Vt return: AllocStructSlot, Fast writes struct bytes
|
||
// - primitive (R4/I4) return: AllocSlot of that numeric type, Fast
|
||
// writes the primitive bytes (no float/int box)
|
||
// - everything else: O slot, boxed Fast / Invoke path
|
||
// Flat dst whenever the return type HAS a layout — even on the slow
|
||
// Invoke path the executor unboxes the boxed return straight into the
|
||
// Vt slot (one boundary box), so downstream consumers (field reads,
|
||
// operator args, further calls) stay flat instead of re-boxing.
|
||
bool useFlatVt = hostEntry.ReturnStruct != null;
|
||
SType retSt = SType.O;
|
||
if (!isVoid2 && (hostEntry.ResolvedMethod ?? hostEntry.Binding.Method) is MethodInfo mi5)
|
||
{
|
||
var rt5 = mi5.ReturnType;
|
||
if (rt5 == typeof(float)) retSt = SType.R4;
|
||
else if (rt5 == typeof(int) || rt5 == typeof(bool) || rt5 == typeof(byte)
|
||
|| rt5 == typeof(sbyte) || rt5 == typeof(short) || rt5 == typeof(ushort)) retSt = SType.I4;
|
||
}
|
||
else if (!isVoid2)
|
||
{
|
||
// String-keyed shim (no MethodInfo): type the result slot from the
|
||
// call site's metadata signature. Leaving numeric returns O boxed
|
||
// them, so `a.Length != a.Length` reference-compared two boxes.
|
||
retSt = hostEntry.SigRetSType;
|
||
}
|
||
// Optional explicit override (e.g. AllowBcl AttachFlatFloat sets it to R4
|
||
// because Math.Sqrt is C# (double)→double but we narrow to float at the closure).
|
||
// default(SType) == SType.O means "unset" — leaves the Method-derived retSt alone.
|
||
if (!isVoid2 && hostEntry.Binding.FlatReturnSType != SType.O)
|
||
retSt = hostEntry.Binding.FlatReturnSType;
|
||
// Numeric returns get a NUMERIC dst even on the slow Invoke path — the
|
||
// executor unboxes into it (WrObj), and Entry.Invoke normalizes sub-int
|
||
// returns to int boxes. Leaving them O made downstream arithmetic pick
|
||
// the int fallback over boxed floats: `r.width + r.height` returned 0.
|
||
bool useFlatNum = !useFlatVt && (retSt == SType.R4 || retSt == SType.I4
|
||
|| retSt == SType.I8 || retSt == SType.R8);
|
||
int dst;
|
||
if (isVoid2) dst = -1;
|
||
else if (useFlatVt) dst = AllocStructSlot(ref frameSize, slotTypes, slotStructs, hostEntry.ReturnStruct!);
|
||
else if (useFlatNum) dst = AllocSlot(ref frameSize, slotTypes, slotStructs, retSt, null);
|
||
else dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
// willCallFlat: when true, the executor's Fast closure expects Vt args
|
||
// to be flat (no box_vt boundary). Void calls with FastIsFlat also need
|
||
// this — their closure unboxes args directly from numFrame.
|
||
bool willCallFlat = hostEntry.Binding.FastIsFlat && (useFlatVt || useFlatNum || isVoid2);
|
||
int recvSlotRaw = hostEntry.Binding.HasThis ? (int)evalStack[sp - argc - 1].slot : -1;
|
||
// Resolve receiver address phantoms to the actual value slot:
|
||
// • frame-slot address (addrTagBySlot >= 0): the local slot IS the value
|
||
// • field address (addrTagBySlot == -1): emit ldfld to load the field value
|
||
int recvSlot = recvSlotRaw;
|
||
// Write-back for a mutating value-type instance method called through a field
|
||
// address (`h.P.Scale()`): the receiver is materialised into a boxed slot, the
|
||
// call mutates that box in place, and we must store it back into the field
|
||
// afterwards — otherwise the mutation is lost. Only for value-type receivers.
|
||
bool recvWriteBack = false;
|
||
int wbFldObj = 0, wbFldToki = 0, wbFldOff = 0; bool wbSc = false; SType wbFldSt = SType.O;
|
||
int recvVtOff = 0; // composed byte offset when the receiver address points inside a Vt slot
|
||
if (recvSlotRaw >= 0)
|
||
{
|
||
int addrTag = addrTagBySlot[recvSlotRaw];
|
||
if (addrTag >= 0)
|
||
{
|
||
// Local address → use actual local slot (value is in numFrame or refFrame)
|
||
recvSlot = addrTag;
|
||
recvVtOff = addrByteOffBySlot[recvSlotRaw];
|
||
}
|
||
else if (addrTag == -3)
|
||
{
|
||
// Array-element receiver (`a[i].ToString()`): mirror the script-call
|
||
// resolution — materialize the element. Flat struct elements load
|
||
// into a Vt temp; scalar/ref elements box through ldelem_o (its
|
||
// executor dispatches on the array's runtime type). Previously this
|
||
// tag was unhandled here and the executor read a never-written O
|
||
// phantom slot: null receiver (found by fuzzing).
|
||
int arrS2 = addrFldObjBySlot[recvSlotRaw];
|
||
int idxS2 = addrFldTokBySlot[recvSlotRaw];
|
||
addrTagBySlot[recvSlotRaw] = -2;
|
||
if (addrElemLayoutBySlot[recvSlotRaw] is { } elay)
|
||
{
|
||
int tmp2 = AllocStructSlot(ref frameSize, slotTypes, slotStructs, elay);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, tmp2, arrS2, idxS2, instrStart);
|
||
recvSlot = tmp2;
|
||
// a[i].fK: the receiver is a primitive field at a byte offset
|
||
// inside the materialized element — extracted below.
|
||
recvVtOff = addrByteOffBySlot[recvSlotRaw];
|
||
}
|
||
else
|
||
{
|
||
int tmp2 = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit4(ir, irToIl, Op.ldelem_o, tmp2, arrS2, idxS2, instrStart);
|
||
recvSlot = tmp2;
|
||
}
|
||
}
|
||
else if (addrTag == -1)
|
||
{
|
||
// Field address → emit ldfld to materialise the value into a new slot
|
||
int fldObj = addrFldObjBySlot[recvSlotRaw];
|
||
int fldToki = addrFldTokBySlot[recvSlotRaw];
|
||
int fldTok = tokens[fldToki];
|
||
asm.FieldSTypes.TryGetValue(fldTok, out var fldSt);
|
||
bool isScField = asm.FieldSlots.TryGetValue(fldTok, out var fldFs);
|
||
// A Vt field (a flat HOST struct stored inline in PrimBytes —
|
||
// e.g. Roslyn SPILLS a struct receiver into an iterator
|
||
// state-machine field around a switch expression's branches)
|
||
// needs a layout-carrying struct slot and a byte-range copy;
|
||
// the ldfld_sc_o fallback read RefSlots at a PrimBytes offset
|
||
// and threw IndexOutOfRange (found by fuzzing).
|
||
var fldVtLay = isScField && fldSt == SType.Vt
|
||
? fldFs.Item1.VtFieldLayouts?[fldFs.Item2] : null;
|
||
recvSlot = fldVtLay != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, fldVtLay)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, fldSt, null);
|
||
if (isScField)
|
||
{
|
||
// Prefer the COMPOSED offset when the phantom accumulated one
|
||
// (`o.sf.f1`: sf's base + f1's offset) — recomputing from the
|
||
// token alone gives f1's offset within sf, dropping sf's base
|
||
// and reading the wrong PrimBytes (found by fuzzing).
|
||
int composed = addrFldOffBySlot[recvSlotRaw];
|
||
int fldOff = composed >= 0 ? composed : fldFs.Item1.FieldOffsets[fldFs.Item2];
|
||
var fldOp2 = fldVtLay != null ? Op.ldfld_sc_vt
|
||
: fldSt == SType.I4 ? Op.ldfld_sc_i4 : fldSt == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, fldOp2, recvSlot, fldObj, fldOff, instrStart);
|
||
wbFldOff = fldOff;
|
||
}
|
||
else
|
||
{
|
||
var fldLoadOp = fldSt == SType.I4 ? Op.ldfld_i4 : fldSt == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, fldLoadOp, recvSlot, fldObj, fldToki, instrStart);
|
||
}
|
||
// A mutating instance method on a boxed value-type receiver needs its
|
||
// result written back to the field. Reference-type receivers mutate in
|
||
// place; flat (Vt) receivers take a different path and are left as-is.
|
||
if (fldSt == SType.O && hostEntry.Binding.HasThis
|
||
&& hostEntry.Binding.Method?.DeclaringType?.IsValueType == true)
|
||
{
|
||
recvWriteBack = true; wbFldObj = fldObj; wbFldToki = fldToki; wbSc = isScField; wbFldSt = fldSt;
|
||
}
|
||
}
|
||
}
|
||
|
||
// `constrained. <host enum>` on an I4-slot receiver — an enum
|
||
// local/field calling ToString/GetHashCode/HasFlag. RdObj would box
|
||
// the receiver as INT and ToString would render "1", not the member
|
||
// name; box as the real enum type (the box_enum op covers explicit
|
||
// `box` IL, this is the callvirt-receiver path). Script enums keep
|
||
// the int form — no runtime Type exists.
|
||
if (ctok != 0 && hostEntry.Binding.HasThis
|
||
&& recvSlot >= 0 && recvSlot < slotTypes.Count && slotTypes[recvSlot] == SType.I4
|
||
&& ResolveHostEnumType(asm, ctok) is { } cEnumType)
|
||
{
|
||
int ctokIdx = tokens.Count; tokens.Add(ctok);
|
||
asm.TokenTypes[ctok] = cEnumType;
|
||
int boxedEnumRecv = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit4(ir, irToIl, Op.box_enum, boxedEnumRecv, recvSlot, ctokIdx, instrStart);
|
||
recvSlot = boxedEnumRecv;
|
||
}
|
||
|
||
// Receiver inside a FLAT struct slot with no Vt-aware entry (no
|
||
// ReceiverStruct, no AccessorOffset — typically an inherited virtual
|
||
// like Object.ToString reached via `constrained.`). RdObj on a Vt slot
|
||
// yields null, so the value must be materialized first. Two shapes:
|
||
// • constrained to the slot's OWN struct type (`"impact" + quaternion`
|
||
// → constrained callvirt Object.ToString when the struct overrides
|
||
// it): the receiver is the WHOLE struct — box it (box_vt) and
|
||
// dispatch virtually on the box. Without the constrained-type check
|
||
// the field extraction below misread the struct's first field as
|
||
// the receiver (Quaternion printed as its x component).
|
||
// • constrained to a PRIMITIVE field (`v.f0.ToString()`, or a struct
|
||
// array element's field): extract the primitive at its byte offset
|
||
// into a typed slot (found by fuzzing: ToString returned "").
|
||
if (hostEntry.ReceiverStruct == null && hostEntry.Binding.AccessorOffset < 0
|
||
&& recvSlot >= 0 && recvSlot < slotTypes.Count
|
||
&& slotTypes[recvSlot] == SType.Vt && slotStructs[recvSlot] is { } lay0)
|
||
{
|
||
if (recvVtOff == 0 && ctok != 0
|
||
&& TokenSimpleTypeName(asm, ctok) == lay0.TypeName)
|
||
{
|
||
int boxedRecv = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxedRecv, recvSlot, instrStart);
|
||
recvSlot = boxedRecv;
|
||
}
|
||
else
|
||
{
|
||
foreach (var fld in lay0.Fields.Values)
|
||
{
|
||
if (fld.Offset != recvVtOff || fld.St is not (SType.I4 or SType.R4)) continue;
|
||
int prim = AllocSlot(ref frameSize, slotTypes, slotStructs, fld.St, null);
|
||
Emit4(ir, irToIl, fld.St == SType.I4 ? Op.ldfld_vt_i4 : Op.ldfld_vt_r4,
|
||
prim, recvSlot, recvVtOff, instrStart);
|
||
recvSlot = prim;
|
||
recvVtOff = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
int spBase = sp - argc - (hostEntry.Binding.HasThis ? 1 : 0);
|
||
|
||
// Verified trivial accessor on a FLAT receiver: `r.width` / `r.width = v`
|
||
// becomes a direct byte read/write of the backing field — no host call,
|
||
// no receiver box. (The accessor IL was verified at registration.)
|
||
if (hostEntry.Binding.AccessorOffset >= 0 && recvSlot >= 0
|
||
&& recvSlot < slotTypes.Count && slotTypes[recvSlot] == SType.Vt)
|
||
{
|
||
int accOff = recvVtOff + hostEntry.Binding.AccessorOffset;
|
||
if (argc == 0 && !isVoid2)
|
||
{
|
||
int accDst;
|
||
if (hostEntry.Binding.AccessorSt == SType.Vt)
|
||
{
|
||
// Struct-typed backing field (Bounds.center → Vector3):
|
||
// byte-range copy into a Vt slot sized by the field layout.
|
||
accDst = AllocStructSlot(ref frameSize, slotTypes, slotStructs,
|
||
hostEntry.Binding.AccessorVtLayout!);
|
||
Emit4(ir, irToIl, Op.ldfld_vt_vt, accDst, recvSlot, accOff, instrStart);
|
||
}
|
||
else
|
||
{
|
||
accDst = AllocSlot(ref frameSize, slotTypes, slotStructs, hostEntry.Binding.AccessorSt, null);
|
||
Emit4(ir, irToIl,
|
||
hostEntry.Binding.AccessorSt == SType.I4 ? Op.ldfld_vt_i4 : Op.ldfld_vt_r4,
|
||
accDst, recvSlot, accOff, instrStart);
|
||
}
|
||
sp = spBase;
|
||
evalStack[sp++] = (accDst, hostEntry.Binding.AccessorSt);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
if (argc == 1 && isVoid2)
|
||
{
|
||
int accSrc = evalStack[sp - 1].slot;
|
||
if (hostEntry.Binding.AccessorSt == SType.Vt)
|
||
{
|
||
// Struct value into a struct-typed backing field. A boxed
|
||
// source (O slot from a host return that wasn't flattened)
|
||
// unboxes into a Vt temp first, so stfld_vt_vt always
|
||
// copies real frame bytes at the field layout's size.
|
||
if (evalStack[sp - 1].type != SType.Vt)
|
||
{
|
||
int vtmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs,
|
||
hostEntry.Binding.AccessorVtLayout!);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmp, accSrc, instrStart);
|
||
accSrc = vtmp;
|
||
}
|
||
Emit4(ir, irToIl, Op.stfld_vt_vt, recvSlot, accOff, accSrc, instrStart);
|
||
}
|
||
else
|
||
Emit4(ir, irToIl,
|
||
hostEntry.Binding.AccessorSt == SType.I4 ? Op.stfld_vt_i4 : Op.stfld_vt_r4,
|
||
recvSlot, accOff, accSrc, instrStart);
|
||
sp = spBase;
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Vt receiver addressed at a NONZERO byte offset — a method call on a
|
||
// flat struct nested inside another flat struct (`w.p.Scale(3)` where p
|
||
// sits at offset 4 of w): call_host encodes only a slot, so the executor
|
||
// would box the WRONG bytes (from the slot start). Materialize the
|
||
// sub-struct into its own Vt temp and copy it back after the call (the
|
||
// callee may mutate the receiver).
|
||
bool recvVtWb = false; int recvVtWbBase = -1;
|
||
if (hostEntry.Binding.HasThis && recvSlot >= 0 && recvVtOff != 0
|
||
&& recvSlot < slotTypes.Count && slotTypes[recvSlot] == SType.Vt
|
||
&& hostEntry.ReceiverStruct != null)
|
||
{
|
||
int rTmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, hostEntry.ReceiverStruct);
|
||
Emit4(ir, irToIl, Op.ldfld_vt_vt, rTmp, recvSlot, recvVtOff, instrStart);
|
||
recvVtWb = true; recvVtWbBase = recvSlot;
|
||
recvSlot = rTmp;
|
||
}
|
||
|
||
// The operator-inlining decision must come BEFORE the boundary boxing:
|
||
// EmitInlinedOp synthesizes per-field arithmetic over the FLAT arg
|
||
// slots, so its args must not be boxed away first.
|
||
bool inlineOp = hostEntry.Binding.IsInlineableOp && useFlatVt
|
||
&& hostEntry.ReturnStruct != null
|
||
&& CanInlineOp(hostEntry, argc, hostEntry.ReturnStruct, isCtor: false);
|
||
|
||
// Boundary box: any Vt-slot arg gets converted to a fresh O slot via
|
||
// box_vt before the call — UNLESS we'll dispatch to FastFlat, which
|
||
// reads Vt arg bytes directly from numFrame (or the op inlines).
|
||
int argBase = sp - argc;
|
||
if (!willCallFlat && !inlineOp)
|
||
{
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
if (evalStack[argBase + k].type == SType.Vt)
|
||
{
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, evalStack[argBase + k].slot, instrStart);
|
||
evalStack[argBase + k] = (boxed, SType.O);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pure operator returning Vt: synthesize per-field arithmetic instead
|
||
// of calling through call_host.
|
||
if (inlineOp)
|
||
{
|
||
EmitInlinedOp(ir, irToIl, evalStack, argBase, argc, dst,
|
||
hostEntry.ReturnStruct!, hostEntry, instrStart, isCtor: false);
|
||
sp = spBase;
|
||
evalStack[sp++] = (dst, SType.Vt);
|
||
break;
|
||
}
|
||
|
||
ir.Add((uint)Op.call_host); irToIl.Add(instrStart);
|
||
ir.Add((uint)(isVoid2 ? -1 : dst)); irToIl.Add(instrStart);
|
||
ir.Add((uint)(hostEntry.Binding.HasThis ? recvSlot : -1)); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)argc); irToIl.Add(instrStart);
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
ir.Add((uint)evalStack[argBase + k].slot); irToIl.Add(instrStart);
|
||
}
|
||
// Nested-offset Vt receiver: copy the (possibly mutated) temp back into
|
||
// its byte range of the outer struct.
|
||
if (recvVtWb)
|
||
{
|
||
ir.Add((uint)Op.stfld_vt_vt); irToIl.Add(instrStart);
|
||
ir.Add((uint)recvVtWbBase); irToIl.Add(instrStart);
|
||
ir.Add((uint)recvVtOff); irToIl.Add(instrStart);
|
||
ir.Add((uint)recvSlot); irToIl.Add(instrStart);
|
||
}
|
||
// Mutating value-type method via field address: store the mutated receiver box
|
||
// back into the field (see recvWriteBack above).
|
||
if (recvWriteBack)
|
||
{
|
||
if (wbSc)
|
||
{
|
||
var wbOp = wbFldSt == SType.I4 ? Op.stfld_sc_i4 : wbFldSt == SType.R4 ? Op.stfld_sc_r4 : Op.stfld_sc_o;
|
||
ir.Add((uint)wbOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbFldObj); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbFldOff); irToIl.Add(instrStart);
|
||
ir.Add((uint)recvSlot); irToIl.Add(instrStart);
|
||
}
|
||
else
|
||
{
|
||
var wbOp = wbFldSt == SType.I4 ? Op.stfld_i4 : wbFldSt == SType.R4 ? Op.stfld_r4 : Op.stfld_o;
|
||
ir.Add((uint)wbOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbFldObj); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbFldToki); irToIl.Add(instrStart);
|
||
ir.Add((uint)recvSlot); irToIl.Add(instrStart);
|
||
}
|
||
}
|
||
sp = spBase;
|
||
if (!isVoid2) evalStack[sp++] = (dst, useFlatVt ? SType.Vt : useFlatNum ? retSt : SType.O);
|
||
}
|
||
else if (asm.HostCtors.TryGetValue(tok, out var callCtor))
|
||
{
|
||
// ldloca.s + call hostCtor pattern: create host object, store into local slot
|
||
int callCtorArgc = callCtor.Binding.ParamCount; // explicit params only
|
||
int spBase2 = sp - callCtorArgc - 1; // -1 for the this-addr
|
||
int thisAddrSlot = evalStack[spBase2].slot;
|
||
if (addrTagBySlot[thisAddrSlot] >= 0)
|
||
{
|
||
int destSlot = addrTagBySlot[thisAddrSlot];
|
||
addrTagBySlot[thisAddrSlot] = -2; // consume addr tag
|
||
ir.Add((uint)Op.newobj_host); irToIl.Add(instrStart);
|
||
ir.Add((uint)destSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)callCtorArgc); irToIl.Add(instrStart);
|
||
int argBase2 = sp - callCtorArgc;
|
||
for (int k = 0; k < callCtorArgc; k++)
|
||
{ ir.Add((uint)evalStack[argBase2 + k].slot); irToIl.Add(instrStart); }
|
||
sp = spBase2;
|
||
}
|
||
else
|
||
goto notSupported;
|
||
}
|
||
else
|
||
{
|
||
// unresolved — treat as not-supported so IL path handles it
|
||
goto notSupported;
|
||
}
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
// --- Newobj ---
|
||
case ILOpCode.Ldftn:
|
||
{
|
||
pendingFtnTok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
// Placeholder for the native fn pointer; the delegate newobj pops it.
|
||
evalStack[sp++] = (-1, SType.I4);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldvirtftn:
|
||
{
|
||
pendingFtnTok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
// Pops the dup'd receiver copy, pushes the fn pointer placeholder — the
|
||
// original receiver stays beneath for the delegate newobj.
|
||
evalStack[sp - 1] = (-1, SType.I4);
|
||
break;
|
||
}
|
||
|
||
case ILOpCode.Newobj:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
|
||
if (pendingFtnTok != 0)
|
||
{
|
||
// Delegate construction: `recv|null; ftn; newobj D::.ctor(object, IntPtr)`.
|
||
int ftnTok = pendingFtnTok; pendingFtnTok = 0;
|
||
var delegateType = ResolveCtorParentType(asm, tok);
|
||
if (delegateType == null || !typeof(Delegate).IsAssignableFrom(delegateType))
|
||
throw new NotSupportedException(
|
||
$"IR lowering: delegate construction in '{method.Name}' at IL+0x{instrStart:X4}" +
|
||
$" over an unresolvable delegate type (token 0x{tok:X8})");
|
||
var site = new DelegateSite { DelegateType = delegateType };
|
||
if (asm.ByToken.TryGetValue(ftnTok, out var scriptTarget))
|
||
site.ScriptMethod = scriptTarget;
|
||
else if (asm.HostCalls.TryGetValue(ftnTok, out var hostTarget)
|
||
&& (hostTarget.ResolvedMethod ?? hostTarget.Binding.Method as MethodInfo) is { } mi)
|
||
site.HostMethod = mi;
|
||
else
|
||
{
|
||
asm.TokenNames.TryGetValue(ftnTok, out var ftnName);
|
||
throw new NotSupportedException(
|
||
$"IR lowering: method group '{ftnName ?? $"token 0x{ftnTok:X8}"}' in " +
|
||
$"'{method.Name}' at IL+0x{instrStart:X4} did not resolve to a host or script method");
|
||
}
|
||
(delegateSiteByTokIdx ??= new Dictionary<int, DelegateSite>())[tokIdx] = site;
|
||
|
||
int recvSlot = evalStack[sp - 2].slot;
|
||
if (recvSlot < 0 || addrTagBySlot[recvSlot] != -2)
|
||
goto notSupported; // phantom/placeholder receiver — not a plain O value
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
ir.Add((uint)Op.new_delegate); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)recvSlot); irToIl.Add(instrStart);
|
||
sp -= 2;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
if (asm.CtorToType.TryGetValue(tok, out var newObjDesc))
|
||
{
|
||
// Script-defined type
|
||
int argc = asm.ByToken.TryGetValue(tok, out var ctorM) ? ctorM.ArgCount - 1 : 0;
|
||
if (newObjDesc.FlatLayout != null)
|
||
{
|
||
// FLAT struct `new S(...)` as an expression: zeroed Vt temp +
|
||
// ctor call with `this` = the temp (VtThisWb copies mutations back).
|
||
int vdst = AllocStructSlot(ref frameSize, slotTypes, slotStructs, newObjDesc.FlatLayout);
|
||
Emit2(ir, irToIl, Op.initobj, vdst, instrStart);
|
||
int argBaseF = sp - argc;
|
||
ir.Add((uint)Op.call_script); irToIl.Add(instrStart);
|
||
ir.Add(unchecked((uint)-1)); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)(argc + 1)); irToIl.Add(instrStart);
|
||
ir.Add((uint)vdst); irToIl.Add(instrStart); // `this`
|
||
for (int k = 0; k < argc; k++)
|
||
{ ir.Add((uint)evalStack[argBaseF + k].slot); irToIl.Add(instrStart); }
|
||
sp = argBaseF;
|
||
evalStack[sp++] = (vdst, SType.Vt);
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
ir.Add((uint)Op.newobj_script); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)argc); irToIl.Add(instrStart);
|
||
int argBase = sp - argc;
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
ir.Add((uint)evalStack[argBase + k].slot); irToIl.Add(instrStart);
|
||
}
|
||
sp = argBase;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
}
|
||
else if (asm.HostCtors.TryGetValue(tok, out var hostCtor))
|
||
{
|
||
int argc = hostCtor.Binding.ParamCount;
|
||
// A host ctor whose declaring type is a flat-registered struct and that
|
||
// has a flat-write Fast → allocate dst as Vt and let the executor write
|
||
// the struct bytes directly. Otherwise stay on O.
|
||
// Same slow-path-flat reasoning as call_host's useFlatVt above.
|
||
bool useFlat = hostCtor.ReturnStruct != null;
|
||
int dst = useFlat
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, hostCtor.ReturnStruct!)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
int argBase = sp - argc;
|
||
// Inline the ctor when field order is verified and the dst is flat Vt.
|
||
if (hostCtor.Binding.IsInlineableOp && useFlat && hostCtor.ReturnStruct != null
|
||
&& CanInlineOp(hostCtor, argc, hostCtor.ReturnStruct, isCtor: true))
|
||
{
|
||
EmitInlinedOp(ir, irToIl, evalStack, argBase, argc, dst,
|
||
hostCtor.ReturnStruct, hostCtor, instrStart, isCtor: true);
|
||
sp = argBase;
|
||
evalStack[sp++] = (dst, SType.Vt);
|
||
break;
|
||
}
|
||
ir.Add((uint)Op.newobj_host); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)argc); irToIl.Add(instrStart);
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
ir.Add((uint)evalStack[argBase + k].slot); irToIl.Add(instrStart);
|
||
}
|
||
sp = argBase;
|
||
evalStack[sp++] = (dst, useFlat ? SType.Vt : SType.O);
|
||
}
|
||
else
|
||
{
|
||
goto notSupported;
|
||
}
|
||
ensuredScriptSlots.Clear();
|
||
break;
|
||
}
|
||
|
||
// --- Field access ---
|
||
case ILOpCode.Ldfld:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (obj, _) = evalStack[--sp];
|
||
// Resolve address phantoms to actual object slots
|
||
int actualObj;
|
||
int recvOff = 0; // composed byte offset within a Vt receiver slot
|
||
int heapOff = -1; // composed PrimBytes offset for a heap receiver (-1 = token mode)
|
||
if (addrTagBySlot[obj] >= 0)
|
||
{
|
||
// Frame-slot address (ldloca.s / composed inline chain)
|
||
actualObj = addrTagBySlot[obj];
|
||
recvOff = addrByteOffBySlot[obj];
|
||
}
|
||
else if (addrTagBySlot[obj] == -1 && addrFldOffBySlot[obj] >= 0)
|
||
{
|
||
// Composed inline address on a heap ScriptObject: parent + byte offset.
|
||
actualObj = addrFldObjBySlot[obj];
|
||
heapOff = addrFldOffBySlot[obj];
|
||
}
|
||
else if (addrTagBySlot[obj] == -1)
|
||
{
|
||
// Field-address phantom (ldflda): materialise the intermediate field first
|
||
int midFldObj = addrFldObjBySlot[obj];
|
||
int midFldToki = addrFldTokBySlot[obj];
|
||
int midFldTok = tokens[midFldToki];
|
||
asm.FieldSTypes.TryGetValue(midFldTok, out var midFst);
|
||
if (midFst == SType.Vt && asm.FieldSlots.TryGetValue(midFldTok, out var midFsVt))
|
||
{
|
||
// Vt field on a script class — allocate a Vt slot and emit ldfld_sc_vt.
|
||
var midVtLay = midFsVt.Item1.VtFieldLayouts?[midFsVt.Item2];
|
||
int midSlotVt = midVtLay != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, midVtLay)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
int midOffVt = midFsVt.Item1.FieldOffsets[midFsVt.Item2];
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, midSlotVt, midFldObj, midOffVt, instrStart);
|
||
actualObj = midSlotVt;
|
||
}
|
||
else
|
||
{
|
||
int midSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, midFst, null);
|
||
if (asm.FieldSlots.TryGetValue(midFldTok, out var midFs))
|
||
{
|
||
int midOff = midFs.Item1.FieldOffsets[midFs.Item2];
|
||
var midOp2 = midFst == SType.I4 ? Op.ldfld_sc_i4 : midFst == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, midOp2, midSlot, midFldObj, midOff, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var midOp = midFst == SType.I4 ? Op.ldfld_i4 : midFst == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, midOp, midSlot, midFldObj, midFldToki, instrStart);
|
||
}
|
||
actualObj = midSlot;
|
||
}
|
||
}
|
||
else if (addrTagBySlot[obj] == -3 && addrElemLayoutBySlot[obj] != null)
|
||
{
|
||
// Flat array element: memcpy it into a Vt temp — the read flows through
|
||
// the Vt-receiver field branch below (no write-back needed for loads).
|
||
int arrSlot = addrFldObjBySlot[obj];
|
||
int idxSlot = addrFldTokBySlot[obj];
|
||
int midVt = AllocStructSlot(ref frameSize, slotTypes, slotStructs, addrElemLayoutBySlot[obj]!);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, midVt, arrSlot, idxSlot, instrStart);
|
||
actualObj = midVt;
|
||
recvOff = addrByteOffBySlot[obj];
|
||
}
|
||
else if (addrTagBySlot[obj] == -3)
|
||
{
|
||
// Array-element address (ldelema): materialise the element via ldelem.
|
||
int arrSlot = addrFldObjBySlot[obj];
|
||
int idxSlot = addrFldTokBySlot[obj];
|
||
int midSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit4(ir, irToIl, Op.ldelem_o, midSlot, arrSlot, idxSlot, instrStart);
|
||
actualObj = midSlot;
|
||
}
|
||
else
|
||
{
|
||
actualObj = obj;
|
||
}
|
||
// HOST-struct field read through a composed heap offset: the flat struct
|
||
// lives inline in a ScriptObject's PrimBytes — e.g. a Vector3 hoisted into
|
||
// an iterator state-machine field (`target.y` after a yield). Without this
|
||
// the read fell through to the boxed host-field path with the ScriptObject
|
||
// as receiver ("Field y ... is not a field on ScriptObject").
|
||
if (heapOff >= 0 && asm.HostFields.TryGetValue(tok, out var hfheap)
|
||
&& hfheap.DeclaringStruct != null)
|
||
{
|
||
if (hfheap.PrimitiveSt == SType.I4 || hfheap.PrimitiveSt == SType.R4)
|
||
{
|
||
// Primitive field: materialize the declaring struct into a Vt temp
|
||
// so the flat fast path below applies (widening kinds included).
|
||
int midVtH = AllocStructSlot(ref frameSize, slotTypes, slotStructs, hfheap.DeclaringStruct);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, midVtH, actualObj, heapOff, instrStart);
|
||
actualObj = midVtH;
|
||
recvOff = 0;
|
||
heapOff = -1;
|
||
}
|
||
else if (hfheap.FieldTypeName != null && asm.HostSurface != null
|
||
&& asm.HostSurface.TryGetStructLayout(hfheap.FieldTypeName, out var nLayH)
|
||
&& nLayH != null)
|
||
{
|
||
// NESTED struct field (`pose.P` value read): copy its byte range
|
||
// straight out at the summed offset.
|
||
int dstNH = AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLayH);
|
||
evalStack[sp++] = (dstNH, SType.Vt);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, dstNH, actualObj, heapOff + hfheap.ByteOffset, instrStart);
|
||
break;
|
||
}
|
||
}
|
||
// Flat-struct fast path: receiver is a Vt slot AND the field is a registered
|
||
// primitive on that struct → emit ldfld_vt_* with the byte offset, bypassing
|
||
// the boxed Get delegate entirely.
|
||
if (heapOff < 0 && slotTypes[actualObj] == SType.Vt
|
||
&& asm.HostFields.TryGetValue(tok, out var hfvt)
|
||
&& hfvt.DeclaringStruct != null
|
||
&& (hfvt.PrimitiveSt == SType.I4 || hfvt.PrimitiveSt == SType.R4))
|
||
{
|
||
int dstv = AllocSlot(ref frameSize, slotTypes, slotStructs, hfvt.PrimitiveSt, null);
|
||
evalStack[sp++] = (dstv, hfvt.PrimitiveSt);
|
||
Emit4(ir, irToIl, LdFldVtOpFor(hfvt), dstv, actualObj, recvOff + hfvt.ByteOffset, instrStart);
|
||
break;
|
||
}
|
||
// Flat script-struct receiver (Vt frame slot): field reads are direct
|
||
// byte-offset loads; nested blittable struct fields memcpy a sub-range.
|
||
if (heapOff < 0 && slotTypes[actualObj] == SType.Vt
|
||
&& asm.FieldSlots.TryGetValue(tok, out var vfs))
|
||
{
|
||
var vfst = vfs.Item1.FieldTypes[vfs.Item2];
|
||
int vOff = recvOff + vfs.Item1.FieldOffsets[vfs.Item2];
|
||
if (vfst == SType.Vt)
|
||
{
|
||
var nLay = vfs.Item1.VtFieldLayouts?[vfs.Item2];
|
||
int dstn = nLay != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLay)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dstn, SType.Vt);
|
||
Emit4(ir, irToIl, Op.ldfld_vt_vt, dstn, actualObj, vOff, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var pst = vfst == SType.R4 ? SType.R4 : SType.I4;
|
||
int dstn = AllocSlot(ref frameSize, slotTypes, slotStructs, pst, null);
|
||
evalStack[sp++] = (dstn, pst);
|
||
Emit4(ir, irToIl, pst == SType.I4 ? Op.ldfld_vt_i4 : Op.ldfld_vt_r4,
|
||
dstn, actualObj, vOff, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
// Heap receiver with a COMPOSED inline offset (nested blittable struct
|
||
// inside a heap ScriptObject): direct PrimBytes access at the summed offset.
|
||
if (heapOff >= 0 && asm.FieldSlots.TryGetValue(tok, out var cfs))
|
||
{
|
||
var cfst = cfs.Item1.FieldTypes[cfs.Item2];
|
||
int cOff = heapOff + cfs.Item1.FieldOffsets[cfs.Item2];
|
||
if (cfst == SType.Vt)
|
||
{
|
||
var nLay = cfs.Item1.VtFieldLayouts?[cfs.Item2];
|
||
int dstn = nLay != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLay)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dstn, SType.Vt);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, dstn, actualObj, cOff, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var pst = cfst == SType.R4 ? SType.R4 : SType.I4;
|
||
int dstn = AllocSlot(ref frameSize, slotTypes, slotStructs, pst, null);
|
||
evalStack[sp++] = (dstn, pst);
|
||
Emit4(ir, irToIl, pst == SType.I4 ? Op.ldfld_sc_i4 : Op.ldfld_sc_r4,
|
||
dstn, actualObj, cOff, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
asm.FieldSTypes.TryGetValue(tok, out var fst);
|
||
if (asm.FieldSlots.TryGetValue(tok, out var fsLo))
|
||
{
|
||
int off = fsLo.Item1.FieldOffsets[fsLo.Item2];
|
||
if (fst == SType.Vt)
|
||
{
|
||
// Host-struct Vt field stored inline in PrimBytes; copy bytes out.
|
||
var vtLay = fsLo.Item1.VtFieldLayouts?[fsLo.Item2];
|
||
int dst2 = vtLay != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, vtLay)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst2, fst);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, dst2, actualObj, off, instrStart);
|
||
}
|
||
else
|
||
{
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, fst, null);
|
||
evalStack[sp++] = (dst2, fst);
|
||
var op2 = fst == SType.I4 ? Op.ldfld_sc_i4 : fst == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, op2, dst2, actualObj, off, instrStart);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, fst, null);
|
||
evalStack[sp++] = (dst2, fst);
|
||
var ldfldOp = fst == SType.I4 ? Op.ldfld_i4 : fst == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, ldfldOp, dst2, actualObj, tokIdx, instrStart);
|
||
}
|
||
// Value-load of a script-struct field: clone so the pushed value is an
|
||
// independent copy (struct copy semantics). Field mutation uses ldflda, which
|
||
// takes a different path and is unaffected.
|
||
if (fst == SType.O && asm.FieldIsScriptStruct.Contains(tok))
|
||
{
|
||
var (fldSlot, _) = evalStack[sp - 1];
|
||
int cl = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.clone_sc, cl, fldSlot, instrStart);
|
||
evalStack[sp - 1] = (cl, SType.O);
|
||
}
|
||
break;
|
||
}
|
||
case ILOpCode.Ldflda:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (obj, _) = evalStack[--sp];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null); // phantom slot
|
||
asm.FieldSTypes.TryGetValue(tok, out var fldaFst);
|
||
bool isScFldA = asm.FieldSlots.TryGetValue(tok, out var fldaFs);
|
||
int inTag = addrTagBySlot[obj];
|
||
|
||
// Address of a field INSIDE flat struct bytes: accumulate the byte offset
|
||
// instead of materializing — this is what makes nested chains like
|
||
// `a.inner.x = v` (ldloca a; ldflda inner; stfld x) work. HOST flat-struct
|
||
// fields (Pose.P where Pose is a registered flat struct) compose the same
|
||
// way via the FieldEntry byte offset — without this a chain rooted at a
|
||
// hoisted iterator field fell to the token-mode phantom and consumers
|
||
// reflected on a null or ScriptObject receiver.
|
||
HostBinding.FieldEntry? hfA = null;
|
||
bool isHostFlatA = !isScFldA
|
||
&& asm.HostFields.TryGetValue(tok, out hfA) && hfA!.DeclaringStruct != null;
|
||
if ((isScFldA && (fldaFst == SType.Vt || fldaFst == SType.I4 || fldaFst == SType.R4))
|
||
|| isHostFlatA)
|
||
{
|
||
int fOff = isScFldA
|
||
? fldaFs.Item1.FieldOffsets[fldaFs.Item2]
|
||
: hfA!.ByteOffset;
|
||
// "Is the addressed field itself a struct?" — gates the heap-receiver
|
||
// branch below. For host fields, anything non-primitive is a nested struct.
|
||
bool fldaIsVt = isScFldA
|
||
? fldaFst == SType.Vt
|
||
: hfA!.PrimitiveSt != SType.I4 && hfA.PrimitiveSt != SType.R4;
|
||
if (inTag >= 0 && slotTypes[inTag] == SType.Vt)
|
||
{
|
||
// base = frame Vt slot (+ prior offset)
|
||
addrTagBySlot[dst] = inTag;
|
||
addrByteOffBySlot[dst] = addrByteOffBySlot[obj] + fOff;
|
||
addrFldTokBySlot[dst] = tokIdx;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
if (inTag == -1 && addrFldOffBySlot[obj] >= 0)
|
||
{
|
||
// base = heap parent + prior PrimBytes offset
|
||
addrTagBySlot[dst] = -1;
|
||
addrFldObjBySlot[dst] = addrFldObjBySlot[obj];
|
||
addrFldOffBySlot[dst] = addrFldOffBySlot[obj] + fOff;
|
||
addrFldTokBySlot[dst] = tokIdx;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
if (inTag == -1 && addrFldOffBySlot[obj] < 0)
|
||
{
|
||
// base = token-addressed field phantom. Composition is only valid
|
||
// when the OUTER field is itself inline (sc-Vt); resolve its offset.
|
||
int outTok = tokens[addrFldTokBySlot[obj]];
|
||
asm.FieldSTypes.TryGetValue(outTok, out var outFst);
|
||
if (outFst == SType.Vt && asm.FieldSlots.TryGetValue(outTok, out var outFs))
|
||
{
|
||
addrTagBySlot[dst] = -1;
|
||
addrFldObjBySlot[dst] = addrFldObjBySlot[obj];
|
||
addrFldOffBySlot[dst] = outFs.Item1.FieldOffsets[outFs.Item2] + fOff;
|
||
addrFldTokBySlot[dst] = tokIdx;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
}
|
||
if (inTag == -2 && slotTypes[obj] == SType.Vt)
|
||
{
|
||
// base = a Vt VALUE slot on the stack (r-value receiver)
|
||
addrTagBySlot[dst] = obj;
|
||
addrByteOffBySlot[dst] = fOff;
|
||
addrFldTokBySlot[dst] = tokIdx;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
if (inTag == -3 && addrElemLayoutBySlot[obj] != null)
|
||
{
|
||
// Address of a field inside a FLAT ARRAY ELEMENT: keep the element
|
||
// address, accumulate the byte offset (consumers materialize the
|
||
// element and write it back).
|
||
addrTagBySlot[dst] = -3;
|
||
addrFldObjBySlot[dst] = addrFldObjBySlot[obj];
|
||
addrFldTokBySlot[dst] = addrFldTokBySlot[obj];
|
||
addrElemLayoutBySlot[dst] = addrElemLayoutBySlot[obj];
|
||
addrByteOffBySlot[dst] = addrByteOffBySlot[obj] + fOff;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
if (fldaIsVt)
|
||
{
|
||
// Address of an INLINE (Vt) struct field on a HEAP receiver (class
|
||
// instance or heap-struct local): offset-mode phantom rooted at the
|
||
// OBJECT slot, so consumers read/write PrimBytes in place —
|
||
// materializing would copy and lose mutations (h.pos.x = 3).
|
||
int baseObj = inTag >= 0 ? inTag : obj;
|
||
addrTagBySlot[dst] = -1;
|
||
addrFldObjBySlot[dst] = baseObj;
|
||
addrFldOffBySlot[dst] = fOff;
|
||
addrFldTokBySlot[dst] = tokIdx;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Classic path: address of a field on a (heap) object — resolve ldloca
|
||
// bases to the actual slot; consumers materialize via the token.
|
||
int actualObj = inTag >= 0 ? inTag : obj;
|
||
addrTagBySlot[dst] = -1; // field address
|
||
addrFldObjBySlot[dst] = actualObj;
|
||
addrFldTokBySlot[dst] = tokIdx;
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
|
||
case ILOpCode.Stfld:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (src, srcT) = evalStack[--sp];
|
||
var (obj, _) = evalStack[--sp];
|
||
int tag = addrTagBySlot[obj];
|
||
int recvOffS = tag >= 0 ? addrByteOffBySlot[obj] : 0;
|
||
int heapOffS = tag == -1 ? addrFldOffBySlot[obj] : -1;
|
||
// If obj is a frame-slot addr (from ldloca.s), use the actual local slot as receiver.
|
||
// If obj is a field-address phantom (ldflda on a struct class-field), materialise the
|
||
// field into a temp slot. ScriptObject is a reference type, so writing through the
|
||
// reference propagates to the parent's field slot automatically — no write-back needed.
|
||
// If obj is an array-element phantom (ldelema on a struct array), materialise the
|
||
// element the same way.
|
||
int actualObj;
|
||
if (tag >= 0)
|
||
{
|
||
actualObj = tag; // ldloca.s → resolve to actual local
|
||
}
|
||
else if (tag == -1 && heapOffS >= 0)
|
||
{
|
||
// Composed inline address on a heap ScriptObject: use the parent directly.
|
||
actualObj = addrFldObjBySlot[obj];
|
||
}
|
||
else if (tag == -1)
|
||
{
|
||
// ldflda phantom: load the struct reference stored in the parent class field
|
||
int midFldObj = addrFldObjBySlot[obj];
|
||
int midFldToki = addrFldTokBySlot[obj];
|
||
int midTok = tokens[midFldToki];
|
||
asm.FieldSTypes.TryGetValue(midTok, out var midFst);
|
||
if (midFst == SType.Vt && asm.FieldSlots.TryGetValue(midTok, out var midFs2Vt))
|
||
{
|
||
// Vt field on a script class — allocate Vt slot and emit ldfld_sc_vt.
|
||
var midVtLay2 = midFs2Vt.Item1.VtFieldLayouts?[midFs2Vt.Item2];
|
||
int midSlotVt2 = midVtLay2 != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, midVtLay2)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
int midOff2Vt = midFs2Vt.Item1.FieldOffsets[midFs2Vt.Item2];
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, midSlotVt2, midFldObj, midOff2Vt, instrStart);
|
||
actualObj = midSlotVt2;
|
||
}
|
||
else
|
||
{
|
||
int midSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, midFst, null);
|
||
if (asm.FieldSlots.TryGetValue(midTok, out var midFs2))
|
||
{
|
||
int midOff2 = midFs2.Item1.FieldOffsets[midFs2.Item2];
|
||
var midOp2 = midFst == SType.I4 ? Op.ldfld_sc_i4 : midFst == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, midOp2, midSlot, midFldObj, midOff2, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var midOp = midFst == SType.I4 ? Op.ldfld_i4 : midFst == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, midOp, midSlot, midFldObj, midFldToki, instrStart);
|
||
}
|
||
actualObj = midSlot;
|
||
}
|
||
}
|
||
else if (tag == -3 && addrElemLayoutBySlot[obj] != null)
|
||
{
|
||
// Flat array element write (`arr[i].f = v`): materialize → mutate the
|
||
// Vt temp → memcpy the whole element back. Handled inline because the
|
||
// write-back must follow the field store.
|
||
int arrSlotW = addrFldObjBySlot[obj];
|
||
int idxSlotW = addrFldTokBySlot[obj];
|
||
var elemLayW = addrElemLayoutBySlot[obj]!;
|
||
int elemTmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, elemLayW);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, elemTmp, arrSlotW, idxSlotW, instrStart);
|
||
int baseOffW = addrByteOffBySlot[obj];
|
||
// Host-struct element field (arr[i].x on Vector3[]): FieldEntry byte offset.
|
||
if (asm.HostFields.TryGetValue(tok, out var whf) && whf.DeclaringStruct != null
|
||
&& (whf.PrimitiveSt == SType.I4 || whf.PrimitiveSt == SType.R4))
|
||
{
|
||
Emit4(ir, irToIl, StFldVtOpFor(whf),
|
||
elemTmp, baseOffW + whf.ByteOffset, src, instrStart);
|
||
Emit4(ir, irToIl, Op.stelem_vt, arrSlotW, idxSlotW, elemTmp, instrStart);
|
||
break;
|
||
}
|
||
if (asm.FieldSlots.TryGetValue(tok, out var wfs))
|
||
{
|
||
var wfst = wfs.Item1.FieldTypes[wfs.Item2];
|
||
int wOff = baseOffW + wfs.Item1.FieldOffsets[wfs.Item2];
|
||
if (wfst == SType.Vt)
|
||
{
|
||
if (srcT != SType.Vt)
|
||
{
|
||
var nLayW = wfs.Item1.VtFieldLayouts?[wfs.Item2];
|
||
if (nLayW != null)
|
||
{
|
||
int vtmpW = AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLayW);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmpW, src, instrStart);
|
||
src = vtmpW;
|
||
}
|
||
}
|
||
Emit4(ir, irToIl, Op.stfld_vt_vt, elemTmp, wOff, src, instrStart);
|
||
}
|
||
else
|
||
{
|
||
Emit4(ir, irToIl, wfst == SType.R4 ? Op.stfld_vt_r4 : Op.stfld_vt_i4,
|
||
elemTmp, wOff, src, instrStart);
|
||
}
|
||
}
|
||
Emit4(ir, irToIl, Op.stelem_vt, arrSlotW, idxSlotW, elemTmp, instrStart);
|
||
break;
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
// ldelema phantom: load the struct reference stored in the array element
|
||
int arrSlot = addrFldObjBySlot[obj];
|
||
int idxSlot = addrFldTokBySlot[obj];
|
||
int midSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit4(ir, irToIl, Op.ldelem_o, midSlot, arrSlot, idxSlot, instrStart);
|
||
actualObj = midSlot;
|
||
}
|
||
else
|
||
{
|
||
actualObj = obj;
|
||
}
|
||
// Auto-init: if this stfld writes into an O slot (script-defined struct local
|
||
// addressed via ldloca.s), emit ensure_script before the first field write.
|
||
// Roslyn omits initobj when the struct is "definitely assigned" by consecutive
|
||
// field stores, but our runtime O slot starts null — so the first stfld on
|
||
// any execution path would NRE without this guard.
|
||
// Optimization: skip if the slot is already known initialized in this basic block
|
||
// (tracked in ensuredScriptSlots). Cleared at block boundaries and after calls/branches.
|
||
// Same for a BOXED host struct local (`S s; s.x = 1; s.y = 2;` with no
|
||
// initobj): the field's declaring TypeRef token has a synthetic default
|
||
// ctor (see the parse-time hostCtors registration), and ensure_script
|
||
// creates the box through it when the slot is still null.
|
||
if (tag >= 0 && slotTypes[actualObj] == SType.O
|
||
&& (asm.FieldToTypeDef.TryGetValue(tok, out int fieldTypeDef)
|
||
|| (asm.HostFieldDeclTypeTok.TryGetValue(tok, out fieldTypeDef)
|
||
&& asm.HostCtors.ContainsKey(fieldTypeDef))))
|
||
{
|
||
if (!ensuredScriptSlots.Contains(actualObj))
|
||
{
|
||
int typeDefTokIdx2 = tokens.Count; tokens.Add(fieldTypeDef);
|
||
ir.Add((uint)Op.ensure_script); irToIl.Add(instrStart);
|
||
ir.Add((uint)actualObj); irToIl.Add(instrStart);
|
||
ir.Add((uint)typeDefTokIdx2); irToIl.Add(instrStart);
|
||
}
|
||
ensuredScriptSlots.Add(actualObj);
|
||
}
|
||
// HOST-struct field write through a composed heap offset (the mirror of the
|
||
// Ldfld handling above — a flat host struct inlined in a ScriptObject's
|
||
// PrimBytes, e.g. `target.y = v` on a hoisted iterator local).
|
||
if (heapOffS >= 0 && asm.HostFields.TryGetValue(tok, out var hfheapS)
|
||
&& hfheapS.DeclaringStruct != null)
|
||
{
|
||
if (hfheapS.PrimitiveSt == SType.I4 || hfheapS.PrimitiveSt == SType.R4)
|
||
{
|
||
// Primitive field: the temp is a copy, so read-modify-WRITE-BACK —
|
||
// copy the struct out, store the field into the temp (widening/
|
||
// truncating kinds included), copy the whole struct back.
|
||
int midVtS = AllocStructSlot(ref frameSize, slotTypes, slotStructs, hfheapS.DeclaringStruct);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, midVtS, actualObj, heapOffS, instrStart);
|
||
Emit4(ir, irToIl, StFldVtOpFor(hfheapS), midVtS, hfheapS.ByteOffset, src, instrStart);
|
||
Emit4(ir, irToIl, Op.stfld_sc_vt, actualObj, heapOffS, midVtS, instrStart);
|
||
break;
|
||
}
|
||
if (srcT == SType.Vt)
|
||
{
|
||
// NESTED struct field (`pose.P = v`): write the source's byte range
|
||
// (stfld_sc_vt sizes from the SRC slot's layout) at the summed offset.
|
||
Emit4(ir, irToIl, Op.stfld_sc_vt, actualObj, heapOffS + hfheapS.ByteOffset, src, instrStart);
|
||
break;
|
||
}
|
||
}
|
||
// Flat-struct fast path: stfld of a primitive into a Vt slot.
|
||
if (heapOffS < 0 && slotTypes[actualObj] == SType.Vt
|
||
&& asm.HostFields.TryGetValue(tok, out var hfvtS)
|
||
&& hfvtS.DeclaringStruct != null
|
||
&& (hfvtS.PrimitiveSt == SType.I4 || hfvtS.PrimitiveSt == SType.R4))
|
||
{
|
||
Emit4(ir, irToIl, StFldVtOpFor(hfvtS), actualObj, recvOffS + hfvtS.ByteOffset, src, instrStart);
|
||
break;
|
||
}
|
||
// Flat script-struct receiver (Vt frame slot).
|
||
if (heapOffS < 0 && slotTypes[actualObj] == SType.Vt
|
||
&& asm.FieldSlots.TryGetValue(tok, out var vfsS))
|
||
{
|
||
var vfst = vfsS.Item1.FieldTypes[vfsS.Item2];
|
||
int vOff = recvOffS + vfsS.Item1.FieldOffsets[vfsS.Item2];
|
||
if (vfst == SType.Vt)
|
||
{
|
||
if (srcT != SType.Vt)
|
||
{
|
||
var nLay = vfsS.Item1.VtFieldLayouts?[vfsS.Item2];
|
||
if (nLay != null)
|
||
{
|
||
int vtmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLay);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmp, src, instrStart);
|
||
src = vtmp;
|
||
}
|
||
}
|
||
Emit4(ir, irToIl, Op.stfld_vt_vt, actualObj, vOff, src, instrStart);
|
||
}
|
||
else
|
||
{
|
||
Emit4(ir, irToIl, vfst == SType.R4 ? Op.stfld_vt_r4 : Op.stfld_vt_i4,
|
||
actualObj, vOff, src, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
// Heap receiver with a COMPOSED inline offset.
|
||
if (heapOffS >= 0 && asm.FieldSlots.TryGetValue(tok, out var cfsS))
|
||
{
|
||
var cfst = cfsS.Item1.FieldTypes[cfsS.Item2];
|
||
int cOff = heapOffS + cfsS.Item1.FieldOffsets[cfsS.Item2];
|
||
if (cfst == SType.Vt)
|
||
{
|
||
if (srcT != SType.Vt)
|
||
{
|
||
var nLay = cfsS.Item1.VtFieldLayouts?[cfsS.Item2];
|
||
if (nLay != null)
|
||
{
|
||
int vtmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLay);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmp, src, instrStart);
|
||
src = vtmp;
|
||
}
|
||
}
|
||
Emit4(ir, irToIl, Op.stfld_sc_vt, actualObj, cOff, src, instrStart);
|
||
}
|
||
else
|
||
{
|
||
Emit4(ir, irToIl, cfst == SType.R4 ? Op.stfld_sc_r4 : Op.stfld_sc_i4,
|
||
actualObj, cOff, src, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
asm.FieldSTypes.TryGetValue(tok, out var sfSType);
|
||
if (asm.FieldSlots.TryGetValue(tok, out var sfScFs))
|
||
{
|
||
int sfOff = sfScFs.Item1.FieldOffsets[sfScFs.Item2];
|
||
// Vt-typed destination field (inline in PrimBytes): emit stfld_sc_vt.
|
||
if (sfSType == SType.Vt)
|
||
{
|
||
// The runtime stfld_sc_vt copies the struct bytes out of the source's
|
||
// Vt frame slot. If the value instead arrived boxed in an O slot (e.g.
|
||
// a host call/ctor whose return wasn't flattened — common when a struct
|
||
// type is registered after its consumers), unbox it into a Vt temp using
|
||
// the field's layout first, so the store always reads real frame bytes.
|
||
if (srcT != SType.Vt)
|
||
{
|
||
var fieldLay = sfScFs.Item1.VtFieldLayouts?[sfScFs.Item2];
|
||
if (fieldLay != null)
|
||
{
|
||
int vtmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, fieldLay);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmp, src, instrStart);
|
||
src = vtmp;
|
||
}
|
||
}
|
||
ir.Add((uint)Op.stfld_sc_vt); irToIl.Add(instrStart);
|
||
ir.Add((uint)actualObj); irToIl.Add(instrStart);
|
||
ir.Add((uint)sfOff); irToIl.Add(instrStart);
|
||
ir.Add((uint)src); irToIl.Add(instrStart);
|
||
break;
|
||
}
|
||
// Vt source into a non-Vt receiver field (script struct field boxed in
|
||
// RefSlots): box it first.
|
||
if (srcT == SType.Vt)
|
||
{
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, src, instrStart);
|
||
src = boxed;
|
||
}
|
||
var scOp = sfSType == SType.I4 ? Op.stfld_sc_i4 : sfSType == SType.R4 ? Op.stfld_sc_r4 : Op.stfld_sc_o;
|
||
ir.Add((uint)scOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)actualObj); irToIl.Add(instrStart);
|
||
ir.Add((uint)sfOff); irToIl.Add(instrStart);
|
||
ir.Add((uint)src); irToIl.Add(instrStart);
|
||
}
|
||
else
|
||
{
|
||
// Non-script-class field (host field). If src is Vt, box it first.
|
||
if (srcT == SType.Vt)
|
||
{
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, src, instrStart);
|
||
src = boxed;
|
||
}
|
||
var sfldOp = sfSType == SType.I4 ? Op.stfld_i4 : sfSType == SType.R4 ? Op.stfld_r4 : Op.stfld_o;
|
||
ir.Add((uint)sfldOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)actualObj); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)src); irToIl.Add(instrStart);
|
||
}
|
||
break;
|
||
}
|
||
case ILOpCode.Ldsfld:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
// Host STRUCT field (the Vector3.one shape): unbox the Get() result into
|
||
// a flat Vt slot. The O-slot form handed a BOXED struct to flat consumers
|
||
// (inlined operators, Vt args, component reads), which read the numeric
|
||
// frame and saw zeros (found by fuzzing: HVec.One + v summed only v).
|
||
if (asm.HostFields.TryGetValue(tok, out var sfe) && sfe.FieldTypeName != null
|
||
&& asm.HostSurface != null
|
||
&& asm.HostSurface.TryGetStructLayout(sfe.FieldTypeName, out var sLay) && sLay != null)
|
||
{
|
||
int dstv = AllocStructSlot(ref frameSize, slotTypes, slotStructs, sLay);
|
||
evalStack[sp++] = (dstv, SType.Vt);
|
||
Emit3(ir, irToIl, Op.ldsfld_struct, dstv, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
// SCRIPT static of primitive type: load into a typed numeric slot, not O.
|
||
// The boxed O form broke every consumer that keys off the slot type —
|
||
// `if (s_hp == 0 && …)` compiles to brtrue on the load, and brtrue_o
|
||
// tested the BOX for null (boxed 0 is non-null → wrong branch).
|
||
if (ScriptStaticPrimitiveSType(asm, tok) is { } sst)
|
||
{
|
||
int dstn = AllocSlot(ref frameSize, slotTypes, slotStructs, sst, null);
|
||
evalStack[sp++] = (dstn, sst);
|
||
Emit3(ir, irToIl, sst == SType.I4 ? Op.ldsfld_i4 : Op.ldsfld_r4,
|
||
dstn, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst, SType.O);
|
||
Emit3(ir, irToIl, Op.ldsfld_o, dst, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldsflda:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
// Address of a static field. C# emits this for member calls THROUGH the
|
||
// address — `str + s_count` becomes `ldsflda s_count; call int.ToString()`.
|
||
// For IMMUTABLE field types (primitives, enums, string) the address is
|
||
// equivalent to the value. Script statics load into TYPED slots like
|
||
// Ldsfld does (an enum static then hits the constrained-enum boxing on
|
||
// the call); host prims/strings load boxed. Mutable struct statics keep
|
||
// failing — a value copy would silently drop their mutations.
|
||
if (ScriptStaticPrimitiveSType(asm, tok) is { } ast)
|
||
{
|
||
int tdst = AllocSlot(ref frameSize, slotTypes, slotStructs, ast, null);
|
||
evalStack[sp++] = (tdst, ast);
|
||
Emit3(ir, irToIl, ast == SType.I4 ? Op.ldsfld_i4 : Op.ldsfld_r4,
|
||
tdst, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
if (!StaticFieldIsImmutableValue(asm, tok))
|
||
goto notSupported;
|
||
int adst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (adst, SType.O);
|
||
Emit3(ir, irToIl, Op.ldsfld_o, adst, tokIdx, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Stsfld:
|
||
{
|
||
int tok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tokIdx = tokens.Count; tokens.Add(tok);
|
||
var (src, srcT) = evalStack[--sp];
|
||
// Primitive script static: emit an unboxed typed store (mirrors Ldsfld). The old
|
||
// unconditional stsfld_o boxed every primitive static write.
|
||
if (srcT != SType.Vt && ScriptStaticPrimitiveSType(asm, tok) is { } sst)
|
||
{
|
||
Emit3(ir, irToIl, sst == SType.I4 ? Op.stsfld_i4 : Op.stsfld_r4, tokIdx, src, instrStart);
|
||
break;
|
||
}
|
||
// Host static fields take a boxed value (FieldEntry.Set); box a flat struct first.
|
||
if (srcT == SType.Vt)
|
||
{
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, src, instrStart);
|
||
src = boxed;
|
||
}
|
||
Emit3(ir, irToIl, Op.stsfld_o, tokIdx, src, instrStart);
|
||
break;
|
||
}
|
||
// --- Arrays ---
|
||
case ILOpCode.Newarr:
|
||
{
|
||
int elemTok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int elemTokIdx = tokens.Count; tokens.Add(elemTok);
|
||
var (lenSlot, _) = evalStack[--sp];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst, SType.O);
|
||
|
||
// Typed backing when the element type is statically int/float: the executor
|
||
// allocates int[]/float[] and element accesses skip boxing entirely.
|
||
// Flat script-struct elements get a ScriptVtArray (single byte[] backing,
|
||
// no per-element ScriptObject). Host flat structs (Vector3[]) still use the
|
||
// boxed object?[] path — the lowerer has no binding access here; follow-up.
|
||
var arrOp = Op.newarr;
|
||
var elemHandle2 = MetadataTokens.EntityHandle(elemTok);
|
||
if (elemHandle2.Kind == HandleKind.TypeReference)
|
||
{
|
||
var elemRef2 = asm.Reader.GetTypeReference((TypeReferenceHandle)elemHandle2);
|
||
if (asm.Reader.GetString(elemRef2.Namespace) == "System")
|
||
{
|
||
var elemName2 = asm.Reader.GetString(elemRef2.Name);
|
||
if (elemName2 == "Int32") arrOp = Op.newarr_i4;
|
||
else if (elemName2 == "Single") arrOp = Op.newarr_r4;
|
||
// Other primitives keep Op.newarr but get a real typed runtime
|
||
// array (bool[]/char[]/…) via the side table, so element reads box
|
||
// with the correct type (element ToString/identity were wrong when
|
||
// object?[]-backed: bool boxed as int, defaults null).
|
||
else if (PrimArrayElemType(elemName2) is { } pt)
|
||
(primArrayElemTypeByTokIdx ??= new Dictionary<int, Type>())[elemTokIdx] = pt;
|
||
}
|
||
}
|
||
// Host ENUM elements need a real typed array too: Roslyn skips storing
|
||
// zero-valued members into `{ ... }` initializers (newarr zeroes them),
|
||
// so an object?[] backing left NULLS where C# guarantees the zero member.
|
||
if (arrOp == Op.newarr
|
||
&& (primArrayElemTypeByTokIdx == null || !primArrayElemTypeByTokIdx.ContainsKey(elemTokIdx))
|
||
&& ResolveHostEnumType(asm, elemTok) is { } enumElem2)
|
||
(primArrayElemTypeByTokIdx ??= new Dictionary<int, Type>())[elemTokIdx] = enumElem2;
|
||
if (arrOp == Op.newarr && FlatElemLayout(asm, elemTok) is { } elemFlat2)
|
||
{
|
||
// Flat elements — script structs AND flat host structs (new Vector3[n]).
|
||
// Also fixes uninitialized-element access: object?[] held nulls where C#
|
||
// guarantees zeroed structs; the byte backing is zeroed by construction.
|
||
arrOp = Op.newarr_vt;
|
||
(layoutByTokIdx ??= new Dictionary<int, HostBinding.StructLayout>())[elemTokIdx] = elemFlat2;
|
||
}
|
||
|
||
// Check for eager InitializeArray pattern (dup; ldtoken; call InitializeArray)
|
||
// If present, we can't lower it cleanly — fall through to not-supported
|
||
if (ip + 11 <= il.Length && il[ip] == 0x25 && il[ip + 1] == 0xD0)
|
||
{
|
||
if (il[ip + 6] == 0x28)
|
||
{
|
||
int callTok2 = BitConverter.ToInt32(il, ip + 7);
|
||
if (asm.TokenNames.TryGetValue(callTok2, out var calleeName2)
|
||
&& calleeName2 == "RuntimeHelpers.InitializeArray/2")
|
||
{
|
||
// Skip the dup+ldtoken+call pattern and emit with init flag
|
||
int fieldTok = BitConverter.ToInt32(il, ip + 2);
|
||
int fieldTokIdx = tokens.Count; tokens.Add(fieldTok);
|
||
ip += 11;
|
||
// Emit: [newarr*, dst, lenSlot, elemTokIdx, fieldTokIdx] (5 words)
|
||
ir.Add((uint)arrOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)lenSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)elemTokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)fieldTokIdx); irToIl.Add(instrStart);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Normal newarr (no initializer): fieldTokIdx = 0xFFFFFFFF
|
||
ir.Add((uint)arrOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)lenSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)elemTokIdx); irToIl.Add(instrStart);
|
||
ir.Add(unchecked((uint)-1)); irToIl.Add(instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldlen:
|
||
{
|
||
var (src, _) = evalStack[sp - 1];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
evalStack[sp - 1] = (dst, SType.I4);
|
||
Emit3(ir, irToIl, Op.ldlen, dst, src, instrStart);
|
||
break;
|
||
}
|
||
// Generic `ldelem <T>` — emitted by Roslyn only for value-type elements (reference
|
||
// types use ldelem.ref). In this representation a struct element is an O
|
||
// ScriptObject, so the destination is O, and the loaded value must be cloned for
|
||
// struct copy semantics. clone_sc is a safe pass-through for any non-ScriptObject.
|
||
case ILOpCode.Ldelem:
|
||
{
|
||
int ldeTok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
var (idxSlot, _) = evalStack[--sp];
|
||
var (arrSlot, _) = evalStack[sp - 1];
|
||
if (FlatElemLayout(asm, ldeTok) is { } ldeFlat)
|
||
{
|
||
// Flat element: memcpy out of the ScriptVtArray backing — value
|
||
// semantics without a clone allocation.
|
||
int vdst = AllocStructSlot(ref frameSize, slotTypes, slotStructs, ldeFlat);
|
||
evalStack[sp - 1] = (vdst, SType.Vt);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, vdst, arrSlot, idxSlot, instrStart);
|
||
break;
|
||
}
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp - 1] = (dst, SType.O);
|
||
Emit4(ir, irToIl, Op.ldelem_o, dst, arrSlot, idxSlot, instrStart);
|
||
int cl = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.clone_sc, cl, dst, instrStart);
|
||
evalStack[sp - 1] = (cl, SType.O);
|
||
break;
|
||
}
|
||
case ILOpCode.Ldelem_i:
|
||
case ILOpCode.Ldelem_i1:
|
||
case ILOpCode.Ldelem_u1:
|
||
case ILOpCode.Ldelem_i2:
|
||
case ILOpCode.Ldelem_u2:
|
||
case ILOpCode.Ldelem_i4:
|
||
case ILOpCode.Ldelem_u4:
|
||
case ILOpCode.Ldelem_i8:
|
||
case ILOpCode.Ldelem_r4:
|
||
case ILOpCode.Ldelem_r8:
|
||
case ILOpCode.Ldelem_ref:
|
||
{
|
||
var (idxSlot, _) = evalStack[--sp];
|
||
var (arrSlot, _) = evalStack[sp - 1];
|
||
SType et = op == ILOpCode.Ldelem_r4 ? SType.R4
|
||
: op == ILOpCode.Ldelem_r8 ? SType.R8
|
||
: op == ILOpCode.Ldelem_i8 ? SType.I8
|
||
: op == ILOpCode.Ldelem_ref ? SType.O : SType.I4;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, et, null);
|
||
evalStack[sp - 1] = (dst, et);
|
||
Emit4(ir, irToIl, Op.ldelem_o, dst, arrSlot, idxSlot, instrStart);
|
||
break;
|
||
}
|
||
case ILOpCode.Stelem:
|
||
case ILOpCode.Stelem_i:
|
||
case ILOpCode.Stelem_i1:
|
||
case ILOpCode.Stelem_i2:
|
||
case ILOpCode.Stelem_i4:
|
||
case ILOpCode.Stelem_i8:
|
||
case ILOpCode.Stelem_r4:
|
||
case ILOpCode.Stelem_r8:
|
||
case ILOpCode.Stelem_ref:
|
||
{
|
||
int steTok = 0;
|
||
if (op == ILOpCode.Stelem) { steTok = BitConverter.ToInt32(il, ip); ip += 4; }
|
||
var (src, srcTE) = evalStack[--sp];
|
||
var (idxSlot, _) = evalStack[--sp];
|
||
var (arrSlot, _) = evalStack[--sp];
|
||
if (steTok != 0 && FlatElemLayout(asm, steTok) is { } steFlat)
|
||
{
|
||
// Flat element: memcpy into the ScriptVtArray backing. A boxed (O)
|
||
// source is unboxed into a Vt temp first.
|
||
if (srcTE != SType.Vt)
|
||
{
|
||
int vtmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, steFlat);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmp, src, instrStart);
|
||
src = vtmp;
|
||
}
|
||
Emit4(ir, irToIl, Op.stelem_vt, arrSlot, idxSlot, src, instrStart);
|
||
break;
|
||
}
|
||
// [stelem_o, arr, idx, src]
|
||
ir.Add((uint)Op.stelem_o); irToIl.Add(instrStart);
|
||
ir.Add((uint)arrSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)idxSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)src); irToIl.Add(instrStart);
|
||
break;
|
||
}
|
||
|
||
// ldelema — push a phantom array-element address. Consumed in-place by
|
||
// ldind/stind (read or write the element directly), or as a byref call arg
|
||
// (then call_host_byref reads/writes through the array slot post-call).
|
||
case ILOpCode.Ldelema:
|
||
{
|
||
int lmaTok = BitConverter.ToInt32(il, ip); ip += 4;
|
||
var (idxSlot, _) = evalStack[--sp];
|
||
var (arrSlot, _) = evalStack[--sp];
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null); // phantom
|
||
addrTagBySlot[dst] = -3; // array-element address
|
||
addrFldObjBySlot[dst] = arrSlot;
|
||
addrFldTokBySlot[dst] = idxSlot; // reuse slot — points to index slot, not a token
|
||
if (FlatElemLayout(asm, lmaTok) is { } lmaFlat)
|
||
addrElemLayoutBySlot[dst] = lmaFlat; // flat elem: consumers ldelem_vt/stelem_vt
|
||
evalStack[sp++] = (dst, SType.O);
|
||
break;
|
||
}
|
||
|
||
// initobj — zero-initialize the value-type at the address. For our model:
|
||
// - phantom frame-slot address pointing to a Vt slot: zero the slot's bytes.
|
||
// - phantom frame-slot address pointing to an O slot (script struct): emit newobj_script
|
||
// with 0 explicit args to create a zero-initialized ScriptObject.
|
||
// - phantom field address: stfld of null/zero.
|
||
// - phantom array-element address: stelem of null/zero.
|
||
case ILOpCode.Initobj:
|
||
{
|
||
int typeDefTok = BitConverter.ToInt32(il, ip);
|
||
ip += 4; // skip type token
|
||
var (addrSlot, _) = evalStack[--sp];
|
||
int tag = addrTagBySlot[addrSlot];
|
||
if (tag >= 0)
|
||
{
|
||
int targetSlot = tag;
|
||
SType ts = slotTypes[targetSlot];
|
||
// Roslyn REUSES one temp local across consecutive struct initializers
|
||
// (`F(new V{...}, new V{...})`): the first arg's eval-stack entry still
|
||
// points at the slot this initobj is about to zero. CLR stack semantics
|
||
// say that entry is a COPY — materialize it before overwriting.
|
||
ProtectStackAliases(ir, irToIl, evalStack, sp, ref frameSize,
|
||
slotTypes, slotStructs, targetSlot, instrStart);
|
||
if (ts == SType.Vt)
|
||
{
|
||
ir.Add((uint)Op.initobj); irToIl.Add(instrStart);
|
||
ir.Add((uint)targetSlot); irToIl.Add(instrStart);
|
||
}
|
||
else if (ts == SType.I4 || ts == SType.R4)
|
||
{
|
||
Emit3(ir, irToIl, Op.ldc_i4, targetSlot, 0, instrStart);
|
||
}
|
||
else if (asm.TypeDefToType.ContainsKey(typeDefTok))
|
||
{
|
||
// Script-defined struct/class O slot.
|
||
// If there is an explicit 0-arg ctor, call it via newobj_script.
|
||
// Otherwise (default struct ctor with RVA=0), zero-init without a call.
|
||
if (asm.TypeDefToCtorTok.TryGetValue(typeDefTok, out int ctorTok))
|
||
{
|
||
int ctorTokIdx = tokens.Count; tokens.Add(ctorTok);
|
||
ir.Add((uint)Op.newobj_script); irToIl.Add(instrStart);
|
||
ir.Add((uint)targetSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)ctorTokIdx); irToIl.Add(instrStart);
|
||
ir.Add(0u); irToIl.Add(instrStart); // argc = 0 explicit args
|
||
}
|
||
else
|
||
{
|
||
// No body ctor — emit initobj_script with TypeDef token.
|
||
int typeDefTokIdx = tokens.Count; tokens.Add(typeDefTok);
|
||
ir.Add((uint)Op.initobj_script); irToIl.Add(instrStart);
|
||
ir.Add((uint)targetSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)typeDefTokIdx); irToIl.Add(instrStart);
|
||
}
|
||
// Slot is now initialized: subsequent stfld in this block won't need ensure_script.
|
||
ensuredScriptSlots.Add(targetSlot);
|
||
}
|
||
else if (PrimitiveSTypeForTypeToken(asm, typeDefTok) is { } pst)
|
||
{
|
||
// `initobj System.Int32` on a local PreClassifyLocals never saw a
|
||
// store to (Roslyn rewrites `stloc <const 0>` into `ldloca; initobj`
|
||
// when the constant is default(T)) — the slot defaulted to O. Retype
|
||
// it numeric and zero it; otherwise the value read null (found by
|
||
// fuzzing: `(byte)(p*0)` folds to 0, so `(...).ToString()` saw null).
|
||
slotTypes[targetSlot] = pst;
|
||
Emit3(ir, irToIl, pst == SType.R4 ? Op.ldc_r4 : Op.ldc_i4, targetSlot, 0, instrStart);
|
||
}
|
||
else if (asm.HostCtors.ContainsKey(typeDefTok))
|
||
{
|
||
// Boxed host struct local: materialize the default box so the
|
||
// initializer's stfld sequence has a target (registered at parse
|
||
// time, keyed by the type token).
|
||
int hostTypeTokIdx = tokens.Count; tokens.Add(typeDefTok);
|
||
ir.Add((uint)Op.newobj_host); irToIl.Add(instrStart);
|
||
ir.Add((uint)targetSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)hostTypeTokIdx); irToIl.Add(instrStart);
|
||
ir.Add(0u); irToIl.Add(instrStart); // argc = 0
|
||
ensuredScriptSlots.Add(targetSlot);
|
||
}
|
||
else
|
||
{
|
||
Emit2(ir, irToIl, Op.ldnull, targetSlot, instrStart);
|
||
}
|
||
}
|
||
else if (tag == -1)
|
||
{
|
||
// field-address initobj: store null / 0 / zero-Vt into the field.
|
||
int objSlot = addrFldObjBySlot[addrSlot];
|
||
int tokIdx2 = addrFldTokBySlot[addrSlot];
|
||
int tok2 = tokens[tokIdx2];
|
||
asm.FieldSTypes.TryGetValue(tok2, out var fst2);
|
||
// Vt (flat host struct) field inline in the ScriptObject's PrimBytes —
|
||
// Roslyn zero-inits hoisted iterator locals through their field address
|
||
// (`ldarg.0; ldflda '<v>5__N'; initobj Vec2`). Zero a Vt temp and copy
|
||
// its bytes in. The reference path below would emit stfld_sc_o with the
|
||
// field's BYTE offset as a RefSlots INDEX (found live: RefSlots[24] out
|
||
// of bounds on an iterator state machine).
|
||
if (fst2 == SType.Vt && asm.FieldSlots.TryGetValue(tok2, out var vtFsI)
|
||
&& vtFsI.Item1.VtFieldLayouts?[vtFsI.Item2] is { } vtLayI)
|
||
{
|
||
int zeroVt = AllocStructSlot(ref frameSize, slotTypes, slotStructs, vtLayI);
|
||
ir.Add((uint)Op.initobj); irToIl.Add(instrStart);
|
||
ir.Add((uint)zeroVt); irToIl.Add(instrStart);
|
||
// A composed phantom carries the summed PrimBytes offset; a plain
|
||
// field phantom resolves it from the descriptor.
|
||
int vtOffI = addrFldOffBySlot[addrSlot] >= 0
|
||
? addrFldOffBySlot[addrSlot]
|
||
: vtFsI.Item1.FieldOffsets[vtFsI.Item2];
|
||
Emit4(ir, irToIl, Op.stfld_sc_vt, objSlot, vtOffI, zeroVt, instrStart);
|
||
break;
|
||
}
|
||
// Same shape one level deeper: initobj on a HOST struct member of a
|
||
// hoisted flat struct (`pose.P = default` lowers to
|
||
// `ldflda '<pose>'; ldflda Pose::P; initobj Vec2`). The composed
|
||
// phantom's offset already sums the whole chain; zero the nested
|
||
// struct's byte range there.
|
||
if (addrFldOffBySlot[addrSlot] >= 0
|
||
&& asm.HostFields.TryGetValue(tok2, out var hfI) && hfI.DeclaringStruct != null
|
||
&& hfI.PrimitiveSt != SType.I4 && hfI.PrimitiveSt != SType.R4
|
||
&& hfI.FieldTypeName != null && asm.HostSurface != null
|
||
&& asm.HostSurface.TryGetStructLayout(hfI.FieldTypeName, out var nLayI)
|
||
&& nLayI != null)
|
||
{
|
||
int zeroVtH = AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLayI);
|
||
ir.Add((uint)Op.initobj); irToIl.Add(instrStart);
|
||
ir.Add((uint)zeroVtH); irToIl.Add(instrStart);
|
||
Emit4(ir, irToIl, Op.stfld_sc_vt, objSlot, addrFldOffBySlot[addrSlot], zeroVtH, instrStart);
|
||
break;
|
||
}
|
||
int zeroSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, fst2, null);
|
||
if (fst2 == SType.I4 || fst2 == SType.R4)
|
||
Emit3(ir, irToIl, Op.ldc_i4, zeroSlot, 0, instrStart);
|
||
else
|
||
Emit2(ir, irToIl, Op.ldnull, zeroSlot, instrStart);
|
||
if (asm.FieldSlots.TryGetValue(tok2, out var sfScFsI))
|
||
{
|
||
int sfOffI = sfScFsI.Item1.FieldOffsets[sfScFsI.Item2];
|
||
var scOpI = fst2 == SType.I4 ? Op.stfld_sc_i4 : fst2 == SType.R4 ? Op.stfld_sc_r4 : Op.stfld_sc_o;
|
||
ir.Add((uint)scOpI); irToIl.Add(instrStart);
|
||
ir.Add((uint)objSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)sfOffI); irToIl.Add(instrStart);
|
||
ir.Add((uint)zeroSlot); irToIl.Add(instrStart);
|
||
}
|
||
else
|
||
{
|
||
var sfldOp = fst2 == SType.I4 ? Op.stfld_i4 : fst2 == SType.R4 ? Op.stfld_r4 : Op.stfld_o;
|
||
ir.Add((uint)sfldOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)objSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx2); irToIl.Add(instrStart);
|
||
ir.Add((uint)zeroSlot); irToIl.Add(instrStart);
|
||
}
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
// array-element initobj: stelem of null/0.
|
||
int arrSlot = addrFldObjBySlot[addrSlot];
|
||
int idxSlot = addrFldTokBySlot[addrSlot];
|
||
int zeroSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit2(ir, irToIl, Op.ldnull, zeroSlot, instrStart);
|
||
ir.Add((uint)Op.stelem_o); irToIl.Add(instrStart);
|
||
ir.Add((uint)arrSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)idxSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)zeroSlot); irToIl.Add(instrStart);
|
||
}
|
||
else
|
||
goto notSupported;
|
||
break;
|
||
}
|
||
|
||
// ldind.* — dereference an address. The address may be:
|
||
// • a frame-slot address (addrTagBySlot[s] >= 0): read from that slot
|
||
// • a field address (addrTagBySlot[s] == -1): emit ldfld
|
||
// • an array-element address (addrTagBySlot[s] == -3): emit ldelem
|
||
case ILOpCode.Ldind_i1: case ILOpCode.Ldind_u1: case ILOpCode.Ldind_i2:
|
||
case ILOpCode.Ldind_u2: case ILOpCode.Ldind_i4: case ILOpCode.Ldind_u4:
|
||
case ILOpCode.Ldind_i: case ILOpCode.Ldind_r4: case ILOpCode.Ldind_r8:
|
||
case ILOpCode.Ldind_ref:
|
||
{
|
||
var (addrSlot, _) = evalStack[--sp];
|
||
int tag = addrTagBySlot[addrSlot];
|
||
SType ldindT = (op == ILOpCode.Ldind_r4 || op == ILOpCode.Ldind_r8) ? SType.R4
|
||
: (op == ILOpCode.Ldind_ref) ? SType.O : SType.I4;
|
||
if (tag == -1 && addrFldOffBySlot[addrSlot] >= 0)
|
||
{
|
||
// Composed inline address on a heap ScriptObject: direct PrimBytes access.
|
||
int objSlot = addrFldObjBySlot[addrSlot];
|
||
int cOff = addrFldOffBySlot[addrSlot];
|
||
var pst = ldindT == SType.R4 ? SType.R4 : SType.I4;
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, pst, null);
|
||
evalStack[sp++] = (dst2, pst);
|
||
Emit4(ir, irToIl, pst == SType.I4 ? Op.ldfld_sc_i4 : Op.ldfld_sc_r4, dst2, objSlot, cOff, instrStart);
|
||
}
|
||
else if (tag == -1)
|
||
{
|
||
// field address: emit ldfld
|
||
int objSlot = addrFldObjBySlot[addrSlot];
|
||
int tokIdx2 = addrFldTokBySlot[addrSlot];
|
||
int tok2 = tokens[tokIdx2];
|
||
asm.FieldSTypes.TryGetValue(tok2, out var fst2);
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, fst2, null);
|
||
evalStack[sp++] = (dst2, fst2);
|
||
if (asm.FieldSlots.TryGetValue(tok2, out var fsLo2))
|
||
{
|
||
int off2 = fsLo2.Item1.FieldOffsets[fsLo2.Item2];
|
||
var ldfldOp2 = fst2 == SType.I4 ? Op.ldfld_sc_i4 : fst2 == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, ldfldOp2, dst2, objSlot, off2, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var ldfldOp2 = fst2 == SType.I4 ? Op.ldfld_i4 : fst2 == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, ldfldOp2, dst2, objSlot, tokIdx2, instrStart);
|
||
}
|
||
}
|
||
else if (tag == -3 && addrElemLayoutBySlot[addrSlot] != null)
|
||
{
|
||
// Flat array element field read via address: materialize the element
|
||
// and read the field at the composed offset.
|
||
int arrSlot = addrFldObjBySlot[addrSlot];
|
||
int idxSlot = addrFldTokBySlot[addrSlot];
|
||
int elemTmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, addrElemLayoutBySlot[addrSlot]!);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, elemTmp, arrSlot, idxSlot, instrStart);
|
||
var pst = ldindT == SType.R4 ? SType.R4 : SType.I4;
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, pst, null);
|
||
evalStack[sp++] = (dst2, pst);
|
||
Emit4(ir, irToIl, pst == SType.I4 ? Op.ldfld_vt_i4 : Op.ldfld_vt_r4,
|
||
dst2, elemTmp, addrByteOffBySlot[addrSlot], instrStart);
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
// array element address: emit ldelem (chooses dst type from ldind suffix)
|
||
int arrSlot = addrFldObjBySlot[addrSlot];
|
||
int idxSlot = addrFldTokBySlot[addrSlot];
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, ldindT, null);
|
||
evalStack[sp++] = (dst2, ldindT);
|
||
Emit4(ir, irToIl, Op.ldelem_o, dst2, arrSlot, idxSlot, instrStart);
|
||
}
|
||
else if (tag >= 0)
|
||
{
|
||
int targetSlot = tag;
|
||
SType tgt = slotTypes[targetSlot];
|
||
if (tgt == SType.Vt)
|
||
{
|
||
// Address inside a flat struct's bytes: read the field at the
|
||
// composed byte offset — a plain mov reads word 0 regardless of
|
||
// the addressed field and silently misreads offsets > 0.
|
||
int vOff2 = addrByteOffBySlot[addrSlot];
|
||
var pst = ldindT == SType.R4 ? SType.R4 : SType.I4;
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, pst, null);
|
||
evalStack[sp++] = (dst2, pst);
|
||
Emit4(ir, irToIl, pst == SType.I4 ? Op.ldfld_vt_i4 : Op.ldfld_vt_r4, dst2, targetSlot, vOff2, instrStart);
|
||
}
|
||
else
|
||
{
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, tgt, null);
|
||
evalStack[sp++] = (dst2, tgt);
|
||
Emit3(ir, irToIl, Op.mov, dst2, targetSlot, instrStart);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// normal slot (e.g. ldind after non-phantom): copy it
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, slotTypes[addrSlot], null);
|
||
evalStack[sp++] = (dst2, slotTypes[addrSlot]);
|
||
Emit3(ir, irToIl, Op.mov, dst2, addrSlot, instrStart);
|
||
}
|
||
break;
|
||
}
|
||
|
||
// stind.* — write through an address. The address may be:
|
||
// • a frame-slot address (addrTagBySlot[s] >= 0): mov val → that slot
|
||
// • a field address (addrTagBySlot[s] == -1): emit stfld
|
||
// • an array-element address (addrTagBySlot[s] == -3): emit stelem
|
||
case ILOpCode.Stind_ref: case ILOpCode.Stind_i1: case ILOpCode.Stind_i2:
|
||
case ILOpCode.Stind_i4: case ILOpCode.Stind_r4: case ILOpCode.Stind_r8:
|
||
{
|
||
var (val, _) = evalStack[--sp];
|
||
var (addrSlot, _) = evalStack[--sp];
|
||
int tag = addrTagBySlot[addrSlot];
|
||
|
||
if (tag == -1 && addrFldOffBySlot[addrSlot] >= 0)
|
||
{
|
||
// Composed inline address on a heap ScriptObject: write PrimBytes direct.
|
||
int objSlot = addrFldObjBySlot[addrSlot];
|
||
int cOff = addrFldOffBySlot[addrSlot];
|
||
bool isR4s = op == ILOpCode.Stind_r4 || op == ILOpCode.Stind_r8;
|
||
Emit4(ir, irToIl, isR4s ? Op.stfld_sc_r4 : Op.stfld_sc_i4, objSlot, cOff, val, instrStart);
|
||
}
|
||
else if (tag == -1)
|
||
{
|
||
// field address: emit stfld
|
||
int objSlot = addrFldObjBySlot[addrSlot];
|
||
int tokIdx2 = addrFldTokBySlot[addrSlot];
|
||
int tok2 = tokens[tokIdx2];
|
||
asm.FieldSTypes.TryGetValue(tok2, out var fst2);
|
||
if (asm.FieldSlots.TryGetValue(tok2, out var sfScFs2))
|
||
{
|
||
int sfOff2 = sfScFs2.Item1.FieldOffsets[sfScFs2.Item2];
|
||
var scOp2 = fst2 == SType.I4 ? Op.stfld_sc_i4 : fst2 == SType.R4 ? Op.stfld_sc_r4 : Op.stfld_sc_o;
|
||
ir.Add((uint)scOp2); irToIl.Add(instrStart);
|
||
ir.Add((uint)objSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)sfOff2); irToIl.Add(instrStart);
|
||
ir.Add((uint)val); irToIl.Add(instrStart);
|
||
}
|
||
else
|
||
{
|
||
var sfldOp = fst2 == SType.I4 ? Op.stfld_i4 : fst2 == SType.R4 ? Op.stfld_r4 : Op.stfld_o;
|
||
ir.Add((uint)sfldOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)objSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx2); irToIl.Add(instrStart);
|
||
ir.Add((uint)val); irToIl.Add(instrStart);
|
||
}
|
||
}
|
||
else if (tag == -3 && addrElemLayoutBySlot[addrSlot] != null)
|
||
{
|
||
// Flat array element field write via address: read-modify-write the
|
||
// whole element (materialize, patch the field, memcpy back).
|
||
int arrSlot = addrFldObjBySlot[addrSlot];
|
||
int idxSlot = addrFldTokBySlot[addrSlot];
|
||
int elemTmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, addrElemLayoutBySlot[addrSlot]!);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, elemTmp, arrSlot, idxSlot, instrStart);
|
||
bool isR4st = op == ILOpCode.Stind_r4 || op == ILOpCode.Stind_r8;
|
||
Emit4(ir, irToIl, isR4st ? Op.stfld_vt_r4 : Op.stfld_vt_i4,
|
||
elemTmp, addrByteOffBySlot[addrSlot], val, instrStart);
|
||
Emit4(ir, irToIl, Op.stelem_vt, arrSlot, idxSlot, elemTmp, instrStart);
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
int arrSlot = addrFldObjBySlot[addrSlot];
|
||
int idxSlot = addrFldTokBySlot[addrSlot];
|
||
ir.Add((uint)Op.stelem_o); irToIl.Add(instrStart);
|
||
ir.Add((uint)arrSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)idxSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)val); irToIl.Add(instrStart);
|
||
}
|
||
else if (tag >= 0)
|
||
{
|
||
if (slotTypes[tag] == SType.Vt)
|
||
{
|
||
// Write the field at the composed byte offset within the flat
|
||
// struct's bytes (a plain mov would clobber word 0 AND retarget
|
||
// nothing at offsets > 0).
|
||
bool isR4s2 = op == ILOpCode.Stind_r4 || op == ILOpCode.Stind_r8;
|
||
Emit4(ir, irToIl, isR4s2 ? Op.stfld_vt_r4 : Op.stfld_vt_i4, tag, addrByteOffBySlot[addrSlot], val, instrStart);
|
||
}
|
||
else
|
||
{
|
||
// frame-slot address: mov val → target slot
|
||
Emit3(ir, irToIl, Op.mov, tag, val, instrStart);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Shouldn't happen — no valid stind target
|
||
throw new NotSupportedException(
|
||
$"IR lowering: stind with non-address operand in '{method.Name}' at IL+0x{instrStart:X4}");
|
||
}
|
||
break;
|
||
}
|
||
|
||
// ldobj — load a value-type from an address. Like ldind but for whole structs.
|
||
case ILOpCode.Ldobj:
|
||
{
|
||
ip += 4; // skip type token
|
||
var (addrSlot2, _) = evalStack[--sp];
|
||
int tag = addrTagBySlot[addrSlot2];
|
||
if (tag == -1)
|
||
{
|
||
// field address: emit ldfld
|
||
int objSlot = addrFldObjBySlot[addrSlot2];
|
||
int tokIdx2 = addrFldTokBySlot[addrSlot2];
|
||
int tok2 = tokens[tokIdx2];
|
||
asm.FieldSTypes.TryGetValue(tok2, out var fst2);
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, fst2, null);
|
||
evalStack[sp++] = (dst2, fst2);
|
||
if (asm.FieldSlots.TryGetValue(tok2, out var fsLo3))
|
||
{
|
||
int off3 = fsLo3.Item1.FieldOffsets[fsLo3.Item2];
|
||
var ldfldOp2 = fst2 == SType.I4 ? Op.ldfld_sc_i4 : fst2 == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, ldfldOp2, dst2, objSlot, off3, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var ldfldOp2 = fst2 == SType.I4 ? Op.ldfld_i4 : fst2 == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, ldfldOp2, dst2, objSlot, tokIdx2, instrStart);
|
||
}
|
||
}
|
||
else if (tag == -3 && addrElemLayoutBySlot[addrSlot2] != null)
|
||
{
|
||
int arrSlot = addrFldObjBySlot[addrSlot2];
|
||
int idxSlot = addrFldTokBySlot[addrSlot2];
|
||
int dst2 = AllocStructSlot(ref frameSize, slotTypes, slotStructs, addrElemLayoutBySlot[addrSlot2]!);
|
||
evalStack[sp++] = (dst2, SType.Vt);
|
||
Emit4(ir, irToIl, Op.ldelem_vt, dst2, arrSlot, idxSlot, instrStart);
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
int arrSlot = addrFldObjBySlot[addrSlot2];
|
||
int idxSlot = addrFldTokBySlot[addrSlot2];
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst2, SType.O);
|
||
Emit4(ir, irToIl, Op.ldelem_o, dst2, arrSlot, idxSlot, instrStart);
|
||
}
|
||
else if (tag >= 0)
|
||
{
|
||
// frame-slot address: mov / mov_vt from that slot (ldfld_vt_vt when the
|
||
// address points INSIDE the flat struct at a composed offset).
|
||
SType tgt = slotTypes[tag];
|
||
if (tgt == SType.Vt)
|
||
{
|
||
int vOff3 = addrByteOffBySlot[addrSlot2];
|
||
if (vOff3 > 0)
|
||
{
|
||
int lTok = tokens[addrFldTokBySlot[addrSlot2]];
|
||
HostBinding.StructLayout? nLay = null;
|
||
if (asm.FieldSlots.TryGetValue(lTok, out var lfs))
|
||
nLay = lfs.Item1.VtFieldLayouts?[lfs.Item2];
|
||
int dst3 = nLay != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, nLay)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
evalStack[sp++] = (dst3, SType.Vt);
|
||
Emit4(ir, irToIl, Op.ldfld_vt_vt, dst3, tag, vOff3, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var lay = slotStructs[tag];
|
||
int dst2 = AllocStructSlot(ref frameSize, slotTypes, slotStructs, lay!);
|
||
evalStack[sp++] = (dst2, SType.Vt);
|
||
Emit3(ir, irToIl, Op.mov_vt, dst2, tag, instrStart);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
int dst2 = AllocSlot(ref frameSize, slotTypes, slotStructs, tgt, null);
|
||
evalStack[sp++] = (dst2, tgt);
|
||
Emit3(ir, irToIl, Op.mov, dst2, tag, instrStart);
|
||
}
|
||
}
|
||
else
|
||
goto notSupported;
|
||
break;
|
||
}
|
||
|
||
// stobj — store a value-type to an address. Like stind but for whole structs.
|
||
case ILOpCode.Stobj:
|
||
{
|
||
ip += 4; // skip type token
|
||
var (val, _) = evalStack[--sp];
|
||
var (addrSlot2, _) = evalStack[--sp];
|
||
int tag = addrTagBySlot[addrSlot2];
|
||
if (tag == -1)
|
||
{
|
||
int objSlot = addrFldObjBySlot[addrSlot2];
|
||
int tokIdx2 = addrFldTokBySlot[addrSlot2];
|
||
int tok2 = tokens[tokIdx2];
|
||
asm.FieldSTypes.TryGetValue(tok2, out var fst2);
|
||
var sfldOp = fst2 == SType.I4 ? Op.stfld_i4 : fst2 == SType.R4 ? Op.stfld_r4 : Op.stfld_o;
|
||
ir.Add((uint)sfldOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)objSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx2); irToIl.Add(instrStart);
|
||
ir.Add((uint)val); irToIl.Add(instrStart);
|
||
}
|
||
else if (tag == -3 && addrElemLayoutBySlot[addrSlot2] != null)
|
||
{
|
||
int arrSlot = addrFldObjBySlot[addrSlot2];
|
||
int idxSlot = addrFldTokBySlot[addrSlot2];
|
||
int vSrc = val;
|
||
if (slotTypes[val] != SType.Vt)
|
||
{
|
||
int vtmpO = AllocStructSlot(ref frameSize, slotTypes, slotStructs, addrElemLayoutBySlot[addrSlot2]!);
|
||
Emit3(ir, irToIl, Op.unbox_vt, vtmpO, val, instrStart);
|
||
vSrc = vtmpO;
|
||
}
|
||
Emit4(ir, irToIl, Op.stelem_vt, arrSlot, idxSlot, vSrc, instrStart);
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
int arrSlot = addrFldObjBySlot[addrSlot2];
|
||
int idxSlot = addrFldTokBySlot[addrSlot2];
|
||
ir.Add((uint)Op.stelem_o); irToIl.Add(instrStart);
|
||
ir.Add((uint)arrSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)idxSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)val); irToIl.Add(instrStart);
|
||
}
|
||
else if (tag >= 0)
|
||
{
|
||
// frame-slot address: mov / mov_vt val → target slot (sub-range write
|
||
// when the address carries a composed offset into the flat bytes).
|
||
int vOffS = addrByteOffBySlot[addrSlot2];
|
||
if (slotTypes[tag] == SType.Vt && slotTypes[val] == SType.Vt)
|
||
{
|
||
if (vOffS > 0) Emit4(ir, irToIl, Op.stfld_vt_vt, tag, vOffS, val, instrStart);
|
||
else Emit3(ir, irToIl, Op.mov_vt, tag, val, instrStart);
|
||
}
|
||
else
|
||
Emit3(ir, irToIl, Op.mov, tag, val, instrStart);
|
||
}
|
||
else
|
||
goto notSupported;
|
||
break;
|
||
}
|
||
|
||
// cpobj is in Rejected (validator bans it as unsafe), so we never see it
|
||
// here. If a valid script ever needs struct-to-struct copy, Roslyn emits
|
||
// ldobj + stobj instead, both of which are handled above.
|
||
|
||
default:
|
||
goto notSupported;
|
||
}
|
||
continue;
|
||
|
||
notSupported:
|
||
// Name the opcode: "ldftn" tells the user it's a method group / delegate
|
||
// creation; "0xFE06" tells them nothing.
|
||
throw new NotSupportedException(
|
||
$"IR lowering: opcode {op.ToString().ToLowerInvariant()} (0x{(int)op:X4}) in '{method.Name}' at IL+0x{instrStart:X4} not yet supported");
|
||
}
|
||
|
||
// Patch forward branch targets
|
||
var irArray = ir.ToArray();
|
||
foreach (var (pIdx, ilTarget) in patchList)
|
||
{
|
||
if (ilToIrIp.TryGetValue(ilTarget, out int irTarget))
|
||
irArray[pIdx] = (uint)irTarget;
|
||
else if (ilTarget == il.Length)
|
||
irArray[pIdx] = (uint)irArray.Length; // legit branch to method end -> ret sentinel
|
||
else
|
||
// Out of range, or mid-instruction (not an IL boundary). Rejecting instead of the
|
||
// old silent "jump past end = return" rewrite, which turned corrupt/hostile control
|
||
// flow into silently-wrong control flow with no diagnostic.
|
||
throw new NotSupportedException(
|
||
$"branch target IL+0x{ilTarget:X4} in '{method.Name}' is out of range or not at an instruction boundary");
|
||
}
|
||
|
||
var slotTypesArr = slotTypes.ToArray();
|
||
var irToIlArr = irToIl.ToArray();
|
||
var tokensArr = tokens.ToArray();
|
||
// Wide (I8/R8) slots span two cells under one index; the pass tables (rename,
|
||
// coalesce, retarget) reason per-cell and would merge another value into a wide
|
||
// slot's continuation cell. Until the tables learn wide widths, a method whose
|
||
// frame contains any wide slot runs unoptimized — correctness first.
|
||
bool hasWideSlots = false;
|
||
for (int wi = 0; wi < slotTypesArr.Length; wi++)
|
||
if (slotTypesArr[wi] is SType.I8 or SType.R8) { hasWideSlots = true; break; }
|
||
if (!hasWideSlots && Environment.GetEnvironmentVariable("ILINTERPRETER_NOOPT") == null) {
|
||
var skip = Environment.GetEnvironmentVariable("ILINTERPRETER_SKIP_PASS") ?? "";
|
||
if (!skip.Contains("frl")) ForwardRedundantLoads(ref irArray, ref irToIlArr, slotTypesArr, tokensArr);
|
||
if (!skip.Contains("fci")) FoldConstantImmediates(ref irArray, ref irToIlArr, slotTypesArr);
|
||
if (!skip.Contains("cdm")) CoalesceDestMovs(ref irArray, ref irToIlArr, slotTypesArr);
|
||
if (!skip.Contains("fcb")) FoldCompareBranch(ref irArray, ref irToIlArr, slotTypesArr);
|
||
if (!skip.Contains("ffl")) FoldForIntLoop(ref irArray, ref irToIlArr, slotTypesArr);
|
||
if (!skip.Contains("hlc")) HoistLoopConsts(ref irArray, ref irToIlArr, slotTypesArr, method.ArgCount);
|
||
}
|
||
|
||
// Pre-resolve fast delegates per tokIdx so call_host can skip the dict
|
||
// lookup. Setup-time cost; eliminates ~37 ns/iter from mixed bench
|
||
// (call_host is dispatched 3+ times per inner step there).
|
||
FastCallDelegate?[]? hostFastByTokIdx = null;
|
||
bool[]? hostFastVtRecvByTokIdx = null;
|
||
bool[]? hostFastWideOkByTokIdx = null;
|
||
ParsedMethod?[]? calleeByTokIdx = null;
|
||
for (int t = 0; t < tokensArr.Length; t++)
|
||
{
|
||
int tok = tokensArr[t];
|
||
if (asm.HostCalls.TryGetValue(tok, out var he) && he.Binding.Fast != null)
|
||
{
|
||
if (hostFastByTokIdx == null) hostFastByTokIdx = new FastCallDelegate?[tokensArr.Length];
|
||
hostFastByTokIdx[t] = he.Binding.Fast;
|
||
if (he.Binding.FastVtRecv)
|
||
{
|
||
if (hostFastVtRecvByTokIdx == null) hostFastVtRecvByTokIdx = new bool[tokensArr.Length];
|
||
hostFastVtRecvByTokIdx[t] = true;
|
||
}
|
||
if (he.Binding.FastWideOk)
|
||
{
|
||
if (hostFastWideOkByTokIdx == null) hostFastWideOkByTokIdx = new bool[tokensArr.Length];
|
||
hostFastWideOkByTokIdx[t] = true;
|
||
}
|
||
}
|
||
if (asm.ByToken.TryGetValue(tok, out var callee))
|
||
{
|
||
if (calleeByTokIdx == null) calleeByTokIdx = new ParsedMethod?[tokensArr.Length];
|
||
calleeByTokIdx[t] = callee;
|
||
}
|
||
}
|
||
|
||
int refClearLen = 0;
|
||
for (int s = slotTypesArr.Length - 1; s >= 0; s--)
|
||
if (slotTypesArr[s] == SType.O) { refClearLen = s + 1; break; }
|
||
|
||
// A frame larger than either VM arena would write past its backing block on the first
|
||
// slot store. Reject at lowering (a clean load failure) instead of corrupting memory at
|
||
// invoke. Both arenas are indexed by the same slot number, but the reference arena is
|
||
// smaller, so a ref-heavy frame can exceed it while the numeric count still fits.
|
||
// Reachable from a pathological method body or a crafted LOCAL_SIG.
|
||
if (frameSize > Vm.NumSlots || refClearLen > Vm.RefSlots)
|
||
throw new NotSupportedException(
|
||
$"method '{method.Name}' needs {frameSize} frame slots and {refClearLen} reference slots, " +
|
||
$"over the {Vm.NumSlots}/{Vm.RefSlots}-slot arena limits");
|
||
|
||
return new LoweredMethod
|
||
{
|
||
Ir = irArray,
|
||
FrameSize = frameSize,
|
||
IrToIlOffset = irToIlArr,
|
||
Strings = strings.ToArray(),
|
||
Tokens = tokensArr,
|
||
SlotTypes = slotTypesArr,
|
||
StructLayouts = slotStructs.ToArray(),
|
||
HostFastByTokIdx = hostFastByTokIdx,
|
||
HostFastVtRecvByTokIdx = hostFastVtRecvByTokIdx,
|
||
HostFastWideOkByTokIdx = hostFastWideOkByTokIdx,
|
||
CalleeByTokIdx = calleeByTokIdx,
|
||
RefClearLen = refClearLen,
|
||
ArgSlot = argIndirect ? argFrameSlot : null,
|
||
LayoutByTokIdx = BuildLayoutByTokIdx(layoutByTokIdx, tokensArr.Length),
|
||
PrimArrayElemTypeByTokIdx = BuildPrimArrayTypes(primArrayElemTypeByTokIdx, tokensArr.Length),
|
||
DelegateSiteByTokIdx = BuildDelegateSites(delegateSiteByTokIdx, tokensArr.Length),
|
||
};
|
||
}
|
||
|
||
// Op selection for flat host-struct field access: sub-4-byte integer fields
|
||
// (Color32.r-class) need widening loads / truncating stores; everything else uses
|
||
// the plain 4-byte cell ops.
|
||
static Op LdFldVtOpFor(HostBinding.FieldEntry fe)
|
||
{
|
||
if (fe.PrimitiveSt == SType.R4) return Op.ldfld_vt_r4;
|
||
switch (fe.PrimitiveKind)
|
||
{
|
||
case 1: return Op.ldfld_vt_u1;
|
||
case 2: return Op.ldfld_vt_i1;
|
||
case 3: return Op.ldfld_vt_u2;
|
||
case 4: return Op.ldfld_vt_i2;
|
||
default: return Op.ldfld_vt_i4;
|
||
}
|
||
}
|
||
|
||
static Op StFldVtOpFor(HostBinding.FieldEntry fe)
|
||
{
|
||
if (fe.PrimitiveSt == SType.R4) return Op.stfld_vt_r4;
|
||
switch (fe.PrimitiveKind)
|
||
{
|
||
case 1: case 2: return Op.stfld_vt_b1;
|
||
case 3: case 4: return Op.stfld_vt_b2;
|
||
default: return Op.stfld_vt_i4;
|
||
}
|
||
}
|
||
|
||
// Runtime Type for a `box` token that names a HOST enum (TypeRef parent), null otherwise.
|
||
// Script-declared enums are TypeDefs — no runtime Type exists, so they keep the int form.
|
||
static Type? ResolveHostEnumType(ParsedAssembly asm, int tok)
|
||
{
|
||
if (asm.HostSurface == null) return null;
|
||
try
|
||
{
|
||
var h = MetadataTokens.EntityHandle(tok);
|
||
if (h.Kind != HandleKind.TypeReference) return null;
|
||
var t = ResolveSigTypeRef(asm.Reader, (TypeReferenceHandle)h, asm.HostSurface);
|
||
return t is { IsEnum: true } ? t : null;
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
// Slot type for a SCRIPT static field (FieldDef) whose signature is an I4/R4-mapped
|
||
// primitive; null for host fields, reference/struct/wide types (those stay boxed O).
|
||
// Enums included: a script enum static's underlying value rides the I4 slot.
|
||
static SType? ScriptStaticPrimitiveSType(ParsedAssembly asm, int tok)
|
||
{
|
||
var h = MetadataTokens.EntityHandle(tok);
|
||
if (h.Kind != HandleKind.FieldDefinition) return null;
|
||
try
|
||
{
|
||
var blob = asm.Reader.GetBlobBytes(
|
||
asm.Reader.GetFieldDefinition((FieldDefinitionHandle)h).Signature);
|
||
if (blob.Length < 2 || blob[0] != 0x06) return null;
|
||
switch (blob[1])
|
||
{
|
||
case 0x02: case 0x03: case 0x04: case 0x05: case 0x06:
|
||
case 0x07: case 0x08: case 0x09: // bool..uint
|
||
return SType.I4;
|
||
case 0x0C: // float
|
||
return SType.R4;
|
||
case 0x11: // VALUETYPE coded-index — a script ENUM static maps to I4 (its
|
||
{ // value flows as the underlying int everywhere else already)
|
||
int idx = 2;
|
||
int coded = DecompressIntAdv(blob, ref idx);
|
||
if ((coded & 0x03) == 0 && IsEnumTypeDef(asm.Reader, coded >> 2))
|
||
return SType.I4;
|
||
return null;
|
||
}
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
// True when a static field's type is immutable-by-value (primitive or string), so its
|
||
// address can be lowered as a value load (see the Ldsflda case). Script FieldDefs decode
|
||
// the field signature blob; host MemberRefs consult the registered FieldEntry.
|
||
static bool StaticFieldIsImmutableValue(ParsedAssembly asm, int tok)
|
||
{
|
||
var h = MetadataTokens.EntityHandle(tok);
|
||
if (h.Kind == HandleKind.FieldDefinition)
|
||
{
|
||
try
|
||
{
|
||
var blob = asm.Reader.GetBlobBytes(
|
||
asm.Reader.GetFieldDefinition((FieldDefinitionHandle)h).Signature);
|
||
// Field sig: FIELD (0x06) then the type. 0x02..0x0D = bool..double, 0x0E = string.
|
||
return blob.Length >= 2 && blob[0] == 0x06 && blob[1] >= 0x02 && blob[1] <= 0x0E;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
if (asm.HostFields.TryGetValue(tok, out var fe))
|
||
return fe.PrimitiveSt is SType.I4 or SType.R4 || fe.FieldTypeName == "String";
|
||
return false;
|
||
}
|
||
|
||
// Flat layout for an array-element type token: script TypeDefs use the descriptor's
|
||
// synthesized FlatLayout; host TypeRefs resolve by simple name through the binding.
|
||
static HostBinding.StructLayout? FlatElemLayout(ParsedAssembly asm, int tok)
|
||
{
|
||
if (asm.TypeDefToType.TryGetValue(tok, out var d)) return d.FlatLayout;
|
||
var h = MetadataTokens.EntityHandle(tok);
|
||
if (h.Kind == HandleKind.TypeReference && asm.HostSurface != null)
|
||
{
|
||
string name;
|
||
try { name = asm.Reader.GetString(asm.Reader.GetTypeReference((TypeReferenceHandle)h).Name); }
|
||
catch (Exception) { return null; }
|
||
if (asm.HostSurface.TryGetStructLayout(name, out var lay)) return lay;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static HostBinding.StructLayout?[]? BuildLayoutByTokIdx(
|
||
Dictionary<int, HostBinding.StructLayout>? sparse, int tokenCount)
|
||
{
|
||
if (sparse == null) return null;
|
||
var arr = new HostBinding.StructLayout?[tokenCount];
|
||
foreach (var kv in sparse) arr[kv.Key] = kv.Value;
|
||
return arr;
|
||
}
|
||
|
||
static Type?[]? BuildPrimArrayTypes(Dictionary<int, Type>? sparse, int tokenCount)
|
||
{
|
||
if (sparse == null) return null;
|
||
var arr = new Type?[tokenCount];
|
||
foreach (var kv in sparse) arr[kv.Key] = kv.Value;
|
||
return arr;
|
||
}
|
||
|
||
static DelegateSite?[]? BuildDelegateSites(Dictionary<int, DelegateSite>? sparse, int tokenCount)
|
||
{
|
||
if (sparse == null) return null;
|
||
var arr = new DelegateSite?[tokenCount];
|
||
foreach (var kv in sparse) arr[kv.Key] = kv.Value;
|
||
return arr;
|
||
}
|
||
|
||
// Primitive element types that need a real typed runtime array (I4-mapped but whose boxed
|
||
// identity differs from int, or that C# zero-inits differently than object?[] null). Int32
|
||
// and Single are handled by newarr_i4/newarr_r4 and are intentionally not here.
|
||
static Type? PrimArrayElemType(string elemName) => elemName switch
|
||
{
|
||
"Boolean" => typeof(bool),
|
||
"Char" => typeof(char),
|
||
"Byte" => typeof(byte),
|
||
"SByte" => typeof(sbyte),
|
||
"Int16" => typeof(short),
|
||
"UInt16" => typeof(ushort),
|
||
"Int64" => typeof(long),
|
||
"UInt64" => typeof(ulong),
|
||
"Double" => typeof(double),
|
||
_ => null,
|
||
};
|
||
|
||
// Constant-immediate fold: `ldc_X tmp; binop_X_nn dst, src, tmp` with a single-use
|
||
// tmp folds into `binop_X_nk dst, src, K` (K inline in the IR word) — one dispatch
|
||
// less per pair. RHS-constant only (no _kn opcodes exist); commutative ops also
|
||
// accept tmp on the LEFT via operand swap. Skipped when a branch target lands on
|
||
// the binop (the ldc would be lost on that edge). Runs BEFORE CoalesceDestMovs so
|
||
// the trailing `mov local <- tmp_dst` is still available for coalescing.
|
||
static void FoldConstantImmediates(ref uint[] ir, ref int[] irToIl, SType[] slotTypes)
|
||
{
|
||
int n = ir.Length;
|
||
if (n == 0) return;
|
||
|
||
// Walk IR, record (ip, op, width).
|
||
var ips = new List<int>(n / 3);
|
||
var ops = new List<Op>(n / 3);
|
||
var widths = new List<int>(n / 3);
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0) return;
|
||
ips.Add(pos); ops.Add(op); widths.Add(w);
|
||
pos += w;
|
||
}
|
||
int insnCount = ips.Count;
|
||
|
||
int slotCount = slotTypes.Length;
|
||
var reads = new int[slotCount];
|
||
var writes = new int[slotCount];
|
||
for (int i = 0; i < insnCount; i++)
|
||
AccumulateSlotUses(ir, ips[i], ops[i], widths[i], reads, writes, slotCount);
|
||
|
||
var isBranchTarget = new bool[n + 1];
|
||
for (int i = 0; i < insnCount; i++)
|
||
MarkInsnBranchTargets(ir, ips[i], ops[i], isBranchTarget);
|
||
|
||
var deleted = new bool[insnCount];
|
||
int deletedWords = 0;
|
||
for (int i = 0; i < insnCount - 1; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
var ldcOp = ops[i];
|
||
if (ldcOp != Op.ldc_i4 && ldcOp != Op.ldc_r4) continue;
|
||
|
||
int ldcIp = ips[i];
|
||
int tmpSlot = (int)ir[ldcIp + 1];
|
||
uint kBits = ir[ldcIp + 2];
|
||
|
||
if (writes[tmpSlot] != 1 || reads[tmpSlot] != 1) continue;
|
||
|
||
// Find the next *non-deleted* instruction.
|
||
int j = i + 1;
|
||
while (j < insnCount && deleted[j]) j++;
|
||
if (j >= insnCount) continue;
|
||
|
||
var binOp = ops[j];
|
||
int binIp = ips[j];
|
||
if (widths[j] != 4) continue;
|
||
if (isBranchTarget[binIp]) continue;
|
||
|
||
int s1 = (int)ir[binIp + 2];
|
||
int s2 = (int)ir[binIp + 3];
|
||
bool tmpRight = (s2 == tmpSlot);
|
||
bool tmpLeft = (s1 == tmpSlot);
|
||
if (!tmpRight && !tmpLeft) continue;
|
||
|
||
if (!TryFoldNN(ldcOp, binOp, tmpRight, out Op nkOp)) continue;
|
||
|
||
// Apply: rewrite binop in place, mark ldc deleted.
|
||
ir[binIp] = (uint)nkOp;
|
||
if (tmpLeft)
|
||
{
|
||
// Commutative swap: move src1 into the operand-slot position
|
||
// (word 2), put k in word 3. Original encoding was [op, dst,
|
||
// tmp_left, src_right] — we want [nk, dst, src_right, k].
|
||
ir[binIp + 2] = (uint)s2;
|
||
}
|
||
ir[binIp + 3] = kBits;
|
||
|
||
deleted[i] = true;
|
||
deletedWords += widths[i];
|
||
|
||
// Maintain reads/writes so a future pass over the same IR sees
|
||
// the correct counts (we don't iterate to fixpoint, but defensive).
|
||
reads[tmpSlot] = 0; writes[tmpSlot] = 0;
|
||
}
|
||
|
||
if (deletedWords == 0) return;
|
||
|
||
// Compact (same shift-table machinery as CoalesceDestMovs).
|
||
var deletedWord = new bool[n];
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (!deleted[i]) continue;
|
||
for (int k = 0; k < widths[i]; k++) deletedWord[ips[i] + k] = true;
|
||
}
|
||
var shift = new int[n + 1];
|
||
int run = 0;
|
||
for (int oldIp = 0; oldIp <= n; oldIp++)
|
||
{
|
||
shift[oldIp] = run;
|
||
if (oldIp < n && deletedWord[oldIp]) run++;
|
||
}
|
||
|
||
int newLen = n - deletedWords;
|
||
var newIr = new uint[newLen];
|
||
var newToIl = new int[newLen];
|
||
int dstIdx = 0;
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int oldIp = ips[i];
|
||
int w = widths[i];
|
||
for (int k = 0; k < w; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
PatchInsnBranchTargets(newIr, dstIdx, ops[i], shift, ir, oldIp);
|
||
dstIdx += w;
|
||
}
|
||
|
||
ir = newIr;
|
||
irToIl = newToIl;
|
||
}
|
||
|
||
// Map an (ldc, binop_nn) pair to the matching binop_nk. Returns false if
|
||
// we don't have an _nk variant for this combination — no fold.
|
||
// tmpRight=true means the constant is on the right (no swap needed); for
|
||
// commutative ops we may also fold tmpLeft via operand swap.
|
||
static bool TryFoldNN(Op ldcOp, Op binOp, bool tmpRight, out Op nkOp)
|
||
{
|
||
nkOp = default;
|
||
// Type compatibility: ldc_i4 with i4 binop, ldc_r4 with r4 binop.
|
||
bool isI4Ldc = ldcOp == Op.ldc_i4;
|
||
switch (binOp)
|
||
{
|
||
case Op.add_i4_nn: if (!isI4Ldc) return false; nkOp = Op.add_i4_nk; return true; // commutative
|
||
case Op.add_r4_nn: if (isI4Ldc) return false; nkOp = Op.add_r4_nk; return true; // commutative
|
||
case Op.mul_r4_nn: if (isI4Ldc) return false; nkOp = Op.mul_r4_nk; return true; // commutative
|
||
case Op.ceq_i4_nn: if (!isI4Ldc) return false; nkOp = Op.ceq_i4_nk; return true; // commutative
|
||
case Op.mul_i4_nn: if (!isI4Ldc) return false; nkOp = Op.mul_i4_nk; return true; // commutative
|
||
case Op.and_i4_nn: if (!isI4Ldc) return false; nkOp = Op.and_i4_nk; return true; // commutative
|
||
case Op.or_i4_nn: if (!isI4Ldc) return false; nkOp = Op.or_i4_nk; return true; // commutative
|
||
case Op.xor_i4_nn: if (!isI4Ldc) return false; nkOp = Op.xor_i4_nk; return true; // commutative
|
||
case Op.ceq_r4_nn: if (isI4Ldc) return false; nkOp = Op.ceq_r4_nk; return true; // commutative
|
||
// Non-commutative: tmp must be on the right.
|
||
case Op.sub_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.sub_i4_nk; return true;
|
||
case Op.sub_r4_nn: if (!tmpRight || isI4Ldc) return false; nkOp = Op.sub_r4_nk; return true;
|
||
case Op.clt_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.clt_i4_nk; return true;
|
||
case Op.clt_r4_nn: if (!tmpRight || isI4Ldc) return false; nkOp = Op.clt_r4_nk; return true;
|
||
case Op.cgt_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.cgt_i4_nk; return true;
|
||
case Op.cgt_r4_nn: if (!tmpRight || isI4Ldc) return false; nkOp = Op.cgt_r4_nk; return true;
|
||
case Op.div_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.div_i4_nk; return true;
|
||
case Op.rem_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.rem_i4_nk; return true;
|
||
case Op.shl_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.shl_i4_nk; return true;
|
||
case Op.shr_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.shr_i4_nk; return true;
|
||
case Op.shr_un_i4_nn: if (!tmpRight || !isI4Ldc) return false; nkOp = Op.shr_un_i4_nk; return true;
|
||
case Op.div_r4_nn: if (!tmpRight || isI4Ldc) return false; nkOp = Op.div_r4_nk; return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
// --- Redundant-load forwarding ---
|
||
//
|
||
// Within each basic block, tracks for each (objSlot, resolvedFieldToken)
|
||
// pair which slot currently holds the field's value:
|
||
// ldfld_X dst, obj, tok → cache[(obj, tok)] = dst
|
||
// stfld_X obj, tok, src → cache[(obj, tok)] = src
|
||
//
|
||
// On a subsequent matching ldfld_X with cache hit (and matching SType),
|
||
// the second load is redundant: rewrite all reads of its dst slot to
|
||
// the cached value slot, delete the load. Slot rename is global,
|
||
// applied via a slotMap[] table; safe because the deleted load was the
|
||
// sole writer of its dst within the live region (we're conservative
|
||
// with cache invalidation).
|
||
//
|
||
// Cache invalidation events (cleared aggressively):
|
||
// - Any call (host/script/byref) — could mutate any field.
|
||
// - Any newobj — constructor side effects.
|
||
// - Any stsfld_*, stelem_* — written field/elem could alias.
|
||
// - Any branch — basic-block boundary.
|
||
// - A target landing on the next instruction also resets the block.
|
||
//
|
||
// Subset is corpus-driven: ldfld_o, ldfld_r4, ldfld_i4 (with matching
|
||
// stfld variants for store-to-load forwarding). Other field-bearing
|
||
// ops (ldelem, ldfld_struct, ldsfld) deferred — no bench evidence.
|
||
/// <summary>Removes cached (obj, field) forwards for <paramref name="fieldKey"/> held under
|
||
/// any object slot other than <paramref name="objS"/> — a store through one slot must not
|
||
/// leave stale forwards alive for other slots that may alias the same object.</summary>
|
||
static void DropAliasedField(Dictionary<(int, int), int> cache, int objS, int fieldKey)
|
||
{
|
||
List<(int, int)>? stale = null;
|
||
foreach (var kv in cache)
|
||
if (kv.Key.Item2 == fieldKey && kv.Key.Item1 != objS) (stale ??= new()).Add(kv.Key);
|
||
if (stale != null) foreach (var k in stale) cache.Remove(k);
|
||
}
|
||
|
||
static void ForwardRedundantLoads(ref uint[] ir, ref int[] irToIl,
|
||
SType[] slotTypes, int[] tokens)
|
||
{
|
||
int n = ir.Length;
|
||
if (n == 0) return;
|
||
|
||
var ips = new List<int>(n / 3);
|
||
var ops = new List<Op>(n / 3);
|
||
var widths = new List<int>(n / 3);
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0) return;
|
||
ips.Add(pos); ops.Add(op); widths.Add(w);
|
||
pos += w;
|
||
}
|
||
int insnCount = ips.Count;
|
||
|
||
var isBranchTarget = new bool[n + 1];
|
||
for (int i = 0; i < insnCount; i++)
|
||
MarkInsnBranchTargets(ir, ips[i], ops[i], isBranchTarget);
|
||
|
||
int slotCount = slotTypes.Length;
|
||
var slotMap = new int[slotCount];
|
||
for (int i = 0; i < slotCount; i++) slotMap[i] = i;
|
||
|
||
var deleted = new bool[insnCount];
|
||
int deletedWords = 0;
|
||
|
||
// (objSlot, resolvedTok) → valueSlot. Cleared at block boundaries
|
||
// and on any side effect.
|
||
var cache = new Dictionary<(int, int), int>();
|
||
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
int ip = ips[i];
|
||
var op = ops[i];
|
||
|
||
// Block boundary: an instruction that's a branch target starts
|
||
// a new block (we may have entered via either fall-through or
|
||
// jump — conservative).
|
||
if (i > 0 && isBranchTarget[ip]) cache.Clear();
|
||
|
||
// Invalidate cache entries whose object slot OR cached value slot is overwritten by
|
||
// this instruction. Without this, store-to-load forwarding (stfld caches the SOURCE
|
||
// slot) or load forwarding (keyed by the OBJECT slot) survives a later reassignment of
|
||
// that slot and forwards a stale value — e.g. `bx.p = s1; s1 = other; read bx.p`.
|
||
if (WritesFrameDstAt1(op))
|
||
{
|
||
int wdst = (int)ir[ip + 1];
|
||
if (wdst >= 0)
|
||
{
|
||
List<(int, int)>? drop = null;
|
||
foreach (var kv in cache)
|
||
if (kv.Value == wdst || kv.Key.Item1 == wdst) (drop ??= new()).Add(kv.Key);
|
||
if (drop != null) foreach (var k in drop) cache.Remove(k);
|
||
}
|
||
}
|
||
|
||
switch (op)
|
||
{
|
||
case Op.ldfld_o:
|
||
case Op.ldfld_r4:
|
||
case Op.ldfld_i4:
|
||
{
|
||
int dst = (int)ir[ip + 1];
|
||
int objS = (int)ir[ip + 2];
|
||
int tokIdx = (int)ir[ip + 3];
|
||
if ((uint)tokIdx >= (uint)tokens.Length) break;
|
||
int resolvedTok = tokens[tokIdx];
|
||
var key = (objS, resolvedTok);
|
||
if (cache.TryGetValue(key, out int valueSlot)
|
||
&& (uint)valueSlot < (uint)slotCount
|
||
&& slotTypes[valueSlot] == slotTypes[dst])
|
||
{
|
||
// Forward: subsequent reads of dst become reads of
|
||
// valueSlot; this load is redundant.
|
||
slotMap[dst] = valueSlot;
|
||
deleted[i] = true;
|
||
deletedWords += widths[i];
|
||
}
|
||
else
|
||
{
|
||
cache[key] = dst;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Op.ldfld_sc_o:
|
||
case Op.ldfld_sc_r4:
|
||
case Op.ldfld_sc_i4:
|
||
{
|
||
int dst = (int)ir[ip + 1];
|
||
int objS = (int)ir[ip + 2];
|
||
int off = (int)ir[ip + 3];
|
||
// Negated keys avoid collision with raw token values; the ref/prim split
|
||
// (~(2*off) vs ~(2*off+1)) keeps a stfld_sc_o REF-SLOT index from aliasing
|
||
// a stfld_sc_i4 PRIM BYTE offset of the same number — that collision
|
||
// forwarded a field read to an unrelated O field's source slot (found by
|
||
// fuzzing: a hoisted char at ref slot 4 hijacked the int at prim byte 4).
|
||
var key = (objS, op == Op.ldfld_sc_o ? ~(2 * off + 1) : ~(2 * off));
|
||
if (cache.TryGetValue(key, out int valueSlot)
|
||
&& (uint)valueSlot < (uint)slotCount
|
||
&& slotTypes[valueSlot] == slotTypes[dst])
|
||
{
|
||
slotMap[dst] = valueSlot;
|
||
deleted[i] = true;
|
||
deletedWords += widths[i];
|
||
}
|
||
else
|
||
{
|
||
cache[key] = dst;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Op.stfld_o:
|
||
case Op.stfld_r4:
|
||
case Op.stfld_i4:
|
||
{
|
||
int objS = (int)ir[ip + 1];
|
||
int tokIdx = (int)ir[ip + 2];
|
||
int srcS = (int)ir[ip + 3];
|
||
if ((uint)tokIdx >= (uint)tokens.Length) break;
|
||
int resolvedTok = tokens[tokIdx];
|
||
// Alias hazard: another slot may reference the SAME object (`C0 b = a;`),
|
||
// so a store through one slot must drop every cached entry for the same
|
||
// field under any OTHER object slot — else `a.f0 = 5; C0 b = a;
|
||
// b.f0 = 9; read a.f0` forwards the stale 5 (found by fuzzing).
|
||
DropAliasedField(cache, objS, resolvedTok);
|
||
// The stored value lives in srcS; subsequent loads of
|
||
// (objS, tok) can forward to it.
|
||
cache[(objS, resolvedTok)] = srcS;
|
||
break;
|
||
}
|
||
|
||
case Op.stfld_sc_o:
|
||
case Op.stfld_sc_r4:
|
||
case Op.stfld_sc_i4:
|
||
{
|
||
int objS = (int)ir[ip + 1];
|
||
int off = (int)ir[ip + 2];
|
||
int srcS = (int)ir[ip + 3];
|
||
int koff = op == Op.stfld_sc_o ? ~(2 * off + 1) : ~(2 * off); // see ldfld_sc key split
|
||
DropAliasedField(cache, objS, koff); // same alias hazard as stfld_o above
|
||
cache[(objS, koff)] = srcS;
|
||
break;
|
||
}
|
||
|
||
case Op.call_script:
|
||
case Op.call_host:
|
||
case Op.call_host_byref:
|
||
case Op.newobj_script:
|
||
case Op.newobj_host:
|
||
case Op.stsfld_i4: case Op.stsfld_r4:
|
||
case Op.stsfld_o: case Op.stsfld_struct:
|
||
case Op.stelem_i4: case Op.stelem_r4:
|
||
case Op.stelem_o: case Op.stelem_struct:
|
||
case Op.stfld_struct:
|
||
case Op.box_vt: case Op.unbox_vt: case Op.mov_vt:
|
||
case Op.stfld_vt_i4: case Op.stfld_vt_r4:
|
||
case Op.stfld_vt_b1: case Op.stfld_vt_b2:
|
||
// Multi-byte flat-struct stores overwrite RANGES that can cover any cached
|
||
// (obj, offset) entry of the same object — e.g. `bx.p = s1` (stfld_sc_vt)
|
||
// rewrites every field of p, so a cached forward of bx.p.n.v goes stale.
|
||
case Op.stfld_sc_vt: case Op.stfld_vt_vt: case Op.stelem_vt:
|
||
cache.Clear();
|
||
break;
|
||
|
||
// stfld_sc_* only update the cache (handled above); no full clear needed.
|
||
|
||
case Op.br:
|
||
case Op.brtrue_i4: case Op.brfalse_i4:
|
||
case Op.brtrue_o: case Op.brfalse_o:
|
||
case Op.blt_i4_nn: case Op.blt_i4_nk:
|
||
case Op.bgt_r4_nn: case Op.bgt_r4_nk:
|
||
case Op.beq_i4_nn: case Op.beq_i4_nk:
|
||
case Op.bne_i4_nn: case Op.bne_i4_nk:
|
||
case Op.for_i4_nk:
|
||
case Op.switch_i4:
|
||
case Op.push_cont: case Op.br_cont:
|
||
case Op.ret_void: case Op.ret_i4: case Op.ret_r4: case Op.ret_o: case Op.ret_vt:
|
||
case Op.throw_o:
|
||
// End-of-block: anything past here gets a fresh cache.
|
||
cache.Clear();
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (deletedWords == 0) return;
|
||
|
||
// Apply slotMap globally to all slot operands.
|
||
int Resolve(int s) { while (slotMap[s] != s) s = slotMap[s]; return s; }
|
||
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
RenameSlots(ir, ips[i], ops[i], Resolve);
|
||
}
|
||
|
||
// Compact + branch-target patch.
|
||
var deletedWord = new bool[n];
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (!deleted[i]) continue;
|
||
for (int k = 0; k < widths[i]; k++) deletedWord[ips[i] + k] = true;
|
||
}
|
||
var shift = new int[n + 1];
|
||
int run = 0;
|
||
for (int oldIp = 0; oldIp <= n; oldIp++)
|
||
{
|
||
shift[oldIp] = run;
|
||
if (oldIp < n && deletedWord[oldIp]) run++;
|
||
}
|
||
|
||
int newLen = n - deletedWords;
|
||
var newIr = new uint[newLen];
|
||
var newToIl = new int[newLen];
|
||
int dstIdx = 0;
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int oldIp = ips[i];
|
||
int w = widths[i];
|
||
for (int k = 0; k < w; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
PatchInsnBranchTargets(newIr, dstIdx, ops[i], shift, ir, oldIp);
|
||
dstIdx += w;
|
||
}
|
||
|
||
ir = newIr;
|
||
irToIl = newToIl;
|
||
}
|
||
|
||
// For each slot operand of `op` at `ip` in `ir`, apply `resolve` and write
|
||
// back. Mirrors AccumulateSlotUses' positions; keep both in sync.
|
||
static void RenameSlots(uint[] ir, int ip, Op op, Func<int, int> resolve)
|
||
{
|
||
void RW(int wOff)
|
||
{
|
||
int s = (int)ir[ip + wOff];
|
||
if (s < 0) return; // sentinel (-1 for void-call dst, no-receiver, etc.)
|
||
ir[ip + wOff] = (uint)resolve(s);
|
||
}
|
||
|
||
if (Is4WordArithForCoalesce(op))
|
||
{
|
||
RW(1); RW(2);
|
||
if (!IsNkForm(op)) RW(3);
|
||
return;
|
||
}
|
||
if (IsFusedCmpBranch(op))
|
||
{
|
||
RW(1);
|
||
if (!IsFusedCmpBranchNk(op)) RW(2);
|
||
return;
|
||
}
|
||
if (op == Op.for_i4_nk) { RW(1); return; }
|
||
|
||
switch (op)
|
||
{
|
||
case Op.nop: case Op.ret_void: case Op.br:
|
||
case Op.push_cont: case Op.br_cont: return;
|
||
|
||
case Op.mov: case Op.box: case Op.mov_vt: case Op.box_vt: case Op.unbox_vt:
|
||
case Op.clone_sc:
|
||
case Op.ldlen:
|
||
case Op.neg_i4: case Op.neg_i4_n: case Op.neg_r4: case Op.neg_r4_n:
|
||
case Op.not_i4: case Op.not_i4_n:
|
||
case Op.conv_i4_r4: case Op.conv_r4_i4:
|
||
case Op.conv_i4_i1: case Op.conv_i4_u1:
|
||
case Op.conv_i4_i2: case Op.conv_i4_u2:
|
||
RW(1); RW(2); return;
|
||
|
||
case Op.ldc_i4: case Op.ldc_r4: case Op.ldstr:
|
||
case Op.ldsfld_i4: case Op.ldsfld_r4: case Op.ldsfld_o: case Op.ldsfld_struct:
|
||
case Op.ldtoken:
|
||
case Op.ldnull: case Op.initobj: case Op.initobj_script: case Op.ensure_script:
|
||
RW(1); return;
|
||
|
||
case Op.stsfld_i4: case Op.stsfld_r4: case Op.stsfld_o: case Op.stsfld_struct:
|
||
RW(2); return;
|
||
|
||
case Op.ret_i4: case Op.ret_r4: case Op.ret_o: case Op.ret_vt:
|
||
case Op.throw_o:
|
||
RW(1); return;
|
||
|
||
case Op.brtrue_i4: case Op.brfalse_i4: case Op.brtrue_o: case Op.brfalse_o:
|
||
RW(1); return;
|
||
|
||
case Op.ldfld_i4: case Op.ldfld_r4: case Op.ldfld_o:
|
||
case Op.ldfld_struct: case Op.ldflda:
|
||
case Op.ldfld_vt_i4: case Op.ldfld_vt_r4: case Op.ldfld_vt_vt:
|
||
case Op.ldfld_vt_u1: case Op.ldfld_vt_i1: case Op.ldfld_vt_u2: case Op.ldfld_vt_i2:
|
||
case Op.ldfld_sc_i4: case Op.ldfld_sc_r4: case Op.ldfld_sc_o: case Op.ldfld_sc_vt:
|
||
RW(1); RW(2); return;
|
||
|
||
case Op.stfld_i4: case Op.stfld_r4: case Op.stfld_o: case Op.stfld_struct:
|
||
case Op.stfld_vt_i4: case Op.stfld_vt_r4: case Op.stfld_vt_vt:
|
||
case Op.stfld_vt_b1: case Op.stfld_vt_b2:
|
||
case Op.stfld_sc_i4: case Op.stfld_sc_r4: case Op.stfld_sc_o: case Op.stfld_sc_vt:
|
||
RW(1); RW(3); return;
|
||
|
||
case Op.ldelem_i4: case Op.ldelem_r4: case Op.ldelem_o: case Op.ldelem_struct:
|
||
case Op.ldelem_vt:
|
||
RW(1); RW(2); RW(3); return;
|
||
|
||
case Op.stelem_i4: case Op.stelem_r4: case Op.stelem_o: case Op.stelem_struct:
|
||
case Op.stelem_vt:
|
||
RW(1); RW(2); RW(3); return;
|
||
|
||
case Op.castclass: case Op.unbox_any: case Op.isinst: case Op.box_prim:
|
||
case Op.box_enum:
|
||
RW(1); RW(2); return;
|
||
|
||
case Op.newarr: case Op.newarr_i4: case Op.newarr_r4: case Op.newarr_vt:
|
||
RW(1); RW(2); return;
|
||
|
||
case Op.new_delegate:
|
||
RW(1); RW(3); return; // dst, receiver slot (word 2 is a tokIdx)
|
||
|
||
case Op.call_script:
|
||
case Op.newobj_script:
|
||
case Op.newobj_host:
|
||
{
|
||
RW(1);
|
||
int argc = (int)ir[ip + 3];
|
||
for (int k = 0; k < argc; k++) RW(4 + k);
|
||
return;
|
||
}
|
||
|
||
case Op.call_host:
|
||
{
|
||
RW(1); RW(2);
|
||
int argc = (int)ir[ip + 4];
|
||
for (int k = 0; k < argc; k++) RW(5 + k);
|
||
return;
|
||
}
|
||
|
||
case Op.call_host_byref:
|
||
{
|
||
RW(1); RW(2);
|
||
int argc = (int)ir[ip + 4];
|
||
for (int k = 0; k < argc; k++) RW(5 + k);
|
||
int wbBase = ip + 5 + argc;
|
||
int wbCount = (int)ir[wbBase];
|
||
for (int k = 0; k < wbCount; k++)
|
||
{
|
||
int kind = (int)ir[wbBase + 1 + k * 4 + 1];
|
||
// word offsets relative to ip:
|
||
int t1Off = wbBase + 1 + k * 4 + 2 - ip;
|
||
int t2Off = wbBase + 1 + k * 4 + 3 - ip;
|
||
if (kind == 0) { RW(t1Off); }
|
||
else if (kind == 1) { RW(t1Off); }
|
||
else { RW(t1Off); RW(t2Off); }
|
||
}
|
||
return;
|
||
}
|
||
|
||
case Op.switch_i4:
|
||
RW(1); return;
|
||
}
|
||
}
|
||
|
||
// --- Numeric for-loop super-instruction ---
|
||
//
|
||
// Detects the canonical Roslyn-emitted for-loop tail
|
||
// add_i4_nk slot[N], slot[N], 1
|
||
// blt_i4_nk slot[N], K_limit, body_top
|
||
// plus the matching "test-at-bottom" loop entry
|
||
// ldc_i4 slot[N] = K_init
|
||
// br -> blt_ip
|
||
// and fuses the add+blt into one `for_i4_nk slot[N], K_limit, body_top`
|
||
// dispatch. Redirects the entry br from blt_ip to body_top so the first
|
||
// body iteration isn't skipped (the executor's increment-then-test
|
||
// would otherwise consume the first slot value).
|
||
//
|
||
// Safety:
|
||
// - Step is hardcoded to 1 (the add's K must be exactly 1).
|
||
// - K_init < K_limit must hold statically — without the entry test we
|
||
// can't guarantee zero-iter loops behave correctly.
|
||
// - The induction slot is single-write across the loop entry pattern
|
||
// (the ldc + the add). This rules out odd cases where the body
|
||
// re-initializes the counter.
|
||
//
|
||
// Not handled:
|
||
// - Reverse loops (`i--`).
|
||
// - Step != 1.
|
||
// - r4 induction (rare in idiomatic C#, accumulates error).
|
||
// - Slot-valued limit (`for (int i=0; i<n; i++)` where n is a local).
|
||
// Bench corpus has no instances; add `for_i4_n` on demand.
|
||
static void FoldForIntLoop(ref uint[] ir, ref int[] irToIl, SType[] slotTypes)
|
||
{
|
||
int n = ir.Length;
|
||
if (n == 0) return;
|
||
|
||
var ips = new List<int>(n / 3);
|
||
var ops = new List<Op>(n / 3);
|
||
var widths = new List<int>(n / 3);
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0) return;
|
||
ips.Add(pos); ops.Add(op); widths.Add(w);
|
||
pos += w;
|
||
}
|
||
int insnCount = ips.Count;
|
||
|
||
// Map IP → instruction index for lookup.
|
||
var ipToIdx = new Dictionary<int, int>(insnCount);
|
||
for (int i = 0; i < insnCount; i++) ipToIdx[ips[i]] = i;
|
||
|
||
// Branch-target set: which IPs are jumped to by some branch?
|
||
var isBranchTarget = new bool[n + 1];
|
||
for (int i = 0; i < insnCount; i++)
|
||
MarkInsnBranchTargets(ir, ips[i], ops[i], isBranchTarget);
|
||
|
||
var deleted = new bool[insnCount];
|
||
int deletedWords = 0;
|
||
|
||
for (int i = 1; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
if (ops[i] != Op.blt_i4_nk) continue;
|
||
int bltIp = ips[i];
|
||
int slotN = (int)ir[bltIp + 1];
|
||
int kLimit = (int)ir[bltIp + 2];
|
||
int bodyTop = (int)ir[bltIp + 3];
|
||
if (bodyTop >= bltIp) continue; // must be a backward branch
|
||
if (!ipToIdx.ContainsKey(bodyTop)) continue;
|
||
|
||
// Preceding instruction must be add_i4_nk slot[N], slot[N], 1.
|
||
int j = i - 1;
|
||
if (deleted[j] || ops[j] != Op.add_i4_nk) continue;
|
||
int addIp = ips[j];
|
||
if ((int)ir[addIp + 1] != slotN) continue; // dst == N
|
||
if ((int)ir[addIp + 2] != slotN) continue; // src == N (in-place)
|
||
if ((int)ir[addIp + 3] != 1) continue; // step == 1
|
||
|
||
// Two cases:
|
||
// (a) Test-at-bottom loop: an entry `br -> bltIp` exists. We must
|
||
// redirect it to body_top so the for_i4_nk doesn't consume the
|
||
// first body iteration. Requires statically provable
|
||
// K_init < K_limit (otherwise we'd execute the body once when
|
||
// the original loop wouldn't have run at all).
|
||
// (b) Test-at-top / do-while loop: nothing branches to the blt. The
|
||
// loop is entered by fall-through and exited by fall-through past
|
||
// the blt. for_i4_nk has identical semantics to add+blt at the
|
||
// loop tail, so the fuse is unconditionally safe.
|
||
bool needRedirect = isBranchTarget[bltIp];
|
||
if (needRedirect)
|
||
{
|
||
if (!ipToIdx.TryGetValue(bodyTop, out int topIdx)) continue;
|
||
if (topIdx < 2) continue;
|
||
int brIdx = topIdx - 1;
|
||
if (deleted[brIdx] || ops[brIdx] != Op.br) continue;
|
||
int brIp = ips[brIdx];
|
||
if ((int)ir[brIp + 1] != bltIp) continue; // br must target the blt
|
||
|
||
int initIdx = brIdx - 1;
|
||
if (initIdx < 0 || deleted[initIdx]) continue;
|
||
if (ops[initIdx] != Op.ldc_i4) continue;
|
||
int initIp = ips[initIdx];
|
||
if ((int)ir[initIp + 1] != slotN) continue;
|
||
int kInit = (int)ir[initIp + 2];
|
||
if (kInit >= kLimit) continue; // entry-check guard
|
||
|
||
// Redirect entry br to body_top.
|
||
ir[brIp + 1] = (uint)bodyTop;
|
||
}
|
||
|
||
// Apply: rewrite add → for_i4_nk in place; mark blt for deletion.
|
||
ir[addIp + 0] = (uint)Op.for_i4_nk;
|
||
ir[addIp + 1] = (uint)slotN;
|
||
ir[addIp + 2] = (uint)kLimit;
|
||
ir[addIp + 3] = (uint)bodyTop;
|
||
ops[j] = Op.for_i4_nk;
|
||
|
||
deleted[i] = true;
|
||
deletedWords += widths[i];
|
||
}
|
||
|
||
if (deletedWords == 0) return;
|
||
|
||
var deletedWord = new bool[n];
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (!deleted[i]) continue;
|
||
for (int k = 0; k < widths[i]; k++) deletedWord[ips[i] + k] = true;
|
||
}
|
||
var shift = new int[n + 1];
|
||
int run = 0;
|
||
for (int oldIp = 0; oldIp <= n; oldIp++)
|
||
{
|
||
shift[oldIp] = run;
|
||
if (oldIp < n && deletedWord[oldIp]) run++;
|
||
}
|
||
|
||
int newLen = n - deletedWords;
|
||
var newIr = new uint[newLen];
|
||
var newToIl = new int[newLen];
|
||
int dstIdx = 0;
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int oldIp = ips[i];
|
||
int w = widths[i];
|
||
for (int k = 0; k < w; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
PatchInsnBranchTargets(newIr, dstIdx, ops[i], shift, ir, oldIp);
|
||
dstIdx += w;
|
||
}
|
||
|
||
ir = newIr;
|
||
irToIl = newToIl;
|
||
}
|
||
|
||
// --- Loop-invariant constant hoisting ---
|
||
//
|
||
// A `ldc_i4 / ldc_r4 / ldstr` inside a loop re-executes every iteration even though it
|
||
// always produces the same value into the same slot. Profiling showed constant reloads at
|
||
// 10–38% of all dispatches on loop-heavy bodies (`i & 255`, `Translate(0.01f, 0f, -0.01f)`).
|
||
// This pass moves qualifying ldcs into a prologue emitted once at IR entry.
|
||
//
|
||
// A candidate ldc must satisfy:
|
||
// - writes[dst] == 1: this ldc is the slot's only writer in the whole method. C# definite
|
||
// assignment then guarantees the write dominates every read, so "execute once up front"
|
||
// observes the same values (re-executions were idempotent).
|
||
// - dst >= argCount: arg slots are populated by the CALLER before Run enters the IR;
|
||
// a prologue write would clobber the incoming argument.
|
||
// - the ldc sits inside a loop body — between a backward branch and its target. Hoisting
|
||
// straight-line or cold-branch constants would ADD a dispatch per call, not remove one.
|
||
//
|
||
// Branch targets all shift by the prologue length; the shared shift-table mechanism handles
|
||
// that with negative entries (newTarget = old - shift[old]).
|
||
static void HoistLoopConsts(ref uint[] ir, ref int[] irToIl, SType[] slotTypes, int argCount)
|
||
{
|
||
int n = ir.Length;
|
||
if (n == 0) return;
|
||
|
||
var ips = new List<int>(n / 3);
|
||
var ops = new List<Op>(n / 3);
|
||
var widths = new List<int>(n / 3);
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0) return;
|
||
ips.Add(pos); ops.Add(op); widths.Add(w);
|
||
pos += w;
|
||
}
|
||
int insnCount = ips.Count;
|
||
|
||
int slotCount = slotTypes.Length;
|
||
var reads = new int[slotCount];
|
||
var writes = new int[slotCount];
|
||
for (int i = 0; i < insnCount; i++)
|
||
AccumulateSlotUses(ir, ips[i], ops[i], widths[i], reads, writes, slotCount);
|
||
|
||
// Mark loop-body words: for every backward branch at ip with target t <= ip, the
|
||
// whole [t, ip] range is (over-approximately) a loop body. Difference array over words.
|
||
var loopDelta = new int[n + 2];
|
||
void MarkLoop(int from, int to) { if (to <= from) { loopDelta[to]++; loopDelta[from + 1]--; } }
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
int ip = ips[i];
|
||
switch (ops[i])
|
||
{
|
||
case Op.br: case Op.push_cont: MarkLoop(ip, (int)ir[ip + 1]); break;
|
||
case Op.brtrue_i4: case Op.brfalse_i4:
|
||
case Op.brtrue_o: case Op.brfalse_o: MarkLoop(ip, (int)ir[ip + 2]); break;
|
||
case Op.blt_i4_nn: case Op.blt_i4_nk:
|
||
case Op.bgt_r4_nn: case Op.bgt_r4_nk:
|
||
case Op.beq_i4_nn: case Op.beq_i4_nk:
|
||
case Op.bne_i4_nn: case Op.bne_i4_nk:
|
||
case Op.for_i4_nk: MarkLoop(ip, (int)ir[ip + 3]); break;
|
||
case Op.switch_i4:
|
||
{
|
||
int cnt = (int)ir[ip + 2];
|
||
MarkLoop(ip, (int)ir[ip + 3]);
|
||
for (int k = 0; k < cnt; k++) MarkLoop(ip, (int)ir[ip + 4 + k]);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
var inLoop = new bool[n];
|
||
int depth = 0;
|
||
for (int w2 = 0; w2 < n; w2++) { depth += loopDelta[w2]; inLoop[w2] = depth > 0; }
|
||
|
||
var hoisted = new List<int>(); // instruction indexes to move
|
||
var deleted = new bool[insnCount];
|
||
int hoistWords = 0;
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
var op = ops[i];
|
||
if (op != Op.ldc_i4 && op != Op.ldc_r4 && op != Op.ldstr) continue;
|
||
int ip = ips[i];
|
||
if (!inLoop[ip]) continue;
|
||
int dst = (int)ir[ip + 1];
|
||
if (dst < argCount) continue;
|
||
if ((uint)dst >= (uint)slotCount) continue;
|
||
if (writes[dst] != 1 || reads[dst] == 0) continue;
|
||
hoisted.Add(i);
|
||
deleted[i] = true;
|
||
hoistWords += widths[i];
|
||
}
|
||
if (hoisted.Count == 0) return;
|
||
|
||
// shift[t] = (deleted words strictly before t) - prologueLen, so
|
||
// newTarget = t - shift[t] lands on the surviving instruction, offset by the prologue.
|
||
var deletedWord = new bool[n];
|
||
foreach (var i in hoisted)
|
||
for (int k = 0; k < widths[i]; k++) deletedWord[ips[i] + k] = true;
|
||
var shift = new int[n + 1];
|
||
int run = 0;
|
||
for (int oldIp = 0; oldIp <= n; oldIp++)
|
||
{
|
||
shift[oldIp] = run - hoistWords;
|
||
if (oldIp < n && deletedWord[oldIp]) run++;
|
||
}
|
||
|
||
int newLen = n; // deleted words re-emitted verbatim in the prologue
|
||
var newIr = new uint[newLen];
|
||
var newToIl = new int[newLen];
|
||
int dstIdx = 0;
|
||
foreach (var i in hoisted)
|
||
{
|
||
int oldIp = ips[i];
|
||
for (int k = 0; k < widths[i]; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
dstIdx += widths[i];
|
||
}
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int oldIp = ips[i];
|
||
int w = widths[i];
|
||
for (int k = 0; k < w; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
PatchInsnBranchTargets(newIr, dstIdx, ops[i], shift, ir, oldIp);
|
||
dstIdx += w;
|
||
}
|
||
|
||
ir = newIr;
|
||
irToIl = newToIl;
|
||
}
|
||
|
||
// --- Fused compare-and-branch ---
|
||
//
|
||
// Detects `compare_X_form dst, ...; brtrue/brfalse dst, target` where
|
||
// dst is single-use (the brtrue/brfalse is its only reader) and folds
|
||
// into one fused `bX_form ...args, target`. Runs AFTER CoalesceDestMovs
|
||
// so that the compare's dst is already the local slot the branch
|
||
// reads — pattern detection becomes a clean two-instruction window.
|
||
//
|
||
// Mappings (only ones with bench-corpus evidence):
|
||
// clt_i4_nn + brtrue → blt_i4_nn ceq_i4_nn + brtrue → beq_i4_nn
|
||
// clt_i4_nk + brtrue → blt_i4_nk ceq_i4_nk + brtrue → beq_i4_nk
|
||
// cgt_r4_nn + brtrue → bgt_r4_nn ceq_i4_nn + brfalse → bne_i4_nn
|
||
// cgt_r4_nk + brtrue → bgt_r4_nk ceq_i4_nk + brfalse → bne_i4_nk
|
||
//
|
||
// Other polarity combinations (clt+brfalse, cgt+brfalse, etc.) are
|
||
// left as two ops — adding "bge"/"ble" etc. would double the opcode
|
||
// count for patterns we don't have. Add on demand.
|
||
static void FoldCompareBranch(ref uint[] ir, ref int[] irToIl, SType[] slotTypes)
|
||
{
|
||
int n = ir.Length;
|
||
if (n == 0) return;
|
||
|
||
var ips = new List<int>(n / 3);
|
||
var ops = new List<Op>(n / 3);
|
||
var widths = new List<int>(n / 3);
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0) return;
|
||
ips.Add(pos); ops.Add(op); widths.Add(w);
|
||
pos += w;
|
||
}
|
||
int insnCount = ips.Count;
|
||
|
||
int slotCount = slotTypes.Length;
|
||
var reads = new int[slotCount];
|
||
var writes = new int[slotCount];
|
||
for (int i = 0; i < insnCount; i++)
|
||
AccumulateSlotUses(ir, ips[i], ops[i], widths[i], reads, writes, slotCount);
|
||
|
||
var isBranchTarget = new bool[n + 1];
|
||
for (int i = 0; i < insnCount; i++)
|
||
MarkInsnBranchTargets(ir, ips[i], ops[i], isBranchTarget);
|
||
|
||
var deleted = new bool[insnCount];
|
||
int deletedWords = 0;
|
||
for (int i = 0; i < insnCount - 1; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int cmpIp = ips[i];
|
||
if (widths[i] != 4) continue;
|
||
int dstSlot = (int)ir[cmpIp + 1];
|
||
if ((uint)dstSlot >= (uint)slotCount) continue; // -1 sentinel (e.g. zero-arg void call_script)
|
||
if (writes[dstSlot] != 1 || reads[dstSlot] != 1) continue;
|
||
|
||
int j = i + 1;
|
||
if (j >= insnCount || deleted[j]) continue;
|
||
var brOp = ops[j];
|
||
if (brOp != Op.brtrue_i4 && brOp != Op.brfalse_i4) continue;
|
||
int brIp = ips[j];
|
||
if (isBranchTarget[brIp]) continue;
|
||
if ((int)ir[brIp + 1] != dstSlot) continue;
|
||
|
||
bool brIfTrue = (brOp == Op.brtrue_i4);
|
||
if (!TryFoldCompareBranch(ops[i], brIfTrue, out Op fusedOp)) continue;
|
||
|
||
uint target = ir[brIp + 2];
|
||
|
||
// Rewrite cmp in place: [op, s1, s2_or_k, target_ip]
|
||
ir[cmpIp] = (uint)fusedOp;
|
||
ir[cmpIp + 1] = ir[cmpIp + 2]; // slide src1 from word 2 → word 1
|
||
ir[cmpIp + 2] = ir[cmpIp + 3]; // slide src2/k from word 3 → word 2
|
||
ir[cmpIp + 3] = target; // target at word 3
|
||
|
||
// Update cached op so PatchInsnBranchTargets sees the new opcode and
|
||
// applies the shift to word 3 (not the original cmp's word 3, which
|
||
// was an operand and shouldn't be shifted).
|
||
ops[i] = fusedOp;
|
||
|
||
deleted[j] = true;
|
||
deletedWords += widths[j];
|
||
|
||
reads[dstSlot] = 0; writes[dstSlot] = 0;
|
||
}
|
||
|
||
if (deletedWords == 0) return;
|
||
|
||
// Compact + branch-target patch.
|
||
var deletedWord = new bool[n];
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (!deleted[i]) continue;
|
||
for (int k = 0; k < widths[i]; k++) deletedWord[ips[i] + k] = true;
|
||
}
|
||
var shift = new int[n + 1];
|
||
int run = 0;
|
||
for (int oldIp = 0; oldIp <= n; oldIp++)
|
||
{
|
||
shift[oldIp] = run;
|
||
if (oldIp < n && deletedWord[oldIp]) run++;
|
||
}
|
||
|
||
int newLen = n - deletedWords;
|
||
var newIr = new uint[newLen];
|
||
var newToIl = new int[newLen];
|
||
int dstIdx = 0;
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int oldIp = ips[i];
|
||
int w = widths[i];
|
||
for (int k = 0; k < w; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
PatchInsnBranchTargets(newIr, dstIdx, ops[i], shift, ir, oldIp);
|
||
dstIdx += w;
|
||
}
|
||
|
||
ir = newIr;
|
||
irToIl = newToIl;
|
||
}
|
||
|
||
// True when the op writes a frame slot at ir[ip+1] (its dst). False for control flow, stores
|
||
// (which write memory, not a frame slot), and returns. Used by ForwardRedundantLoads to know
|
||
// which cached slots a reassignment invalidates. Calls/newobj also write a dst but clear the
|
||
// whole cache separately, so treating them as writers here is harmless.
|
||
static bool WritesFrameDstAt1(Op op) => op switch
|
||
{
|
||
Op.nop or Op.br
|
||
or Op.brtrue_i4 or Op.brfalse_i4 or Op.brtrue_o or Op.brfalse_o
|
||
or Op.ret_void or Op.ret_i4 or Op.ret_r4 or Op.ret_o or Op.throw_o
|
||
or Op.stfld_i4 or Op.stfld_r4 or Op.stfld_o or Op.stfld_struct
|
||
or Op.stfld_sc_i4 or Op.stfld_sc_r4 or Op.stfld_sc_o or Op.stfld_sc_vt
|
||
or Op.stfld_vt_i4 or Op.stfld_vt_r4 or Op.stfld_vt_b1 or Op.stfld_vt_b2
|
||
or Op.stelem_i4 or Op.stelem_r4 or Op.stelem_o or Op.stelem_struct
|
||
or Op.stsfld_i4 or Op.stsfld_r4 or Op.stsfld_o or Op.stsfld_struct
|
||
or Op.switch_i4
|
||
or Op.blt_i4_nn or Op.blt_i4_nk or Op.bgt_r4_nn or Op.bgt_r4_nk
|
||
or Op.beq_i4_nn or Op.beq_i4_nk or Op.bne_i4_nn or Op.bne_i4_nk
|
||
or Op.push_cont or Op.br_cont => false,
|
||
_ => true,
|
||
};
|
||
|
||
static bool TryFoldCompareBranch(Op cmpOp, bool brIfTrue, out Op fused)
|
||
{
|
||
fused = default;
|
||
switch (cmpOp)
|
||
{
|
||
case Op.clt_i4_nn: if (brIfTrue) { fused = Op.blt_i4_nn; return true; } return false;
|
||
case Op.clt_i4_nk: if (brIfTrue) { fused = Op.blt_i4_nk; return true; } return false;
|
||
case Op.cgt_r4_nn: if (brIfTrue) { fused = Op.bgt_r4_nn; return true; } return false;
|
||
case Op.cgt_r4_nk: if (brIfTrue) { fused = Op.bgt_r4_nk; return true; } return false;
|
||
case Op.ceq_i4_nn: fused = brIfTrue ? Op.beq_i4_nn : Op.bne_i4_nn; return true;
|
||
case Op.ceq_i4_nk: fused = brIfTrue ? Op.beq_i4_nk : Op.bne_i4_nk; return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
// Helpers shared by all three peephole passes.
|
||
static void MarkInsnBranchTargets(uint[] ir, int ip, Op op, bool[] isBranchTarget)
|
||
{
|
||
switch (op)
|
||
{
|
||
case Op.br:
|
||
case Op.push_cont:
|
||
MarkTarget(isBranchTarget, ir[ip + 1]); break;
|
||
case Op.brtrue_i4: case Op.brfalse_i4:
|
||
case Op.brtrue_o: case Op.brfalse_o:
|
||
MarkTarget(isBranchTarget, ir[ip + 2]); break;
|
||
case Op.blt_i4_nn: case Op.blt_i4_nk:
|
||
case Op.bgt_r4_nn: case Op.bgt_r4_nk:
|
||
case Op.beq_i4_nn: case Op.beq_i4_nk:
|
||
case Op.bne_i4_nn: case Op.bne_i4_nk:
|
||
case Op.for_i4_nk:
|
||
MarkTarget(isBranchTarget, ir[ip + 3]); break;
|
||
case Op.switch_i4:
|
||
{
|
||
int cnt = (int)ir[ip + 2];
|
||
MarkTarget(isBranchTarget, ir[ip + 3]);
|
||
for (int k = 0; k < cnt; k++)
|
||
MarkTarget(isBranchTarget, ir[ip + 4 + k]);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
static void PatchInsnBranchTargets(uint[] newIr, int dstIdx, Op op, int[] shift,
|
||
uint[] oldIr, int oldIp)
|
||
{
|
||
switch (op)
|
||
{
|
||
case Op.br:
|
||
case Op.push_cont:
|
||
newIr[dstIdx + 1] = (uint)((int)oldIr[oldIp + 1] - shift[(int)oldIr[oldIp + 1]]);
|
||
break;
|
||
case Op.brtrue_i4: case Op.brfalse_i4:
|
||
case Op.brtrue_o: case Op.brfalse_o:
|
||
case Op.brtrue_i8: case Op.brfalse_i8:
|
||
newIr[dstIdx + 2] = (uint)((int)oldIr[oldIp + 2] - shift[(int)oldIr[oldIp + 2]]);
|
||
break;
|
||
case Op.blt_i4_nn: case Op.blt_i4_nk:
|
||
case Op.bgt_r4_nn: case Op.bgt_r4_nk:
|
||
case Op.beq_i4_nn: case Op.beq_i4_nk:
|
||
case Op.bne_i4_nn: case Op.bne_i4_nk:
|
||
case Op.for_i4_nk:
|
||
newIr[dstIdx + 3] = (uint)((int)oldIr[oldIp + 3] - shift[(int)oldIr[oldIp + 3]]);
|
||
break;
|
||
case Op.switch_i4:
|
||
{
|
||
int cnt = (int)oldIr[oldIp + 2];
|
||
int defT = (int)oldIr[oldIp + 3];
|
||
newIr[dstIdx + 3] = (uint)(defT - shift[defT]);
|
||
for (int k = 0; k < cnt; k++)
|
||
{
|
||
int t = (int)oldIr[oldIp + 4 + k];
|
||
newIr[dstIdx + 4 + k] = (uint)(t - shift[t]);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Dest-coalescing peephole ---
|
||
//
|
||
// Eliminates `mov dst=X, src=tmp` where `tmp` is a single-use temp produced by
|
||
// the immediately-preceding instruction. Rewrites that instruction's dst to X
|
||
// and deletes the mov. Run after branch-target patching so target IPs are
|
||
// word-indexed; we re-patch them after compaction.
|
||
//
|
||
// Safety:
|
||
// - tmp has exactly 1 read (the candidate mov) and 1 write (prev op) globally.
|
||
// - slotTypes[tmp] == slotTypes[X], and neither is Vt — guarantees the prev
|
||
// op's frame write (numFrame vs refFrame, byte vs ref) is valid for X.
|
||
// - The mov's IP is not a branch target.
|
||
// - Prev op is in a conservative whitelist (IsRetargetable) of ops that write
|
||
// their dst at words[1] with no other side effects on dst.
|
||
static void CoalesceDestMovs(ref uint[] ir, ref int[] irToIl, SType[] slotTypes)
|
||
{
|
||
int n = ir.Length;
|
||
if (n == 0) return;
|
||
|
||
// Walk IR once: record (ip, op, width) per instruction. Bail out if any
|
||
// op has unknown width — leaves IR untouched.
|
||
var ips = new List<int>(n / 3);
|
||
var ops = new List<Op>(n / 3);
|
||
var widths = new List<int>(n / 3);
|
||
int pos = 0;
|
||
while (pos < n)
|
||
{
|
||
var op = (Op)ir[pos];
|
||
int w = OpWidthForCoalesce(op, ir, pos);
|
||
if (w <= 0) return;
|
||
ips.Add(pos); ops.Add(op); widths.Add(w);
|
||
pos += w;
|
||
}
|
||
int insnCount = ips.Count;
|
||
|
||
// Per-slot read/write counts. Conservative: any value in a "slot position"
|
||
// is counted, even if -1 (sentinel for void/static); -1 just bumps a phantom
|
||
// entry that nothing else references.
|
||
int slotCount = slotTypes.Length;
|
||
var reads = new int[slotCount];
|
||
var writes = new int[slotCount];
|
||
for (int i = 0; i < insnCount; i++)
|
||
AccumulateSlotUses(ir, ips[i], ops[i], widths[i], reads, writes, slotCount);
|
||
|
||
// Set of branch-target word IPs. Branches target alive-instruction IPs; we
|
||
// refuse to delete a mov that anything jumps to.
|
||
var isBranchTarget = new bool[n + 1];
|
||
for (int i = 0; i < insnCount; i++)
|
||
MarkInsnBranchTargets(ir, ips[i], ops[i], isBranchTarget);
|
||
|
||
// Identify mov instructions to coalesce.
|
||
var deleted = new bool[insnCount];
|
||
int deletedWords = 0;
|
||
for (int i = 1; i < insnCount; i++)
|
||
{
|
||
if (ops[i] != Op.mov) continue;
|
||
int movIp = ips[i];
|
||
if (isBranchTarget[movIp]) continue;
|
||
|
||
int dstSlot = (int)ir[movIp + 1];
|
||
int srcSlot = (int)ir[movIp + 2];
|
||
if ((uint)dstSlot >= (uint)slotCount || (uint)srcSlot >= (uint)slotCount) continue;
|
||
|
||
var stSrc = slotTypes[srcSlot];
|
||
var stDst = slotTypes[dstSlot];
|
||
if (stSrc == SType.Vt || stDst == SType.Vt) continue;
|
||
if (stSrc != stDst) continue;
|
||
|
||
if (writes[srcSlot] != 1 || reads[srcSlot] != 1) continue;
|
||
|
||
// Prev instruction must define srcSlot in retargetable position.
|
||
var prevOp = ops[i - 1];
|
||
if (deleted[i - 1]) continue;
|
||
if (!IsRetargetable(prevOp)) continue;
|
||
int prevDstWord = ips[i - 1] + 1;
|
||
if ((int)ir[prevDstWord] != srcSlot) continue;
|
||
|
||
// Apply rewrite + mark deletion.
|
||
ir[prevDstWord] = (uint)dstSlot;
|
||
deleted[i] = true;
|
||
deletedWords += widths[i];
|
||
|
||
// Update counts so a chained mov (X <- Y where Y was just rewritten
|
||
// away) sees the new state. srcSlot loses its lone read+write; dstSlot
|
||
// gains one of each (the rewritten prev op now reads/writes dst).
|
||
reads[srcSlot] = 0; writes[srcSlot] = 0;
|
||
writes[dstSlot]++;
|
||
}
|
||
|
||
if (deletedWords == 0) return;
|
||
|
||
// Build old-IP → new-IP shift table for branch-target patching.
|
||
// shift[oldIp] = total deleted words at positions strictly less than oldIp.
|
||
var deletedWord = new bool[n];
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (!deleted[i]) continue;
|
||
for (int k = 0; k < widths[i]; k++) deletedWord[ips[i] + k] = true;
|
||
}
|
||
var shift = new int[n + 1];
|
||
int run = 0;
|
||
for (int oldIp = 0; oldIp <= n; oldIp++)
|
||
{
|
||
shift[oldIp] = run;
|
||
if (oldIp < n && deletedWord[oldIp]) run++;
|
||
}
|
||
|
||
// Compact: copy alive instructions, patching branch targets via shift.
|
||
int newLen = n - deletedWords;
|
||
var newIr = new uint[newLen];
|
||
var newToIl = new int[newLen];
|
||
int dstIdx = 0;
|
||
for (int i = 0; i < insnCount; i++)
|
||
{
|
||
if (deleted[i]) continue;
|
||
int oldIp = ips[i];
|
||
int w = widths[i];
|
||
for (int k = 0; k < w; k++)
|
||
{
|
||
newIr[dstIdx + k] = ir[oldIp + k];
|
||
newToIl[dstIdx + k] = irToIl[oldIp + k];
|
||
}
|
||
switch (ops[i])
|
||
{
|
||
case Op.br:
|
||
case Op.push_cont:
|
||
newIr[dstIdx + 1] = (uint)((int)ir[oldIp + 1] - shift[(int)ir[oldIp + 1]]);
|
||
break;
|
||
case Op.brtrue_i4: case Op.brfalse_i4:
|
||
case Op.brtrue_o: case Op.brfalse_o:
|
||
newIr[dstIdx + 2] = (uint)((int)ir[oldIp + 2] - shift[(int)ir[oldIp + 2]]);
|
||
break;
|
||
case Op.switch_i4:
|
||
{
|
||
int cnt = (int)ir[oldIp + 2];
|
||
int defT = (int)ir[oldIp + 3];
|
||
newIr[dstIdx + 3] = (uint)(defT - shift[defT]);
|
||
for (int k = 0; k < cnt; k++)
|
||
{
|
||
int t = (int)ir[oldIp + 4 + k];
|
||
newIr[dstIdx + 4 + k] = (uint)(t - shift[t]);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
dstIdx += w;
|
||
}
|
||
|
||
ir = newIr;
|
||
irToIl = newToIl;
|
||
}
|
||
|
||
static void MarkTarget(bool[] arr, uint target)
|
||
{
|
||
int t = (int)target;
|
||
if (t >= 0 && t < arr.Length) arr[t] = true;
|
||
}
|
||
|
||
static bool IsNkForm(Op op) => op switch
|
||
{
|
||
Op.add_i4_nk or Op.add_r4_nk or Op.sub_i4_nk or Op.sub_r4_nk
|
||
or Op.mul_r4_nk
|
||
or Op.clt_i4_nk or Op.clt_r4_nk or Op.cgt_i4_nk or Op.cgt_r4_nk
|
||
or Op.ceq_i4_nk
|
||
or Op.mul_i4_nk or Op.div_i4_nk or Op.rem_i4_nk
|
||
or Op.and_i4_nk or Op.or_i4_nk or Op.xor_i4_nk
|
||
or Op.shl_i4_nk or Op.shr_i4_nk or Op.shr_un_i4_nk
|
||
or Op.div_r4_nk or Op.ceq_r4_nk => true,
|
||
_ => false,
|
||
};
|
||
|
||
static bool IsFusedCmpBranch(Op op) => op switch
|
||
{
|
||
Op.blt_i4_nn or Op.blt_i4_nk
|
||
or Op.bgt_r4_nn or Op.bgt_r4_nk
|
||
or Op.beq_i4_nn or Op.beq_i4_nk
|
||
or Op.bne_i4_nn or Op.bne_i4_nk => true,
|
||
_ => false,
|
||
};
|
||
|
||
// True when the second operand at word 2 is an inline immediate, not a slot.
|
||
static bool IsFusedCmpBranchNk(Op op) => op switch
|
||
{
|
||
Op.blt_i4_nk or Op.bgt_r4_nk or Op.beq_i4_nk or Op.bne_i4_nk => true,
|
||
_ => false,
|
||
};
|
||
|
||
// Whitelist of ops whose only effect (on the frame) is writing words[1] as a slot index.
|
||
// call_host_byref and Vt-dst ops are excluded conservatively (byref write-back touches
|
||
// slots beyond dst; Vt dsts span multiple numeric words — the coalesce pass's "types
|
||
// equal, neither Vt" check would reject them anyway).
|
||
// Calls and newobjs ARE retargetable: their dst is written exactly once, type-routed by
|
||
// slotT (numeric or ref frame), after all argument reads — the profile showed 12-25% of
|
||
// dispatches were `call tmp; mov local <- tmp` pairs this unlocks.
|
||
static bool IsRetargetable(Op op)
|
||
{
|
||
if (Is4WordArithForCoalesce(op)) return true;
|
||
return op switch
|
||
{
|
||
Op.call_host or Op.call_script or Op.newobj_host or Op.newobj_script
|
||
or Op.new_delegate => true,
|
||
Op.ldc_i4 or Op.ldc_r4 or Op.ldnull or Op.ldstr => true,
|
||
Op.neg_i4 or Op.neg_i4_n or Op.neg_r4 or Op.neg_r4_n
|
||
or Op.not_i4 or Op.not_i4_n => true,
|
||
Op.conv_i4_r4 or Op.conv_r4_i4 or Op.conv_i4_i1 or Op.conv_i4_u1
|
||
or Op.conv_i4_i2 or Op.conv_i4_u2 => true,
|
||
Op.ldfld_i4 or Op.ldfld_r4 or Op.ldfld_o or Op.ldflda => true,
|
||
Op.ldfld_sc_i4 or Op.ldfld_sc_r4 or Op.ldfld_sc_o or Op.ldfld_sc_vt => true,
|
||
Op.ldsfld_i4 or Op.ldsfld_r4 or Op.ldsfld_o => true,
|
||
Op.ldlen => true,
|
||
Op.ldelem_i4 or Op.ldelem_r4 or Op.ldelem_o => true,
|
||
Op.ldtoken => true,
|
||
Op.castclass or Op.unbox_any or Op.isinst => true,
|
||
Op.box or Op.box_prim or Op.box_enum => true,
|
||
Op.ldfld_vt_i4 or Op.ldfld_vt_r4 => true, // dst is numeric, not Vt
|
||
Op.ldfld_vt_u1 or Op.ldfld_vt_i1 or Op.ldfld_vt_u2 or Op.ldfld_vt_i2 => true,
|
||
Op.box_vt => true, // dst is O
|
||
_ => false,
|
||
};
|
||
}
|
||
|
||
// Mirrors the executor's per-op ip-advance. Returns -1 for ops we don't
|
||
// recognise (causes the pass to bail out, leaving IR untouched).
|
||
internal static int OpWidthForCoalesce(Op op, uint[] ir, int ip)
|
||
{
|
||
if (Is4WordArithForCoalesce(op)) return 4;
|
||
if (IsFusedCmpBranch(op)) return 4;
|
||
if (op == Op.for_i4_nk) return 4;
|
||
return op switch
|
||
{
|
||
Op.nop or Op.ret_void or Op.br_cont => 1,
|
||
Op.br or Op.push_cont or Op.ret_i4 or Op.ret_r4 or Op.ret_o or Op.ret_vt or Op.throw_o or Op.ldnull or Op.initobj
|
||
or Op.ret_i8 or Op.ret_r8 => 2,
|
||
|
||
// 64-bit family — 4-word binops/constants, 3-word convs/unaries/branches. These
|
||
// never reach the optimizer passes (wide frames skip them) but the blob relocator
|
||
// widths must be exact.
|
||
Op.ldc_i8 or Op.ldc_r8
|
||
or Op.add_i8 or Op.sub_i8 or Op.mul_i8 or Op.div_i8 or Op.rem_i8
|
||
or Op.div_un_i8 or Op.rem_un_i8
|
||
or Op.and_i8 or Op.or_i8 or Op.xor_i8
|
||
or Op.shl_i8 or Op.shr_i8 or Op.shr_un_i8
|
||
or Op.add_r8 or Op.sub_r8 or Op.mul_r8 or Op.div_r8 or Op.rem_r8
|
||
or Op.ceq_i8 or Op.cgt_i8 or Op.clt_i8 or Op.cgt_un_i8 or Op.clt_un_i8
|
||
or Op.ceq_r8 or Op.cgt_r8 or Op.clt_r8 or Op.cgt_un_r8 or Op.clt_un_r8 => 4,
|
||
|
||
Op.neg_i8 or Op.not_i8 or Op.neg_r8
|
||
or Op.conv_i8_i4 or Op.conv_i8_u4 or Op.conv_i4_i8
|
||
or Op.conv_i8_r4 or Op.conv_r4_i8 or Op.conv_i8_r8 or Op.conv_r8_i8
|
||
or Op.conv_r8_r4 or Op.conv_r4_r8 or Op.conv_r8_i4 or Op.conv_i4_r8
|
||
or Op.conv_r8_u8
|
||
or Op.brtrue_i8 or Op.brfalse_i8 => 3,
|
||
|
||
Op.mov or Op.box or Op.mov_vt or Op.box_vt or Op.unbox_vt or Op.clone_sc
|
||
or Op.ldlen or Op.neg_i4 or Op.neg_i4_n or Op.neg_r4 or Op.neg_r4_n
|
||
or Op.not_i4 or Op.not_i4_n
|
||
or Op.conv_i4_r4 or Op.conv_r4_i4 or Op.conv_i4_i1 or Op.conv_i4_u1
|
||
or Op.conv_i4_i2 or Op.conv_i4_u2
|
||
or Op.initobj_script or Op.ensure_script or Op.ldtoken
|
||
or Op.ldc_i4 or Op.ldc_r4 or Op.ldstr
|
||
or Op.ldsfld_i4 or Op.ldsfld_r4 or Op.ldsfld_o or Op.ldsfld_struct
|
||
or Op.stsfld_i4 or Op.stsfld_r4 or Op.stsfld_o or Op.stsfld_struct
|
||
or Op.brtrue_i4 or Op.brfalse_i4 or Op.brtrue_o or Op.brfalse_o => 3,
|
||
|
||
Op.ldfld_i4 or Op.ldfld_r4 or Op.ldfld_o or Op.ldfld_struct or Op.ldflda
|
||
or Op.stfld_i4 or Op.stfld_r4 or Op.stfld_o or Op.stfld_struct
|
||
or Op.ldelem_i4 or Op.ldelem_r4 or Op.ldelem_o or Op.ldelem_struct or Op.ldelem_vt
|
||
or Op.stelem_i4 or Op.stelem_r4 or Op.stelem_o or Op.stelem_struct or Op.stelem_vt
|
||
or Op.ldfld_vt_i4 or Op.ldfld_vt_r4 or Op.stfld_vt_i4 or Op.stfld_vt_r4
|
||
or Op.ldfld_vt_u1 or Op.ldfld_vt_i1 or Op.ldfld_vt_u2 or Op.ldfld_vt_i2
|
||
or Op.stfld_vt_b1 or Op.stfld_vt_b2
|
||
or Op.ldfld_vt_vt or Op.stfld_vt_vt
|
||
or Op.ldfld_sc_i4 or Op.ldfld_sc_r4 or Op.ldfld_sc_o or Op.ldfld_sc_vt
|
||
or Op.stfld_sc_i4 or Op.stfld_sc_r4 or Op.stfld_sc_o or Op.stfld_sc_vt
|
||
or Op.castclass or Op.unbox_any or Op.isinst or Op.box_prim or Op.box_enum
|
||
or Op.new_delegate => 4,
|
||
|
||
Op.newarr or Op.newarr_i4 or Op.newarr_r4 or Op.newarr_vt => 5,
|
||
|
||
Op.call_script or Op.newobj_script or Op.newobj_host
|
||
=> ip + 3 < ir.Length ? 4 + (int)ir[ip + 3] : -1,
|
||
Op.call_host
|
||
=> ip + 4 < ir.Length ? 5 + (int)ir[ip + 4] : -1,
|
||
Op.call_host_byref => CallHostByrefWidthForCoalesce(ir, ip),
|
||
Op.switch_i4 => ip + 2 < ir.Length ? 4 + (int)ir[ip + 2] : -1,
|
||
|
||
_ => -1,
|
||
};
|
||
}
|
||
|
||
static bool Is4WordArithForCoalesce(Op op) => op switch
|
||
{
|
||
Op.add_i4 or Op.add_i4_nn or Op.sub_i4 or Op.sub_i4_nn
|
||
or Op.mul_i4 or Op.mul_i4_nn or Op.div_i4 or Op.div_i4_nn
|
||
or Op.rem_i4 or Op.rem_i4_nn
|
||
or Op.add_r4 or Op.add_r4_nn or Op.sub_r4 or Op.sub_r4_nn
|
||
or Op.mul_r4 or Op.mul_r4_nn or Op.div_r4 or Op.div_r4_nn
|
||
or Op.rem_r4 or Op.rem_r4_nn
|
||
or Op.and_i4 or Op.and_i4_nn or Op.or_i4 or Op.or_i4_nn
|
||
or Op.xor_i4 or Op.xor_i4_nn
|
||
or Op.shl_i4 or Op.shl_i4_nn or Op.shr_i4 or Op.shr_i4_nn
|
||
or Op.shr_un_i4 or Op.shr_un_i4_nn
|
||
or Op.div_un_i4 or Op.div_un_i4_nn or Op.rem_un_i4 or Op.rem_un_i4_nn
|
||
or Op.ceq_i4 or Op.ceq_i4_nn or Op.cgt_i4 or Op.cgt_i4_nn
|
||
or Op.clt_i4 or Op.clt_i4_nn or Op.cgt_un_i4 or Op.cgt_un_i4_nn
|
||
or Op.clt_un_i4 or Op.clt_un_i4_nn
|
||
or Op.ceq_r4 or Op.ceq_r4_nn or Op.cgt_r4 or Op.cgt_r4_nn
|
||
or Op.clt_r4 or Op.clt_r4_nn or Op.cgt_un_r4 or Op.clt_un_r4
|
||
or Op.ceq_o or Op.cgt_un_o
|
||
// _nk forms also encode as [op, dst, src, k] — 4 words. dst at words[1].
|
||
or Op.add_i4_nk or Op.add_r4_nk or Op.sub_i4_nk or Op.sub_r4_nk
|
||
or Op.mul_r4_nk
|
||
or Op.clt_i4_nk or Op.clt_r4_nk or Op.cgt_i4_nk or Op.cgt_r4_nk
|
||
or Op.ceq_i4_nk
|
||
or Op.mul_i4_nk or Op.div_i4_nk or Op.rem_i4_nk
|
||
or Op.and_i4_nk or Op.or_i4_nk or Op.xor_i4_nk
|
||
or Op.shl_i4_nk or Op.shr_i4_nk or Op.shr_un_i4_nk
|
||
or Op.div_r4_nk or Op.ceq_r4_nk => true,
|
||
_ => false,
|
||
};
|
||
|
||
static int CallHostByrefWidthForCoalesce(uint[] ir, int ip)
|
||
{
|
||
if (ip + 4 >= ir.Length) return -1;
|
||
int argc = (int)ir[ip + 4];
|
||
int wbBase = ip + 5 + argc;
|
||
if (wbBase >= ir.Length) return -1;
|
||
int wbCount = (int)ir[wbBase];
|
||
return (wbBase + 1 + wbCount * 4) - ip;
|
||
}
|
||
|
||
// Conservative slot read/write accumulation. Counts every value at a known
|
||
// slot position. Tokens, immediates, and IP targets are skipped.
|
||
static void AccumulateSlotUses(uint[] ir, int ip, Op op, int width,
|
||
int[] reads, int[] writes, int slotCount)
|
||
{
|
||
void R(int s) { if ((uint)s < (uint)slotCount) reads[s]++; }
|
||
void W(int s) { if ((uint)s < (uint)slotCount) writes[s]++; }
|
||
|
||
if (Is4WordArithForCoalesce(op))
|
||
{
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]);
|
||
// _nk forms have an immediate at word 3; everything else has a 2nd slot.
|
||
if (!IsNkForm(op)) R((int)ir[ip + 3]);
|
||
return;
|
||
}
|
||
if (IsFusedCmpBranch(op))
|
||
{
|
||
R((int)ir[ip + 1]);
|
||
if (!IsFusedCmpBranchNk(op)) R((int)ir[ip + 2]);
|
||
// word 3 is target_ip, not a slot
|
||
return;
|
||
}
|
||
if (op == Op.for_i4_nk)
|
||
{
|
||
// induction slot is read AND written; words 2 and 3 are limit + target.
|
||
int s = (int)ir[ip + 1];
|
||
R(s); W(s);
|
||
return;
|
||
}
|
||
switch (op)
|
||
{
|
||
case Op.nop: case Op.ret_void: case Op.br:
|
||
case Op.push_cont: case Op.br_cont: return;
|
||
|
||
case Op.mov: case Op.box: case Op.mov_vt: case Op.box_vt: case Op.unbox_vt:
|
||
case Op.clone_sc:
|
||
case Op.ldlen:
|
||
case Op.neg_i4: case Op.neg_i4_n: case Op.neg_r4: case Op.neg_r4_n:
|
||
case Op.not_i4: case Op.not_i4_n:
|
||
case Op.conv_i4_r4: case Op.conv_r4_i4:
|
||
case Op.conv_i4_i1: case Op.conv_i4_u1:
|
||
case Op.conv_i4_i2: case Op.conv_i4_u2:
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]); return;
|
||
|
||
case Op.ldc_i4: case Op.ldc_r4: case Op.ldstr:
|
||
case Op.ldsfld_i4: case Op.ldsfld_r4: case Op.ldsfld_o: case Op.ldsfld_struct:
|
||
case Op.ldtoken:
|
||
case Op.ldnull: case Op.initobj: case Op.initobj_script: case Op.ensure_script:
|
||
W((int)ir[ip + 1]); return;
|
||
|
||
case Op.stsfld_i4: case Op.stsfld_r4: case Op.stsfld_o: case Op.stsfld_struct:
|
||
R((int)ir[ip + 2]); return;
|
||
|
||
case Op.ret_i4: case Op.ret_r4: case Op.ret_o: case Op.ret_vt:
|
||
case Op.throw_o:
|
||
R((int)ir[ip + 1]); return;
|
||
|
||
case Op.brtrue_i4: case Op.brfalse_i4: case Op.brtrue_o: case Op.brfalse_o:
|
||
R((int)ir[ip + 1]); return;
|
||
|
||
case Op.ldfld_i4: case Op.ldfld_r4: case Op.ldfld_o:
|
||
case Op.ldfld_struct: case Op.ldflda:
|
||
case Op.ldfld_vt_i4: case Op.ldfld_vt_r4: case Op.ldfld_vt_vt:
|
||
case Op.ldfld_vt_u1: case Op.ldfld_vt_i1: case Op.ldfld_vt_u2: case Op.ldfld_vt_i2:
|
||
case Op.ldfld_sc_i4: case Op.ldfld_sc_r4: case Op.ldfld_sc_o: case Op.ldfld_sc_vt:
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]); return;
|
||
|
||
case Op.stfld_i4: case Op.stfld_r4: case Op.stfld_o: case Op.stfld_struct:
|
||
case Op.stfld_vt_i4: case Op.stfld_vt_r4: case Op.stfld_vt_vt:
|
||
case Op.stfld_vt_b1: case Op.stfld_vt_b2:
|
||
case Op.stfld_sc_i4: case Op.stfld_sc_r4: case Op.stfld_sc_o: case Op.stfld_sc_vt:
|
||
R((int)ir[ip + 1]); R((int)ir[ip + 3]); return;
|
||
|
||
case Op.ldelem_i4: case Op.ldelem_r4: case Op.ldelem_o: case Op.ldelem_struct:
|
||
case Op.ldelem_vt:
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]); R((int)ir[ip + 3]); return;
|
||
|
||
case Op.stelem_i4: case Op.stelem_r4: case Op.stelem_o: case Op.stelem_struct:
|
||
case Op.stelem_vt:
|
||
R((int)ir[ip + 1]); R((int)ir[ip + 2]); R((int)ir[ip + 3]); return;
|
||
|
||
case Op.castclass: case Op.unbox_any: case Op.isinst: case Op.box_prim:
|
||
case Op.box_enum:
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]); return;
|
||
|
||
case Op.newarr: case Op.newarr_i4: case Op.newarr_r4: case Op.newarr_vt:
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]); return;
|
||
|
||
case Op.new_delegate:
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 3]); return; // word 2 is a tokIdx
|
||
|
||
case Op.call_script:
|
||
case Op.newobj_script:
|
||
case Op.newobj_host:
|
||
{
|
||
W((int)ir[ip + 1]);
|
||
int argc = (int)ir[ip + 3];
|
||
for (int k = 0; k < argc; k++) R((int)ir[ip + 4 + k]);
|
||
return;
|
||
}
|
||
|
||
case Op.call_host:
|
||
{
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]);
|
||
int argc = (int)ir[ip + 4];
|
||
for (int k = 0; k < argc; k++) R((int)ir[ip + 5 + k]);
|
||
return;
|
||
}
|
||
|
||
case Op.call_host_byref:
|
||
{
|
||
W((int)ir[ip + 1]); R((int)ir[ip + 2]);
|
||
int argc = (int)ir[ip + 4];
|
||
for (int k = 0; k < argc; k++) R((int)ir[ip + 5 + k]);
|
||
int wbBase = ip + 5 + argc;
|
||
int wbCount = (int)ir[wbBase];
|
||
for (int k = 0; k < wbCount; k++)
|
||
{
|
||
int kind = (int)ir[wbBase + 1 + k * 4 + 1];
|
||
int t1 = (int)ir[wbBase + 1 + k * 4 + 2];
|
||
int t2 = (int)ir[wbBase + 1 + k * 4 + 3];
|
||
if (kind == 0) { W(t1); } // frame slot writeback
|
||
else if (kind == 1) { R(t1); } // field on object
|
||
else { R(t1); R(t2); } // array[idx]
|
||
}
|
||
return;
|
||
}
|
||
|
||
case Op.switch_i4:
|
||
R((int)ir[ip + 1]); return;
|
||
}
|
||
}
|
||
|
||
// --- Pre-scan helpers ---
|
||
|
||
// Scan the IL bytes and collect all branch target offsets into a HashSet.
|
||
// Used by LowerMethod to detect basic-block boundaries and clear ensuredScriptSlots.
|
||
static HashSet<int> CollectIlBranchTargets(byte[] il)
|
||
{
|
||
var targets = new HashSet<int>();
|
||
int ip = 0;
|
||
while (ip < il.Length)
|
||
{
|
||
byte b = il[ip++];
|
||
ILOpCode op = b == 0xFE ? (ILOpCode)(0xFE00 | il[ip++]) : (ILOpCode)b;
|
||
switch (op)
|
||
{
|
||
case ILOpCode.Br_s:
|
||
case ILOpCode.Brtrue_s: case ILOpCode.Brfalse_s:
|
||
case ILOpCode.Beq_s: case ILOpCode.Bne_un_s:
|
||
case ILOpCode.Blt_s: case ILOpCode.Blt_un_s:
|
||
case ILOpCode.Bgt_s: case ILOpCode.Bgt_un_s:
|
||
case ILOpCode.Ble_s: case ILOpCode.Ble_un_s:
|
||
case ILOpCode.Bge_s: case ILOpCode.Bge_un_s:
|
||
case ILOpCode.Leave_s:
|
||
{
|
||
int off = (sbyte)il[ip++];
|
||
targets.Add(ip + off);
|
||
break;
|
||
}
|
||
case ILOpCode.Br:
|
||
case ILOpCode.Brtrue: case ILOpCode.Brfalse:
|
||
case ILOpCode.Beq: case ILOpCode.Bne_un:
|
||
case ILOpCode.Blt: case ILOpCode.Blt_un:
|
||
case ILOpCode.Bgt: case ILOpCode.Bgt_un:
|
||
case ILOpCode.Ble: case ILOpCode.Ble_un:
|
||
case ILOpCode.Bge: case ILOpCode.Bge_un:
|
||
case ILOpCode.Leave:
|
||
{
|
||
int off = BitConverter.ToInt32(il, ip); ip += 4;
|
||
targets.Add(ip + off);
|
||
break;
|
||
}
|
||
case ILOpCode.Switch:
|
||
{
|
||
int n = BitConverter.ToInt32(il, ip); ip += 4;
|
||
int tableStart = ip;
|
||
ip += 4 * n;
|
||
for (int ci = 0; ci < n; ci++)
|
||
{
|
||
int off = BitConverter.ToInt32(il, tableStart + ci * 4);
|
||
targets.Add(ip + off);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
ip += ILOperandSize(op, il, ip);
|
||
break;
|
||
}
|
||
}
|
||
return targets;
|
||
}
|
||
|
||
// Handler entry points of every finally region a `leave` at leaveOffset must run before
|
||
// reaching `target`: try region contains the leave but not the target. Innermost first
|
||
// (smallest try region), matching the order the CLR runs them on the non-exceptional path.
|
||
static List<int> FinallyChain(
|
||
(int TryStart, int TryEnd, int HandlerStart, int HandlerEnd)[]? regions,
|
||
int leaveOffset, int target)
|
||
{
|
||
var chain = new List<int>();
|
||
if (regions == null) return chain;
|
||
// Sort by try-region size so nesting order is explicit (metadata usually orders
|
||
// inner clauses first, but don't rely on it).
|
||
var hits = new List<(int size, int handlerStart)>();
|
||
foreach (var r in regions)
|
||
{
|
||
if (r.TryStart <= leaveOffset && leaveOffset < r.TryEnd
|
||
&& !(r.TryStart <= target && target < r.TryEnd))
|
||
hits.Add((r.TryEnd - r.TryStart, r.HandlerStart));
|
||
}
|
||
hits.Sort((a, b) => a.size.CompareTo(b.size));
|
||
foreach (var h in hits) chain.Add(h.handlerStart);
|
||
return chain;
|
||
}
|
||
|
||
// Forward-type-flow pre-pass classifying every local's SType before IR emission:
|
||
// a simplified run of the main lowering pass that only tracks types (no IR output),
|
||
// pre-populating slotTypes[] for args+locals so the main pass allocates correctly
|
||
// from the start.
|
||
/// <summary>Maps a metadata type token to the interpreter SType for a system PRIMITIVE
|
||
/// (Int32/Boolean/Byte/…/Char → I4, Single/Double → R4); null for anything else (script
|
||
/// types, reference types, unresolvable). Used by <c>initobj</c> to classify a target the
|
||
/// local-type inference couldn't see a store to.</summary>
|
||
// Box typecode for a primitive value type (else -1). Codes: 0=Int32 1=Boolean 2=Char
|
||
// 3=Byte 4=SByte 5=Int16 6=UInt16 7=Single. Others (UInt32/Int64/Double/enum/struct/ref)
|
||
// return -1 and keep box a no-op (their value-uses already round-trip correctly).
|
||
static int BoxPrimTypeCode(ParsedAssembly asm, int token)
|
||
{
|
||
try
|
||
{
|
||
var h = MetadataTokens.EntityHandle(token);
|
||
string ns, name;
|
||
if (h.Kind == HandleKind.TypeReference)
|
||
{ var tr = asm.Reader.GetTypeReference((TypeReferenceHandle)h); ns = asm.Reader.GetString(tr.Namespace); name = asm.Reader.GetString(tr.Name); }
|
||
else if (h.Kind == HandleKind.TypeDefinition)
|
||
{ var td = asm.Reader.GetTypeDefinition((TypeDefinitionHandle)h); ns = asm.Reader.GetString(td.Namespace); name = asm.Reader.GetString(td.Name); }
|
||
else return -1;
|
||
if (ns != "System") return -1;
|
||
return name switch
|
||
{
|
||
"Int32" => 0, "Boolean" => 1, "Char" => 2, "Byte" => 3,
|
||
"SByte" => 4, "Int16" => 5, "UInt16" => 6, "Single" => 7,
|
||
"UInt32" => 8, // uint is analyzer-legal; boxing as int flips big values negative
|
||
"Int64" => 9, "UInt64" => 10, "Double" => 11, // wide slots
|
||
_ => -1,
|
||
};
|
||
}
|
||
catch { return -1; }
|
||
}
|
||
|
||
static SType? PrimitiveSTypeForTypeToken(ParsedAssembly asm, int token)
|
||
{
|
||
try
|
||
{
|
||
var h = MetadataTokens.EntityHandle(token);
|
||
string ns, name;
|
||
if (h.Kind == HandleKind.TypeReference)
|
||
{
|
||
var tr = asm.Reader.GetTypeReference((TypeReferenceHandle)h);
|
||
ns = asm.Reader.GetString(tr.Namespace); name = asm.Reader.GetString(tr.Name);
|
||
}
|
||
else if (h.Kind == HandleKind.TypeDefinition)
|
||
{
|
||
var td = asm.Reader.GetTypeDefinition((TypeDefinitionHandle)h);
|
||
ns = asm.Reader.GetString(td.Namespace); name = asm.Reader.GetString(td.Name);
|
||
}
|
||
else return null;
|
||
if (ns != "System") return null;
|
||
return name switch
|
||
{
|
||
"Int32" or "Boolean" or "Byte" or "SByte" or "Int16" or "UInt16"
|
||
or "UInt32" or "Char" => SType.I4,
|
||
"Single" => SType.R4,
|
||
"Int64" or "UInt64" => SType.I8,
|
||
"Double" => SType.R8,
|
||
_ => null,
|
||
};
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
static void PreClassifyLocals(byte[] il, ParsedMethod m, List<SType> slotTypes,
|
||
int[] localFrameSlot, HostBinding.StructLayout?[]? localStrs, bool[] localDeclared)
|
||
{
|
||
// Simulated eval stack (just types, no slot indices)
|
||
var stack = new SType[64];
|
||
int sp = 0;
|
||
var localTypes = new SType[m.LocalCount]; // O by default; Vt locals seeded below
|
||
var argTypes = new SType[m.ArgCount]; // O by default
|
||
// Seed Vt locals so a later ldloc returns Vt instead of O.
|
||
if (localStrs != null)
|
||
for (int i = 0; i < m.LocalCount; i++)
|
||
if (localStrs[i] != null) localTypes[i] = SType.Vt;
|
||
// Seed declared-typed locals from their frozen type so the abstract walk sees them
|
||
// correctly and never re-infers them (PreStoreLocal skips them below).
|
||
for (int i = 0; i < m.LocalCount; i++)
|
||
if (localDeclared[i]) localTypes[i] = slotTypes[localFrameSlot[i]];
|
||
|
||
int ip = 0;
|
||
while (ip < il.Length)
|
||
{
|
||
byte b = il[ip++];
|
||
ILOpCode op = b == 0xFE ? (ILOpCode)(0xFE00 | il[ip++]) : (ILOpCode)b;
|
||
switch (op)
|
||
{
|
||
// Constants
|
||
case ILOpCode.Ldc_i4_m1: case ILOpCode.Ldc_i4_0: case ILOpCode.Ldc_i4_1:
|
||
case ILOpCode.Ldc_i4_2: case ILOpCode.Ldc_i4_3: case ILOpCode.Ldc_i4_4:
|
||
case ILOpCode.Ldc_i4_5: case ILOpCode.Ldc_i4_6: case ILOpCode.Ldc_i4_7:
|
||
case ILOpCode.Ldc_i4_8: if(sp<64)stack[sp++]=SType.I4; break;
|
||
case ILOpCode.Ldc_i4_s: ip++; if(sp<64)stack[sp++]=SType.I4; break;
|
||
case ILOpCode.Ldc_i4: ip+=4; if(sp<64)stack[sp++]=SType.I4; break;
|
||
case ILOpCode.Ldc_r4: ip+=4; if(sp<64)stack[sp++]=SType.R4; break;
|
||
case ILOpCode.Ldc_r8: ip+=8; if(sp<64)stack[sp++]=SType.R8; break;
|
||
case ILOpCode.Ldc_i8: ip+=8; if(sp<64)stack[sp++]=SType.I8; break;
|
||
case ILOpCode.Ldnull: if(sp<64)stack[sp++]=SType.O; break;
|
||
case ILOpCode.Ldstr: ip+=4; if(sp<64)stack[sp++]=SType.O; break;
|
||
|
||
// Locals
|
||
// Bounds-guarded like Ldloc_s below: valid IL never underflows, but this pass
|
||
// walks unknown opcodes by table — a size mistake there must degrade, not throw.
|
||
case ILOpCode.Ldloc_0: if(sp<64)stack[sp++]=m.LocalCount>0?localTypes[0]:SType.O; break;
|
||
case ILOpCode.Ldloc_1: if(sp<64)stack[sp++]=m.LocalCount>1?localTypes[1]:SType.O; break;
|
||
case ILOpCode.Ldloc_2: if(sp<64)stack[sp++]=m.LocalCount>2?localTypes[2]:SType.O; break;
|
||
case ILOpCode.Ldloc_3: if(sp<64)stack[sp++]=m.LocalCount>3?localTypes[3]:SType.O; break;
|
||
case ILOpCode.Ldloc_s: { int i=il[ip++]; if(sp<64)stack[sp++]=i<m.LocalCount?localTypes[i]:SType.O; break; }
|
||
case ILOpCode.Ldloc: { int i=il[ip]|(il[ip+1]<<8); ip+=2; if(sp<64)stack[sp++]=i<m.LocalCount?localTypes[i]:SType.O; break; }
|
||
case ILOpCode.Stloc_0: PreStoreLocal(stack, ref sp, 0, m, localTypes, slotTypes, localFrameSlot, localStrs, localDeclared); break;
|
||
case ILOpCode.Stloc_1: PreStoreLocal(stack, ref sp, 1, m, localTypes, slotTypes, localFrameSlot, localStrs, localDeclared); break;
|
||
case ILOpCode.Stloc_2: PreStoreLocal(stack, ref sp, 2, m, localTypes, slotTypes, localFrameSlot, localStrs, localDeclared); break;
|
||
case ILOpCode.Stloc_3: PreStoreLocal(stack, ref sp, 3, m, localTypes, slotTypes, localFrameSlot, localStrs, localDeclared); break;
|
||
case ILOpCode.Stloc_s: { int i=il[ip++]; PreStoreLocal(stack, ref sp, i, m, localTypes, slotTypes, localFrameSlot, localStrs, localDeclared); break; }
|
||
case ILOpCode.Stloc: { int i=il[ip]|(il[ip+1]<<8); ip+=2; PreStoreLocal(stack, ref sp, i, m, localTypes, slotTypes, localFrameSlot, localStrs, localDeclared); break; }
|
||
|
||
// Args
|
||
case ILOpCode.Ldarg_0: if(sp<64)stack[sp++]=argTypes.Length>0?argTypes[0]:SType.O; break;
|
||
case ILOpCode.Ldarg_1: if(sp<64)stack[sp++]=argTypes.Length>1?argTypes[1]:SType.O; break;
|
||
case ILOpCode.Ldarg_2: if(sp<64)stack[sp++]=argTypes.Length>2?argTypes[2]:SType.O; break;
|
||
case ILOpCode.Ldarg_3: if(sp<64)stack[sp++]=argTypes.Length>3?argTypes[3]:SType.O; break;
|
||
case ILOpCode.Ldarg_s: { ip++; if(sp<64)stack[sp++]=SType.O; break; }
|
||
case ILOpCode.Ldarg: { ip+=2; if(sp<64)stack[sp++]=SType.O; break; }
|
||
case ILOpCode.Starg_s: ip++; if(sp>0)sp--; break;
|
||
case ILOpCode.Starg: ip+=2; if(sp>0)sp--; break;
|
||
|
||
// Arithmetic (consume 2, push 1 of same type)
|
||
case ILOpCode.Add: case ILOpCode.Sub: case ILOpCode.Mul: case ILOpCode.Div: case ILOpCode.Rem:
|
||
{
|
||
if(sp>=2){SType t2=stack[--sp];SType t1=stack[sp-1];
|
||
stack[sp-1]=t1==SType.R8||t2==SType.R8?SType.R8
|
||
:t1==SType.R4||t2==SType.R4?SType.R4
|
||
:t1==SType.I8||t2==SType.I8?SType.I8:SType.I4;}
|
||
break;
|
||
}
|
||
case ILOpCode.Neg:
|
||
if(sp>0){SType t=stack[sp-1];stack[sp-1]=t is SType.R4 or SType.R8 or SType.I8?t:SType.I4;} break;
|
||
case ILOpCode.And: case ILOpCode.Or: case ILOpCode.Xor: case ILOpCode.Shl: case ILOpCode.Shr: case ILOpCode.Shr_un:
|
||
if(sp>=2){SType t2=stack[--sp];SType t1=stack[sp-1];stack[sp-1]=t1==SType.I8||t2==SType.I8?SType.I8:SType.I4;} break;
|
||
case ILOpCode.Not:
|
||
if(sp>0)stack[sp-1]=SType.I4; break;
|
||
|
||
// Comparisons → I4
|
||
case ILOpCode.Ceq: case ILOpCode.Cgt: case ILOpCode.Clt: case ILOpCode.Cgt_un: case ILOpCode.Clt_un:
|
||
if(sp>=2){sp--;stack[sp-1]=SType.I4;} break;
|
||
|
||
// Conversions
|
||
case ILOpCode.Conv_i4: case ILOpCode.Conv_u4:
|
||
case ILOpCode.Conv_i1: case ILOpCode.Conv_u1:
|
||
case ILOpCode.Conv_i2: case ILOpCode.Conv_u2:
|
||
if(sp>0)stack[sp-1]=SType.I4; break;
|
||
case ILOpCode.Conv_r4:
|
||
if(sp>0)stack[sp-1]=SType.R4; break;
|
||
case ILOpCode.Conv_r8: case ILOpCode.Conv_r_un:
|
||
if(sp>0)stack[sp-1]=SType.R8; break;
|
||
case ILOpCode.Conv_i8: case ILOpCode.Conv_u8:
|
||
if(sp>0)stack[sp-1]=SType.I8; break;
|
||
|
||
// Box, dup, pop
|
||
case ILOpCode.Box: ip+=4; break;
|
||
case ILOpCode.Dup: if(sp>0&&sp<64){stack[sp]=stack[sp-1];sp++;} break;
|
||
case ILOpCode.Pop: if(sp>0)sp--; break;
|
||
|
||
// Branches (pop 0, 1, or 2)
|
||
case ILOpCode.Br_s: ip+=1; sp=0; break;
|
||
case ILOpCode.Br: ip+=4; sp=0; break;
|
||
case ILOpCode.Leave_s: ip+=1; sp=0; break;
|
||
case ILOpCode.Leave: ip+=4; sp=0; break;
|
||
case ILOpCode.Endfinally: sp=0; break;
|
||
case ILOpCode.Brtrue_s: case ILOpCode.Brfalse_s: ip+=1; if(sp>0)sp--; break;
|
||
case ILOpCode.Brtrue: case ILOpCode.Brfalse: ip+=4; if(sp>0)sp--; break;
|
||
case ILOpCode.Beq_s: case ILOpCode.Bne_un_s: case ILOpCode.Blt_s: case ILOpCode.Bgt_s:
|
||
case ILOpCode.Ble_s: case ILOpCode.Bge_s: case ILOpCode.Blt_un_s: case ILOpCode.Bgt_un_s:
|
||
case ILOpCode.Ble_un_s: case ILOpCode.Bge_un_s: ip+=1; if(sp>=2)sp-=2; break;
|
||
case ILOpCode.Beq: case ILOpCode.Bne_un: case ILOpCode.Blt: case ILOpCode.Bgt:
|
||
case ILOpCode.Ble: case ILOpCode.Bge: case ILOpCode.Blt_un: case ILOpCode.Bgt_un:
|
||
case ILOpCode.Ble_un: case ILOpCode.Bge_un: ip+=4; if(sp>=2)sp-=2; break;
|
||
|
||
// Everything else: skip operands, reset stack conservatively (don't corrupt)
|
||
default:
|
||
{
|
||
int sz = ILOperandSize(op, il, ip);
|
||
ip += sz;
|
||
// For calls, field access, etc. — conservatively push/pop O values
|
||
// We don't need perfect accuracy here; only stloc tracking matters.
|
||
if (op == ILOpCode.Call || op == ILOpCode.Callvirt || op == ILOpCode.Newobj)
|
||
{
|
||
// Just clear the stack to be safe — we can't know arg counts without parsing sigs
|
||
sp = 0;
|
||
}
|
||
else if (op == ILOpCode.Ldfld || op == ILOpCode.Ldflda || op == ILOpCode.Ldsfld)
|
||
{
|
||
if(sp>0)sp--;
|
||
if(sp<64)stack[sp++]=SType.O;
|
||
}
|
||
else if (op == ILOpCode.Stfld)
|
||
{
|
||
if(sp>=2)sp-=2;
|
||
}
|
||
else if (op == ILOpCode.Stsfld)
|
||
{
|
||
if(sp>0)sp--;
|
||
}
|
||
else if (op == ILOpCode.Ldlen || op == ILOpCode.Newarr || op == ILOpCode.Ldtoken)
|
||
{
|
||
if(sp>0)sp--;
|
||
if(sp<64)stack[sp++]=SType.O;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// On conflicting stores, keep the most recent type — in practice well-typed C#
|
||
// stores only one type into each local.
|
||
static void UpdateSlotType(List<SType> slotTypes, int slot, SType t, string context = "")
|
||
{
|
||
if (slot < slotTypes.Count && slotTypes[slot] != SType.Vt) // never downgrade a Vt slot
|
||
slotTypes[slot] = t;
|
||
}
|
||
|
||
// Stloc handler for the pre-classify pass: writes localTypes[i] = src type, propagates to
|
||
// slotTypes at the local's frame slot. Vt locals are seeded ahead of time and never overwritten.
|
||
static void PreStoreLocal(SType[] stack, ref int sp, int i, ParsedMethod m,
|
||
SType[] localTypes, List<SType> slotTypes, int[] localFrameSlot, HostBinding.StructLayout?[]? localStrs,
|
||
bool[] localDeclared)
|
||
{
|
||
if (sp == 0 || i >= m.LocalCount) { if (sp > 0) sp--; return; }
|
||
SType t = stack[--sp];
|
||
if (i < localDeclared.Length && localDeclared[i]) return; // declared type is frozen — never infer
|
||
if (localStrs != null && localStrs[i] != null) return; // Vt local — keep type
|
||
// A Vt VALUE flowing into a local with no struct layout (e.g. `object o = someStruct;`)
|
||
// BOXES at the store — the local is O. Marking it Vt would make StoreLocal alias the
|
||
// struct bytes (mov_vt with a null layout) and lose box-snapshot semantics.
|
||
if (t == SType.Vt) t = SType.O;
|
||
localTypes[i] = t;
|
||
UpdateSlotType(slotTypes, localFrameSlot[i], t);
|
||
}
|
||
|
||
// Returns the operand size in bytes for an IL opcode at position ip (for skipping in pre-scan).
|
||
static int ILOperandSize(ILOpCode op, byte[] il, int ip)
|
||
{
|
||
switch (op)
|
||
{
|
||
case ILOpCode.Ldarg_s: case ILOpCode.Ldloc_s: case ILOpCode.Stloc_s:
|
||
case ILOpCode.Ldarga_s: case ILOpCode.Ldloca_s: case ILOpCode.Starg_s:
|
||
case ILOpCode.Ldc_i4_s: case ILOpCode.Brfalse_s: case ILOpCode.Brtrue_s:
|
||
case ILOpCode.Br_s: case ILOpCode.Beq_s: case ILOpCode.Bge_s: case ILOpCode.Bgt_s:
|
||
case ILOpCode.Ble_s: case ILOpCode.Blt_s: case ILOpCode.Bne_un_s:
|
||
case ILOpCode.Bge_un_s: case ILOpCode.Bgt_un_s: case ILOpCode.Ble_un_s:
|
||
case ILOpCode.Blt_un_s: case ILOpCode.Leave_s:
|
||
case (ILOpCode)0xFE12: // Unaligned prefix (banned opcode)
|
||
return 1;
|
||
case ILOpCode.Ldarg: case ILOpCode.Ldloc: case ILOpCode.Stloc: case ILOpCode.Ldarga:
|
||
case ILOpCode.Ldloca: case ILOpCode.Starg:
|
||
return 2;
|
||
case ILOpCode.Ldc_i4: case ILOpCode.Ldc_r4: case ILOpCode.Ldstr: case ILOpCode.Newobj:
|
||
case ILOpCode.Ldfld: case ILOpCode.Ldflda: case ILOpCode.Stfld: case ILOpCode.Ldsfld:
|
||
case ILOpCode.Stsfld: case ILOpCode.Leave: case ILOpCode.Constrained:
|
||
case ILOpCode.Call: case ILOpCode.Callvirt: case ILOpCode.Newarr:
|
||
case ILOpCode.Ldelem: case ILOpCode.Stelem: case ILOpCode.Ldelema: case ILOpCode.Unbox_any:
|
||
case ILOpCode.Box: case ILOpCode.Castclass: case ILOpCode.Isinst: case ILOpCode.Ldtoken:
|
||
case ILOpCode.Br: case ILOpCode.Beq: case ILOpCode.Bge: case ILOpCode.Bgt:
|
||
case ILOpCode.Ble: case ILOpCode.Blt: case ILOpCode.Bne_un: case ILOpCode.Bge_un:
|
||
case ILOpCode.Bgt_un: case ILOpCode.Ble_un: case ILOpCode.Blt_un:
|
||
case ILOpCode.Brfalse: case ILOpCode.Brtrue: case ILOpCode.Initobj:
|
||
// Token-operand opcodes the lowerer rejects later — the size must still be right
|
||
// here, or the walk reads the token bytes as opcodes and desyncs (a misread
|
||
// ldloc.N then indexes past the locals array — the ldftn/delegate-creation crash).
|
||
case ILOpCode.Ldftn: case ILOpCode.Ldvirtftn: case ILOpCode.Ldsflda:
|
||
case ILOpCode.Jmp: case ILOpCode.Calli: case ILOpCode.Cpobj: case ILOpCode.Ldobj:
|
||
case ILOpCode.Stobj: case ILOpCode.Unbox: case ILOpCode.Refanyval:
|
||
case ILOpCode.Mkrefany: case ILOpCode.Sizeof:
|
||
return 4;
|
||
case ILOpCode.Ldc_r8: case (ILOpCode)0x21: // 0x21=Ldc_i8 (banned, 8-byte operand)
|
||
return 8;
|
||
case ILOpCode.Switch:
|
||
if (ip < il.Length) { int n = BitConverter.ToInt32(il, ip); return 4 + n * 4; }
|
||
return 0;
|
||
default:
|
||
return 0; // no operand
|
||
}
|
||
}
|
||
|
||
// --- Slot allocation helpers ---
|
||
|
||
// AllocSlot ALSO grows slotStructs (with null) in lockstep so the two lists stay aligned.
|
||
// For Vt slots, use AllocStructSlot which reserves ceil(size/4) consecutive slots.
|
||
static int AllocSlot(ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, SType t, HostBinding.StructLayout? lay)
|
||
{
|
||
int s = frameSize++;
|
||
slotTypes.Add(t);
|
||
slotStructs.Add(lay);
|
||
if (t is SType.I8 or SType.R8)
|
||
{
|
||
// Wide value: reserve the continuation cell, filler-typed like Vt continuation
|
||
// cells so nothing else is allocated over the value's second half.
|
||
slotTypes.Add(SType.O);
|
||
slotStructs.Add(null);
|
||
frameSize++;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
static int AllocStructSlot(ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, HostBinding.StructLayout lay)
|
||
{
|
||
int s = frameSize++;
|
||
slotTypes.Add(SType.Vt);
|
||
slotStructs.Add(lay);
|
||
int filler = (lay.Size + 3) / 4 - 1;
|
||
for (int k = 0; k < filler; k++)
|
||
{
|
||
slotTypes.Add(SType.O);
|
||
slotStructs.Add(null);
|
||
frameSize++;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
// Converge the value currently on top of the eval stack into a single "merge slot" for a forward
|
||
// join, so every forward edge into `target` leaves its result in the same frame slot. Used by the
|
||
// ternary/short-circuit `br` path and by conditional branches that carry a value to a forward join
|
||
// — e.g. `a ?? b`, whose `dup; brtrue L` leaves the left operand on the stack on the taken edge.
|
||
// Emits the mov (unconditionally, before the branch) and repoints the eval-stack top at the merge
|
||
// slot; the join reconciles the other edge's value into the same slot via mergeSlotForEnd.
|
||
static void MergeTopIntoJoinSlot(
|
||
List<uint> ir, List<int> irToIl, (int slot, SType type)[] evalStack, int sp,
|
||
ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs,
|
||
Dictionary<int, (int slot, SType type)> mergeSlotForEnd, int target, int instrStart)
|
||
{
|
||
var (firstSlot, firstType) = evalStack[sp - 1];
|
||
int mSlot;
|
||
if (mergeSlotForEnd.TryGetValue(target, out var existing))
|
||
mSlot = existing.slot; // multiple edges merging into one join
|
||
else if (firstType == SType.Vt)
|
||
{
|
||
mSlot = AllocStructSlot(ref frameSize, slotTypes, slotStructs, slotStructs[firstSlot]!);
|
||
mergeSlotForEnd[target] = (mSlot, firstType);
|
||
}
|
||
else
|
||
{
|
||
mSlot = AllocSlot(ref frameSize, slotTypes, slotStructs, firstType, null);
|
||
mergeSlotForEnd[target] = (mSlot, firstType);
|
||
}
|
||
if (mSlot != firstSlot)
|
||
Emit3(ir, irToIl, firstType == SType.Vt ? Op.mov_vt : Op.mov, mSlot, firstSlot, instrStart);
|
||
evalStack[sp - 1] = (mSlot, firstType);
|
||
}
|
||
|
||
// Emit call_host_byref. Each byref arg is replaced with a fresh boxed O slot pre-loaded
|
||
// with the target's current value (so MethodInfo.Invoke can write through it), then a
|
||
// writeback table is appended so the executor copies the post-call value back into
|
||
// the original frame slot / field / array element.
|
||
static void EmitCallHostByref(List<uint> ir, List<int> irToIl, (int slot, SType type)[] evalStack,
|
||
ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs,
|
||
int[] addrTagBySlot, int[] addrFldObjBySlot, int[] addrFldTokBySlot, int[] addrByteOffBySlot,
|
||
ParsedAssembly asm, List<int> tokens, HostEntry hostEntry, int tokIdx,
|
||
ParameterInfo[] ps, int instrStart)
|
||
{
|
||
int argc = hostEntry.Binding.ParamCount;
|
||
bool isVoid = hostEntry.IsVoid;
|
||
// Type the result slot like the regular call_host dst-picker: from the resolved
|
||
// MethodInfo's return type, falling back to the MemberRef signature for string-keyed
|
||
// shims. A blanket O dst left bool/int results boxed in the ref frame, and a boxed
|
||
// false condition was then null-tested TRUE under brtrue_o's pure reference semantics
|
||
// (found by fuzzing: TryHalve-shaped bool-returning byref calls). Vt returns keep O.
|
||
SType retSt = SType.O;
|
||
if (!isVoid && (hostEntry.ResolvedMethod ?? hostEntry.Binding.Method) is MethodInfo bmi)
|
||
{
|
||
var brt = bmi.ReturnType;
|
||
if (brt == typeof(float)) retSt = SType.R4;
|
||
else if (brt == typeof(int) || brt == typeof(bool) || brt == typeof(byte)
|
||
|| brt == typeof(sbyte) || brt == typeof(short) || brt == typeof(ushort)) retSt = SType.I4;
|
||
}
|
||
else if (!isVoid && hostEntry.SigRetSType is SType.I4 or SType.R4)
|
||
retSt = hostEntry.SigRetSType;
|
||
int dst = isVoid ? -1 : AllocSlot(ref frameSize, slotTypes, slotStructs, retSt, null);
|
||
|
||
// Receiver — same address-resolution rules as the regular call_host path.
|
||
int recvSlotRaw = hostEntry.Binding.HasThis ? evalStack[sp - argc - 1].slot : -1;
|
||
int recvSlot = recvSlotRaw;
|
||
int recvVtOffB = 0; bool recvVtWbB = false; int recvVtWbBaseB = -1;
|
||
if (recvSlotRaw >= 0)
|
||
{
|
||
int addrTag = addrTagBySlot[recvSlotRaw];
|
||
if (addrTag >= 0) { recvSlot = addrTag; recvVtOffB = addrByteOffBySlot[recvSlotRaw]; }
|
||
else if (addrTag == -1)
|
||
{
|
||
int fldObj = addrFldObjBySlot[recvSlotRaw];
|
||
int fldToki = addrFldTokBySlot[recvSlotRaw];
|
||
int fldTok = tokens[fldToki];
|
||
asm.FieldSTypes.TryGetValue(fldTok, out var fldSt);
|
||
bool isScFieldB = asm.FieldSlots.TryGetValue(fldTok, out var fldFsB);
|
||
// Vt field receiver: struct slot + byte-range load (see the call_host site).
|
||
var fldVtLayB = isScFieldB && fldSt == SType.Vt
|
||
? fldFsB.Item1.VtFieldLayouts?[fldFsB.Item2] : null;
|
||
recvSlot = fldVtLayB != null
|
||
? AllocStructSlot(ref frameSize, slotTypes, slotStructs, fldVtLayB)
|
||
: AllocSlot(ref frameSize, slotTypes, slotStructs, fldSt, null);
|
||
if (isScFieldB)
|
||
{
|
||
int fldOffB = fldFsB.Item1.FieldOffsets[fldFsB.Item2];
|
||
var fldOpB = fldVtLayB != null ? Op.ldfld_sc_vt
|
||
: fldSt == SType.I4 ? Op.ldfld_sc_i4 : fldSt == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, fldOpB, recvSlot, fldObj, fldOffB, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var fldLoadOp = fldSt == SType.I4 ? Op.ldfld_i4 : fldSt == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, fldLoadOp, recvSlot, fldObj, fldToki, instrStart);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Nested-offset Vt receiver — same materialize + write-back as the call_host path.
|
||
if (hostEntry.Binding.HasThis && recvSlot >= 0 && recvVtOffB != 0
|
||
&& recvSlot < slotTypes.Count && slotTypes[recvSlot] == SType.Vt
|
||
&& hostEntry.ReceiverStruct != null)
|
||
{
|
||
int rTmpB = AllocStructSlot(ref frameSize, slotTypes, slotStructs, hostEntry.ReceiverStruct);
|
||
Emit4(ir, irToIl, Op.ldfld_vt_vt, rTmpB, recvSlot, recvVtOffB, instrStart);
|
||
recvVtWbB = true; recvVtWbBaseB = recvSlot;
|
||
recvSlot = rTmpB;
|
||
}
|
||
|
||
int argBase = sp - argc;
|
||
int spBase = sp - argc - (hostEntry.Binding.HasThis ? 1 : 0);
|
||
|
||
// Walk byref args: replace each phantom-addr arg slot with a fresh O slot pre-loaded
|
||
// with the current value at the address. Record the writeback target in parallel arrays.
|
||
// Max 8 byref args is plenty (Physics.Raycast is the worst common case at ~3).
|
||
Span<int> wbArgIdx = stackalloc int[8];
|
||
Span<int> wbKind = stackalloc int[8];
|
||
Span<int> wbT1 = stackalloc int[8];
|
||
Span<int> wbT2 = stackalloc int[8];
|
||
int wbCount = 0;
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
if (k >= ps.Length || !ps[k].ParameterType.IsByRef) continue;
|
||
int origSlot = evalStack[argBase + k].slot;
|
||
int tag = addrTagBySlot[origSlot];
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
if (tag >= 0)
|
||
{
|
||
// Pre-load current value (may be irrelevant for `out`). A flat (Vt) source
|
||
// lives in the numeric frame — box it; a plain mov would read the wrong frame.
|
||
Emit3(ir, irToIl, slotTypes[tag] == SType.Vt ? Op.box_vt : Op.mov, boxed, tag, instrStart);
|
||
if (wbCount < 8)
|
||
{ wbArgIdx[wbCount] = k; wbKind[wbCount] = 0; wbT1[wbCount] = tag; wbT2[wbCount] = 0; wbCount++; }
|
||
}
|
||
else if (tag == -1)
|
||
{
|
||
int fldObj = addrFldObjBySlot[origSlot];
|
||
int fldToki = addrFldTokBySlot[origSlot];
|
||
int fldTok = tokens[fldToki];
|
||
asm.FieldSTypes.TryGetValue(fldTok, out var fldSt);
|
||
bool isScFieldC = asm.FieldSlots.TryGetValue(fldTok, out var fldFsC);
|
||
var fldVtLayC = isScFieldC && fldSt == SType.Vt
|
||
? fldFsC.Item1.VtFieldLayouts?[fldFsC.Item2] : null;
|
||
// Pre-load into a slot tagged with the FIELD's type, not O: the field-load ops
|
||
// write I4/R4 values into the numeric frame, so an O-tagged slot reads back
|
||
// null and the host call sees 0 (found by fuzzing: `h.Accumulate(ref acc)` on
|
||
// a hoisted iterator field passed 0 in — the writeback masked the wrong input).
|
||
// RdObj boxes an I4/R4-tagged slot correctly at call time, like the receiver
|
||
// resolution above. Vt and O fields keep the O tag — their pre-load lands a
|
||
// boxed object in the ref frame.
|
||
if (fldSt == SType.I4 || fldSt == SType.R4)
|
||
slotTypes[boxed] = fldSt;
|
||
if (fldVtLayC != null)
|
||
{
|
||
// Flat (Vt) script field — e.g. a Vector3 hoisted into an iterator state
|
||
// machine because an out-arg takes its address: materialize the bytes into
|
||
// a struct slot, then box for the reflective call. The op picker below
|
||
// would emit ldfld_sc_o, indexing RefSlots with a PrimBytes byte offset.
|
||
int tmpVtC = AllocStructSlot(ref frameSize, slotTypes, slotStructs, fldVtLayC);
|
||
Emit4(ir, irToIl, Op.ldfld_sc_vt, tmpVtC, fldObj, fldFsC.Item1.FieldOffsets[fldFsC.Item2], instrStart);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, tmpVtC, instrStart);
|
||
}
|
||
else if (isScFieldC)
|
||
{
|
||
int fldOffC = fldFsC.Item1.FieldOffsets[fldFsC.Item2];
|
||
var fldOpC = fldSt == SType.I4 ? Op.ldfld_sc_i4 : fldSt == SType.R4 ? Op.ldfld_sc_r4 : Op.ldfld_sc_o;
|
||
Emit4(ir, irToIl, fldOpC, boxed, fldObj, fldOffC, instrStart);
|
||
}
|
||
else
|
||
{
|
||
var fldLoadOp = fldSt == SType.I4 ? Op.ldfld_i4 : fldSt == SType.R4 ? Op.ldfld_r4 : Op.ldfld_o;
|
||
Emit4(ir, irToIl, fldLoadOp, boxed, fldObj, fldToki, instrStart);
|
||
}
|
||
if (wbCount < 8)
|
||
{ wbArgIdx[wbCount] = k; wbKind[wbCount] = 1; wbT1[wbCount] = fldObj; wbT2[wbCount] = fldToki; wbCount++; }
|
||
}
|
||
else if (tag == -3)
|
||
{
|
||
int arrSlot = addrFldObjBySlot[origSlot];
|
||
int idxSlot = addrFldTokBySlot[origSlot];
|
||
Emit4(ir, irToIl, Op.ldelem_o, boxed, arrSlot, idxSlot, instrStart);
|
||
if (wbCount < 8)
|
||
{ wbArgIdx[wbCount] = k; wbKind[wbCount] = 2; wbT1[wbCount] = arrSlot; wbT2[wbCount] = idxSlot; wbCount++; }
|
||
}
|
||
else
|
||
{
|
||
// Non-phantom passed by ref — read whatever's there but no writeback target.
|
||
Emit3(ir, irToIl, slotTypes[origSlot] == SType.Vt ? Op.box_vt : Op.mov, boxed, origSlot, instrStart);
|
||
}
|
||
evalStack[argBase + k] = (boxed, SType.O);
|
||
}
|
||
|
||
// Box any leftover Vt args (slow path doesn't read flat).
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
if (evalStack[argBase + k].type == SType.Vt)
|
||
{
|
||
int boxed = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.box_vt, boxed, evalStack[argBase + k].slot, instrStart);
|
||
evalStack[argBase + k] = (boxed, SType.O);
|
||
}
|
||
}
|
||
|
||
ir.Add((uint)Op.call_host_byref); irToIl.Add(instrStart);
|
||
ir.Add((uint)(isVoid ? -1 : dst)); irToIl.Add(instrStart);
|
||
ir.Add((uint)(hostEntry.Binding.HasThis ? recvSlot : -1)); irToIl.Add(instrStart);
|
||
ir.Add((uint)tokIdx); irToIl.Add(instrStart);
|
||
ir.Add((uint)argc); irToIl.Add(instrStart);
|
||
for (int k = 0; k < argc; k++)
|
||
{ ir.Add((uint)evalStack[argBase + k].slot); irToIl.Add(instrStart); }
|
||
ir.Add((uint)wbCount); irToIl.Add(instrStart);
|
||
for (int k = 0; k < wbCount; k++)
|
||
{
|
||
ir.Add((uint)wbArgIdx[k]); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbKind[k]); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbT1[k]); irToIl.Add(instrStart);
|
||
ir.Add((uint)wbT2[k]); irToIl.Add(instrStart);
|
||
}
|
||
if (recvVtWbB)
|
||
Emit4(ir, irToIl, Op.stfld_vt_vt, recvVtWbBaseB, recvVtOffB, recvSlot, instrStart);
|
||
sp = spBase;
|
||
if (!isVoid) evalStack[sp++] = (dst, retSt);
|
||
}
|
||
|
||
// Operator inlining: synthesize per-field arithmetic IR for a pure host-struct operator
|
||
// or constructor. The args on evalStack at [argBase..argBase+argc) are either Vt slots
|
||
// (for struct args) or scalar slots (for float/int args). The result goes into dst (a Vt slot).
|
||
//
|
||
// Strategy: iterate the return struct's fields in byte-offset order, match each to the
|
||
// corresponding field in one of the Vt args, and emit a scalar arithmetic op per field.
|
||
// For scalar args (no struct layout), use the slot directly for all fields.
|
||
// Supported shapes: Vt op Vt, Vt op Scalar, Scalar op Vt, Scalar Scalar Scalar (ctor).
|
||
// Cheap pre-check for EmitInlinedOp, evaluated BEFORE the Vt-arg boxing decision: once
|
||
// the lowerer skips boundary boxing it is committed to inlining, so every precondition
|
||
// must hold up front (a mid-emission bail-out would leave partial IR behind).
|
||
static bool CanInlineOp(HostEntry hostEntry, int argc, HostBinding.StructLayout retLayout, bool isCtor)
|
||
{
|
||
if (retLayout.Fields.Count == 0) return false;
|
||
if (isCtor) return argc == retLayout.Fields.Count;
|
||
var n = hostEntry.Binding.Method?.Name;
|
||
if (n != "op_Addition" && n != "op_Subtraction" && n != "op_Multiply"
|
||
&& n != "op_Division" && n != "op_UnaryNegation") return false;
|
||
return n == "op_UnaryNegation" ? argc == 1 : argc == 2;
|
||
}
|
||
|
||
// Synthesizes per-field IR for a VERIFIED component-wise operator or field-order ctor
|
||
// (see HostBinding.InspectAndMarkOperators — marking guarantees the IL is exactly the
|
||
// per-field pattern, so this name-driven emission is semantically equivalent).
|
||
static void EmitInlinedOp(List<uint> ir, List<int> irToIl, (int slot, SType type)[] evalStack,
|
||
int argBase, int argc, int dst, HostBinding.StructLayout retLayout,
|
||
HostEntry hostEntry, int instrStart, bool isCtor)
|
||
{
|
||
// Build sorted field list: (byteOffset, SType) sorted by byte offset.
|
||
var fields = new (int Off, SType St)[retLayout.Fields.Count];
|
||
int fi = 0;
|
||
foreach (var kv in retLayout.Fields)
|
||
fields[fi++] = (kv.Value.Offset, kv.Value.St);
|
||
// Sort by offset (Fields dict may be unordered)
|
||
for (int a = 0; a < fields.Length - 1; a++)
|
||
for (int b = a + 1; b < fields.Length; b++)
|
||
if (fields[b].Off < fields[a].Off) { var tmp = fields[a]; fields[a] = fields[b]; fields[b] = tmp; }
|
||
|
||
if (isCtor)
|
||
{
|
||
// Verified field-order ctor: param i is exactly field i's value (arity checked
|
||
// by CanInlineOp before any IR was committed).
|
||
for (int f = 0; f < fields.Length; f++)
|
||
{
|
||
var stF = fields[f].St == SType.R4 ? Op.stfld_vt_r4 : Op.stfld_vt_i4;
|
||
ir.Add((uint)stF); irToIl.Add(instrStart);
|
||
ir.Add((uint)dst); irToIl.Add(instrStart);
|
||
ir.Add((uint)fields[f].Off); irToIl.Add(instrStart);
|
||
ir.Add((uint)evalStack[argBase + f].slot); irToIl.Add(instrStart);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// For each result field, emit one arithmetic instruction. Struct-vs-scalar per arg is
|
||
// decided from the CALL SITE's slot types, not the entry's ArgStructs — aliased
|
||
// operator pairs ((V3,float) + (float,V3)) share one Entry, so the entry's signature
|
||
// metadata may describe the other overload.
|
||
for (int f = 0; f < fields.Length; f++)
|
||
{
|
||
int dstSubSlot = dst + fields[f].Off / 4;
|
||
var argSubs = new int[argc];
|
||
for (int k = 0; k < argc; k++)
|
||
{
|
||
int argSlot = evalStack[argBase + k].slot;
|
||
argSubs[k] = evalStack[argBase + k].type == SType.Vt
|
||
? argSlot + fields[f].Off / 4 // struct arg: same-offset component
|
||
: argSlot; // scalar arg: same slot for every field
|
||
}
|
||
|
||
string mName = hostEntry.Binding.Method?.Name ?? "";
|
||
Op fieldOp;
|
||
if (mName == "op_Addition") fieldOp = fields[f].St == SType.R4 ? Op.add_r4_nn : Op.add_i4_nn;
|
||
else if (mName == "op_Subtraction") fieldOp = fields[f].St == SType.R4 ? Op.sub_r4_nn : Op.sub_i4_nn;
|
||
else if (mName == "op_Multiply") fieldOp = fields[f].St == SType.R4 ? Op.mul_r4_nn : Op.mul_i4_nn;
|
||
else if (mName == "op_Division") fieldOp = fields[f].St == SType.R4 ? Op.div_r4_nn : Op.div_i4_nn;
|
||
else fieldOp = fields[f].St == SType.R4 ? Op.neg_r4_n : Op.neg_i4_n; // op_UnaryNegation (CanInlineOp vetted the name)
|
||
|
||
if (argc == 1)
|
||
{
|
||
ir.Add((uint)fieldOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)dstSubSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)argSubs[0]); irToIl.Add(instrStart);
|
||
}
|
||
else
|
||
{
|
||
ir.Add((uint)fieldOp); irToIl.Add(instrStart);
|
||
ir.Add((uint)dstSubSlot); irToIl.Add(instrStart);
|
||
ir.Add((uint)argSubs[0]); irToIl.Add(instrStart);
|
||
ir.Add((uint)argSubs[1]); irToIl.Add(instrStart);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Value-load a local, cloning script-defined structs so the pushed value is an independent
|
||
// copy (struct copy semantics). Address loads (ldloca) never come through here, so in-place
|
||
// field mutation is unaffected.
|
||
static void PushLocal(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp,
|
||
ref int frameSize, int localIdx, int[] localFrameSlot, SType[] localTypes,
|
||
List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs,
|
||
bool[]? localIsScStruct, int instrStart)
|
||
{
|
||
int srcSlot = localFrameSlot[localIdx];
|
||
SType t = localTypes[localIdx];
|
||
if (t == SType.O && localIsScStruct != null && localIdx < localIsScStruct.Length && localIsScStruct[localIdx])
|
||
{
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.O, null);
|
||
Emit3(ir, irToIl, Op.clone_sc, dst, srcSlot, instrStart);
|
||
stack[sp++] = (dst, SType.O);
|
||
}
|
||
else
|
||
{
|
||
stack[sp++] = (srcSlot, t);
|
||
}
|
||
}
|
||
|
||
// Any pending eval-stack entry that ALIASES the slot about to be stored must be
|
||
// materialized into a temp first: PushLocal/PushArg push the local's own frame slot, so
|
||
// `(a, b) = (b, a)` — `ldloc b; ldloc a; stloc b; stloc a` — left the second-from-top
|
||
// entry pointing AT b's slot when b was overwritten, and a came back as the new b
|
||
// (found by fuzzing). Address phantoms don't match here (their entry is a fresh phantom
|
||
// slot), and a store through an address is genuine aliasing anyway.
|
||
static void ProtectStackAliases(List<uint> ir, List<int> irToIl, (int, SType)[] stack, int sp,
|
||
ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs,
|
||
int dst, int instrStart)
|
||
{
|
||
for (int i = 0; i < sp; i++)
|
||
{
|
||
if (stack[i].Item1 != dst) continue;
|
||
var t = stack[i].Item2;
|
||
if (t == SType.Vt && dst < slotStructs.Count && slotStructs[dst] != null)
|
||
{
|
||
int tmp = AllocStructSlot(ref frameSize, slotTypes, slotStructs, slotStructs[dst]!);
|
||
Emit3(ir, irToIl, Op.mov_vt, tmp, dst, instrStart);
|
||
stack[i] = (tmp, t);
|
||
}
|
||
else
|
||
{
|
||
int tmp = AllocSlot(ref frameSize, slotTypes, slotStructs, dst < slotTypes.Count ? slotTypes[dst] : t, null);
|
||
Emit3(ir, irToIl, Op.mov, tmp, dst, instrStart);
|
||
stack[i] = (tmp, t);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void StoreLocal(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, int localIdx, int[] localFrameSlot, SType[] localTypes, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, bool[] localDeclared, ref int frameSize, int instrStart)
|
||
{
|
||
var (src, t) = stack[--sp];
|
||
int dst = localFrameSlot[localIdx];
|
||
ProtectStackAliases(ir, irToIl, stack, sp, ref frameSize, slotTypes, slotStructs, dst, instrStart);
|
||
// Vt local: byte-copy struct bytes from src into the local slot. Requires the dst
|
||
// slot to actually carry a layout — a Vt-typed slot without one must not alias.
|
||
if (slotTypes[dst] == SType.Vt && t == SType.Vt && slotStructs[dst] != null)
|
||
{
|
||
Emit3(ir, irToIl, Op.mov_vt, dst, src, instrStart);
|
||
return;
|
||
}
|
||
// Vt local being stored from O slot: unbox + flat-write.
|
||
if (slotTypes[dst] == SType.Vt && t == SType.O)
|
||
{
|
||
Emit3(ir, irToIl, Op.unbox_vt, dst, src, instrStart);
|
||
return;
|
||
}
|
||
// Vt source going into a non-Vt local (struct-returning ops produce Vt slots even
|
||
// when the local stays O because no ldloca was taken on it). Box the Vt bytes
|
||
// into a refFrame entry so the O local sees a normal boxed reference.
|
||
if (slotTypes[dst] != SType.Vt && t == SType.Vt)
|
||
{
|
||
localTypes[localIdx] = SType.O;
|
||
if (dst < slotTypes.Count) slotTypes[dst] = SType.O;
|
||
Emit3(ir, irToIl, Op.box_vt, dst, src, instrStart);
|
||
return;
|
||
}
|
||
if (localIdx < localDeclared.Length && localDeclared[localIdx])
|
||
{
|
||
// Declared type is FROZEN — never retype the slot. Op.mov routes by the (fixed) dst
|
||
// slot type at runtime, boxing/unboxing the source to match, so an int stored into a
|
||
// declared `object` local boxes and a boxed value read back unboxes, consistently.
|
||
localTypes[localIdx] = slotTypes[dst];
|
||
Emit3(ir, irToIl, Op.mov, dst, src, instrStart);
|
||
return;
|
||
}
|
||
localTypes[localIdx] = t; // remember what type this local holds (non-Vt locals only)
|
||
if (dst < slotTypes.Count) slotTypes[dst] = t;
|
||
Emit3(ir, irToIl, Op.mov, dst, src, instrStart);
|
||
}
|
||
|
||
static void PushArg((int, SType)[] stack, ref int sp, int argIdx, SType[] argTypes, int[] argFrameSlot)
|
||
{
|
||
stack[sp++] = (argFrameSlot[argIdx], argTypes[argIdx]);
|
||
}
|
||
|
||
static void StoreArg(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp,
|
||
int argIdx, SType[] argTypes, int[] argFrameSlot, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, ref int frameSize, int instrStart)
|
||
{
|
||
var (src, t) = stack[--sp];
|
||
int dst = argFrameSlot[argIdx];
|
||
ProtectStackAliases(ir, irToIl, stack, sp, ref frameSize, slotTypes, slotStructs, dst, instrStart);
|
||
// Vt arg slots have a FIXED type (the frame layout is part of the call ABI): copy
|
||
// struct bytes in (or unbox a boxed ScriptObject); never retype the slot.
|
||
if (slotTypes[dst] == SType.Vt)
|
||
{
|
||
Emit3(ir, irToIl, t == SType.Vt ? Op.mov_vt : Op.unbox_vt, dst, src, instrStart);
|
||
return;
|
||
}
|
||
argTypes[argIdx] = t;
|
||
Emit3(ir, irToIl, Op.mov, dst, src, instrStart);
|
||
}
|
||
|
||
static void PushConst(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, int imm, SType t, Op ldc, int instrStart)
|
||
{
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, t, null);
|
||
stack[sp++] = (dst, t);
|
||
Emit3(ir, irToIl, ldc, dst, imm, instrStart);
|
||
}
|
||
|
||
// Map an i4-family op to its wide sibling. Throws for combinations IL can't
|
||
// produce or the subset doesn't support — the LowerAll catch-all keeps it loud.
|
||
static Op Wide64(Op i4Op, bool isR8) => (i4Op, isR8) switch
|
||
{
|
||
(Op.add_i4, false) => Op.add_i8, (Op.add_i4, true) => Op.add_r8,
|
||
(Op.sub_i4, false) => Op.sub_i8, (Op.sub_i4, true) => Op.sub_r8,
|
||
(Op.mul_i4, false) => Op.mul_i8, (Op.mul_i4, true) => Op.mul_r8,
|
||
(Op.div_i4, false) => Op.div_i8, (Op.div_i4, true) => Op.div_r8,
|
||
(Op.rem_i4, false) => Op.rem_i8, (Op.rem_i4, true) => Op.rem_r8,
|
||
(Op.div_un_i4, false) => Op.div_un_i8,
|
||
(Op.rem_un_i4, false) => Op.rem_un_i8,
|
||
(Op.and_i4, false) => Op.and_i8,
|
||
(Op.or_i4, false) => Op.or_i8,
|
||
(Op.xor_i4, false) => Op.xor_i8,
|
||
(Op.shl_i4, false) => Op.shl_i8,
|
||
(Op.shr_i4, false) => Op.shr_i8,
|
||
(Op.shr_un_i4, false) => Op.shr_un_i8,
|
||
(Op.ceq_i4, false) => Op.ceq_i8, (Op.ceq_i4, true) => Op.ceq_r8,
|
||
(Op.cgt_i4, false) => Op.cgt_i8, (Op.cgt_i4, true) => Op.cgt_r8,
|
||
(Op.clt_i4, false) => Op.clt_i8, (Op.clt_i4, true) => Op.clt_r8,
|
||
(Op.cgt_un_i4, false) => Op.cgt_un_i8, (Op.cgt_un_i4, true) => Op.cgt_un_r8,
|
||
(Op.clt_un_i4, false) => Op.clt_un_i8, (Op.clt_un_i4, true) => Op.clt_un_r8,
|
||
_ => throw new NotSupportedException($"IR lowering: no 64-bit form of {i4Op}"),
|
||
};
|
||
|
||
// R8 compare from the R4 op the call site chose — the ordered/unordered intent
|
||
// (NaN semantics through negated branches) lives in that choice, not the I4 op.
|
||
static Op WideCmpR8(Op r4Op) => r4Op switch
|
||
{
|
||
Op.ceq_r4 or Op.ceq_r4_nn => Op.ceq_r8,
|
||
Op.cgt_r4 or Op.cgt_r4_nn => Op.cgt_r8,
|
||
Op.clt_r4 or Op.clt_r4_nn => Op.clt_r8,
|
||
Op.cgt_un_r4 => Op.cgt_un_r8,
|
||
Op.clt_un_r4 => Op.clt_un_r8,
|
||
_ => throw new NotSupportedException($"IR lowering: no R8 form of {r4Op}"),
|
||
};
|
||
|
||
static void EmitBinop(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs,
|
||
Op opI4, Op opR4, Op opI4_nn, Op opR4_nn, int instrStart)
|
||
{
|
||
var (s2, t2) = stack[--sp];
|
||
var (s1, t1) = stack[sp - 1];
|
||
// 64-bit operands (Roslyn emits explicit convs, so pairs arrive homogeneous;
|
||
// a stray I4 operand is widened by the executor's Rd helpers).
|
||
if (t1 is SType.I8 or SType.R8 || t2 is SType.I8 or SType.R8)
|
||
{
|
||
bool isR8w = t1 == SType.R8 || t2 == SType.R8;
|
||
SType wrt = isR8w ? SType.R8 : SType.I8;
|
||
int wdst = AllocSlot(ref frameSize, slotTypes, slotStructs, wrt, null);
|
||
stack[sp - 1] = (wdst, wrt);
|
||
Emit4(ir, irToIl, Wide64(opI4, isR8w), wdst, s1, s2, instrStart);
|
||
return;
|
||
}
|
||
bool isFloat = t1 == SType.R4 || t2 == SType.R4;
|
||
SType rt = isFloat ? SType.R4 : SType.I4;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, rt, null);
|
||
stack[sp - 1] = (dst, rt);
|
||
bool both_r4 = t1 == SType.R4 && t2 == SType.R4 && slotTypes[s1] == SType.R4 && slotTypes[s2] == SType.R4;
|
||
bool both_i4 = t1 == SType.I4 && t2 == SType.I4 && slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4;
|
||
Op op = isFloat ? (both_r4 ? opR4_nn : opR4) : (both_i4 ? opI4_nn : opI4);
|
||
Emit4(ir, irToIl, op, dst, s1, s2, instrStart);
|
||
}
|
||
|
||
static void EmitBinopI4(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, Op opI4, Op opI4_nn, int instrStart)
|
||
{
|
||
var (s2, t2) = stack[--sp];
|
||
var (s1, t1) = stack[sp - 1];
|
||
// 64-bit form: and/or/xor take two I8s; shifts take (I8 value, I4 count) — either
|
||
// way the value operand's width decides.
|
||
if (t1 == SType.I8)
|
||
{
|
||
int wdst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I8, null);
|
||
stack[sp - 1] = (wdst, SType.I8);
|
||
Emit4(ir, irToIl, Wide64(opI4, false), wdst, s1, s2, instrStart);
|
||
return;
|
||
}
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
stack[sp - 1] = (dst, SType.I4);
|
||
bool both_i4 = t1 == SType.I4 && t2 == SType.I4 && slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4;
|
||
Emit4(ir, irToIl, both_i4 ? opI4_nn : opI4, dst, s1, s2, instrStart);
|
||
}
|
||
|
||
static void EmitUnop(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, Op opI4, Op opR4, Op opI4_n, Op opR4_n, int instrStart)
|
||
{
|
||
var (src, t) = stack[sp - 1];
|
||
if (t is SType.I8 or SType.R8)
|
||
{
|
||
int wdst = AllocSlot(ref frameSize, slotTypes, slotStructs, t, null);
|
||
stack[sp - 1] = (wdst, t);
|
||
Emit3(ir, irToIl, t == SType.R8 ? Op.neg_r8 : Op.neg_i8, wdst, src, instrStart);
|
||
return;
|
||
}
|
||
bool is_r4 = t == SType.R4;
|
||
// The dst must be typed by what the op WRITES (always flat numeric), not by the source:
|
||
// an O-typed src (e.g. a boxed int flowing out of a host call) would otherwise stamp the
|
||
// dst O, and every downstream reader would look at the (never-written) ref slot.
|
||
var rt = is_r4 ? SType.R4 : SType.I4;
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, rt, null);
|
||
stack[sp - 1] = (dst, rt);
|
||
bool src_n = is_r4 ? slotTypes[src] == SType.R4 : slotTypes[src] == SType.I4;
|
||
Op op = is_r4 ? (src_n ? opR4_n : opR4) : (src_n ? opI4_n : opI4);
|
||
Emit3(ir, irToIl, op, dst, src, instrStart);
|
||
}
|
||
|
||
static void EmitUnopI4(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs, Op opI4, Op opI4_n, int instrStart)
|
||
{
|
||
var (src, t) = stack[sp - 1];
|
||
if (t == SType.I8)
|
||
{
|
||
int wdst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I8, null);
|
||
stack[sp - 1] = (wdst, SType.I8);
|
||
Emit3(ir, irToIl, Op.not_i8, wdst, src, instrStart);
|
||
return;
|
||
}
|
||
int dst = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
stack[sp - 1] = (dst, SType.I4);
|
||
bool src_n = t == SType.I4 && slotTypes[src] == SType.I4;
|
||
Emit3(ir, irToIl, src_n ? opI4_n : opI4, dst, src, instrStart);
|
||
}
|
||
|
||
static void EmitCmpBranch(List<uint> ir, List<int> irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List<SType> slotTypes, List<HostBinding.StructLayout?> slotStructs,
|
||
Op cmpI4, Op cmpR4, Op cmpO, Op cmpI4_nn, Op cmpR4_nn, Op branch, List<(int, int)> patchList, int ilTarget, int instrStart,
|
||
Dictionary<int, int> branchTargetDepth)
|
||
{
|
||
var (s2, t2) = stack[--sp];
|
||
var (s1, t1) = stack[sp - 1];
|
||
sp--;
|
||
// Both operands are consumed; the compare-branch leaves the base stack. Record the depth
|
||
// it delivers to a forward target so the join reconciles correctly (see branchTargetDepth).
|
||
if (ilTarget > instrStart) branchTargetDepth[ilTarget] = sp;
|
||
int tmp = AllocSlot(ref frameSize, slotTypes, slotStructs, SType.I4, null);
|
||
Op cmp;
|
||
if (t1 == SType.R8 || t2 == SType.R8)
|
||
cmp = WideCmpR8(cmpR4);
|
||
else if (t1 == SType.I8 || t2 == SType.I8)
|
||
cmp = Wide64(cmpI4, false);
|
||
else if (t1 == SType.R4 || t2 == SType.R4)
|
||
cmp = (t1 == SType.R4 && t2 == SType.R4 && slotTypes[s1] == SType.R4 && slotTypes[s2] == SType.R4) ? cmpR4_nn : cmpR4;
|
||
else if (t1 == SType.I4 || t2 == SType.I4)
|
||
// cmpI4 reads each operand from the correct stack at runtime (handles O/I4 mix).
|
||
cmp = (slotTypes[s1] == SType.I4 && slotTypes[s2] == SType.I4) ? cmpI4_nn : cmpI4;
|
||
else
|
||
cmp = cmpO;
|
||
Emit4(ir, irToIl, cmp, tmp, s1, s2, instrStart);
|
||
int patchIdx = ir.Count + 2;
|
||
Emit3(ir, irToIl, branch, tmp, -1, instrStart);
|
||
patchList.Add((patchIdx, ilTarget));
|
||
}
|
||
|
||
// Read a branch target from the IL stream: 1-byte sbyte offset for short, 4-byte int for long.
|
||
static int ReadBranchTarget(byte[] il, ref int ip, bool isShort)
|
||
{
|
||
int off = isShort ? (sbyte)il[ip++] : BitConverter.ToInt32(il, ip);
|
||
if (!isShort) ip += 4;
|
||
return ip + off;
|
||
}
|
||
|
||
// --- IR emission helpers (all params are int; cast to uint internally) ---
|
||
|
||
static void Emit2(List<uint> ir, List<int> irToIl, Op op, int a, int ilOff)
|
||
{
|
||
ir.Add((uint)op); irToIl.Add(ilOff);
|
||
ir.Add((uint)a); irToIl.Add(ilOff);
|
||
}
|
||
|
||
static void Emit3(List<uint> ir, List<int> irToIl, Op op, int a, int b, int ilOff)
|
||
{
|
||
ir.Add((uint)op); irToIl.Add(ilOff);
|
||
ir.Add((uint)a); irToIl.Add(ilOff);
|
||
ir.Add((uint)b); irToIl.Add(ilOff);
|
||
}
|
||
|
||
static void Emit4(List<uint> ir, List<int> irToIl, Op op, int a, int b, int c, int ilOff)
|
||
{
|
||
ir.Add((uint)op); irToIl.Add(ilOff);
|
||
ir.Add((uint)a); irToIl.Add(ilOff);
|
||
ir.Add((uint)b); irToIl.Add(ilOff);
|
||
ir.Add((uint)c); irToIl.Add(ilOff);
|
||
}
|
||
}
|
||
|
||
static (int Line, string Doc) FindSourceLine(ParsedMethod method, int ilOffset)
|
||
{
|
||
var pts = method.SeqPoints;
|
||
if (pts == null) return (-1, "");
|
||
int lo = 0, hi = pts.Length - 1, result = -1;
|
||
while (lo <= hi)
|
||
{
|
||
int mid = (lo + hi) / 2;
|
||
if (pts[mid].IlOffset <= ilOffset) { result = mid; lo = mid + 1; }
|
||
else hi = mid - 1;
|
||
}
|
||
return result >= 0 ? (pts[result].Line, pts[result].Doc) : (-1, "");
|
||
}
|
||
|
||
static string At(ParsedMethod method, int ilOffset)
|
||
{
|
||
var (line, doc) = FindSourceLine(method, ilOffset);
|
||
if (line <= 0) return $" at IL+0x{ilOffset:X4}";
|
||
// Keep the " at line " marker: the VM's host-call rethrow filter matches it to avoid
|
||
// appending a second location when an already-located error bubbles through nested frames.
|
||
return doc.Length > 0 ? $" at line {line} ({doc})" : $" at line {line}";
|
||
}
|
||
|
||
static int ReadU16(byte[] il, ref int ip)
|
||
{
|
||
int v = il[ip] | (il[ip + 1] << 8);
|
||
ip += 2;
|
||
return v;
|
||
}
|
||
|
||
// Fill `arr` from the FieldDef's RVA blob, decoding bytes per element type.
|
||
// Returns false if the element type is non-primitive or the field has no RVA — caller
|
||
// then leaves the array zero-initialized and lets the InitializeArray no-op binding run.
|
||
// Typed-backing fills for newarr_i4/newarr_r4 array initializers: element type is statically
|
||
// Int32/Single (the lowerer only emits the typed ops for those), so only the RVA blob lookup
|
||
// remains.
|
||
static bool TryFillI4ArrayFromFieldBlob(int[] arr, ParsedAssembly asm, int fieldTok)
|
||
{
|
||
if (!TryReadFieldBlob(asm, fieldTok, arr.Length * 4, out var bytes)) return false;
|
||
for (int i = 0; i < arr.Length; i++) arr[i] = BitConverter.ToInt32(bytes, i * 4);
|
||
return true;
|
||
}
|
||
|
||
static bool TryFillR4ArrayFromFieldBlob(float[] arr, ParsedAssembly asm, int fieldTok)
|
||
{
|
||
if (!TryReadFieldBlob(asm, fieldTok, arr.Length * 4, out var bytes)) return false;
|
||
for (int i = 0; i < arr.Length; i++) arr[i] = BitConverter.ToSingle(bytes, i * 4);
|
||
return true;
|
||
}
|
||
|
||
// Fill a typed primitive array (bool[]/char[]/byte[]/…) from a RuntimeHelpers.InitializeArray
|
||
// field blob. The blob layout (little-endian, element-sized) matches the primitive array's
|
||
// memory, so a raw block copy is correct.
|
||
static bool TryFillTypedArrayFromFieldBlob(Array arr, int elemSize, ParsedAssembly asm, int fieldTok)
|
||
{
|
||
if (!TryReadFieldBlob(asm, fieldTok, arr.Length * elemSize, out var bytes)) return false;
|
||
Buffer.BlockCopy(bytes, 0, arr, 0, arr.Length * elemSize);
|
||
return true;
|
||
}
|
||
|
||
static bool TryReadFieldBlob(ParsedAssembly asm, int fieldTok, int byteLen, out byte[] bytes)
|
||
{
|
||
bytes = Array.Empty<byte>();
|
||
var fieldHandle = MetadataTokens.EntityHandle(fieldTok);
|
||
if (fieldHandle.Kind != HandleKind.FieldDefinition) return false;
|
||
var fd = asm.Reader.GetFieldDefinition((FieldDefinitionHandle)fieldHandle);
|
||
int rva = fd.GetRelativeVirtualAddress();
|
||
if (rva == 0) return false;
|
||
var section = asm.Pe.GetSectionData(rva);
|
||
var blobReader = section.GetReader();
|
||
bytes = blobReader.ReadBytes(byteLen);
|
||
return true;
|
||
}
|
||
|
||
static bool TryFillArrayFromFieldBlob(object?[] arr, ParsedAssembly asm, int elemTok, int fieldTok)
|
||
{
|
||
var elemHandle = MetadataTokens.EntityHandle(elemTok);
|
||
if (elemHandle.Kind != HandleKind.TypeReference) return false;
|
||
var elemRef = asm.Reader.GetTypeReference((TypeReferenceHandle)elemHandle);
|
||
var elemName = asm.Reader.GetString(elemRef.Name);
|
||
|
||
int elemSize = elemName switch
|
||
{
|
||
"Boolean" or "Byte" or "SByte" => 1,
|
||
"Int16" or "UInt16" or "Char" => 2,
|
||
"Int32" or "UInt32" or "Single" => 4,
|
||
"Int64" or "UInt64" or "Double" => 8,
|
||
_ => 0,
|
||
};
|
||
if (elemSize == 0) return false;
|
||
|
||
var fieldHandle = MetadataTokens.EntityHandle(fieldTok);
|
||
if (fieldHandle.Kind != HandleKind.FieldDefinition) return false;
|
||
var fd = asm.Reader.GetFieldDefinition((FieldDefinitionHandle)fieldHandle);
|
||
int rva = fd.GetRelativeVirtualAddress();
|
||
if (rva == 0) return false;
|
||
|
||
var section = asm.Pe.GetSectionData(rva);
|
||
var blobReader = section.GetReader();
|
||
var bytes = blobReader.ReadBytes(arr.Length * elemSize);
|
||
|
||
for (int i = 0; i < arr.Length; i++)
|
||
{
|
||
int o = i * elemSize;
|
||
arr[i] = elemName switch
|
||
{
|
||
"Boolean" => bytes[o] != 0 ? 1 : 0, // bools live as int (1/0) on the stack
|
||
"Byte" => (int)bytes[o],
|
||
"SByte" => (int)(sbyte)bytes[o],
|
||
"Int16" => (int)BitConverter.ToInt16(bytes, o),
|
||
"UInt16" => (int)BitConverter.ToUInt16(bytes, o),
|
||
"Char" => (int)BitConverter.ToChar(bytes, o),
|
||
"Int32" => BitConverter.ToInt32(bytes, o),
|
||
"UInt32" => (int)BitConverter.ToUInt32(bytes, o),
|
||
"Single" => BitConverter.ToSingle(bytes, o),
|
||
"Int64" => (object)BitConverter.ToInt64(bytes, o),
|
||
"UInt64" => (object)(long)BitConverter.ToUInt64(bytes, o),
|
||
"Double" => (object)(float)BitConverter.ToDouble(bytes, o), // float-only subset
|
||
_ => null,
|
||
};
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Simple stack-type tag (what the lowerer tracks per eval-stack slot).
|
||
// Placed at namespace level so HostBinding.cs can use it in FastCallDelegate.
|
||
// Vt = a host value type stored as contiguous bytes in numFrame, occupying
|
||
// ceil(byteSize/4) consecutive logical slot indices. Only the first slot is
|
||
// referenced; the rest are filler. byteSize and Type live in parallel
|
||
// LoweredMethod tables. See PLAN_FLAT_STRUCTS.md.
|
||
// O is first so default(SType) == SType.O. Anything that holds an SType field
|
||
// without initializing it (e.g. HostBinding.Entry.FlatReturnSType) gets the
|
||
// "no specific numeric type, use refFrame slot" default, which is what every
|
||
// caller wants. Changing this order will break any code that depends on the
|
||
// underlying byte values — none does today (no serialization, no (SType)0 casts).
|
||
internal enum SType : byte { O, I4, R4, Vt, I8, R8 }
|
||
|
||
|
||
// Typed return value from the Vm engine — eliminates boxing of primitive return values
|
||
// on script-to-script call paths. I4 covers both int and bool (bool stored as 0/1).
|
||
// Void (no-return) is represented by default(CallReturn): Type == I4, I4 == 0, O == null.
|
||
// The Type field disambiguates: I4 means no value or an int/bool; callers that need to
|
||
// know whether the callee was void check the calling method's IsVoid flag separately.
|
||
readonly struct CallReturn
|
||
{
|
||
public readonly SType Type;
|
||
public readonly int I4; // valid when Type == I4 (includes bool as 0/1, and Void)
|
||
public readonly float R4; // valid when Type == R4
|
||
public readonly long I8; // valid when Type == I8
|
||
public readonly double R8; // valid when Type == R8
|
||
public readonly object? O; // valid when Type == O or Vt
|
||
|
||
CallReturn(SType t, int i4, float r4, long i8, double r8, object? o)
|
||
{ Type = t; I4 = i4; R4 = r4; I8 = i8; R8 = r8; O = o; }
|
||
|
||
public static CallReturn FromI4(int v) => new CallReturn(SType.I4, v, 0f, 0L, 0d, null);
|
||
public static CallReturn FromR4(float v) => new CallReturn(SType.R4, 0, v, 0L, 0d, null);
|
||
public static CallReturn FromI8(long v) => new CallReturn(SType.I8, 0, 0f, v, 0d, null);
|
||
public static CallReturn FromR8(double v) => new CallReturn(SType.R8, 0, 0f, 0L, v, null);
|
||
public static CallReturn FromO(object? v) => new CallReturn(SType.O, 0, 0f, 0L, 0d, v);
|
||
}
|
||
}
|