Add VSMX bytecode interpreter

- VsmxInterpreter: stack machine executing all 74 VSMX opcodes
  (arithmetic, comparisons, bitwise, jumps, calls, arrays, objects).
- Object model: VsmxBaseObject/Undefined/Null/Boolean/Number/String/
  Object/Array/Function/Reference/LocalVarReference/Method/CallState/
  NativeObject, plus BaseNativeObject for future native objects.
- CLI 'rco' now runs the menu script headlessly and prints the 172
  resulting global variables (state, chapter/language config, focus).
- Validated against Batman Begins: script runs with zero unimplemented-
  opcode warnings and defines all menu functions (on_cancel, mm_on_push,
  trans_to_play, play_movie_beginning, ...).
This commit is contained in:
2026-09-15 22:39:36 -04:00
parent d3e3cbd017
commit d56f6f12b0
17 changed files with 1627 additions and 6 deletions
+19 -6
View File
@@ -11,6 +11,9 @@ import dev.quoije.umd.player.PlaybackEngine;
import dev.quoije.umd.playlist.Playlist;
import dev.quoije.umd.rco.RcoFile;
import dev.quoije.umd.rco.vsmx.Vsmx;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxObject;
import dev.quoije.umd.sfo.Sfo;
import java.io.ByteArrayOutputStream;
@@ -414,13 +417,23 @@ public final class Main {
Vsmx vsmx = new Vsmx(vsmxData, "menu");
System.out.printf("vsmx: %d code groups, %d texts, %d props, %d names%n",
vsmx.codes().length, vsmx.texts().length, vsmx.properties().length, vsmx.names().length);
List<String> lines = vsmx.disassemble();
int shown = Math.min(lines.size(), 100);
for (int i = 0; i < shown; i++) {
System.out.println(" " + lines.get(i));
VsmxInterpreter interpreter = new VsmxInterpreter();
interpreter.setVsmx(vsmx);
VsmxObject globals = new VsmxObject(interpreter, null);
interpreter.run(globals);
System.out.printf("global variables (%d):%n", globals.getPropertyNames().size());
for (String varName : globals.getPropertyNames()) {
VsmxBaseObject value = globals.getPropertyValue(varName);
System.out.printf(" %s = %s%n", varName, value);
}
if (!interpreter.getWarnings().isEmpty()) {
System.out.printf("warnings (%d):%n", interpreter.getWarnings().size());
for (String warning : interpreter.getWarnings()) {
System.out.println(" " + warning);
}
if (lines.size() > shown) {
System.out.printf(" ... (%d more lines)%n", lines.size() - shown);
}
}
}
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** An array object with an explicit length. */
public class VsmxArray extends VsmxObject {
private static final String CLASS_NAME = "Array";
private int length;
public VsmxArray(VsmxInterpreter interpreter) {
super(interpreter, CLASS_NAME);
}
public VsmxArray(VsmxInterpreter interpreter, int size) {
super(interpreter, CLASS_NAME);
if (size > 0) {
length = size;
for (int i = 0; i < size; i++) {
create(i);
}
}
}
public int getLength() {
return length;
}
private void create(int index) {
super.setPropertyValue(Integer.toString(index), VsmxUndefined.singleton);
}
private void delete(int index) {
super.deletePropertyValue(Integer.toString(index));
}
private void updateLength(int index) {
if (index >= length) {
for (int i = length; i <= index; i++) {
create(i);
}
length = index + 1;
}
}
@Override
public VsmxBaseObject getPropertyValue(String name) {
if (LENGTH_NAME.equals(name)) {
return new VsmxNumber(interpreter, length);
}
int index = getIndex(name);
if (index >= 0) {
return getPropertyValue(index);
}
return super.getPropertyValue(name);
}
@Override
public void setPropertyValue(String name, VsmxBaseObject value) {
if (LENGTH_NAME.equals(name)) {
int newLength = value.getIntValue();
if (newLength > length) {
for (int i = length; i < newLength; i++) {
create(i);
}
} else if (newLength < length) {
for (int i = newLength; i < length; i++) {
delete(i);
}
}
return;
}
int index = getIndex(name);
if (index >= 0) {
setPropertyValue(index, value);
} else {
super.setPropertyValue(name, value);
}
}
@Override
public void deletePropertyValue(String name) {
if (LENGTH_NAME.equals(name)) {
return;
}
int index = getIndex(name);
if (index >= 0) {
deletePropertyValue(index);
} else {
super.deletePropertyValue(name);
}
}
@Override
public VsmxBaseObject getPropertyValue(int index) {
if (index < 0) {
return VsmxUndefined.singleton;
}
updateLength(index);
return super.getPropertyValue(Integer.toString(index));
}
@Override
public void setPropertyValue(int index, VsmxBaseObject value) {
if (index >= 0) {
updateLength(index);
super.setPropertyValue(Integer.toString(index), value);
}
}
@Override
public void deletePropertyValue(int index) {
if (index >= 0) {
if (index == length - 1) {
delete(index);
length = index;
} else if (index < length) {
create(index);
}
}
}
@Override
public boolean hasPropertyValue(String name) {
if (LENGTH_NAME.equals(name)) {
return true;
}
return super.hasPropertyValue(name);
}
@Override
public boolean getBooleanValue() {
return length > 0;
}
@Override
public boolean equals(VsmxBaseObject value) {
if (value instanceof VsmxArray) {
if (getLength() == 0 && ((VsmxArray) value).getLength() == 0) {
return true;
}
}
return super.equals(value);
}
@Override
public String toString() {
StringBuilder s = new StringBuilder("[length=").append(length);
for (String name : getPropertyNames()) {
if (getIndex(name) < 0) {
s.append(", ").append(name).append('=').append(properties.get(name));
}
}
return s.append(']').toString();
}
}
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import java.util.List;
/**
* Base of every value in the VSMX script runtime. Ported from JPCSP.
*/
public abstract class VsmxBaseObject {
protected static final String LENGTH_NAME = "length";
protected static final String PROTOTYPE_NAME = "prototype";
protected static final String CALL_NAME = "call";
protected VsmxInterpreter interpreter;
protected VsmxBaseObject(VsmxInterpreter interpreter) {
this.interpreter = interpreter;
}
protected void setInterpreter(VsmxInterpreter interpreter) {
this.interpreter = interpreter;
}
public VsmxInterpreter getInterpreter() {
return interpreter;
}
public VsmxBaseObject getValue() {
return this;
}
public VsmxBaseObject getValueWithArguments(int numberOfArguments) {
return getValue();
}
public float getFloatValue() {
return 0f;
}
public int getIntValue() {
return (int) getFloatValue();
}
public boolean getBooleanValue() {
return getFloatValue() != 0f;
}
public String getStringValue() {
return toString();
}
public boolean equals(VsmxBaseObject value) {
return getFloatValue() == value.getFloatValue();
}
public boolean identity(VsmxBaseObject value) {
return this == value;
}
public VsmxBaseObject getPropertyValue(String name) {
if (PROTOTYPE_NAME.equals(name)) {
VsmxObject prototype = getPrototype();
if (prototype != null) {
return prototype;
}
}
return VsmxUndefined.singleton;
}
public VsmxBaseObject getPropertyValue(int index) {
return getPropertyValue(Integer.toString(index));
}
public void setPropertyValue(String name, VsmxBaseObject value) {
}
public void setPropertyValue(int index, VsmxBaseObject value) {
setPropertyValue(Integer.toString(index), value);
}
public void deletePropertyValue(String name) {
setPropertyValue(name, VsmxUndefined.singleton);
}
public void deletePropertyValue(int index) {
deletePropertyValue(Integer.toString(index));
}
public boolean hasPropertyValue(String name) {
return !VsmxUndefined.singleton.equals(getPropertyValue(name));
}
public List<String> getPropertyNames() {
return List.of();
}
public void setFloatValue(float value) {
}
public abstract String typeOf();
public abstract String getClassName();
protected VsmxObject getPrototype() {
String className = getClassName();
if (className == null) {
return null;
}
if (!interpreter.getGlobalVariables().hasPropertyValue(className)) {
return null;
}
VsmxBaseObject classObject = interpreter.getGlobalVariables().getPropertyValue(className).getPrototype();
if (!(classObject instanceof VsmxObject)) {
classObject = new VsmxObject(interpreter, className);
interpreter.getGlobalVariables().setPropertyValue(className, classObject);
}
return (VsmxObject) classObject;
}
@Override
public String toString() {
if (getFloatValue() == (float) getIntValue()) {
return Integer.toString(getIntValue());
}
return Float.toString(getFloatValue());
}
}
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** Boolean value, interned into two singletons. */
public final class VsmxBoolean extends VsmxBaseObject {
private static final VsmxBoolean singletonTrue = new VsmxBoolean(true);
private static final VsmxBoolean singletonFalse = new VsmxBoolean(false);
private boolean value;
private VsmxBoolean(boolean value) {
super(null);
this.value = value;
}
public static void init(VsmxInterpreter interpreter) {
singletonTrue.setInterpreter(interpreter);
singletonFalse.setInterpreter(interpreter);
}
public static VsmxBoolean getValue(boolean value) {
return value ? singletonTrue : singletonFalse;
}
public static VsmxBoolean getValue(int value) {
return getValue(value != 0);
}
@Override
public float getFloatValue() {
return value ? 1f : 0f;
}
@Override
public int getIntValue() {
return value ? 1 : 0;
}
@Override
public boolean getBooleanValue() {
return value;
}
@Override
public String typeOf() {
return "boolean";
}
@Override
public String getClassName() {
return "Boolean";
}
@Override
public String toString() {
return value ? "true" : "false";
}
}
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
/** A single interpreter call frame (this object, locals, operand stack). */
public final class VsmxCallState {
private final VsmxBaseObject thisObject;
private final VsmxBaseObject[] localVariables;
private final Deque<VsmxBaseObject> stack = new ArrayDeque<>();
private final int returnPc;
private final boolean returnThis;
private final boolean exitAfterCall;
public VsmxCallState(VsmxBaseObject thisObject, int numberOfLocalVariables,
int returnPc, boolean returnThis, boolean exitAfterCall) {
this.thisObject = thisObject;
this.localVariables = new VsmxBaseObject[numberOfLocalVariables];
Arrays.fill(localVariables, VsmxUndefined.singleton);
this.returnPc = returnPc;
this.returnThis = returnThis;
this.exitAfterCall = exitAfterCall;
}
public int getReturnPc() {
return returnPc;
}
public VsmxBaseObject getThisObject() {
return thisObject;
}
public VsmxBaseObject getLocalVar(int i) {
if (i <= 0 || i > localVariables.length) {
return VsmxUndefined.singleton;
}
return localVariables[i - 1];
}
public void setLocalVar(int i, VsmxBaseObject value) {
if (i > 0 && i <= localVariables.length) {
localVariables[i - 1] = value;
}
}
public Deque<VsmxBaseObject> getStack() {
return stack;
}
public boolean getReturnThis() {
return returnThis;
}
public boolean getExitAfterCall() {
return exitAfterCall;
}
@Override
public String toString() {
StringBuilder s = new StringBuilder("CallState[returnPc=").append(returnPc).append(", this=").append(thisObject);
for (int i = 1; i <= localVariables.length; i++) {
s.append(", var").append(i).append('=').append(getLocalVar(i));
}
return s.append(']').toString();
}
}
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** A compiled function value. */
public class VsmxFunction extends VsmxBaseObject {
private final int args;
private final int localVars;
private final int startLine;
private final VsmxObject prototype;
public VsmxFunction(VsmxInterpreter interpreter, int args, int localVars, int startLine) {
super(interpreter);
this.args = args;
this.localVars = localVars;
this.startLine = startLine;
this.prototype = new VsmxObject(interpreter, "Function");
}
public int getArgs() {
return args;
}
public int getLocalVars() {
return localVars;
}
public int getStartLine() {
return startLine;
}
public void call(VsmxCallState callState) {
}
public VsmxBaseObject getReturnValue() {
return null;
}
@Override
public String typeOf() {
return "function";
}
@Override
public String getClassName() {
return "Function";
}
@Override
protected VsmxObject getPrototype() {
return prototype;
}
@Override
public String toString() {
return "Function(args=" + args + ", localVars=" + localVars + ", startLine=" + startLine + ")";
}
}
@@ -0,0 +1,530 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import dev.quoije.umd.rco.vsmx.Vsmx;
import dev.quoije.umd.rco.vsmx.VsmxCode;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* Interpreter for VSMX bytecode (the compiled menu script). Ported from
* JPCSP's {@code VSMXInterpreter}.
*/
public final class VsmxInterpreter {
private Vsmx.Group[] codes;
private String[] texts;
private String[] properties;
private String[] names;
private String name;
private int pc;
private boolean exit;
private Deque<VsmxBaseObject> stack;
private Deque<VsmxCallState> callStates;
private VsmxCallState callState;
private VsmxObject globalVariables;
private final List<String> warnings = new ArrayList<>();
public void setVsmx(Vsmx vsmx) {
this.codes = vsmx.codes();
this.texts = vsmx.texts();
this.properties = vsmx.properties();
this.names = vsmx.names();
this.name = vsmx.name();
}
public VsmxObject getGlobalVariables() {
return globalVariables;
}
public List<String> getWarnings() {
return warnings;
}
private void warn(String message) {
warnings.add(message);
}
private VsmxBaseObject[] popValues(int n) {
VsmxBaseObject[] values = new VsmxBaseObject[n];
for (int i = n - 1; i >= 0; i--) {
values[i] = stack.pop().getValue();
}
return values;
}
private void pushCallState(VsmxBaseObject thisObject, int numberOfLocalVariables, boolean returnThis, boolean exitAfterCall) {
if (callState != null) {
callStates.push(callState);
}
callState = new VsmxCallState(thisObject, numberOfLocalVariables, pc, returnThis, exitAfterCall);
stack = callState.getStack();
}
private void popCallState() {
if (callStates.isEmpty()) {
callState = null;
stack = null;
} else {
callState = callStates.pop();
stack = callState.getStack();
}
}
private void interpret(Vsmx.Group code) {
VsmxBaseObject o1, o2, o3, o, r;
VsmxBaseObject[] arguments;
float f1, f2, f;
String s1, s2, s;
int i1, i2, i;
boolean b;
switch (code.opcode()) {
case VsmxCode.VID_NOTHING -> { }
case VsmxCode.VID_OPERATOR_ASSIGN -> {
o1 = stack.pop().getValue();
o2 = stack.pop();
if (o2 instanceof VsmxReference ref) {
ref.assign(o1);
stack.push(o1);
} else {
warn("Line#" + (pc - 1) + " non-ref assignment");
}
}
case VsmxCode.VID_OPERATOR_ADD -> {
o1 = stack.pop().getValue();
o2 = stack.pop().getValue();
if (o1 instanceof VsmxString || o2 instanceof VsmxString) {
s1 = o1.getStringValue();
s2 = o2.getStringValue();
stack.push(new VsmxString(this, s2 + s1));
} else {
f1 = o1.getFloatValue();
f2 = o2.getFloatValue();
stack.push(new VsmxNumber(this, f2 + f1));
}
}
case VsmxCode.VID_OPERATOR_SUBTRACT -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(new VsmxNumber(this, f2 - f1));
}
case VsmxCode.VID_OPERATOR_MULTIPLY -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(new VsmxNumber(this, f2 * f1));
}
case VsmxCode.VID_OPERATOR_DIVIDE -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(new VsmxNumber(this, f2 / f1));
}
case VsmxCode.VID_OPERATOR_MOD -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(new VsmxNumber(this, f2 % f1));
}
case VsmxCode.VID_OPERATOR_POSITIVE -> {
f = stack.pop().getFloatValue();
stack.push(new VsmxNumber(this, f));
}
case VsmxCode.VID_OPERATOR_NEGATE -> {
f = stack.pop().getFloatValue();
stack.push(new VsmxNumber(this, -f));
}
case VsmxCode.VID_OPERATOR_NOT -> {
b = stack.pop().getBooleanValue();
stack.push(VsmxBoolean.getValue(!b));
}
case VsmxCode.VID_P_INCREMENT -> {
o = stack.pop();
f = o.getFloatValue() + 1f;
stack.push(new VsmxNumber(this, f));
if (o instanceof VsmxReference ref) {
ref.assign(new VsmxNumber(this, f));
}
}
case VsmxCode.VID_P_DECREMENT -> {
o = stack.pop();
f = o.getFloatValue() - 1f;
stack.push(new VsmxNumber(this, f));
if (o instanceof VsmxReference ref) {
ref.assign(new VsmxNumber(this, f));
}
}
case VsmxCode.VID_INCREMENT -> {
o = stack.pop();
f = o.getFloatValue();
stack.push(new VsmxNumber(this, f));
if (o instanceof VsmxReference ref) {
ref.assign(new VsmxNumber(this, f + 1f));
}
}
case VsmxCode.VID_DECREMENT -> {
o = stack.pop();
f = o.getFloatValue();
stack.push(new VsmxNumber(this, f));
if (o instanceof VsmxReference ref) {
ref.assign(new VsmxNumber(this, f - 1f));
}
}
case VsmxCode.VID_OPERATOR_EQUAL -> {
o1 = stack.pop().getValue();
o2 = stack.pop().getValue();
stack.push(VsmxBoolean.getValue(o1.equals(o2)));
}
case VsmxCode.VID_OPERATOR_NOT_EQUAL -> {
o1 = stack.pop().getValue();
o2 = stack.pop().getValue();
stack.push(VsmxBoolean.getValue(!o1.equals(o2)));
}
case VsmxCode.VID_OPERATOR_IDENTITY -> {
o1 = stack.pop().getValue();
o2 = stack.pop().getValue();
stack.push(VsmxBoolean.getValue(o1.identity(o2)));
}
case VsmxCode.VID_OPERATOR_NON_IDENTITY -> {
o1 = stack.pop().getValue();
o2 = stack.pop().getValue();
stack.push(VsmxBoolean.getValue(!o1.identity(o2)));
}
case VsmxCode.VID_OPERATOR_LT -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(VsmxBoolean.getValue(f2 < f1));
}
case VsmxCode.VID_OPERATOR_LTE -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(VsmxBoolean.getValue(f2 <= f1));
}
case VsmxCode.VID_OPERATOR_GTE -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(VsmxBoolean.getValue(f2 >= f1));
}
case VsmxCode.VID_OPERATOR_GT -> {
f1 = stack.pop().getFloatValue();
f2 = stack.pop().getFloatValue();
stack.push(VsmxBoolean.getValue(f2 > f1));
}
case VsmxCode.VID_OPERATOR_INSTANCEOF -> warn("Line#" + (pc - 1) + " unimplemented INSTANCEOF");
case VsmxCode.VID_OPERATOR_IN -> warn("Line#" + (pc - 1) + " unimplemented IN");
case VsmxCode.VID_OPERATOR_TYPEOF -> {
o = stack.pop().getValue();
stack.push(new VsmxString(this, o.typeOf()));
}
case VsmxCode.VID_OPERATOR_B_AND -> {
i1 = stack.pop().getIntValue();
i2 = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, i1 & i2));
}
case VsmxCode.VID_OPERATOR_B_XOR -> {
i1 = stack.pop().getIntValue();
i2 = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, i1 ^ i2));
}
case VsmxCode.VID_OPERATOR_B_OR -> {
i1 = stack.pop().getIntValue();
i2 = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, i1 | i2));
}
case VsmxCode.VID_OPERATOR_B_NOT -> {
i = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, ~i));
}
case VsmxCode.VID_OPERATOR_LSHIFT -> {
i1 = stack.pop().getIntValue();
i2 = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, i2 << i1));
}
case VsmxCode.VID_OPERATOR_RSHIFT -> {
i1 = stack.pop().getIntValue();
i2 = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, i2 >> i1));
}
case VsmxCode.VID_OPERATOR_URSHIFT -> {
i1 = stack.pop().getIntValue();
i2 = stack.pop().getIntValue();
stack.push(new VsmxNumber(this, i2 >>> i1));
}
case VsmxCode.VID_STACK_COPY -> stack.push(stack.peek());
case VsmxCode.VID_STACK_SWAP -> {
o1 = stack.pop();
o2 = stack.pop();
stack.push(o1);
stack.push(o2);
}
case VsmxCode.VID_END_STMT -> stack.clear();
case VsmxCode.VID_CONST_NULL -> stack.push(VsmxNull.singleton);
case VsmxCode.VID_CONST_EMPTYARRAY -> stack.push(new VsmxArray(this));
case VsmxCode.VID_CONST_BOOL -> stack.push(VsmxBoolean.getValue(code.value()));
case VsmxCode.VID_CONST_INT -> stack.push(new VsmxNumber(this, code.value()));
case VsmxCode.VID_CONST_FLOAT -> stack.push(new VsmxNumber(this, code.floatValue()));
case VsmxCode.VID_CONST_STRING -> stack.push(new VsmxString(this, texts[code.value()]));
case VsmxCode.VID_CONST_OBJECT -> { }
case VsmxCode.VID_FUNCTION ->
stack.push(new VsmxFunction(this, (code.id() >> 8) & 0xFF, (code.id() >> 24) & 0xFF, code.value()));
case VsmxCode.VID_ARRAY -> stack.push(new VsmxArray(this));
case VsmxCode.VID_THIS -> stack.push(callState.getThisObject());
case VsmxCode.VID_UNNAMED_VAR -> stack.push(new VsmxLocalVarReference(this, callState, code.value()));
case VsmxCode.VID_VARIABLE -> stack.push(new VsmxReference(this, globalVariables, names[code.value()]));
case VsmxCode.VID_PROPERTY -> {
o = stack.pop().getValue();
if (o instanceof VsmxObject obj) {
stack.push(new VsmxReference(this, obj, properties[code.value()]));
} else {
stack.push(o.getPropertyValue(properties[code.value()]));
}
}
case VsmxCode.VID_METHOD -> {
o = stack.pop().getValue();
stack.push(new VsmxMethod(this, o, properties[code.value()]));
}
case VsmxCode.VID_SET_ATTR -> {
o1 = stack.pop().getValue();
o2 = stack.pop();
o2.setPropertyValue(properties[code.value()], o1);
}
case VsmxCode.VID_UNSET -> stack.pop().deletePropertyValue(properties[code.value()]);
case VsmxCode.VID_OBJ_ADD_ATTR -> warn("Line#" + (pc - 1) + " unimplemented OBJ_ADD_ATTR");
case VsmxCode.VID_ARRAY_INDEX -> {
o1 = stack.pop();
o2 = stack.pop().getValue();
if (o2 instanceof VsmxArray arr) {
o = new VsmxReference(this, arr, o1.getIntValue());
} else if (o2 instanceof VsmxObject obj) {
o = new VsmxReference(this, obj, o1.getStringValue());
} else {
o = o2.getPropertyValue(o1.getStringValue());
}
stack.push(o);
}
case VsmxCode.VID_ARRAY_INDEX_KEEP_OBJ -> {
o1 = stack.pop();
o2 = stack.peek().getValue();
if (o2 instanceof VsmxArray) {
o = o2.getPropertyValue(o1.getIntValue());
} else {
o = o2.getPropertyValue(o1.getStringValue());
}
stack.push(o);
}
case VsmxCode.VID_ARRAY_INDEX_ASSIGN -> {
o1 = stack.pop().getValue();
o2 = stack.pop();
o3 = stack.pop().getValue();
if (o3 instanceof VsmxArray) {
o3.setPropertyValue(o2.getIntValue(), o1);
} else {
warn("Line#" + (pc - 1) + " non-array index assignment");
}
}
case VsmxCode.VID_ARRAY_DELETE -> {
o1 = stack.pop();
o2 = stack.pop().getValue();
if (o2 instanceof VsmxArray) {
o2.deletePropertyValue(o1.getIntValue());
} else {
warn("Line#" + (pc - 1) + " non-array delete");
}
}
case VsmxCode.VID_ARRAY_PUSH -> {
o1 = stack.pop().getValue();
o2 = stack.pop().getValue();
if (o2 instanceof VsmxArray arr) {
int length = arr.getLength();
o2.setPropertyValue(length, o1);
stack.push(o2);
} else {
warn("Line#" + (pc - 1) + " non-array push");
}
}
case VsmxCode.VID_JUMP -> pc = code.value();
case VsmxCode.VID_JUMP_TRUE -> {
b = stack.pop().getBooleanValue();
if (b) {
pc = code.value();
}
}
case VsmxCode.VID_JUMP_FALSE -> {
b = !stack.pop().getBooleanValue();
if (b) {
pc = code.value();
}
}
case VsmxCode.VID_CALL_FUNC -> {
arguments = popValues(code.value());
o = stack.pop().getValueWithArguments(code.value());
if (o instanceof VsmxFunction function) {
callFunction(function, VsmxNull.singleton, arguments, code.value(), false);
} else {
stack.push(VsmxNull.singleton);
warn("Line#" + (pc - 1) + " non-function call");
}
}
case VsmxCode.VID_CALL_METHOD -> {
arguments = popValues(code.value());
o = stack.pop().getValueWithArguments(code.value());
if (o instanceof VsmxMethod method) {
VsmxFunction function = method.getFunction(code.value(), arguments);
if (function == null) {
stack.push(VsmxNull.singleton);
warn("Line#" + (pc - 1) + " non-existing method " + method.getName() + "()");
} else {
callFunction(function, method.getThisObject(), method.getArguments(), method.getNumberOfArguments(), false);
}
} else if (o instanceof VsmxFunction function) {
o = stack.pop().getValue();
callFunction(function, o, arguments, code.value(), false);
} else {
stack.push(VsmxNull.singleton);
warn("Line#" + (pc - 1) + " non-method call");
}
}
case VsmxCode.VID_CALL_NEW -> {
arguments = popValues(code.value());
r = stack.pop();
o = r.getValue();
if (o instanceof VsmxArray) {
if (code.value() == 0) {
stack.push(new VsmxArray(this));
} else if (code.value() == 1) {
stack.push(new VsmxArray(this, arguments[0].getIntValue()));
} else {
VsmxArray array = new VsmxArray(this, code.value());
for (int j = 0; j < code.value(); j++) {
array.setPropertyValue(j, arguments[j]);
}
stack.push(array);
}
} else if (o instanceof VsmxFunction function) {
String className = r instanceof VsmxReference ref ? ref.getRefProperty() : null;
VsmxObject thisObject = new VsmxObject(this, className);
callFunction(function, thisObject, arguments, code.value(), true);
} else if (o instanceof VsmxObject) {
if (code.value() == 0) {
stack.push(new VsmxObject(this, null));
} else {
warn("Line#" + (pc - 1) + " wrong number of arguments for new Object");
}
} else {
stack.push(new VsmxArray(this));
warn("Line#" + (pc - 1) + " unimplemented CALL_NEW");
}
}
case VsmxCode.VID_RETURN -> {
o = stack.pop().getValue();
if (callState.getReturnThis()) {
o = callState.getThisObject();
}
pc = callState.getReturnPc();
if (callState.getExitAfterCall()) {
exit = true;
}
popCallState();
if (callState == null) {
exit = true;
} else {
stack.push(o);
}
}
case VsmxCode.VID_THROW -> warn("Line#" + (pc - 1) + " unimplemented THROW");
case VsmxCode.VID_TRY_BLOCK_IN -> warn("Line#" + (pc - 1) + " unimplemented TRY_BLOCK_IN");
case VsmxCode.VID_TRY_BLOCK_OUT -> warn("Line#" + (pc - 1) + " unimplemented TRY_BLOCK_OUT");
case VsmxCode.VID_CATCH_FINALLY_BLOCK_IN -> warn("Line#" + (pc - 1) + " unimplemented CATCH_FINALLY_BLOCK_IN");
case VsmxCode.VID_CATCH_FINALLY_BLOCK_OUT -> warn("Line#" + (pc - 1) + " unimplemented CATCH_FINALLY_BLOCK_OUT");
case VsmxCode.VID_END -> exit = true;
case VsmxCode.VID_DEBUG_FILE -> { }
case VsmxCode.VID_DEBUG_LINE -> { }
case VsmxCode.VID_MAKE_FLOAT_ARRAY -> warn("Line#" + (pc - 1) + " unimplemented MAKE_FLOAT_ARRAY");
default -> warn("Line#" + (pc - 1) + " unimplemented opcode " + Integer.toHexString(code.opcode()));
}
}
private void interpret() {
exit = false;
while (!exit) {
Vsmx.Group code = codes[pc];
pc++;
interpret(code);
}
exit = false;
}
/** Runs the script's main body, populating the given global variables object. */
public synchronized void run(VsmxObject globalVariables) {
pc = 0;
exit = false;
callStates = new ArrayDeque<>();
pushCallState(VsmxNull.singleton, 0, false, true);
this.globalVariables = globalVariables;
VsmxBoolean.init(this);
VsmxNull.init(this);
VsmxUndefined.init(this);
interpret();
callStates.clear();
callState = null;
}
private void callFunction(VsmxFunction function, VsmxBaseObject thisObject, VsmxBaseObject[] arguments,
int numberArguments, boolean returnThis) {
pushCallState(thisObject, function.getLocalVars() + function.getArgs(), returnThis, false);
for (int i = 1; i <= function.getArgs() && i <= numberArguments; i++) {
callState.setLocalVar(i, arguments[i - 1]);
}
function.call(callState);
int startLine = function.getStartLine();
if (startLine >= 0 && startLine < codes.length) {
pc = startLine;
} else {
popCallState();
VsmxBaseObject returnValue = function.getReturnValue();
if (returnThis) {
stack.push(thisObject);
} else if (returnValue != null) {
stack.push(returnValue);
}
}
}
/** Interprets a function body in a fresh call frame. */
public synchronized void interpretFunction(VsmxFunction function, VsmxBaseObject object, VsmxBaseObject[] arguments) {
pushCallState(object, function.getLocalVars(), false, true);
for (int i = 1; i <= function.getArgs(); i++) {
if (arguments == null || i > arguments.length) {
callState.setLocalVar(i, VsmxNull.singleton);
} else {
callState.setLocalVar(i, arguments[i - 1]);
}
}
pc = function.getStartLine();
interpret();
}
/** Interprets a script event handler by name (e.g. "script:/main/onPush"). */
public synchronized void interpretScript(VsmxBaseObject object, String script) {
if (script == null) {
return;
}
if (object == null) {
object = VsmxNull.singleton;
}
String scriptPrefix = "script:/" + name + "/";
if (script.startsWith(scriptPrefix)) {
String functionName = script.substring(scriptPrefix.length());
VsmxBaseObject functionObject = globalVariables.getPropertyValue(functionName);
if (functionObject instanceof VsmxFunction function) {
interpretFunction(function, object, null);
}
}
}
}
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** A reference to a local variable of the current call frame. */
public final class VsmxLocalVarReference extends VsmxReference {
private final VsmxCallState callState;
private final int index;
public VsmxLocalVarReference(VsmxInterpreter interpreter, VsmxCallState callState, int index) {
super(interpreter, null, (String) null);
this.callState = callState;
this.index = index;
}
@Override
protected VsmxBaseObject getRef(int numberOfArguments) {
return callState.getLocalVar(index);
}
@Override
public void assign(VsmxBaseObject value) {
callState.setLocalVar(index, value);
}
@Override
public String toString() {
return "LocalVar#" + index;
}
}
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/**
* A method reference: an object plus a method name, resolved at call time.
*/
public class VsmxMethod extends VsmxBaseObject {
private final VsmxBaseObject object;
private final String name;
private VsmxBaseObject thisObject;
private int numberOfArguments;
private VsmxBaseObject[] arguments;
public VsmxMethod(VsmxInterpreter interpreter, VsmxBaseObject object, String name) {
super(interpreter);
this.object = object;
this.name = name;
}
public String getName() {
return name;
}
public VsmxBaseObject getThisObject() {
return thisObject;
}
public VsmxBaseObject[] getArguments() {
return arguments;
}
public int getNumberOfArguments() {
return numberOfArguments;
}
public VsmxFunction getFunction(int numberOfArguments, VsmxBaseObject[] arguments) {
this.numberOfArguments = numberOfArguments;
this.arguments = arguments;
thisObject = object.getValue();
if (object.hasPropertyValue(name)) {
VsmxBaseObject function = object.getPropertyValue(name).getValue();
if (function instanceof VsmxFunction) {
return (VsmxFunction) function;
}
}
if (object instanceof VsmxFunction && CALL_NAME.equals(name)) {
if (numberOfArguments > 0) {
this.numberOfArguments--;
if (arguments.length > 0) {
thisObject = arguments[0];
this.arguments = new VsmxBaseObject[this.numberOfArguments];
System.arraycopy(arguments, 1, this.arguments, 0, this.numberOfArguments);
}
}
return (VsmxFunction) object;
}
return null;
}
@Override
public String typeOf() {
return "function";
}
@Override
public String getClassName() {
return name;
}
@Override
public String toString() {
return object + "." + name + "()";
}
}
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject;
/** A VSMX object wrapping a native Java object. */
public class VsmxNativeObject extends VsmxObject {
private final BaseNativeObject object;
public VsmxNativeObject(VsmxInterpreter interpreter, BaseNativeObject object) {
super(interpreter, null);
this.object = object;
}
public BaseNativeObject getObject() {
return object;
}
@Override
public String toString() {
return object == null ? super.toString() : object.toString() + super.toString();
}
}
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** The singleton {@code null} value. */
public final class VsmxNull extends VsmxBaseObject {
public static final VsmxNull singleton = new VsmxNull();
private VsmxNull() {
super(null);
}
public static void init(VsmxInterpreter interpreter) {
singleton.setInterpreter(interpreter);
}
@Override
public boolean equals(VsmxBaseObject value) {
return this == value || value == VsmxUndefined.singleton;
}
@Override
public String typeOf() {
return "undefined";
}
@Override
public String getClassName() {
return "Null";
}
@Override
public String toString() {
return "null";
}
}
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** Numeric value, stored as a float. */
public final class VsmxNumber extends VsmxBaseObject {
private float value;
public VsmxNumber(VsmxInterpreter interpreter, float value) {
super(interpreter);
this.value = value;
}
public VsmxNumber(VsmxInterpreter interpreter, int value) {
super(interpreter);
this.value = (float) value;
}
@Override
public float getFloatValue() {
return value;
}
@Override
public String typeOf() {
return "number";
}
@Override
public String getClassName() {
return "Number";
}
@Override
public void setFloatValue(float value) {
this.value = value;
}
@Override
public boolean identity(VsmxBaseObject value) {
if (value instanceof VsmxNumber) {
return getFloatValue() == value.getFloatValue();
}
return super.identity(value);
}
}
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** A plain script object with dynamic named properties. */
public class VsmxObject extends VsmxBaseObject {
protected final Map<String, VsmxBaseObject> properties = new LinkedHashMap<>();
private final String className;
private final List<String> sortedPropertyNames = new ArrayList<>();
public VsmxObject(VsmxInterpreter interpreter, String className) {
super(interpreter);
this.className = className;
}
private void addProperty(String name, VsmxBaseObject value) {
properties.put(name, value);
sortedPropertyNames.add(name);
}
protected static int getIndex(String name) {
try {
return Integer.parseInt(name);
} catch (NumberFormatException e) {
return -1;
}
}
@Override
public VsmxBaseObject getPropertyValue(String name) {
if (LENGTH_NAME.equals(name)) {
return new VsmxNumber(interpreter, properties.size());
}
if (PROTOTYPE_NAME.equals(name)) {
VsmxObject prototype = getPrototype();
if (prototype != null) {
return prototype;
}
return VsmxUndefined.singleton;
}
VsmxBaseObject value = properties.get(name);
if (value == null) {
int index = getIndex(name);
if (index >= 0) {
if (index < properties.size()) {
value = properties.get(sortedPropertyNames.get(index));
} else {
value = VsmxUndefined.singleton;
}
} else {
VsmxObject prototype = getPrototype();
if (prototype != null && prototype.properties.containsKey(name)) {
value = prototype.getPropertyValue(name);
} else {
value = VsmxUndefined.singleton;
addProperty(name, value);
}
}
}
return value;
}
@Override
public List<String> getPropertyNames() {
return sortedPropertyNames;
}
@Override
public void setPropertyValue(String name, VsmxBaseObject value) {
if (properties.containsKey(name)) {
properties.put(name, value);
} else {
addProperty(name, value);
}
}
@Override
public void deletePropertyValue(String name) {
properties.remove(name);
sortedPropertyNames.remove(name);
}
@Override
public boolean hasPropertyValue(String name) {
if (PROTOTYPE_NAME.equals(name)) {
return true;
}
if (properties.containsKey(name)) {
return true;
}
VsmxObject prototype = getPrototype();
return prototype != null && prototype.hasPropertyValue(name);
}
@Override
public boolean equals(VsmxBaseObject value) {
if (value instanceof VsmxObject) {
return this == value;
}
return false;
}
@Override
public String typeOf() {
return "object";
}
@Override
public String getClassName() {
return className;
}
@Override
public String toString() {
StringBuilder s = new StringBuilder("[");
for (Map.Entry<String, VsmxBaseObject> entry : properties.entrySet()) {
if (s.length() > 1) {
s.append(", ");
}
s.append(entry.getKey()).append('=').append(entry.getValue());
}
return s.append(']').toString();
}
}
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/**
* A reference to a property or array index of an object, so assignments and
* increments/decrements can write back through it.
*/
public class VsmxReference extends VsmxBaseObject {
private final VsmxObject refObject;
private final String refProperty;
private final int refIndex;
public VsmxReference(VsmxInterpreter interpreter, VsmxObject refObject, String refProperty) {
super(interpreter);
this.refObject = refObject;
this.refProperty = refProperty;
this.refIndex = -1;
}
public VsmxReference(VsmxInterpreter interpreter, VsmxObject refObject, int refIndex) {
super(interpreter);
this.refObject = refObject;
this.refProperty = null;
this.refIndex = refIndex;
}
public void assign(VsmxBaseObject value) {
if (refProperty == null) {
refObject.setPropertyValue(refIndex, value.getValue());
} else {
refObject.setPropertyValue(refProperty, value.getValue());
}
}
public String getRefProperty() {
if (refProperty == null) {
return Integer.toString(refIndex);
}
return refProperty;
}
protected VsmxBaseObject getRef() {
return getRef(0);
}
protected VsmxBaseObject getRef(int numberOfArguments) {
if (refProperty == null) {
return refObject.getPropertyValue(refIndex);
}
return refObject.getPropertyValue(refProperty);
}
@Override
public VsmxBaseObject getValue() {
return getRef();
}
@Override
public VsmxBaseObject getValueWithArguments(int numberOfArguments) {
return getRef(numberOfArguments);
}
@Override
public float getFloatValue() {
return getRef().getFloatValue();
}
@Override
public int getIntValue() {
return getRef().getIntValue();
}
@Override
public boolean getBooleanValue() {
return getRef().getBooleanValue();
}
@Override
public String getStringValue() {
return getRef().getStringValue();
}
@Override
public boolean equals(VsmxBaseObject value) {
return getRef().equals(value);
}
@Override
public VsmxBaseObject getPropertyValue(String name) {
return getRef().getPropertyValue(name);
}
@Override
public void setPropertyValue(String name, VsmxBaseObject value) {
getRef().setPropertyValue(name, value);
}
@Override
public void deletePropertyValue(String name) {
getRef().deletePropertyValue(name);
}
@Override
public void setFloatValue(float value) {
getRef().setFloatValue(value);
}
@Override
public String typeOf() {
return getRef().typeOf();
}
@Override
public String getClassName() {
return getRef().getClassName();
}
@Override
public String toString() {
if (refProperty == null) {
return "@OBJ[" + refIndex + "]=" + getRef();
}
return "@OBJ." + refProperty + "=" + getRef();
}
}
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** String value. */
public final class VsmxString extends VsmxBaseObject {
private String value;
public VsmxString(VsmxInterpreter interpreter, String value) {
super(interpreter);
this.value = value;
}
@Override
public float getFloatValue() {
return Float.parseFloat(value);
}
@Override
public void setFloatValue(float value) {
this.value = Float.toString(value);
}
@Override
public String getStringValue() {
return value;
}
@Override
public VsmxBaseObject getPropertyValue(String name) {
if (LENGTH_NAME.equals(name)) {
return new VsmxNumber(interpreter, value.length());
}
return super.getPropertyValue(name);
}
@Override
public boolean equals(VsmxBaseObject value) {
return getStringValue().equals(value.getStringValue());
}
@Override
public boolean identity(VsmxBaseObject value) {
if (value instanceof VsmxString) {
return getStringValue().equals(value.getStringValue());
}
return super.identity(value);
}
@Override
public String typeOf() {
return "string";
}
@Override
public String getClassName() {
return "String";
}
@Override
public String toString() {
return "\"" + value + "\"";
}
}
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
/** The singleton {@code undefined} value. */
public final class VsmxUndefined extends VsmxBaseObject {
public static final VsmxUndefined singleton = new VsmxUndefined();
private VsmxUndefined() {
super(null);
}
public static void init(VsmxInterpreter interpreter) {
singleton.setInterpreter(interpreter);
}
@Override
public boolean equals(VsmxBaseObject value) {
return this == value || value == VsmxNull.singleton;
}
@Override
public String typeOf() {
return "undefined";
}
@Override
public String getClassName() {
return "Undefined";
}
@Override
public String toString() {
return "undefined";
}
}
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
/**
* Base of native (Java-implemented) objects exposed to the VSMX script
* (Controller, MoviePlayer, Resource, etc.).
*/
public abstract class BaseNativeObject {
private VsmxNativeObject object;
private String name;
public VsmxNativeObject getObject() {
return object;
}
public void setObject(VsmxNativeObject object) {
this.object = object;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void onDisplayUpdated() {
}
@Override
public String toString() {
return getClass().getSimpleName() + (name != null ? "[name=" + name + "]" : "");
}
}