For clarification, this is a question regarding the specification of the bytecode verifier, especially when dealing with bytecode that isn't generated by javac.
Can the operand stack be modified in a non-deterministic way? In other words, is it possible to have conditional code transition the stack in a different state (size, types at the top of the stack, ...) depending on information that will only be known at runtime?
Examples of possibly non-validating methods (pseudo-code)
float f(boolean b) {
if (b) {
push 1.0 // float
} else {
push 2L // long
}
dup // generic instructions that work for both floats and longs
pop
if (b) {
return // return the float
} else {
l2f // convert the long to float and return it
return
}
}
// Pushes a variable amount of ints on the stack, and pop them all after
void f(int n) {
assert(n > 1)
push 0
push 1
for (i from 0 to n) {
dup
}
do {
istore val // pop the int (1 or 0) and store it in the "val" local
} while (val != 0); // stop once we've popped the original 0 at the base
}
Regarding the second example, I know that the operand stack is bounded, so having an arbitrarily large stack is already problematic in itself.
While both of these methods are semantically correct, I don't think it is really possible to check their correctness in constant time on non-trivial cases. I smell a bit of halting problem / Rice's theorem.
It could also have been possible for a Java runtime to fall back to an interpreter when it cannot prove the correctness of bytecode, but as far as I know, in Java, bytecode is always checked proactively (during link time).
In short, the operand stack has to be deterministic.
There are two places in the specification addressing most of your question:
§2.6.2. Operand Stacks:
Note the sentence “These restrictions on operand stack manipulation are enforced through class file verification” telling you that verification is mandatory.
Further,
§4.9.2. Structural Constraints:
So, there’s no way to push or pop a dynamic number of elements.
Note that since Java 6, branch merge points must have a stack map frame describing the local variables and operand stack entries at this point and all code paths are verified against this description. This static description also rules out dynamic stack values on a technical level.
Note further, that a type description in such a frame can express,
int,long,float,double, and reference types, as well as “top”, the root of the verification type system, which denotes an unusable entry, but not “type 1” nor “type 2”.So, when the specification says that “dup can handle a value of a category 1 computational type”, it’s a short-hand for saying that it can equally handle
int,float, and reference types, but it’s always either of them at a specific code location. There is no scenario where it could encounter an unspecific “type 1”.While it’s possible to have two branches, one pushing a
floatand the other pushing anint, merging both to an entry of the “top” type, it’s not possible to use it in any way, not even to usepopto drop the value, as the value’s type “top” is neither “type 1” nor “type 2”.The only thing, the subsequent code could do, is to ignore this value until exiting the method via
returnorthrow.Before Java 6, there were no stack maps and branches had to be merged by the verifier, according to the following rules:
Note that there is no rule for merging different primitive types or merging a primitive type and a reference type, not even to something like “top”, as with newer class files.
In other words, in older class files, two code paths like described above can’t merge at all.