diff --git a/cli/src/main/java/dev/quoije/umd/cli/Main.java b/cli/src/main/java/dev/quoije/umd/cli/Main.java index aa9463d..b501dbe 100644 --- a/cli/src/main/java/dev/quoije/umd/cli/Main.java +++ b/cli/src/main/java/dev/quoije/umd/cli/Main.java @@ -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 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 (lines.size() > shown) { - System.out.printf(" ... (%d more lines)%n", lines.size() - shown); + + if (!interpreter.getWarnings().isEmpty()) { + System.out.printf("warnings (%d):%n", interpreter.getWarnings().size()); + for (String warning : interpreter.getWarnings()) { + System.out.println(" " + warning); + } } } } diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxArray.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxArray.java new file mode 100644 index 0000000..5e8ea42 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxArray.java @@ -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(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxBaseObject.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxBaseObject.java new file mode 100644 index 0000000..aa2ebd7 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxBaseObject.java @@ -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 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()); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxBoolean.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxBoolean.java new file mode 100644 index 0000000..9775c4f --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxBoolean.java @@ -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"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxCallState.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxCallState.java new file mode 100644 index 0000000..0b43643 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxCallState.java @@ -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 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 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(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxFunction.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxFunction.java new file mode 100644 index 0000000..17a580b --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxFunction.java @@ -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 + ")"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java new file mode 100644 index 0000000..af68d87 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java @@ -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 stack; + private Deque callStates; + private VsmxCallState callState; + private VsmxObject globalVariables; + private final List 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 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); + } + } + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxLocalVarReference.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxLocalVarReference.java new file mode 100644 index 0000000..f50399a --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxLocalVarReference.java @@ -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; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java new file mode 100644 index 0000000..0963879 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java @@ -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 + "()"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeObject.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeObject.java new file mode 100644 index 0000000..993cffb --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeObject.java @@ -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(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNull.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNull.java new file mode 100644 index 0000000..40d9fa4 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNull.java @@ -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"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNumber.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNumber.java new file mode 100644 index 0000000..33019a1 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNumber.java @@ -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); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxObject.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxObject.java new file mode 100644 index 0000000..c1fdc70 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxObject.java @@ -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 properties = new LinkedHashMap<>(); + private final String className; + private final List 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 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 entry : properties.entrySet()) { + if (s.length() > 1) { + s.append(", "); + } + s.append(entry.getKey()).append('=').append(entry.getValue()); + } + return s.append(']').toString(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java new file mode 100644 index 0000000..59a3ade --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java @@ -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(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java new file mode 100644 index 0000000..35ffed0 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java @@ -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 + "\""; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxUndefined.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxUndefined.java new file mode 100644 index 0000000..c2ab02f --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxUndefined.java @@ -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"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java new file mode 100644 index 0000000..828a6cc --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java @@ -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 + "]" : ""); + } +}