The VM has one contiguous VirtualMachineValue[] stack and one Push/Pop pair. Its tagged value struct stores numeric values inline, so arithmetic does not box each intermediate result. The interpreter compiles bytecode once, then dispatches each decoded instruction through a 256-entry opcode table. There is no numeric-only stack API.

The before-and-after implementation details and a narrowly scoped measurement are in Reducing repeated work in the interpreter.

From C# to MirrorVM bytecode

These small methods are part of the sample:

static int Add(int left, int right)
{
    return left + right;
}

static int Subtract(int left, int right)
{
    return left - right;
}

static int AddThenSubtract(int left, int right, int adjustment)
{
    return left + right - adjustment;
}

static string MyString()
{
    return "Test";
}

For Add, the converter reads the CIL argument loads and add, then emits:

01 00 00    LoadArgument 0
01 01 00    LoadArgument 1
10       Add

LoadArgument uses opcode ID 0x01 followed by a two-byte little-endian unsigned CIL argument index. Each load pushes that argument. Add has ID 0x10, pops right then left, calculates left + right, and pushes one result. The CIL ret is consumed by the converter; compilation ends when the byte sequence ends, and execution walks the decoded instruction array.

Subtract changes the last byte to 0x13 (Sub). The three-argument method becomes:

01 00 00    LoadArgument 0
01 01 00    LoadArgument 1
10       Add
01 02 00    LoadArgument 2
13       Sub

For (40 + 4) - 2, the stack evolves as [40], [40, 4], [44], [44, 2], then [42]. Each arithmetic handler leaves its own output on that same stack.

String values use the same return path

MyString compiles to CIL ldstr "Test" followed by ret. The converter stores each UTF-16 code unit in little-endian order, preceded by a four-byte little-endian byte count. This preserves unpaired surrogate code units too:

03                         Ldstr opcode ID
08 00 00 00                UTF-16 byte count: 8
54 00 65 00 73 00 74 00    UTF-16 code units: "Test"

Ldstr pushes the decoded string. VirtualMachine.Execute(VirtualMachineProgram, object[]) returns the one value left on the stack as object. A rewritten string method casts that object back to string; an Int32 or UInt32 method unboxes the I4 result. This is one runtime entrypoint for all supported result types.

Arithmetic names and behavior

Bytecode IDCIL operationHandler
0x10addAdd
0x11, 0x12add.ovf, add.ovf.unAddOvf, AddOvfUn
0x13, 0x14, 0x15sub, sub.ovf, sub.ovf.unSub, SubOvf, SubOvfUn
0x16, 0x17, 0x18mul, mul.ovf, mul.ovf.unMul, MulOvf, MulOvfUn
0x19, 0x1Adiv, div.unDiv, DivUn
0x1B, 0x1Crem, rem.unRem, RemUn
0x1DnegNeg

The VM normalizes small signed and unsigned integers to I4, 64-bit integers to I8, native integers to the process pointer width, and floating-point values to their CIL stack kinds. The opcode selects signed or unsigned behavior. The implementation includes checked arithmetic, bitwise operations, shifts, comparisons, finite checks, and the full mapped conv.* family, including checked unsigned-source forms.

The protected sample exercises numeric signatures, constants, arithmetic, conversions, and typed returns. Its CLR differential checks compare each implemented numeric opcode with a generated CLR method on each of the 26 matrix entries in both x86 and x64 processes. The tests cover signed and unsigned source-width endpoints, checked and unchecked overflow, division and remainder overflow and zero divisors, remainder signs, shift counts around and beyond integer widths, and conversion fractions near target boundaries. Floating tests include signed zero, least subnormals, finite extrema, infinities, and NaN. These are targeted cross-products, not every possible operand pair. Decimal passes through by value, but decimal arithmetic compiles to method calls and remains unsupported. This is the verified numeric subset, not complete CLR numeric emulation.

The matrix also checks the expression 16777216f + 1f - 16777216f. On x86 .NET Framework, the CLR returns 1 because floating values retain wider intermediate precision on the evaluation stack. MirrorVM uses Double intermediates for Single arithmetic on this runtime and rounds at local stores and returns. The same numeric suite probes out-of-width shifts on x86; MirrorVM follows the observed CIL behavior on Framework and .NET Core 2.0, including the legacy 32-bit JIT case. The precision and shift checks are differential comparisons with generated CLR methods.

How the helper compares with HexVM's NumericHelper

MirrorVM's NumericArithmetic is a focused helper for the current opcode subset. It is not a copy or drop-in replacement for HexVM's NumericHelper. Its present value kinds are I4, I8, native integers, Single, and Double, with small integer and enum normalization at the VM boundary. Decimal is passed through but its operators are not implemented here.

The helper does not yet represent Half, Int128, UInt128, BigInteger, pointers, byrefs, typed references, or arbitrary structs. The differential checks cover the implemented numeric opcode set on the configured runtime matrix; they do not prove full CLR compliance or parity with HexVM.

Unsupported opcodes leave the method unchanged

The sample includes a branch method and a method that calls Add. Branches and Call are not implemented in this VM version, so the converter reports UnsupportedBranch — contains unsupported CIL opcode 'ble.s' and UnsupportedCall — contains unsupported CIL opcode 'call'. It leaves those method bodies as they were. The supported arithmetic and string methods are still converted.

Verified numeric scope

The protected sample matched its original CLR output on all 26 configured matrix entries in x86 and x64 processes. All 25 unit tests pass on the .NET 9 x64 host. Framework 4.x runs used the installed 4.8.1 in-place CLR.

Stable opcode IDs and visible stack effects make the execution path straightforward to inspect and emulate. That supports reverse engineering and defensive research. It is not a guarantee that every downstream use is benign.

← Back to all notes