You're both right. It has a stack that it uses for callframes and local variables, but the opcodes have operands to directly reference variables on it by index. In other words, it treats all of the stack for a given callframe as a set of registers.
The stack is an abstraction used in the C API. If you look at the actual bytecode generated, it's all register-based.