Depending on the language, it may not be necessary to use a call stack. Call stacks are only necessary in languages that allow recursion or mutual recursion. If the language does not allow recursion, then only one invocation of any procedure may be active at any moment, and local variables for that procedure may be statically allocated. Such languages do have to make provision for context changes, for interrupt handling, but this still does not require a stack.
Refer to FORTRAN IV (and earlier) and early versions of COBOL for examples of languages that do not require call stacks.
Refer to the Control Data 6600 (and earlier Control Data machines) for an example of a highly-successful early supercomputer that did not provide direct hardware support for call stacks. Refer to the PDP-8 for an example of a very successful early minicomputer that did not support call stacks.
As far as I know, the Burroughs B5000 stack machines were the first machines with hardware call stacks. The B5000 machines were designed from the ground up to run ALGOL, which required recursion. They also had one of the first descriptor-based architectures, which laid groundwork for capability architectures.
As far as I know, it was the PDP-6 (which grew into the DEC-10) that popularized call stack hardware, when the hacker community at MIT took delivery of one and discovered that the PUSHJ (Push Return Address and Jump) operation allowed the decimal print routine to be reduced from 50 instructions to 10.
The most basic function call semantics in a language that allow recursion require capabilities that match nicely with a stack. If that's all you need, then a basic stack is a good, simple match. If you need more than that, then your data structure has to do more.
The best example of needing more that I have encountered is the "continuation", the ability to suspend a computation in the middle, save it as a frozen bubble of state, and fire it off again later, possibly many times. Continuations became popular in the Scheme dialect of LISP, as a way to implement, among other things, error exits. Continuations require the ability to snapshot the current execution environment, and reproduce it later, and a stack is somewhat inconvenient for that.
Abelson & Sussman's "Structure and Interpretation of Computer Programs" goes into some detail on continuations.
A machine, virtual or not, needs a model of computation which describes how computation is carried out on it. By definition, as soon as it computes, it implements some model of computation. The question then is: What model should we choose for our VM? Physical machines are constrained by what can be effectively and efficiently done in hardware. But, as you note, virtual machines have no such constraints, they are defined in software using arbitrarily high level languages.
There are, in fact, virtual machines that are high-level as you describe. They are called programming languages. The C standard for example dedicates the bulk of its pages to defining a model for the so-called "C abstract machine" which describes how C programs behave, and by extension (as-if rule) how a conforming C compiler (or interpreter) should behave.
Of course, we usually don't call that a virtual machine. A VM is usually takes to mean something lower-level, closer to hardware, not intended to be directly programmed, designed to be executed efficiently. This selection bias means that something that accepts high-level composable code (like what you describe) wouldn't be considered a VM because is executes high-level code.
But to get the to the point, here are some reasons to make a VM (as in, something targeted by a bytecode compiler) register-based or the like. Stack and register machines are extremely simple. There's a sequence of instructions, some state, and semantics for each instruction (a function State -> State). No complex tree reductions, no operator precedence. Parsing, analysing and executing it is very simple, because it's a minimal language (syntactic sugar is compiled away) and designed to be machine-read rather than human-read.
In contrast, parsing even the simplest C-like languages is quite hard, and executing it requires non-local analyses like checking and propagating types, resolving overloads, maintaining a symbol table, resolving string identifiers, turning linear text into a precedence-driven AST, and so on. It builds on concepts that come natural to humans but have to be painstakingly reverse engineered by machines.
JVM bytecode, for example, is emitted by javac
. It virtually never needs to be read or written by humans, so it's natural to gear it towards consumption by machines. If you optimized it for humans, the JVM would just on every startup read the code, parse it, analyze is, and then convert it into an intermediate representation resembling such a simplified machine model anyway. Might as well cut out the middle man.
Best Answer
Register machine bytecode often comes in three-address form, that is, it talks about data flow relationships between registers, and operations take explicit destination registers. So a basic instruction set may look like this:
You could assume you have an infinite number of registers
r0
,r1
, etc. and rewrite this in SSA form:Then various analyses and optimisations become easy. You can reuse registers that are no longer active:
You can also allow instructions to take immediate constants, rather than loading everything into registers:
When you do that, you notice a resemblance with existing register architectures such as x86, and indeed you can perform register allocation to map your virtual register machine onto real hardware:
In either case, the bytecode is a flat representation of a data and control flow graph:
In the stack notation, each stack operation corresponds to one row of this diagram, and the width of the diagram at any point corresponds to the maximum stack depth at that point. In the SSA form, each register corresponds to a point in the graph where a value is produced. They’re simply different structures for talking about the same thing.