#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 _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? 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()); } 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 UnboundHostMembers => (IReadOnlyList?)_parsed?.UnboundHostMembers ?? Array.Empty(); // 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(string methodName, T1 a1) { var method = ResolveInvocable(methodName); return WrapForHost((_vm ??= new Vm(this)).InvokeTyped(method, a1)); } public object? Invoke(string methodName, T1 a1, T2 a2) { var method = ResolveInvocable(methodName); return WrapForHost((_vm ??= new Vm(this)).InvokeTyped(method, a1, a2)); } public object? Invoke(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> _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 ByToken = null!; public Dictionary ByName = null!; public Dictionary HostCalls = null!; // MemberRef token → host method public Dictionary HostCtors = null!; // MemberRef token → host constructor public Dictionary HostFields = null!; // MemberRef token → host field public Dictionary HostFieldDeclTypeTok = null!; // host field MemberRef token → declaring TypeRef token (ensure-init of boxed struct locals) public Dictionary TokenNames = null!; // MemberRef token → "Type.Method/N" for diagnostics public Dictionary FieldSlots = null!; // FieldDef token → (type, slot) public Dictionary FieldSTypes = null!; // FieldDef token → numeric type (for IR lowering) public HashSet FieldIsScriptStruct = null!; // FieldDef tokens whose type is a script-defined struct (O storage, needs clone on value-load) public Dictionary FieldToTypeDef = null!; // FieldDef token → declaring TypeDef token public Dictionary CtorToType = null!; // .ctor MethodDef token → type public Dictionary TypeDefToCtorTok = null!; // TypeDef token → default .ctor MethodDef token public Dictionary TypeDefToType = null!; // TypeDef token → script type (for initobj) public Dictionary TypesByName = null!; // Script enumerator types (iterator state machines) → driver members for the host bridge. public Dictionary EnumeratorTypes = null!; public Dictionary 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 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.__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 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 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 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) benefits too. if (binding?.AutoBindResolver != null) foreach (var trHandle in reader.TypeReferences) binding.TryAutoBindType(TypeRefFullName(reader, trHandle)); // --- Script methods --- var byToken = new Dictionary(); var byName = new Dictionary(); 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(); 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? 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()).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(); var fieldSTypes = new Dictionary(); var fieldIsScStruct = new HashSet(); var fieldToTypeDef = new Dictionary(); // FieldDef token → declaring TypeDef token var ctorToType = new Dictionary(); var typeDefToCtorTok = new Dictionary(); // TypeDef token → default .ctor MethodDef token var typeDefToType = new Dictionary(); // TypeDef token → type var typesByName = new Dictionary(); // Script types implementing System.Collections.IEnumerator → their driver members, // so the host bridge (ScriptEnumerator) can pump an interpreted state machine. var enumeratorTypes = new Dictionary(); // (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 GetOwnInstanceFields(TypeDefinition td) { var handles = td.GetFields(); var result = new List(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 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 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() : BuildBaseFirstFields(baseTd, out _, depth + 1); } else { fields = new List(); } 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 == "") 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(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 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(); 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(); var hostCtors = new Dictionary(); var hostFields = new Dictionary(); var hostFieldDeclTypeTok = new Dictionary(); var tokenNames = new Dictionary(); var unboundHostMembers = new List(); 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(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()` / 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) 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 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(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.get_Item, Enumerator.get_Current, KeyValuePair Key/Value, …) types O // from the open signature — but `weights[0] + weights[2]` on a List then // lowered add_i4 over boxed floats (sum 0), and `switch (s)` over a List // 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/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) --- // 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}/{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 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}/{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): 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(x) != Echo(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(); 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 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(); 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 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 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 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 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 visited, Dictionary fieldSTypes, HashSet 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(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(); int idx = 1; // skip calling convention int paramCount = DecompressIntAdv(sig, ref idx); if (paramCount == 0) return Array.Empty(); // 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). /// SType of a Func`N TypeSpec's LAST generic arg — its TResult. Func`N is /// 0x15 (CLASS) coded-outer argCount T1..Tn TResult; 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. // 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 → List). // 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): // 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 { 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) 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, EventCallback`1) 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 _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) 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 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(il.Length * 2); var irToIl = new List(il.Length * 2); var strings = new List(); var tokens = new List(); // 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(frameSize + 32); // Parallel list of per-Vt-slot struct layouts. null for non-Vt slots and filler slots. var slotStructs = new List(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? layoutByTokIdx = null; // Primitive typed-array element types (bool/char/byte/…), resolved at lowering. Dictionary? primArrayElemTypeByTokIdx = null; // Delegate-creation sites (ldftn + newobj), resolved at lowering (sparse, by tokIdx). Dictionary? 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 ` (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(); // 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(); // 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(); // 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(); // 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 — `; br END; 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. ` 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())[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())[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())[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())[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 ` — 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 ` 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 '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 ''; 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? 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? 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? 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(n / 3); var ops = new List(n / 3); var widths = new List(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. /// Removes cached (obj, field) forwards for held under /// any object slot other than — a store through one slot must not /// leave stale forwards alive for other slots that may alias the same object. 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(n / 3); var ops = new List(n / 3); var widths = new List(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 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 / 3); var ops = new List(n / 3); var widths = new List(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(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(n / 3); var ops = new List(n / 3); var widths = new List(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(); // 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(n / 3); var ops = new List(n / 3); var widths = new List(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(n / 3); var ops = new List(n / 3); var widths = new List(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 CollectIlBranchTargets(byte[] il) { var targets = new HashSet(); 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 FinallyChain( (int TryStart, int TryEnd, int HandlerStart, int HandlerEnd)[]? regions, int leaveOffset, int target) { var chain = new List(); 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. /// 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 initobj to classify a target the /// local-type inference couldn't see a store to. // 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 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++]=i0?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 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 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 slotTypes, List 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 slotTypes, List 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 ir, List irToIl, (int slot, SType type)[] evalStack, int sp, ref int frameSize, List slotTypes, List slotStructs, Dictionary 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 ir, List irToIl, (int slot, SType type)[] evalStack, ref int sp, ref int frameSize, List slotTypes, List slotStructs, int[] addrTagBySlot, int[] addrFldObjBySlot, int[] addrFldTokBySlot, int[] addrByteOffBySlot, ParsedAssembly asm, List 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 wbArgIdx = stackalloc int[8]; Span wbKind = stackalloc int[8]; Span wbT1 = stackalloc int[8]; Span 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 ir, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, int localIdx, int[] localFrameSlot, SType[] localTypes, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, int sp, ref int frameSize, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, int localIdx, int[] localFrameSlot, SType[] localTypes, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, int argIdx, SType[] argTypes, int[] argFrameSlot, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List slotTypes, List 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 ir, List irToIl, (int, SType)[] stack, ref int sp, ref int frameSize, List slotTypes, List slotStructs, Op cmpI4, Op cmpR4, Op cmpO, Op cmpI4_nn, Op cmpR4_nn, Op branch, List<(int, int)> patchList, int ilTarget, int instrStart, Dictionary 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 ir, List 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 ir, List 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 ir, List 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(); 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); } }