What M3 adds
The bytecode now has its own handlers for unconditional branches, truth branches, the signed and unsigned comparison branch family, and switch. Both short and long CIL branch encodings map to the same VM opcode. The converter also translates ldloc and stloc forms, including the compact .0 through .3 forms.
Branches change the VM instruction pointer. A comparison branch consumes its two operands and pushes no result. switch consumes an Int32 selector; a negative or out-of-range selector follows the next instruction, as the CIL instruction specifies.
A method becomes a branch and two exits
For a small sign check, the VM bytecode can be written as:
static int Sign(int value)
{
if (value < 0) return -1;
return 1;
}
01 00 00 LoadArgument 0
02 00 00 00 00 LoadInt32 0
64 05 00 00 00 Bge instruction 5
02 FF FF FF FF LoadInt32 -1
0A Ret
02 01 00 00 00 LoadInt32 1
0A Ret
This is one valid lowering of the method. Each bytecode opcode begins one decoded instruction. Bge has ID 0x64; its four-byte little-endian operand is the absolute decoded instruction index 5. It pops the argument and zero. Nonnegative values branch to instruction 5; negative values continue to the -1 return.
Branch behavior follows the operand kinds
| Bytecode group | Stack effect | Implemented behavior |
|---|---|---|
Br | Pop none | Set the instruction pointer to the decoded target. |
Brtrue, Brfalse | Pop one | Test supported integer kinds for zero and supported string/null references for null. |
Beq, BneUn | Pop two | Compare compatible numeric values or string/null references by identity. BneUn takes the branch for unordered floating-point comparisons. |
Bge, Bgt, Ble, Blt and .un forms | Pop two | Use signed or unsigned integer comparison as selected by the opcode. The .un forms also take the branch when either floating-point input is NaN. |
Switch | Pop one Int32 | Use an in-range nonnegative selector as a table index; otherwise continue after the table. |
ldstr values are interned when bytecode is decoded. That keeps ceq, beq, and bne.un identity comparisons consistent when equal CIL string literals are loaded. Ordinary string equality still means reference identity at the CIL comparison instruction; C#'s overloaded string == uses a method call, which remains unsupported.
Locals and control-flow validation
A local instruction carries its index and declared VM kind. When the CIL body has initlocals, the VM creates the corresponding CLR default value. If it is disabled, the converter requires a local to be assigned on every reachable path before a load.
Before emitting bytecode, a worklist starts at the method entry and follows branch targets, switch cases, and reachable fall-through. At each merge, incoming paths must agree on stack height and compatible kinds. The converter also rejects invalid targets, reachable fall-through past the body, and a computed stack depth greater than the CIL maxstack. A method with an invalid or unsupported instruction remains unchanged as a whole.
This is stack validation for the translated subset, not a complete ECMA-335 verifier. Calls, leave, exception regions, fields, arrays, object and arbitrary struct signatures, managed byrefs, pointers, and function pointers remain outside M3.
Verification and scope
The sample exercises nested decisions, a loop with locals, a dense 40-case switch that forces a long conditional branch, unsigned comparison, floating-point NaN, null checks, string/null merges, and an unpaired-surrogate literal. Converter rejection tests cover branch and switch targets outside the method, incompatible stack heights and kinds at merges, reachable fall-through, a maxstack violation, stack underflow, and a local read unassigned on one incoming path. A unit test confirms the CIL contains the long branch and switch. For each target, the matrix runner executes the original sample and compares it line by line with the protected copy. The sample also runs CLR-generated comparisons for the implemented numeric opcodes and a multi-instruction Single-precision stack case.
The Release solution builds all 25 configured sample targets. Original and protected output matched for all 26 matrix entries in x86 and x64 processes: Framework 2.0 through 4.8.1, Core 2.0 through 3.1, and .NET 5 through 10. Framework 3.0 uses the Framework 2.0 sample and CLR line; Framework 4.x uses the installed 4.8.1 in-place CLR. The 25 unit tests pass on .NET 9.
On x86 .NET Framework, the sample shows that floating values can retain wider intermediate precision while they remain on the evaluation stack. MirrorVM uses Double intermediates for Single arithmetic on this runtime and rounds on local stores and returns. The sample checks the expression 16777216f + 1f - 16777216f against a generated CLR method. Numeric comparisons also cover signed zero, least subnormals, finite extrema, infinities, NaN, and selected conversion and shift boundaries. These are broad targeted probes, not an exhaustive floating-point proof. See M2 - Numeric values end to end and the compatibility evidence.