Blog

Why pd-vm Uses a Stack + Local-Slots Architecture

A design rationale for an embedded proxy VM


The Problem

pd-vm runs inside pd-edge, a proxy runtime that may execute user logic on every request. That environment puts pressure on the VM in a specific way:

Those requirements favor a very small execution contract: instruction pointer, operand stack, and local storage.

Why Not Register-Based?

A register VM can reduce stack traffic, but it moves complexity into the instruction format and compiler:

For a VM embedded in a proxy runtime, compactness and simple suspension matter more than minimizing dispatch count on arithmetic-heavy code.

Why Not Pure Stack?

A pure stack machine is compact, but it makes durable program state awkward:

Local slots keep the operand stack short-lived and expression-oriented.

Why Not a Language-Shaped HIR?

Another option is a single language-shaped core IR, such as an expression-oriented or ANF-style HIR, that every frontend must target.

That approach has a real benefit: it gives the compiler one canonical semantic model. Rewrites, normalization, and some whole-program analyses become more uniform.

The cost is that the HIR stops being a neutral boundary. It becomes another language that every frontend must lower into, including frontends whose natural control flow is imperative. Mutable variables, early returns, and loop-heavy code then have to be translated through an opinionated core representation before they can reach execution.

pd-vm uses a language-agnostic execution IR instead. It gives up the elegance of one canonical core language, but it is a better fit for a VM that serves multiple frontends and treats bytecode, not a language-specific HIR, as the runtime contract.

The Design

pd-vm uses a compact bytecode with 25 opcodes. The important split is structural:

ldloc reads a local onto the stack. stloc writes the top-of-stack value back into a slot. Arithmetic and comparisons operate on stack values. That is enough to keep the interpreter small while still giving the compiler stable storage.

Heap-backed values are shared cheaply. Loading a local is non-destructive, and when the compiler knows a value should move rather than stay shared, it emits the explicit ldloc; ldc Null; stloc pattern to release the slot early.

What This Enables

1. Deterministic cleanup

The VM can clear locals and drain the stack without a tracing collector. In pd-edge, instances can also be reset and reused rather than rebuilt for every request.

2. Clean suspension

When a host call blocks on external work, the continuation is already in the right shape: instruction pointer, stack, and locals. There is no separate coroutine stack and no register remapping step.

3. A practical multi-frontend target

Multiple source languages can lower into the same execution format without first agreeing on one language-shaped core runtime.

4. Tiered execution

The same bytecode can start in the interpreter, move into a trace JIT for hot paths, and compile ahead of time for stable deployments. The stack effects are simple enough to reconstruct SSA later rather than carrying SSA in the baseline runtime representation.

5. Compact program bundles

Small opcodes and small operands keep .vmbc bundles compact, which matters when programs are distributed from a control plane to edge nodes.

Where This Falls Short

Summary

The stack + local-slots model is not unusual. What makes it the right choice here is the combination of compact bytecode, trivial suspension, predictable teardown, and low frontend complexity. For an embedded proxy VM, those properties are more important than maximizing interpreter throughput on compute-heavy code.