The execution path
Assembly method body
↓
MethodBodyConverter ── unsupported? ── keep original CIL and report why
↓ supported CIL
Per-type initializer: bytecode decoded once
↓
Rewritten method stub: cached program + object[] arguments
↓
VirtualMachine.Execute(VirtualMachineProgram, object[])
↓
One VirtualMachineValue[] stack with indexed IOpCode handlers
↓
object result → cast/unbox in the rewritten method
Conversion decides for the whole method
The converter reads a method's signature and instructions. It emits bytecode only when the entire body fits the implemented subset. Calls, exception regions, unsupported signatures, and unsupported instructions make the converter keep the original method body and report the reason. It does not replace only part of a method.
For the M3 subset, a control-flow pass follows reachable branches, switch targets, and fall-through edges. It checks branch targets, stack heights and kinds at merges, reachable returns, declared maxstack, and local reads. The converter maps each CIL instruction to one VM instruction, so a branch target becomes an absolute decoded instruction index.
The virtualizer stores each method's bytecode in a generated per-type initializer. The initializer compiles each byte sequence into a decoded VirtualMachineProgram the first time one of that type's protected methods is used. Each method stub then reuses its cached program, creates an object[] for arguments, calls the same Execute(VirtualMachineProgram, object[]) method for every supported return type, and casts or unboxes the returned object to the declared type.
Bytecode dispatch stays explicit
Compile validates the byte stream and decodes its operands, branch targets, and switch tables once. During execution, Execute indexes a 256-entry handler table with each instruction's byte opcode. Branch handlers update the instruction pointer; comparison handlers pop their inputs and do not push a result. Other handlers push their result when the opcode produces one. There is one VM stack and one pair of stack operations; arithmetic does not use a separate numeric stack.
This keeps the correspondence between bytecode and behavior visible. A reader can follow an instruction to its handler and inspect the stack effect. The structure is intended to make protected code easier to study for reverse engineering and defensive research.
One contiguous stack, with inline numeric values
The stack is a contiguous VirtualMachineValue[]. Its tagged value struct stores integers, native integers, and floating-point values inline; strings and other references use its object-reference field. Arithmetic therefore avoids boxing each intermediate result. Arguments still cross the current object[] boundary, and Execute returns an object, so value-type arguments and the final value can still be boxed.
The before-and-after details for indexed dispatch, inline stack values, and compile-once bytecode are in Reducing repeated work in the interpreter, along with a narrow microbenchmark and its limits.
This representation covers the VM's current subset. It does not yet represent managed byrefs, pointers, function pointers, typed references, or arbitrary value-type storage. Those values cannot all be treated as ordinary object references: their lifetime, aliasing, layout, and GC behavior are part of CLR semantics.
As support grows, those representations must still fit the VM's single operand stack and shared execution path. The converter must also validate stack types, control flow, calls, prefixes, and exception regions before claiming broader compatibility.
The current branch family includes unconditional and truth branches, signed and unsigned comparisons, and switch. Locals carry their declared supported kind, receive CLR defaults when initlocals is set, and are checked when loaded. This stack analysis covers the implemented subset; it is not a full ECMA-335 verifier. See M3 - Control flow for the instruction mapping and sample cases.
The current numeric instruction set and representative C#-to-bytecode examples are described in M2 - Numeric values end to end.
The converter handles supported scalar and string methods with arithmetic, conversions, branches, switches, and locals. Calls, fields, arrays, unsupported object signatures, and exception handling remain unsupported. See the current limits, M3 - Control flow, and runtime evidence.