initial commit

This commit is contained in:
2023-08-05 18:37:18 +03:00
commit 2858d685ad
151 changed files with 13448 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package me.topchetoeu.jscript;
public class Location {
public static final Location INTERNAL = new Location(0, 0, "<internal>");
private int line;
private int start;
private String filename;
public int line() { return line; }
public int start() { return start; }
public String filename() { return filename; }
@Override
public String toString() {
return filename + ":" + line + ":" + start;
}
public Location add(int n, boolean clone) {
if (clone) return new Location(line, start + n, filename);
this.start += n;
return this;
}
public Location add(int n) {
return add(n, false);
}
public Location nextLine() {
line++;
start = 0;
return this;
}
public Location clone() {
return new Location(line, start, filename);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + line;
result = prime * result + start;
result = prime * result + ((filename == null) ? 0 : filename.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Location other = (Location) obj;
if (line != other.line) return false;
if (start != other.start) return false;
if (filename == null && other.filename != null) return false;
else if (!filename.equals(other.filename)) return false;
return true;
}
public Location(int line, int start, String filename) {
this.line = line;
this.start = start;
this.filename = filename;
}
}

View File

@@ -0,0 +1,104 @@
package me.topchetoeu.jscript;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.events.Observer;
import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.exceptions.SyntaxException;
import me.topchetoeu.jscript.polyfills.PolyfillEngine;
public class Main {
static Thread task;
static Engine engine;
private static Observer<Object> valuePrinter = new Observer<Object>() {
public void next(Object data) {
try {
Values.printValue(engine.context(), data);
}
catch (InterruptedException e) { }
System.out.println();
}
public void error(RuntimeException err) {
try {
if (err instanceof EngineException) {
System.out.println("Uncaught " + ((EngineException)err).toString(engine.context()));
}
else if (err instanceof SyntaxException) {
System.out.println("Syntax error:" + ((SyntaxException)err).msg);
}
else if (err.getCause() instanceof InterruptedException) return;
else {
System.out.println("Internal error ocurred:");
err.printStackTrace();
}
}
catch (EngineException ex) {
System.out.println("Uncaught [error while converting to string]");
}
catch (InterruptedException ex) {
return;
}
}
};
public static void main(String args[]) {
var in = new BufferedReader(new InputStreamReader(System.in));
engine = new PolyfillEngine(new File("."));
var scope = engine.global().globalChild();
var exited = new boolean[1];
scope.define("exit", ctx -> {
exited[0] = true;
task.interrupt();
throw new InterruptedException();
});
task = engine.start();
var reader = new Thread(() -> {
try {
while (true) {
try {
var raw = in.readLine();
if (raw == null) break;
engine.pushMsg(false, scope, Map.of(), "<stdio>", raw, null).toObservable().once(valuePrinter);
}
catch (EngineException e) {
try {
System.out.println("Uncaught " + e.toString(engine.context()));
}
catch (EngineException ex) {
System.out.println("Uncaught [error while converting to string]");
}
}
}
}
catch (IOException e) {
e.printStackTrace();
return;
}
catch (SyntaxException ex) {
if (exited[0]) return;
System.out.println("Syntax error:" + ex.msg);
}
catch (RuntimeException ex) {
if (exited[0]) return;
System.out.println("Internal error ocurred:");
ex.printStackTrace();
}
catch (InterruptedException e) { return; }
if (exited[0]) return;
});
reader.setDaemon(true);
reader.setName("STD Reader");
reader.start();
}
}

View File

@@ -0,0 +1,6 @@
package me.topchetoeu.jscript;
public interface MessageReceiver {
void sendMessage(String msg);
void sendError(String msg);
}

View File

@@ -0,0 +1,12 @@
package me.topchetoeu.jscript.compilation;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction.Type;
public abstract class AssignableStatement extends Statement {
public abstract Statement toAssign(Statement val, Type operation);
protected AssignableStatement(Location loc) {
super(loc);
}
}

View File

@@ -0,0 +1,11 @@
package me.topchetoeu.jscript.compilation;
public class CompileOptions {
public final boolean emitBpMap;
public final boolean emitVarNames;
public CompileOptions(boolean emitBpMap, boolean emitVarNames) {
this.emitBpMap = emitBpMap;
this.emitVarNames = emitVarNames;
}
}

View File

@@ -0,0 +1,77 @@
package me.topchetoeu.jscript.compilation;
import java.util.ArrayList;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.control.ContinueStatement;
import me.topchetoeu.jscript.compilation.control.ReturnStatement;
import me.topchetoeu.jscript.compilation.control.ThrowStatement;
import me.topchetoeu.jscript.compilation.values.FunctionStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class CompoundStatement extends Statement {
public final Statement[] statements;
@Override
public boolean pollutesStack() {
for (var stm : statements) {
if (stm instanceof FunctionStatement) continue;
return true;
}
return false;
}
@Override
public void declare(ScopeRecord varsScope) {
for (var stm : statements) {
stm.declare(varsScope);
}
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
for (var stm : statements) {
if (stm instanceof FunctionStatement) {
int start = target.size();
((FunctionStatement)stm).compile(target, scope, null, true);
target.get(start).setDebug(true);
target.add(Instruction.discard());
}
}
for (var i = 0; i < statements.length; i++) {
var stm = statements[i];
if (stm instanceof FunctionStatement) continue;
if (i != statements.length - 1) stm.compileNoPollution(target, scope, true);
else stm.compileWithPollution(target, scope);
}
}
@Override
public Statement optimize() {
var res = new ArrayList<Statement>();
for (var i = 0; i < statements.length; i++) {
var stm = statements[i].optimize();
if (i < statements.length - 1 && stm.pure()) continue;
res.add(stm);
if (
stm instanceof ContinueStatement ||
stm instanceof ReturnStatement ||
stm instanceof ThrowStatement ||
stm instanceof ContinueStatement
) break;
}
if (res.size() == 1) return res.get(0);
else return new CompoundStatement(loc(), res.toArray(Statement[]::new));
}
public CompoundStatement(Location loc, Statement... statements) {
super(loc);
this.statements = statements;
}
}

View File

@@ -0,0 +1,33 @@
package me.topchetoeu.jscript.compilation;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.values.ConstantStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class DiscardStatement extends Statement {
public final Statement value;
@Override
public boolean pollutesStack() { return false; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (value == null) return;
value.compile(target, scope);
if (value.pollutesStack()) target.add(Instruction.discard());
}
@Override
public Statement optimize() {
if (value == null) return this;
var val = value.optimize();
if (val.pure()) return new ConstantStatement(loc(), null);
else return new DiscardStatement(loc(), val);
}
public DiscardStatement(Location loc, Statement val) {
super(loc);
this.value = val;
}
}

View File

@@ -0,0 +1,285 @@
package me.topchetoeu.jscript.compilation;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.exceptions.SyntaxException;
public class Instruction {
public static enum Type {
RETURN,
SIGNAL,
THROW,
THROW_SYNTAX,
DELETE,
TRY,
NOP,
CALL,
CALL_NEW,
JMP_IF,
JMP_IFN,
JMP,
LOAD_VALUE,
LOAD_VAR,
LOAD_MEMBER,
LOAD_VAL_MEMBER,
LOAD_GLOB,
LOAD_FUNC,
LOAD_ARR,
LOAD_OBJ,
STORE_SELF_FUNC,
LOAD_REGEX,
DUP,
STORE_VAR,
STORE_MEMBER,
DISCARD,
MAKE_VAR,
DEF_PROP,
KEYS,
TYPEOF,
INSTANCEOF(true),
IN(true),
MULTIPLY(true),
DIVIDE(true),
MODULO(true),
ADD(true),
SUBTRACT(true),
USHIFT_RIGHT(true),
SHIFT_RIGHT(true),
SHIFT_LEFT(true),
GREATER(true),
LESS(true),
GREATER_EQUALS(true),
LESS_EQUALS(true),
LOOSE_EQUALS(true),
LOOSE_NOT_EQUALS(true),
EQUALS(true),
NOT_EQUALS(true),
AND(true),
OR(true),
XOR(true),
NEG(true),
POS(true),
NOT(true),
INVERSE(true);
final boolean isOperation;
private Type(boolean isOperation) {
this.isOperation = isOperation;
}
private Type() {
this(false);
}
}
public final Type type;
public final Object[] params;
public Location location;
public boolean debugged;
public Instruction locate(Location loc) {
this.location = loc;
return this;
}
public Instruction setDebug(boolean debug) {
debugged = debug;
return this;
}
@SuppressWarnings("unchecked")
public <T> T get(int i) {
if (i >= params.length || i < 0) return null;
return (T)params[i];
}
public boolean match(Object ...args) {
if (args.length != params.length) return false;
for (int i = 0; i < args.length; i++) {
var a = params[i];
var b = args[i];
if (a == null || b == null) {
if (!(a == null && b == null)) return false;
}
if (!a.equals(b)) return false;
}
return true;
}
public boolean is(int i, Object arg) {
if (params.length <= i) return false;
return params[i].equals(arg);
}
private Instruction(Location location, Type type, Object... params) {
this.location = location;
this.type = type;
this.params = params;
}
public static Instruction tryInstr(boolean hasCatch, boolean hasFinally) {
return new Instruction(null, Type.TRY, hasCatch, hasFinally);
}
public static Instruction throwInstr() {
return new Instruction(null, Type.THROW);
}
public static Instruction throwSyntax(SyntaxException err) {
return new Instruction(null, Type.THROW_SYNTAX, err.getMessage());
}
public static Instruction delete() {
return new Instruction(null, Type.DELETE);
}
public static Instruction ret() {
return new Instruction(null, Type.RETURN);
}
public static Instruction debug() {
return new Instruction(null, Type.NOP, "debug");
}
public static Instruction debugVarNames(String[] names) {
var args = new Object[names.length + 1];
args[0] = "dbg_vars";
System.arraycopy(names, 0, args, 1, names.length);
return new Instruction(null, Type.NOP, args);
}
/**
* ATTENTION: Usage outside of try/catch is broken af
*/
public static Instruction signal(String name) {
return new Instruction(null, Type.SIGNAL, name);
}
public static Instruction nop(Object ...params) {
for (var param : params) {
if (param instanceof String) continue;
if (param instanceof Boolean) continue;
if (param instanceof Double) continue;
if (param instanceof Integer) continue;
if (param == null) continue;
throw new RuntimeException("NOP params may contain only strings, booleans, doubles, integers and nulls.");
}
return new Instruction(null, Type.NOP, params);
}
public static Instruction call(int argn) {
return new Instruction(null, Type.CALL, argn);
}
public static Instruction callNew(int argn) {
return new Instruction(null, Type.CALL_NEW, argn);
}
public static Instruction jmp(int offset) {
return new Instruction(null, Type.JMP, offset);
}
public static Instruction jmpIf(int offset) {
return new Instruction(null, Type.JMP_IF, offset);
}
public static Instruction jmpIfNot(int offset) {
return new Instruction(null, Type.JMP_IFN, offset);
}
public static Instruction loadValue(Object val) {
return new Instruction(null, Type.LOAD_VALUE, val);
}
public static Instruction makeVar(String name) {
return new Instruction(null, Type.MAKE_VAR, name);
}
public static Instruction loadVar(Object i) {
return new Instruction(null, Type.LOAD_VAR, i);
}
public static Instruction loadGlob() {
return new Instruction(null, Type.LOAD_GLOB);
}
public static Instruction loadMember() {
return new Instruction(null, Type.LOAD_MEMBER);
}
public static Instruction loadMember(Object key) {
if (key instanceof Number) key = ((Number)key).doubleValue();
return new Instruction(null, Type.LOAD_VAL_MEMBER, key);
}
public static Instruction loadRegex(String pattern, String flags) {
return new Instruction(null, Type.LOAD_REGEX, pattern, flags);
}
public static Instruction loadFunc(int instrN, int varN, int len, int[] captures) {
var args = new Object[3 + captures.length];
args[0] = instrN;
args[1] = varN;
args[2] = len;
for (var i = 0; i < captures.length; i++) args[i + 3] = captures[i];
return new Instruction(null, Type.LOAD_FUNC, args);
}
public static Instruction loadObj() {
return new Instruction(null, Type.LOAD_OBJ);
}
public static Instruction loadArr(int count) {
return new Instruction(null, Type.LOAD_ARR, count);
}
public static Instruction dup(int count) {
return new Instruction(null, Type.DUP, count, 0);
}
public static Instruction dup(int count, int offset) {
return new Instruction(null, Type.DUP, count, offset);
}
public static Instruction storeSelfFunc(int i) {
return new Instruction(null, Type.STORE_SELF_FUNC, i);
}
public static Instruction storeVar(Object i) {
return new Instruction(null, Type.STORE_VAR, i, false);
}
public static Instruction storeVar(Object i, boolean keep) {
return new Instruction(null, Type.STORE_VAR, i, keep);
}
public static Instruction storeMember() {
return new Instruction(null, Type.STORE_MEMBER, false);
}
public static Instruction storeMember(boolean keep) {
return new Instruction(null, Type.STORE_MEMBER, keep);
}
public static Instruction discard() {
return new Instruction(null, Type.DISCARD);
}
public static Instruction typeof() {
return new Instruction(null, Type.TYPEOF);
}
public static Instruction typeof(String varName) {
return new Instruction(null, Type.TYPEOF, varName);
}
public static Instruction keys() {
return new Instruction(null, Type.KEYS);
}
public static Instruction defProp() {
return new Instruction(null, Type.DEF_PROP);
}
public static Instruction operation(Type op) {
if (!op.isOperation) throw new IllegalArgumentException("The instruction type %s is not an operation.".formatted(op));
return new Instruction(null, op);
}
@Override
public String toString() {
var res = type.toString();
for (int i = 0; i < params.length; i++) {
res += " " + params[i];
}
return res;
}
}

View File

@@ -0,0 +1,42 @@
package me.topchetoeu.jscript.compilation;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public abstract class Statement {
private Location _loc;
public abstract boolean pollutesStack();
public boolean pure() { return false; }
public abstract void compile(List<Instruction> target, ScopeRecord scope);
public void declare(ScopeRecord varsScope) { }
public Statement optimize() { return this; }
public void compileNoPollution(List<Instruction> target, ScopeRecord scope, boolean debug) {
int start = target.size();
compile(target, scope);
if (debug && target.size() != start) target.get(start).setDebug(true);
if (pollutesStack()) target.add(Instruction.discard().locate(loc()));
}
public void compileWithPollution(List<Instruction> target, ScopeRecord scope, boolean debug) {
int start = target.size();
compile(target, scope);
if (debug && target.size() != start) target.get(start).setDebug(true);
if (!pollutesStack()) target.add(Instruction.loadValue(null).locate(loc()));
}
public void compileNoPollution(List<Instruction> target, ScopeRecord scope) {
compileNoPollution(target, scope, false);
}
public void compileWithPollution(List<Instruction> target, ScopeRecord scope) {
compileWithPollution(target, scope, false);
}
public Location loc() { return _loc; }
public void setLoc(Location loc) { _loc = loc; }
protected Statement(Location loc) {
this._loc = loc;
}
}

View File

@@ -0,0 +1,44 @@
package me.topchetoeu.jscript.compilation;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.values.FunctionStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class VariableDeclareStatement extends Statement {
public static record Pair(String name, Statement value) {}
public final List<Pair> values;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord varsScope) {
for (var key : values) {
varsScope.define(key.name());
}
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
for (var entry : values) {
if (entry.name() == null) continue;
var key = scope.getKey(entry.name());
if (key instanceof String) target.add(Instruction.makeVar((String)key).locate(loc()));
if (entry.value() instanceof FunctionStatement) {
((FunctionStatement)entry.value()).compile(target, scope, entry.name(), false);
target.add(Instruction.storeVar(key).locate(loc()));
}
else if (entry.value() != null) {
entry.value().compileWithPollution(target, scope);
target.add(Instruction.storeVar(key).locate(loc()));
}
}
}
public VariableDeclareStatement(Location loc, List<Pair> values) {
super(loc);
this.values = values;
}
}

View File

@@ -0,0 +1,25 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class BreakStatement extends Statement {
public final String label;
@Override
public boolean pollutesStack() { return false; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.nop("break", label).locate(loc()));
}
public BreakStatement(Location loc, String label) {
super(loc);
this.label = label;
}
}

View File

@@ -0,0 +1,25 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ContinueStatement extends Statement {
public final String label;
@Override
public boolean pollutesStack() { return false; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.nop("cont", label).locate(loc()));
}
public ContinueStatement(Location loc, String label) {
super(loc);
this.label = label;
}
}

View File

@@ -0,0 +1,22 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class DebugStatement extends Statement {
@Override
public boolean pollutesStack() { return false; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.debug().locate(loc()));
}
public DebugStatement(Location loc) {
super(loc);
}
}

View File

@@ -0,0 +1,29 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class DeleteStatement extends Statement {
public final Statement key;
public final Statement value;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
value.compile(target, scope);
key.compile(target, scope);
target.add(Instruction.delete().locate(loc()));
}
public DeleteStatement(Location loc, Statement key, Statement value) {
super(loc);
this.key = key;
this.value = value;
}
}

View File

@@ -0,0 +1,82 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.CompoundStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.values.ConstantStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class DoWhileStatement extends Statement {
public final Statement condition, body;
public final String label;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord globScope) {
body.declare(globScope);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (condition instanceof ConstantStatement) {
int start = target.size();
body.compileNoPollution(target, scope);
int end = target.size();
if (Values.toBoolean(((ConstantStatement)condition).value)) {
WhileStatement.replaceBreaks(target, label, start, end, end + 1, end + 1);
}
else {
target.add(Instruction.jmp(start - end).locate(loc()));
WhileStatement.replaceBreaks(target, label, start, end, start, end + 1);
}
return;
}
int start = target.size();
body.compileNoPollution(target, scope, true);
int mid = target.size();
condition.compileWithPollution(target, scope);
int end = target.size();
WhileStatement.replaceBreaks(target, label, start, mid - 1, mid, end + 1);
target.add(Instruction.jmpIf(start - end).locate(loc()));
}
@Override
public Statement optimize() {
var cond = condition.optimize();
var b = body.optimize();
if (b instanceof CompoundStatement) {
var comp = (CompoundStatement)b;
if (comp.statements.length > 0) {
var last = comp.statements[comp.statements.length - 1];
if (last instanceof ContinueStatement) comp.statements[comp.statements.length - 1] = new CompoundStatement(loc());
if (last instanceof BreakStatement) {
comp.statements[comp.statements.length - 1] = new CompoundStatement(loc());
return new CompoundStatement(loc());
}
}
}
else if (b instanceof ContinueStatement) {
b = new CompoundStatement(loc());
}
else if (b instanceof BreakStatement) return new CompoundStatement(loc());
if (b.pure()) return new DoWhileStatement(loc(), label, cond, new CompoundStatement(loc()));
else return new DoWhileStatement(loc(), label, cond, b);
}
public DoWhileStatement(Location loc, String label, Statement condition, Statement body) {
super(loc);
this.label = label;
this.condition = condition;
this.body = body;
}
}

View File

@@ -0,0 +1,81 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ForInStatement extends Statement {
public final String varName;
public final boolean isDeclaration;
public final Statement varValue, object, body;
public final String label;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord globScope) {
body.declare(globScope);
if (isDeclaration) globScope.define(varName);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
var key = scope.getKey(varName);
if (key instanceof String) target.add(Instruction.makeVar((String)key));
if (varValue != null) {
varValue.compileWithPollution(target, scope);
target.add(Instruction.storeVar(scope.getKey(varName)));
}
object.compileWithPollution(target, scope);
target.add(Instruction.keys());
int start = target.size();
target.add(Instruction.dup(1));
target.add(Instruction.loadMember("length"));
target.add(Instruction.loadValue(0));
target.add(Instruction.operation(Type.LESS_EQUALS));
int mid = target.size();
target.add(Instruction.nop());
target.add(Instruction.dup(2));
target.add(Instruction.loadMember("length"));
target.add(Instruction.loadValue(1));
target.add(Instruction.operation(Type.SUBTRACT));
target.add(Instruction.dup(1, 2));
target.add(Instruction.loadValue("length"));
target.add(Instruction.dup(1, 2));
target.add(Instruction.storeMember());
target.add(Instruction.loadMember());
target.add(Instruction.storeVar(key));
for (var i = start; i < target.size(); i++) target.get(i).locate(loc());
body.compileNoPollution(target, scope, true);
int end = target.size();
WhileStatement.replaceBreaks(target, label, mid + 1, end, start, end + 1);
target.add(Instruction.jmp(start - end).locate(loc()));
target.add(Instruction.discard().locate(loc()));
target.set(mid, Instruction.jmpIf(end - mid + 1).locate(loc()));
}
public ForInStatement(Location loc, String label, boolean isDecl, String varName, Statement varValue, Statement object, Statement body) {
super(loc);
this.label = label;
this.isDeclaration = isDecl;
this.varName = varName;
this.varValue = varValue;
this.object = object;
this.body = body;
}
}

View File

@@ -0,0 +1,97 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.CompoundStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.values.ConstantStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class ForStatement extends Statement {
public final Statement declaration, assignment, condition, body;
public final String label;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord globScope) {
declaration.declare(globScope);
body.declare(globScope);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
declaration.compile(target, scope);
if (condition instanceof ConstantStatement) {
if (Values.toBoolean(((ConstantStatement)condition).value)) {
int start = target.size();
body.compileNoPollution(target, scope);
int mid = target.size();
assignment.compileNoPollution(target, scope, true);
int end = target.size();
WhileStatement.replaceBreaks(target, label, start, mid, mid, end + 1);
target.add(Instruction.jmp(start - target.size()).locate(loc()));
return;
}
}
int start = target.size();
condition.compileWithPollution(target, scope);
int mid = target.size();
target.add(Instruction.nop());
body.compileNoPollution(target, scope);
int beforeAssign = target.size();
assignment.compile(target, scope);
int end = target.size();
WhileStatement.replaceBreaks(target, label, mid + 1, end, beforeAssign, end + 1);
target.add(Instruction.jmp(start - end).locate(loc()));
target.set(mid, Instruction.jmpIfNot(end - mid + 1).locate(loc()));
}
@Override
public Statement optimize() {
var decl = declaration.optimize();
var asgn = assignment.optimize();
var cond = condition.optimize();
var b = body.optimize();
if (asgn.pure()) {
if (decl.pure()) return new WhileStatement(loc(), label, cond, b).optimize();
else return new CompoundStatement(loc(),
decl, new WhileStatement(loc(), label, cond, b)
).optimize();
}
else if (b instanceof ContinueStatement) return new CompoundStatement(loc(),
decl, new WhileStatement(loc(), label, cond, new CompoundStatement(loc(), b, asgn))
);
else if (b instanceof BreakStatement) return decl;
if (b.pure()) return new ForStatement(loc(), label, decl, cond, asgn, new CompoundStatement(null));
else return new ForStatement(loc(), label, decl, cond, asgn, b);
}
public ForStatement(Location loc, String label, Statement declaration, Statement condition, Statement assignment, Statement body) {
super(loc);
this.label = label;
this.declaration = declaration;
this.condition = condition;
this.assignment = assignment;
this.body = body;
}
public static CompoundStatement ofFor(Location loc, String label, Statement declaration, Statement condition, Statement increment, Statement body) {
return new CompoundStatement(loc,
declaration,
new WhileStatement(loc, label, condition, new CompoundStatement(loc,
body,
increment
))
);
}
}

View File

@@ -0,0 +1,80 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.CompoundStatement;
import me.topchetoeu.jscript.compilation.DiscardStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.values.ConstantStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class IfStatement extends Statement {
public final Statement condition, body, elseBody;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord globScope) {
body.declare(globScope);
if (elseBody != null) elseBody.declare(globScope);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (condition instanceof ConstantStatement) {
if (Values.not(((ConstantStatement)condition).value)) {
if (elseBody != null) elseBody.compileNoPollution(target, scope, true);
}
else {
body.compileNoPollution(target, scope, true);
}
return;
}
condition.compileWithPollution(target, scope);
if (elseBody == null) {
int i = target.size();
target.add(Instruction.nop());
body.compileNoPollution(target, scope, true);
int endI = target.size();
target.set(i, Instruction.jmpIfNot(endI - i).locate(loc()));
}
else {
int start = target.size();
target.add(Instruction.nop());
body.compileNoPollution(target, scope, true);
target.add(Instruction.nop());
int mid = target.size();
elseBody.compileNoPollution(target, scope, true);
int end = target.size();
target.set(start, Instruction.jmpIfNot(mid - start).locate(loc()));
target.set(mid - 1, Instruction.jmp(end - mid + 1).locate(loc()));
}
}
@Override
public Statement optimize() {
var cond = condition.optimize();
var b = body.optimize();
var e = elseBody == null ? null : elseBody.optimize();
if (b.pure()) b = new CompoundStatement(null);
if (e != null && e.pure()) e = null;
if (b.pure() && e == null) return new DiscardStatement(loc(), cond).optimize();
else return new IfStatement(loc(), cond, b, e);
}
public IfStatement(Location loc, Statement condition, Statement body, Statement elseBody) {
super(loc);
this.condition = condition;
this.body = body;
this.elseBody = elseBody;
}
}

View File

@@ -0,0 +1,27 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ReturnStatement extends Statement {
public final Statement value;
@Override
public boolean pollutesStack() { return false; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (value == null) target.add(Instruction.loadValue(null).locate(loc()));
else value.compileWithPollution(target, scope);
target.add(Instruction.ret().locate(loc()));
}
public ReturnStatement(Location loc, Statement value) {
super(loc);
this.value = value;
}
}

View File

@@ -0,0 +1,81 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.HashMap;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class SwitchStatement extends Statement {
public static record SwitchCase(Statement value, int statementI) {}
@Override
public boolean pollutesStack() { return false; }
public final Statement value;
public final SwitchCase[] cases;
public final Statement[] body;
public final int defaultI;
@Override
public void declare(ScopeRecord varsScope) {
for (var stm : body) stm.declare(varsScope);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
var caseMap = new HashMap<Integer, Integer>();
var stmIndexMap = new HashMap<Integer, Integer>();
value.compile(target, scope);
for (var ccase : cases) {
target.add(Instruction.dup(1).locate(loc()));
ccase.value.compileWithPollution(target, scope);
target.add(Instruction.operation(Type.EQUALS).locate(loc()));
caseMap.put(target.size(), ccase.statementI);
target.add(Instruction.nop());
}
int start = target.size();
target.add(Instruction.nop());
for (var stm : body) {
stmIndexMap.put(stmIndexMap.size(), target.size());
stm.compileNoPollution(target, scope, true);
}
if (defaultI < 0 || defaultI >= body.length) target.set(start, Instruction.jmp(target.size() - start).locate(loc()));
else target.set(start, Instruction.jmp(stmIndexMap.get(defaultI) - start)).locate(loc());
for (int i = start; i < target.size(); i++) {
var instr = target.get(i);
if (instr.type == Type.NOP && instr.is(0, "break") && instr.get(1) == null) {
target.set(i, Instruction.jmp(target.size() - i).locate(instr.location));
}
if (instr.type == Type.NOP && instr.is(0, "try_break") && instr.get(1) == null) {
target.set(i, Instruction.signal("jmp_" + (target.size() - (Integer)instr.get(2))).locate(instr.location));
}
}
for (var el : caseMap.entrySet()) {
var loc = target.get(el.getKey()).location;
var i = stmIndexMap.get(el.getValue());
if (i == null) i = target.size();
target.set(el.getKey(), Instruction.jmpIf(i - el.getKey()).locate(loc).setDebug(true));
}
target.add(Instruction.discard().locate(loc()));
}
public SwitchStatement(Location loc, Statement value, int defaultI, SwitchCase[] cases, Statement[] body) {
super(loc);
this.value = value;
this.defaultI = defaultI;
this.cases = cases;
this.body = body;
}
}

View File

@@ -0,0 +1,26 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ThrowStatement extends Statement {
public final Statement value;
@Override
public boolean pollutesStack() { return false; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
value.compileWithPollution(target, scope);
target.add(Instruction.throwInstr().locate(loc()));
}
public ThrowStatement(Location loc, Statement value) {
super(loc);
this.value = value;
}
}

View File

@@ -0,0 +1,87 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class TryStatement extends Statement {
public final Statement tryBody;
public final Statement catchBody;
public final Statement finallyBody;
public final String name;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord globScope) {
tryBody.declare(globScope);
if (catchBody != null) catchBody.declare(globScope);
if (finallyBody != null) finallyBody.declare(globScope);
}
private void compileBody(List<Instruction> target, ScopeRecord scope, Statement body, String arg) {
var subscope = scope.child();
int start = target.size();
target.add(Instruction.nop());
subscope.define("this");
var argsVar = subscope.define("<catchargs>");
if (arg != null) {
target.add(Instruction.loadVar(argsVar));
target.add(Instruction.loadMember(0));
target.add(Instruction.storeVar(subscope.define(arg)));
}
int bodyStart = target.size();
body.compile(target, subscope);
target.add(Instruction.signal("no_return"));
target.get(bodyStart).locate(body.loc());
target.set(start, Instruction.loadFunc(target.size() - start, subscope.localsCount(), 0, subscope.getCaptures()));
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
int start = target.size();
compileBody(target, scope, tryBody, null);
if (catchBody != null) {
compileBody(target, scope, catchBody, name);
}
if (finallyBody != null) {
compileBody(target, scope, finallyBody, null);
}
for (int i = start; i < target.size(); i++) {
if (target.get(i).type == Type.NOP) {
var instr = target.get(i);
if (instr.is(0, "break")) {
target.set(i, Instruction.nop("try_break", instr.get(1), target.size()).locate(instr.location));
}
else if (instr.is(0, "cont")) {
target.set(i, Instruction.nop("try_cont", instr.get(1), target.size()).locate(instr.location));
}
}
}
target.add(Instruction.tryInstr(catchBody != null, finallyBody != null).locate(loc()));
}
public TryStatement(Location loc, Statement tryBody, Statement catchBody, Statement finallyBody, String name) {
super(loc);
this.tryBody = tryBody;
this.catchBody = catchBody;
this.finallyBody = finallyBody;
this.name = name;
}
}

View File

@@ -0,0 +1,104 @@
package me.topchetoeu.jscript.compilation.control;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.CompoundStatement;
import me.topchetoeu.jscript.compilation.DiscardStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.compilation.values.ConstantStatement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class WhileStatement extends Statement {
public final Statement condition, body;
public final String label;
@Override
public boolean pollutesStack() { return false; }
@Override
public void declare(ScopeRecord globScope) {
body.declare(globScope);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (condition instanceof ConstantStatement) {
if (Values.toBoolean(((ConstantStatement)condition).value)) {
int start = target.size();
body.compileNoPollution(target, scope);
int end = target.size();
replaceBreaks(target, label, start, end, start, end + 1);
target.add(Instruction.jmp(start - target.size()).locate(loc()));
return;
}
}
int start = target.size();
condition.compileWithPollution(target, scope);
int mid = target.size();
target.add(Instruction.nop());
body.compileNoPollution(target, scope);
int end = target.size();
replaceBreaks(target, label, mid + 1, end, start, end + 1);
target.add(Instruction.jmp(start - end).locate(loc()));
target.set(mid, Instruction.jmpIfNot(end - mid + 1).locate(loc()));
}
@Override
public Statement optimize() {
var cond = condition.optimize();
var b = body.optimize();
if (b instanceof ContinueStatement) {
b = new CompoundStatement(loc());
}
else if (b instanceof BreakStatement) return new DiscardStatement(loc(), cond).optimize();
if (b.pure()) return new WhileStatement(loc(), label, cond, new CompoundStatement(null));
else return new WhileStatement(loc(), label, cond, b);
}
public WhileStatement(Location loc, String label, Statement condition, Statement body) {
super(loc);
this.label = label;
this.condition = condition;
this.body = body;
}
public static void replaceBreaks(List<Instruction> target, String label, int start, int end, int continuePoint, int breakPoint) {
for (int i = start; i < end; i++) {
var instr = target.get(i);
if (instr.type == Type.NOP && instr.is(0, "cont") && (instr.get(1) == null || instr.is(1, label))) {
target.set(i, Instruction.jmp(continuePoint - i));
target.get(i).location = instr.location;
}
if (instr.type == Type.NOP && instr.is(0, "break") && (instr.get(1) == null || instr.is(1, label))) {
target.set(i, Instruction.jmp(breakPoint - i));
target.get(i).location = instr.location;
}
if (instr.type == Type.NOP && instr.is(0, "try_cont") && (instr.get(1) == null || instr.is(1, label))) {
target.set(i, Instruction.signal("jmp_" + (continuePoint - (Integer)instr.get(2))));
target.get(i).location = instr.location;
}
if (instr.type == Type.NOP && instr.is(0, "try_break") && (instr.get(1) == null || instr.is(1, label))) {
target.set(i, Instruction.signal("jmp_" + (breakPoint - (Integer)instr.get(2))));
target.get(i).location = instr.location;
}
}
}
// public static CompoundStatement ofFor(Location loc, String label, Statement declaration, Statement condition, Statement increment, Statement body) {
// return new CompoundStatement(loc,
// declaration,
// new WhileStatement(loc, label, condition, new CompoundStatement(loc,
// body,
// increment
// ))
// );
// }
}

View File

@@ -0,0 +1,37 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ArrayStatement extends Statement {
public final Statement[] statements;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.loadArr(statements.length).locate(loc()));
var i = 0;
for (var el : statements) {
if (el != null) {
target.add(Instruction.dup(1).locate(loc()));
target.add(Instruction.loadValue(i).locate(loc()));
el.compileWithPollution(target, scope);
target.add(Instruction.storeMember().locate(loc()));
}
i++;
}
}
public ArrayStatement(Location loc, Statement[] statements) {
super(loc);
this.statements = statements;
}
}

View File

@@ -0,0 +1,44 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class CallStatement extends Statement {
public final Statement func;
public final Statement[] args;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (func instanceof IndexStatement) {
((IndexStatement)func).compile(target, scope, true);
}
else {
target.add(Instruction.loadValue(null).locate(loc()));
func.compileWithPollution(target, scope);
}
for (var arg : args) {
arg.compileWithPollution(target, scope);
}
target.add(Instruction.call(args.length).locate(loc()).setDebug(true));
}
public CallStatement(Location loc, Statement func, Statement... args) {
super(loc);
this.func = func;
this.args = args;
}
public CallStatement(Location loc, Statement obj, Object key, Statement... args) {
super(loc);
this.func = new IndexStatement(loc, obj, new ConstantStatement(loc, key));
this.args = args;
}
}

View File

@@ -0,0 +1,35 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.AssignableStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ChangeStatement extends Statement {
public final AssignableStatement value;
public final double addAmount;
public final boolean postfix;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
value.toAssign(new ConstantStatement(loc(), -addAmount), Type.SUBTRACT).compileWithPollution(target, scope);
if (postfix) {
target.add(Instruction.loadValue(addAmount).locate(loc()));
target.add(Instruction.operation(Type.SUBTRACT).locate(loc()));
}
}
public ChangeStatement(Location loc, AssignableStatement value, double addAmount, boolean postfix) {
super(loc);
this.value = value;
this.addAmount = addAmount;
this.postfix = postfix;
}
}

View File

@@ -0,0 +1,38 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class CommaStatement extends Statement {
public final Statement first;
public final Statement second;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return first.pure() && second.pure(); }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
first.compileNoPollution(target, scope);
second.compileWithPollution(target, scope);
}
@Override
public Statement optimize() {
var f = first.optimize();
var s = second.optimize();
if (f.pure()) return s;
else return new CommaStatement(loc(), f, s);
}
public CommaStatement(Location loc, Statement first, Statement second) {
super(loc);
this.first = first;
this.second = second;
}
}

View File

@@ -0,0 +1,27 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ConstantStatement extends Statement {
public final Object value;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.loadValue(value).locate(loc()));
}
public ConstantStatement(Location loc, Object val) {
super(loc);
this.value = val;
}
}

View File

@@ -0,0 +1,110 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.CompoundStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.exceptions.SyntaxException;
public class FunctionStatement extends Statement {
public final CompoundStatement body;
public final String name;
public final String[] args;
@Override
public boolean pure() { return name == null; }
@Override
public boolean pollutesStack() { return true; }
@Override
public void declare(ScopeRecord scope) {
if (name != null) scope.define(name);
}
public static void checkBreakAndCont(List<Instruction> target, int start) {
for (int i = start; i < target.size(); i++) {
if (target.get(i).type == Type.NOP) {
if (target.get(i).is(0, "break") || target.get(i).is(0, "try_break")) {
throw new SyntaxException(target.get(i).location, "Break was placed outside a loop.");
}
if (target.get(i).is(0, "cont") || target.get(i).is(0, "try_cont")) {
throw new SyntaxException(target.get(i).location, "Continue was placed outside a loop.");
}
}
}
}
public void compile(List<Instruction> target, ScopeRecord scope, String name, boolean isStatement) {
for (var i = 0; i < args.length; i++) {
for (var j = 0; j < i; j++) {
if (args[i].equals(args[j])){
target.add(Instruction.throwSyntax(new SyntaxException(loc(), "Duplicate parameter '" + args[i] + "'.")));
return;
}
}
}
var subscope = scope.child();
int start = target.size();
target.add(Instruction.nop());
subscope.define("this");
var argsVar = subscope.define("arguments");
if (args.length > 0) {
target.add(Instruction.loadVar(argsVar).locate(loc()));
if (args.length != 1) target.add(Instruction.dup(args.length - 1).locate(loc()));
for (var i = 0; i < args.length; i++) {
target.add(Instruction.loadMember(i).locate(loc()));
target.add(Instruction.storeVar(subscope.define(args[i])).locate(loc()));
}
}
if (!isStatement && this.name != null) {
target.add(Instruction.storeSelfFunc((int)subscope.define(this.name)));
}
body.declare(subscope);
target.add(Instruction.debugVarNames(subscope.locals()));
body.compile(target, subscope);
checkBreakAndCont(target, start);
if (!(body instanceof CompoundStatement)) target.add(Instruction.ret().locate(loc()));
target.set(start, Instruction.loadFunc(target.size() - start, subscope.localsCount(), args.length, subscope.getCaptures()).locate(loc()));
if (name == null) name = this.name;
if (name != null) {
target.add(Instruction.dup(1).locate(loc()));
target.add(Instruction.loadValue("name").locate(loc()));
target.add(Instruction.loadValue(name).locate(loc()));
target.add(Instruction.storeMember().locate(loc()));
}
if (this.name != null && isStatement) {
var key = scope.getKey(this.name);
if (key instanceof String) target.add(Instruction.makeVar((String)key).locate(loc()));
target.add(Instruction.storeVar(scope.getKey(this.name), true).locate(loc()));
}
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
compile(target, scope, null, false);
}
public FunctionStatement(Location loc, String name, String[] args, CompoundStatement body) {
super(loc);
this.name = name;
this.args = args;
this.body = body;
}
}

View File

@@ -0,0 +1,24 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class GlobalThisStatement extends Statement {
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.loadGlob().locate(loc()));
}
public GlobalThisStatement(Location loc) {
super(loc);
}
}

View File

@@ -0,0 +1,51 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class IndexAssignStatement extends Statement {
public final Statement object;
public final Statement index;
public final Statement value;
public final Type operation;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
int start = 0;
if (operation != null) {
object.compileWithPollution(target, scope);
index.compileWithPollution(target, scope);
target.add(Instruction.dup(1, 1).locate(loc()));
target.add(Instruction.dup(1, 1).locate(loc()));
target.add(Instruction.loadMember().locate(loc()));
value.compileWithPollution(target, scope);
target.add(Instruction.operation(operation).locate(loc()));
target.add(Instruction.storeMember(true).locate(loc()));
}
else {
object.compileWithPollution(target, scope);
index.compileWithPollution(target, scope);
value.compileWithPollution(target, scope);
target.add(Instruction.storeMember(true).locate(loc()));
}
target.get(start).setDebug(true);
}
public IndexAssignStatement(Location loc, Statement object, Statement index, Statement value, Type operation) {
super(loc);
this.object = object;
this.index = index;
this.value = value;
this.operation = operation;
}
}

View File

@@ -0,0 +1,53 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.AssignableStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class IndexStatement extends AssignableStatement {
public final Statement object;
public final Statement index;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public Statement toAssign(Statement val, Type operation) {
return new IndexAssignStatement(loc(), object, index, val, operation);
}
public void compile(List<Instruction> target, ScopeRecord scope, boolean dupObj) {
int start = 0;
object.compileWithPollution(target, scope);
if (dupObj) target.add(Instruction.dup(1).locate(loc()));
if (index instanceof ConstantStatement) {
target.add(Instruction.loadMember(((ConstantStatement)index).value).locate(loc()));
return;
}
index.compileWithPollution(target, scope);
target.add(Instruction.loadMember().locate(loc()));
target.get(start).setDebug(true);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
compile(target, scope, false);
}
public IndexStatement(Location loc, Statement object, Statement index) {
super(loc);
this.object = object;
this.index = index;
}
public IndexStatement(Location loc, Statement object, Object index) {
super(loc);
this.object = object;
this.index = new ConstantStatement(loc, index);
}
}

View File

@@ -0,0 +1,45 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class LazyAndStatement extends Statement {
public final Statement first, second;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() {
return first.pure() && second.pure();
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (first instanceof ConstantStatement) {
if (Values.not(((ConstantStatement)first).value)) {
first.compileWithPollution(target, scope);
}
else second.compileWithPollution(target, scope);
return;
}
first.compileWithPollution(target, scope);
target.add(Instruction.dup(1).locate(loc()));
int start = target.size();
target.add(Instruction.nop());
target.add(Instruction.discard().locate(loc()));
second.compileWithPollution(target, scope);
target.set(start, Instruction.jmpIfNot(target.size() - start).locate(loc()));
}
public LazyAndStatement(Location loc, Statement first, Statement second) {
super(loc);
this.first = first;
this.second = second;
}
}

View File

@@ -0,0 +1,45 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class LazyOrStatement extends Statement {
public final Statement first, second;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() {
return first.pure() && second.pure();
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (first instanceof ConstantStatement) {
if (Values.not(((ConstantStatement)first).value)) {
second.compileWithPollution(target, scope);
}
else first.compileWithPollution(target, scope);
return;
}
first.compileWithPollution(target, scope);
target.add(Instruction.dup(1).locate(loc()));
int start = target.size();
target.add(Instruction.nop());
target.add(Instruction.discard().locate(loc()));
second.compileWithPollution(target, scope);
target.set(start, Instruction.jmpIf(target.size() - start).locate(loc()));
}
public LazyOrStatement(Location loc, Statement first, Statement second) {
super(loc);
this.first = first;
this.second = second;
}
}

View File

@@ -0,0 +1,32 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class NewStatement extends Statement {
public final Statement func;
public final Statement[] args;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
func.compileWithPollution(target, scope);
for (var arg : args) {
arg.compileWithPollution(target, scope);
}
target.add(Instruction.callNew(args.length).locate(loc()).setDebug(true));
}
public NewStatement(Location loc, Statement func, Statement... args) {
super(loc);
this.func = func;
this.args = args;
}
}

View File

@@ -0,0 +1,57 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class ObjectStatement extends Statement {
public final Map<Object, Statement> map;
public final Map<Object, FunctionStatement> getters;
public final Map<Object, FunctionStatement> setters;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.loadObj().locate(loc()));
if (!map.isEmpty()) target.add(Instruction.dup(map.size()).locate(loc()));
for (var el : map.entrySet()) {
target.add(Instruction.loadValue(el.getKey()).locate(loc()));
var val = el.getValue();
if (val instanceof FunctionStatement) ((FunctionStatement)val).compile(target, scope, el.getKey().toString(), false);
else val.compileWithPollution(target, scope);
target.add(Instruction.storeMember().locate(loc()));
}
var keys = new ArrayList<Object>();
keys.addAll(getters.keySet());
keys.addAll(setters.keySet());
for (var key : keys) {
if (key instanceof String) target.add(Instruction.loadValue((String)key).locate(loc()));
else target.add(Instruction.loadValue((Double)key).locate(loc()));
if (getters.containsKey(key)) getters.get(key).compileWithPollution(target, scope);
else target.add(Instruction.loadValue(null).locate(loc()));
if (setters.containsKey(key)) setters.get(key).compileWithPollution(target, scope);
else target.add(Instruction.loadValue(null).locate(loc()));
target.add(Instruction.defProp().locate(loc()));
}
}
public ObjectStatement(Location loc, Map<Object, Statement> map, Map<Object, FunctionStatement> getters, Map<Object, FunctionStatement> setters) {
super(loc);
this.map = Map.copyOf(map);
this.getters = Map.copyOf(getters);
this.setters = Map.copyOf(setters);
}
}

View File

@@ -0,0 +1,104 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.control.ThrowStatement;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.exceptions.EngineException;
public class OperationStatement extends Statement {
public final Statement[] args;
public final Instruction.Type operation;
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
for (var arg : args) {
arg.compileWithPollution(target, scope);
}
target.add(Instruction.operation(operation).locate(loc()));
}
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() {
for (var arg : args) {
if (!arg.pure()) return false;
}
return true;
}
@Override
public Statement optimize() {
var args = new Statement[this.args.length];
var allConst = true;
for (var i = 0; i < this.args.length; i++) {
args[i] = this.args[i].optimize();
if (!(args[i] instanceof ConstantStatement)) allConst = false;
}
if (allConst) {
var vals = new Object[this.args.length];
for (var i = 0; i < args.length; i++) {
vals[i] = ((ConstantStatement)args[i]).value;
}
try {
var ctx = new CallContext(null);
switch (operation) {
case ADD: return new ConstantStatement(loc(), Values.add(ctx, vals[0], vals[1]));
case SUBTRACT: return new ConstantStatement(loc(), Values.subtract(ctx, vals[0], vals[1]));
case DIVIDE: return new ConstantStatement(loc(), Values.divide(ctx, vals[0], vals[1]));
case MULTIPLY: return new ConstantStatement(loc(), Values.multiply(ctx, vals[0], vals[1]));
case MODULO: return new ConstantStatement(loc(), Values.modulo(ctx, vals[0], vals[1]));
case AND: return new ConstantStatement(loc(), Values.and(ctx, vals[0], vals[1]));
case OR: return new ConstantStatement(loc(), Values.or(ctx, vals[0], vals[1]));
case XOR: return new ConstantStatement(loc(), Values.xor(ctx, vals[0], vals[1]));
case EQUALS: return new ConstantStatement(loc(), Values.strictEquals(vals[0], vals[1]));
case NOT_EQUALS: return new ConstantStatement(loc(), !Values.strictEquals(vals[0], vals[1]));
case LOOSE_EQUALS: return new ConstantStatement(loc(), Values.looseEqual(ctx, vals[0], vals[1]));
case LOOSE_NOT_EQUALS: return new ConstantStatement(loc(), !Values.looseEqual(ctx, vals[0], vals[1]));
case GREATER: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) < 0);
case GREATER_EQUALS: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) <= 0);
case LESS: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) > 0);
case LESS_EQUALS: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) >= 0);
case INVERSE: return new ConstantStatement(loc(), Values.bitwiseNot(ctx, vals[0]));
case NOT: return new ConstantStatement(loc(), Values.not(vals[0]));
case POS: return new ConstantStatement(loc(), Values.toNumber(ctx, vals[0]));
case NEG: return new ConstantStatement(loc(), Values.negative(ctx, vals[0]));
case SHIFT_LEFT: return new ConstantStatement(loc(), Values.shiftLeft(ctx, vals[0], vals[1]));
case SHIFT_RIGHT: return new ConstantStatement(loc(), Values.shiftRight(ctx, vals[0], vals[1]));
case USHIFT_RIGHT: return new ConstantStatement(loc(), Values.unsignedShiftRight(ctx, vals[0], vals[1]));
default: break;
}
}
catch (EngineException e) {
return new ThrowStatement(loc(), new ConstantStatement(loc(), e.value));
}
catch (InterruptedException e) { return null; }
}
return new OperationStatement(loc(), operation, args);
}
public OperationStatement(Location loc, Instruction.Type operation, Statement... args) {
super(loc);
this.operation = operation;
this.args = args;
}
}

View File

@@ -0,0 +1,28 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class RegexStatement extends Statement {
public final String pattern, flags;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.loadRegex(pattern, flags).locate(loc()));
}
public RegexStatement(Location loc, String pattern, String flags) {
super(loc);
this.pattern = pattern;
this.flags = flags;
}
}

View File

@@ -0,0 +1,58 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.Values;
public class TernaryStatement extends Statement {
public final Statement condition;
public final Statement first;
public final Statement second;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (condition instanceof ConstantStatement) {
if (!Values.toBoolean(((ConstantStatement)condition).value)) {
second.compileWithPollution(target, scope);
}
else first.compileWithPollution(target, scope);
return;
}
condition.compileWithPollution(target, scope);
int start = target.size();
target.add(Instruction.nop());
first.compileWithPollution(target, scope);
int mid = target.size();
target.add(Instruction.nop());
second.compileWithPollution(target, scope);
int end = target.size();
target.set(start, Instruction.jmpIfNot(mid - start + 1).locate(loc()));
target.set(mid, Instruction.jmp(end - mid).locate(loc()));
}
@Override
public Statement optimize() {
var cond = condition.optimize();
var f = first.optimize();
var s = second.optimize();
return new TernaryStatement(loc(), cond, f, s);
}
public TernaryStatement(Location loc, Statement condition, Statement first, Statement second) {
super(loc);
this.condition = condition;
this.first = first;
this.second = second;
}
}

View File

@@ -0,0 +1,55 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.Symbol;
public class TypeofStatement extends Statement {
public final Statement value;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (value instanceof VariableStatement) {
var i = scope.getKey(((VariableStatement)value).name);
if (i instanceof String) {
target.add(Instruction.typeof((String)i));
return;
}
}
value.compileWithPollution(target, scope);
target.add(Instruction.typeof().locate(loc()));
}
@Override
public Statement optimize() {
var val = value.optimize();
if (val instanceof ConstantStatement) {
var cnst = (ConstantStatement)val;
if (cnst.value == null) return new ConstantStatement(loc(), "undefined");
if (cnst.value instanceof Number) return new ConstantStatement(loc(), "number");
if (cnst.value instanceof Boolean) return new ConstantStatement(loc(), "boolean");
if (cnst.value instanceof String) return new ConstantStatement(loc(), "string");
if (cnst.value instanceof Symbol) return new ConstantStatement(loc(), "symbol");
if (cnst.value instanceof FunctionValue) return new ConstantStatement(loc(), "function");
return new ConstantStatement(loc(), "object");
}
return new TypeofStatement(loc(), val);
}
public TypeofStatement(Location loc, Statement value) {
super(loc);
this.value = value;
}
}

View File

@@ -0,0 +1,42 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class VariableAssignStatement extends Statement {
public final String name;
public final Statement value;
public final Type operation;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
var i = scope.getKey(name);
if (operation != null) {
target.add(Instruction.loadVar(i).locate(loc()));
if (value instanceof FunctionStatement) ((FunctionStatement)value).compile(target, scope, name, false);
else value.compileWithPollution(target, scope);
target.add(Instruction.operation(operation).locate(loc()));
}
else {
if (value instanceof FunctionStatement) ((FunctionStatement)value).compile(target, scope, name, false);
else value.compileWithPollution(target, scope);
}
target.add(Instruction.storeVar(i, true).locate(loc()));
}
public VariableAssignStatement(Location loc, String name, Statement val, Type operation) {
super(loc);
this.name = name;
this.value = val;
this.operation = operation;
}
}

View File

@@ -0,0 +1,27 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class VariableIndexStatement extends Statement {
public final int index;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
target.add(Instruction.loadVar(index).locate(loc()));
}
public VariableIndexStatement(Location loc, int i) {
super(loc);
this.index = i;
}
}

View File

@@ -0,0 +1,35 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.AssignableStatement;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class VariableStatement extends AssignableStatement {
public final String name;
@Override
public boolean pollutesStack() { return true; }
@Override
public boolean pure() { return true; }
@Override
public Statement toAssign(Statement val, Type operation) {
return new VariableAssignStatement(loc(), name, val, operation);
}
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
var i = scope.getKey(name);
target.add(Instruction.loadVar(i).locate(loc()));
}
public VariableStatement(Location loc, String name) {
super(loc);
this.name = name;
}
}

View File

@@ -0,0 +1,34 @@
package me.topchetoeu.jscript.compilation.values;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.engine.scope.ScopeRecord;
import me.topchetoeu.jscript.compilation.Instruction;
public class VoidStatement extends Statement {
public final Statement value;
@Override
public boolean pollutesStack() { return true; }
@Override
public void compile(List<Instruction> target, ScopeRecord scope) {
if (value != null) value.compileNoPollution(target, scope);
target.add(Instruction.loadValue(null).locate(loc()));
}
@Override
public Statement optimize() {
if (value == null) return this;
var val = value.optimize();
if (val.pure()) return new ConstantStatement(loc(), null);
else return new VoidStatement(loc(), val);
}
public VoidStatement(Location loc, Statement value) {
super(loc);
this.value = value;
}
}

View File

@@ -0,0 +1,5 @@
package me.topchetoeu.jscript.engine;
import me.topchetoeu.jscript.Location;
public record BreakpointData(Location loc, CallContext ctx) { }

View File

@@ -0,0 +1,58 @@
package me.topchetoeu.jscript.engine;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Map;
public class CallContext {
public static final class DataKey<T> {}
public final Engine engine;
private final Map<DataKey<?>, Object> data = new Hashtable<>();
public Engine engine() { return engine; }
public Map<DataKey<?>, Object> data() { return Collections.unmodifiableMap(data); }
public CallContext copy() {
return new CallContext(engine).mergeData(data);
}
public CallContext mergeData(Map<DataKey<?>, Object> objs) {
data.putAll(objs);
return this;
}
public <T> CallContext setData(DataKey<T> key, T val) {
data.put(key, val);
return this;
}
@SuppressWarnings("unchecked")
public <T> T addData(DataKey<T> key, T val) {
if (data.containsKey(key)) return (T)data.get(key);
else {
data.put(key, val);
return val;
}
}
public boolean hasData(DataKey<?> key) { return data.containsKey(key); }
public <T> T getData(DataKey<T> key) {
return getData(key, null);
}
@SuppressWarnings("unchecked")
public <T> T getData(DataKey<T> key, T defaultVal) {
if (!hasData(key)) return defaultVal;
else return (T)data.get(key);
}
public CallContext changeData(DataKey<Integer> key, int n, int start) {
return setData(key, getData(key, start) + n);
}
public CallContext changeData(DataKey<Integer> key, int n) {
return changeData(key, n, 0);
}
public CallContext changeData(DataKey<Integer> key) {
return changeData(key, 1, 0);
}
public CallContext(Engine engine) {
this.engine = engine;
}
}

View File

@@ -0,0 +1,8 @@
package me.topchetoeu.jscript.engine;
public enum DebugCommand {
NORMAL,
STEP_OVER,
STEP_OUT,
STEP_INTO,
}

View File

@@ -0,0 +1,195 @@
package me.topchetoeu.jscript.engine;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingDeque;
import me.topchetoeu.jscript.engine.CallContext.DataKey;
import me.topchetoeu.jscript.engine.debug.DebugState;
import me.topchetoeu.jscript.engine.modules.ModuleManager;
import me.topchetoeu.jscript.engine.scope.GlobalScope;
import me.topchetoeu.jscript.engine.values.CodeFunction;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.ObjectValue.PlaceholderProto;
import me.topchetoeu.jscript.events.Awaitable;
import me.topchetoeu.jscript.events.DataNotifier;
import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.interop.NativeTypeRegister;
import me.topchetoeu.jscript.parsing.Parsing;
public class Engine {
private static record RawFunction(GlobalScope scope, String filename, String raw) { }
private static class Task {
public final Object func;
public final Object thisArg;
public final Object[] args;
public final Map<DataKey<?>, Object> data;
public final DataNotifier<Object> notifier = new DataNotifier<>();
public Task(Object func, Map<DataKey<?>, Object> data, Object thisArg, Object[] args) {
this.func = func;
this.data = data;
this.thisArg = thisArg;
this.args = args;
}
}
public static final DataKey<DebugState> DEBUG_STATE_KEY = new DataKey<>();
private static int nextId = 0;
private Map<DataKey<?>, Object> callCtxVals = new HashMap<>();
private GlobalScope global = new GlobalScope();
private ObjectValue arrayProto = new ObjectValue();
private ObjectValue boolProto = new ObjectValue();
private ObjectValue funcProto = new ObjectValue();
private ObjectValue numProto = new ObjectValue();
private ObjectValue objProto = new ObjectValue(PlaceholderProto.NONE);
private ObjectValue strProto = new ObjectValue();
private ObjectValue symProto = new ObjectValue();
private ObjectValue errProto = new ObjectValue();
private ObjectValue syntaxErrProto = new ObjectValue(PlaceholderProto.ERROR);
private ObjectValue typeErrProto = new ObjectValue(PlaceholderProto.ERROR);
private ObjectValue rangeErrProto = new ObjectValue(PlaceholderProto.ERROR);
private NativeTypeRegister typeRegister;
private Thread thread;
private LinkedBlockingDeque<Task> macroTasks = new LinkedBlockingDeque<>();
private LinkedBlockingDeque<Task> microTasks = new LinkedBlockingDeque<>();
public final int id = ++nextId;
public final DebugState debugState = new DebugState();
public ObjectValue arrayProto() { return arrayProto; }
public ObjectValue booleanProto() { return boolProto; }
public ObjectValue functionProto() { return funcProto; }
public ObjectValue numberProto() { return numProto; }
public ObjectValue objectProto() { return objProto; }
public ObjectValue stringProto() { return strProto; }
public ObjectValue symbolProto() { return symProto; }
public ObjectValue errorProto() { return errProto; }
public ObjectValue syntaxErrorProto() { return syntaxErrProto; }
public ObjectValue typeErrorProto() { return typeErrProto; }
public ObjectValue rangeErrorProto() { return rangeErrProto; }
public GlobalScope global() { return global; }
public NativeTypeRegister typeRegister() { return typeRegister; }
public void copyFrom(Engine other) {
global = other.global;
typeRegister = other.typeRegister;
arrayProto = other.arrayProto;
boolProto = other.boolProto;
funcProto = other.funcProto;
numProto = other.numProto;
objProto = other.objProto;
strProto = other.strProto;
symProto = other.symProto;
errProto = other.errProto;
syntaxErrProto = other.syntaxErrProto;
typeErrProto = other.typeErrProto;
rangeErrProto = other.rangeErrProto;
}
private void runTask(Task task) throws InterruptedException {
try {
FunctionValue func;
if (task.func instanceof FunctionValue) func = (FunctionValue)task.func;
else {
var raw = (RawFunction)task.func;
func = compile(raw.scope, raw.filename, raw.raw);
}
task.notifier.next(func.call(context().mergeData(task.data), task.thisArg, task.args));
}
catch (InterruptedException e) {
task.notifier.error(new RuntimeException(e));
throw e;
}
catch (EngineException e) {
task.notifier.error(e);
}
catch (RuntimeException e) {
task.notifier.error(e);
e.printStackTrace();
}
}
private void run() {
while (true) {
try {
runTask(macroTasks.take());
while (!microTasks.isEmpty()) {
runTask(microTasks.take());
}
}
catch (InterruptedException e) {
for (var msg : macroTasks) {
msg.notifier.error(new RuntimeException(e));
}
break;
}
}
}
public void exposeClass(String name, Class<?> clazz) {
global.define(name, true, typeRegister.getConstr(clazz));
}
public void exposeNamespace(String name, Class<?> clazz) {
global.define(name, true, NativeTypeRegister.makeNamespace(clazz));
}
public Thread start() {
if (this.thread == null) {
this.thread = new Thread(this::run, "JavaScript Runner #" + id);
this.thread.start();
}
return this.thread;
}
public void stop() {
thread.interrupt();
thread = null;
}
public boolean inExecThread() {
return Thread.currentThread() == thread;
}
public boolean isRunning() {
return this.thread != null;
}
public Object makeRegex(String pattern, String flags) {
throw EngineException.ofError("Regular expressions not supported.");
}
public ModuleManager modules() {
return null;
}
public ObjectValue getPrototype(Class<?> clazz) {
return typeRegister.getProto(clazz);
}
public CallContext context() { return new CallContext(this).mergeData(callCtxVals); }
public Awaitable<Object> pushMsg(boolean micro, FunctionValue func, Map<DataKey<?>, Object> data, Object thisArg, Object... args) {
var msg = new Task(func, data, thisArg, args);
if (micro) microTasks.addLast(msg);
else macroTasks.addLast(msg);
return msg.notifier;
}
public Awaitable<Object> pushMsg(boolean micro, GlobalScope scope, Map<DataKey<?>, Object> data, String filename, String raw, Object thisArg, Object... args) {
var msg = new Task(new RawFunction(scope, filename, raw), data, thisArg, args);
if (micro) microTasks.addLast(msg);
else macroTasks.addLast(msg);
return msg.notifier;
}
public CodeFunction compile(GlobalScope scope, String filename, String raw) throws InterruptedException {
return Parsing.compile(scope, filename, raw);
}
public Engine(NativeTypeRegister register) {
this.typeRegister = register;
this.callCtxVals.put(DEBUG_STATE_KEY, debugState);
}
public Engine() {
this(new NativeTypeRegister());
}
}

View File

@@ -0,0 +1,154 @@
package me.topchetoeu.jscript.engine.debug;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.util.Base64;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
import me.topchetoeu.jscript.engine.debug.handlers.DebuggerHandles;
import me.topchetoeu.jscript.exceptions.SyntaxException;
public class DebugServer {
public static String browserDisplayName = "jscript";
public static String targetName = "target";
public final Engine engine;
private static void send(Socket socket, String val) throws IOException {
Http.writeResponse(socket.getOutputStream(), 200, "OK", "application/json", val.getBytes());
}
// SILENCE JAVA
private MessageDigest getDigestInstance() {
try {
return MessageDigest.getInstance("sha1");
}
catch (Throwable a) { return null; }
}
private static Thread runAsync(Runnable func, String name) {
var res = new Thread(func);
res.setName(name);
res.start();
return res;
}
private void handle(WebSocket ws) throws InterruptedException, IOException {
WebSocketMessage raw;
while ((raw = ws.receive()) != null) {
if (raw.type != Type.Text) {
ws.send(new V8Error("Expected a text message."));
continue;
}
V8Message msg;
try {
msg = new V8Message(raw.textData());
}
catch (SyntaxException e) {
ws.send(new V8Error(e.getMessage()));
return;
}
switch (msg.name) {
case "Debugger.enable": DebuggerHandles.enable(msg, engine, ws); continue;
case "Debugger.disable": DebuggerHandles.disable(msg, engine, ws); continue;
case "Debugger.stepInto": DebuggerHandles.stepInto(msg, engine, ws); continue;
}
}
}
private void onWsConnect(HttpRequest req, Socket socket) throws IOException {
var key = req.headers().get("sec-websocket-key");
if (key == null) {
Http.writeResponse(
socket.getOutputStream(), 426, "Upgrade Required", "text/txt",
"Expected a WS upgrade".getBytes()
);
return;
}
var resKey = Base64.getEncoder().encodeToString(getDigestInstance().digest(
(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes()
));
Http.writeCode(socket.getOutputStream(), 101, "Switching Protocols");
Http.writeHeader(socket.getOutputStream(), "Connection", "Upgrade");
Http.writeHeader(socket.getOutputStream(), "Sec-WebSocket-Accept", resKey);
Http.writeLastHeader(socket.getOutputStream(), "Upgrade", "WebSocket");
var ws = new WebSocket(socket);
runAsync(() -> {
try {
handle(ws);
}
catch (InterruptedException e) { return; }
catch (IOException e) { e.printStackTrace(); }
finally { ws.close(); }
}, "Debug Server Message Reader");
runAsync(() -> {
try {
handle(ws);
}
catch (InterruptedException e) { return; }
catch (IOException e) { e.printStackTrace(); }
finally { ws.close(); }
}, "Debug Server Event Writer");
}
public void open(InetSocketAddress address) throws IOException {
ServerSocket server = new ServerSocket();
server.bind(address);
try {
while (true) {
var socket = server.accept();
var req = Http.readRequest(socket.getInputStream());
switch (req.path()) {
case "/json/version":
send(socket, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.2\"}");
break;
case "/json/list":
case "/json":
var addr = "ws://" + address.getHostString() + ":" + address.getPort() + "/devtools/page/" + targetName;
send(socket, "{\"id\":\"" + browserDisplayName + "\",\"webSocketDebuggerUrl\":\"" + addr + "\"}");
break;
case "/json/new":
case "/json/activate":
case "/json/protocol":
case "/json/close":
case "/devtools/inspector.html":
Http.writeResponse(
socket.getOutputStream(),
501, "Not Implemented", "text/txt",
"This feature isn't (and won't be) implemented.".getBytes()
);
break;
default:
if (req.path().equals("/devtools/page/" + targetName)) onWsConnect(req, socket);
else {
Http.writeResponse(
socket.getOutputStream(),
404, "Not Found", "text/txt",
"Not found :/".getBytes()
);
}
break;
}
}
}
finally { server.close(); }
}
public DebugServer(Engine engine) {
this.engine = engine;
}
}

View File

@@ -0,0 +1,52 @@
package me.topchetoeu.jscript.engine.debug;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.engine.BreakpointData;
import me.topchetoeu.jscript.engine.DebugCommand;
import me.topchetoeu.jscript.engine.frame.CodeFrame;
import me.topchetoeu.jscript.events.Event;
public class DebugState {
private boolean paused = false;
public final HashSet<Location> breakpoints = new HashSet<>();
public final List<CodeFrame> frames = new ArrayList<>();
public final Map<String, String> sources = new HashMap<>();
public final Event<BreakpointData> breakpointNotifier = new Event<>();
public final Event<DebugCommand> commandNotifier = new Event<>();
public final Event<String> sourceAdded = new Event<>();
public DebugState pushFrame(CodeFrame frame) {
frames.add(frame);
return this;
}
public DebugState popFrame() {
if (frames.size() > 0) frames.remove(frames.size() - 1);
return this;
}
public DebugCommand pause(BreakpointData data) throws InterruptedException {
paused = true;
breakpointNotifier.next(data);
return commandNotifier.toAwaitable().await();
}
public void resume(DebugCommand command) {
paused = false;
commandNotifier.next(command);
}
// public void addSource()?
public boolean paused() { return paused; }
public boolean isBreakpoint(Location loc) {
return breakpoints.contains(loc);
}
}

View File

@@ -0,0 +1,65 @@
package me.topchetoeu.jscript.engine.debug;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.IllegalFormatException;
// We dont need no http library
public class Http {
public static void writeCode(OutputStream str, int code, String name) throws IOException {
str.write(("HTTP/1.1 " + code + " " + name + "\r\n").getBytes());
}
public static void writeHeader(OutputStream str, String name, String value) throws IOException {
str.write((name + ": " + value + "\r\n").getBytes());
}
public static void writeLastHeader(OutputStream str, String name, String value) throws IOException {
str.write((name + ": " + value + "\r\n").getBytes());
writeHeadersEnd(str);
}
public static void writeHeadersEnd(OutputStream str) throws IOException {
str.write("\n".getBytes());
}
public static void writeResponse(OutputStream str, int code, String name, String type, byte[] data) throws IOException {
writeCode(str, code, name);
writeHeader(str, "Content-Type", type);
writeLastHeader(str, "Content-Length", data.length + "");
str.write(data);
str.close();
}
public static HttpRequest readRequest(InputStream str) throws IOException {
var lines = new BufferedReader(new InputStreamReader(str));
var line = lines.readLine();
var i1 = line.indexOf(" ");
var i2 = line.lastIndexOf(" ");
var method = line.substring(0, i1).trim().toUpperCase();
var path = line.substring(i1 + 1, i2).trim();
var headers = new HashMap<String, String>();
while (!(line = lines.readLine()).isEmpty()) {
var i = line.indexOf(":");
if (i < 0) continue;
var name = line.substring(0, i).trim().toLowerCase();
var value = line.substring(i + 1).trim();
if (name.length() == 0) continue;
headers.put(name, value);
}
if (headers.containsKey("content-length")) {
try {
var i = Integer.parseInt(headers.get("content-length"));
str.skip(i);
}
catch (IllegalFormatException e) { /* ¯\_(ツ)_/¯ */ }
}
return new HttpRequest(method, path, headers);
}
}

View File

@@ -0,0 +1,6 @@
package me.topchetoeu.jscript.engine.debug;
import java.util.Map;
public record HttpRequest(String method, String path, Map<String, String> headers) {}

View File

@@ -0,0 +1,19 @@
package me.topchetoeu.jscript.engine.debug;
import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONMap;
public class V8Error {
public final String message;
public V8Error(String message) {
this.message = message;
}
@Override
public String toString() {
return JSON.stringify(new JSONMap().set("error", new JSONMap()
.set("message", message)
));
}
}

View File

@@ -0,0 +1,22 @@
package me.topchetoeu.jscript.engine.debug;
import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONMap;
public class V8Event {
public final String name;
public final JSONMap params;
public V8Event(String name, JSONMap params) {
this.name = name;
this.params = params;
}
@Override
public String toString() {
return JSON.stringify(new JSONMap()
.set("method", name)
.set("params", params)
);
}
}

View File

@@ -0,0 +1,50 @@
package me.topchetoeu.jscript.engine.debug;
import java.util.Map;
import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONElement;
import me.topchetoeu.jscript.json.JSONMap;
public class V8Message {
public final String name;
public final int id;
public final JSONMap params;
public V8Message(String name, int id, Map<String, JSONElement> params) {
this.name = name;
this.params = new JSONMap(params);
this.id = id;
}
public V8Result respond(JSONMap result) {
return new V8Result(id, result);
}
public V8Result respond() {
return new V8Result(id, new JSONMap());
}
public V8Message(JSONMap raw) {
if (!raw.isNumber("id")) throw new IllegalArgumentException("Expected number property 'id'.");
if (!raw.isString("method")) throw new IllegalArgumentException("Expected string property 'method'.");
this.name = raw.string("method");
this.id = (int)raw.number("id");
this.params = raw.contains("params") ? raw.map("params") : new JSONMap();
}
public V8Message(String raw) {
this(JSON.parse("json", raw).map());
}
public JSONMap toMap() {
var res = new JSONMap();
return res;
}
@Override
public String toString() {
return JSON.stringify(new JSONMap()
.set("method", name)
.set("params", params)
.set("id", id)
);
}
}

View File

@@ -0,0 +1,22 @@
package me.topchetoeu.jscript.engine.debug;
import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONMap;
public class V8Result {
public final int id;
public final JSONMap result;
public V8Result(int id, JSONMap result) {
this.id = id;
this.result = result;
}
@Override
public String toString() {
return JSON.stringify(new JSONMap()
.set("id", id)
.set("result", result)
);
}
}

View File

@@ -0,0 +1,185 @@
package me.topchetoeu.jscript.engine.debug;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
public class WebSocket implements AutoCloseable {
public long maxLength = 2000000;
private Socket socket;
private boolean closed = false;
private OutputStream out() throws IOException {
return socket.getOutputStream();
}
private InputStream in() throws IOException {
return socket.getInputStream();
}
private long readLen(int byteLen) throws IOException {
long res = 0;
if (byteLen == 126) {
res |= in().read() << 8;
res |= in().read();
return res;
}
else if (byteLen == 127) {
res |= in().read() << 56;
res |= in().read() << 48;
res |= in().read() << 40;
res |= in().read() << 32;
res |= in().read() << 24;
res |= in().read() << 16;
res |= in().read() << 8;
res |= in().read();
return res;
}
else return byteLen;
}
private byte[] readMask(boolean has) throws IOException {
if (has) {
return new byte[] {
(byte)in().read(),
(byte)in().read(),
(byte)in().read(),
(byte)in().read()
};
}
else return new byte[4];
}
private void writeLength(long len) throws IOException {
if (len < 126) {
out().write((int)len);
}
else if (len < 0xFFFF) {
out().write(126);
out().write((int)(len >> 8) & 0xFF);
out().write((int)len & 0xFF);
}
else {
out().write(127);
out().write((int)(len >> 56) & 0xFF);
out().write((int)(len >> 48) & 0xFF);
out().write((int)(len >> 40) & 0xFF);
out().write((int)(len >> 32) & 0xFF);
out().write((int)(len >> 24) & 0xFF);
out().write((int)(len >> 16) & 0xFF);
out().write((int)(len >> 8) & 0xFF);
out().write((int)len & 0xFF);
}
}
private synchronized void write(int type, byte[] data) throws IOException {
out().write(type | 0x80);
writeLength(data.length);
for (int i = 0; i < data.length; i++) {
out().write(data[i]);
}
}
public void send(String data) throws IOException {
if (closed) throw new IllegalStateException("Object is closed.");
write(1, data.getBytes());
}
public void send(byte[] data) throws IOException {
if (closed) throw new IllegalStateException("Object is closed.");
write(2, data);
}
public void send(WebSocketMessage msg) throws IOException {
if (msg.type == Type.Binary) send(msg.binaryData());
else send(msg.textData());
}
public void send(Object data) throws IOException {
if (closed) throw new IllegalStateException("Object is closed.");
write(1, data.toString().getBytes());
}
public void close(String reason) {
if (socket != null) {
try { write(8, reason.getBytes()); } catch (IOException e) { /* ¯\_(ツ)_/¯ */ }
try { socket.close(); } catch (IOException e) { e.printStackTrace(); }
}
socket = null;
closed = true;
}
public void close() {
close("");
}
private WebSocketMessage fail(String reason) {
System.out.println("WebSocket Error: " + reason);
close(reason);
return null;
}
private byte[] readData() throws IOException {
var maskLen = in().read();
var hasMask = (maskLen & 0x80) != 0;
var len = (int)readLen(maskLen & 0x7F);
var mask = readMask(hasMask);
if (len > maxLength) fail("WebSocket Error: client exceeded configured max message size");
else {
var buff = new byte[len];
if (in().read(buff) < len) fail("WebSocket Error: payload too short");
else {
for (int i = 0; i < len; i++) {
buff[i] ^= mask[(int)(i % 4)];
}
return buff;
}
}
return null;
}
public WebSocketMessage receive() throws InterruptedException {
try {
var data = new ByteArrayOutputStream();
var type = 0;
while (socket != null && !closed) {
var finId = in().read();
var fin = (finId & 0x80) != 0;
int id = finId & 0x0F;
if (id == 0x8) { close(); return null; }
if (id >= 0x8) {
if (!fin) return fail("WebSocket Error: client-sent control frame was fragmented");
if (id == 0x9) write(0xA, data.toByteArray());
continue;
}
if (type == 0) type = id;
if (type == 0) return fail("WebSocket Error: client used opcode 0x00 for first fragment");
var buff = readData();
if (buff == null) break;
if (data.size() + buff.length > maxLength) return fail("WebSocket Error: client exceeded configured max message size");
data.write(buff);
if (!fin) continue;
var raw = data.toByteArray();
if (type == 1) return new WebSocketMessage(new String(raw));
else return new WebSocketMessage(raw);
}
}
catch (IOException e) { close(); }
return null;
}
public WebSocket(Socket socket) {
this.socket = socket;
}
}

View File

@@ -0,0 +1,29 @@
package me.topchetoeu.jscript.engine.debug;
public class WebSocketMessage {
public static enum Type {
Text,
Binary,
}
public final Type type;
private final Object data;
public final String textData() {
if (type != Type.Text) throw new IllegalStateException("Message is not text.");
return (String)data;
}
public final byte[] binaryData() {
if (type != Type.Binary) throw new IllegalStateException("Message is not binary.");
return (byte[])data;
}
public WebSocketMessage(String data) {
this.type = Type.Text;
this.data = data;
}
public WebSocketMessage(byte[] data) {
this.type = Type.Binary;
this.data = data;
}
}

View File

@@ -0,0 +1,29 @@
package me.topchetoeu.jscript.engine.debug.handlers;
import java.io.IOException;
import me.topchetoeu.jscript.engine.DebugCommand;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.debug.V8Error;
import me.topchetoeu.jscript.engine.debug.V8Message;
import me.topchetoeu.jscript.engine.debug.WebSocket;
import me.topchetoeu.jscript.json.JSONMap;
public class DebuggerHandles {
public static void enable(V8Message msg, Engine engine, WebSocket ws) throws IOException {
if (engine.debugState == null) ws.send(new V8Error("Debugging is disabled for this engine."));
else ws.send(msg.respond(new JSONMap().set("debuggerId", 1)));
}
public static void disable(V8Message msg, Engine engine, WebSocket ws) throws IOException {
if (engine.debugState == null) ws.send(msg.respond());
else ws.send(new V8Error("Debugger may not be disabled."));
}
public static void stepInto(V8Message msg, Engine engine, WebSocket ws) throws IOException {
if (engine.debugState == null) ws.send(new V8Error("Debugging is disabled for this engine."));
else if (!engine.debugState.paused()) ws.send(new V8Error("Debugger is not paused."));
else {
engine.debugState.resume(DebugCommand.STEP_INTO);
ws.send(msg.respond());
}
}
}

View File

@@ -0,0 +1,177 @@
package me.topchetoeu.jscript.engine.frame;
import java.util.ArrayList;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.BreakpointData;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.DebugCommand;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.CallContext.DataKey;
import me.topchetoeu.jscript.engine.scope.LocalScope;
import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.CodeFunction;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.exceptions.EngineException;
public class CodeFrame {
public static final DataKey<Integer> STACK_N_KEY = new DataKey<>();
public static final DataKey<Integer> MAX_STACK_KEY = new DataKey<>();
public static final DataKey<Boolean> STOP_AT_START_KEY = new DataKey<>();
public static final DataKey<Boolean> STEPPING_TROUGH_KEY = new DataKey<>();
public final LocalScope scope;
public final Object thisArg;
public final Object[] args;
public final List<Object> stack = new ArrayList<>();
public final CodeFunction function;
public int codePtr = 0;
private DebugCommand debugCmd = null;
private Location prevLoc = null;
public Object peek() {
return peek(0);
}
public Object peek(int offset) {
if (stack.size() <= offset) return null;
else return stack.get(stack.size() - 1 - offset);
}
public Object pop() {
if (stack.size() == 0) return null;
else return stack.remove(stack.size() - 1);
}
public void push(Object val) {
stack.add(stack.size(), Values.normalize(val));
}
private void cleanup(CallContext ctx) {
stack.clear();
codePtr = 0;
debugCmd = null;
var debugState = ctx.getData(Engine.DEBUG_STATE_KEY);
if (debugState != null) debugState.popFrame();
ctx.changeData(STACK_N_KEY, -1);
}
public Object next(CallContext ctx) throws InterruptedException {
var debugState = ctx.getData(Engine.DEBUG_STATE_KEY);
if (debugCmd == null) {
if (ctx.getData(STACK_N_KEY, 0) >= ctx.addData(MAX_STACK_KEY, 1000))
throw EngineException.ofRange("Stack overflow!");
ctx.changeData(STACK_N_KEY);
if (ctx.getData(STOP_AT_START_KEY, false)) debugCmd = DebugCommand.STEP_OVER;
else debugCmd = DebugCommand.NORMAL;
if (debugState != null) debugState.pushFrame(this);
}
if (Thread.currentThread().isInterrupted()) throw new InterruptedException();
var instr = function.body[codePtr];
var loc = instr.location;
if (loc != null) prevLoc = loc;
if (debugState != null && loc != null) {
if (
instr.type == Type.NOP && instr.match("debug") || debugState.breakpoints.contains(loc) || (
ctx.getData(STEPPING_TROUGH_KEY, false) &&
(debugCmd == DebugCommand.STEP_INTO || debugCmd == DebugCommand.STEP_OVER)
)
) {
ctx.setData(STEPPING_TROUGH_KEY, true);
debugState.breakpointNotifier.next(new BreakpointData(loc, ctx));
debugCmd = debugState.commandNotifier.toAwaitable().await();
if (debugCmd == DebugCommand.NORMAL) ctx.setData(STEPPING_TROUGH_KEY, false);
}
}
try {
var res = Runners.exec(debugCmd, instr, this, ctx);
if (res != Runners.NO_RETURN) cleanup(ctx);
return res;
}
catch (EngineException e) {
cleanup(ctx);
throw e.add(function.name, prevLoc);
}
catch (RuntimeException e) {
cleanup(ctx);
throw e;
}
}
public Object run(CallContext ctx) throws InterruptedException {
var debugState = ctx.getData(Engine.DEBUG_STATE_KEY);
DebugCommand command = ctx.getData(STOP_AT_START_KEY, false) ? DebugCommand.STEP_OVER : DebugCommand.NORMAL;
if (ctx.getData(STACK_N_KEY, 0) >= ctx.addData(MAX_STACK_KEY, 200)) throw EngineException.ofRange("Stack overflow!");
ctx.changeData(STACK_N_KEY);
if (debugState != null) debugState.pushFrame(this);
Location loc = null;
try {
while (codePtr >= 0 && codePtr < function.body.length) {
var _loc = function.body[codePtr].location;
if (_loc != null) loc = _loc;
if (Thread.currentThread().isInterrupted()) throw new InterruptedException();
var instr = function.body[codePtr];
if (debugState != null && loc != null) {
if (
instr.type == Type.NOP && instr.match("debug") ||
(
(command == DebugCommand.STEP_INTO || command == DebugCommand.STEP_OVER) &&
ctx.getData(STEPPING_TROUGH_KEY, false)
) ||
debugState.breakpoints.contains(loc)
) {
ctx.setData(STEPPING_TROUGH_KEY, true);
debugState.breakpointNotifier.next(new BreakpointData(loc, ctx));
command = debugState.commandNotifier.toAwaitable().await();
if (command == DebugCommand.NORMAL) ctx.setData(STEPPING_TROUGH_KEY, false);
}
}
try {
var res = Runners.exec(command, instr, this, ctx);
if (res != Runners.NO_RETURN) return res;
}
catch (EngineException e) {
throw e.add(function.name, instr.location);
}
}
return null;
}
// catch (StackOverflowError e) {
// e.printStackTrace();
// throw EngineException.ofRange("Stack overflow!").add(function.name, loc);
// }
finally {
ctx.changeData(STACK_N_KEY, -1);
}
}
public CodeFrame(Object thisArg, Object[] args, CodeFunction func) {
this.args = args.clone();
this.scope = new LocalScope(func.localsN, func.captures);
this.scope.get(0).set(null, thisArg);
var argsObj = new ArrayValue();
for (var i = 0; i < args.length; i++) {
argsObj.set(i, args[i]);
}
this.scope.get(1).value = argsObj;
this.thisArg = thisArg;
this.function = func;
}
}

View File

@@ -0,0 +1,6 @@
package me.topchetoeu.jscript.engine.frame;
public enum ConvertHint {
TOSTRING,
VALUEOF,
}

View File

@@ -0,0 +1,9 @@
package me.topchetoeu.jscript.engine.frame;
public class InstructionResult {
public final Object value;
public InstructionResult(Object value) {
this.value = value;
}
}

View File

@@ -0,0 +1,605 @@
package me.topchetoeu.jscript.engine.frame;
import java.util.Collections;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.DebugCommand;
import me.topchetoeu.jscript.engine.scope.ValueVariable;
import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.CodeFunction;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.SignalValue;
import me.topchetoeu.jscript.engine.values.Symbol;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.exceptions.EngineException;
public class Runners {
public static final Object NO_RETURN = new Object();
public static Object execReturn(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.codePtr++;
return frame.pop();
}
public static Object execSignal(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.codePtr++;
return new SignalValue(instr.get(0));
}
public static Object execThrow(Instruction instr, CodeFrame frame, CallContext ctx) {
throw new EngineException(frame.pop());
}
public static Object execThrowSyntax(Instruction instr, CodeFrame frame, CallContext ctx) {
throw EngineException.ofSyntax((String)instr.get(0));
}
private static Object call(DebugCommand state, CallContext ctx, Object func, Object thisArg, Object... args) throws InterruptedException {
ctx.setData(CodeFrame.STOP_AT_START_KEY, state == DebugCommand.STEP_INTO);
return Values.call(ctx, func, thisArg, args);
}
public static Object execCall(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
int n = instr.get(0);
var callArgs = new Object[n];
for (var i = n - 1; i >= 0; i--) callArgs[i] = frame.pop();
var func = frame.pop();
var thisArg = frame.pop();
frame.push(call(state, ctx, func, thisArg, callArgs));
frame.codePtr++;
return NO_RETURN;
}
public static Object execCallNew(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
int n = instr.get(0);
var callArgs = new Object[n];
for (var i = n - 1; i >= 0; i--) callArgs[i] = frame.pop();
var funcObj = frame.pop();
if (Values.isFunction(funcObj) && Values.function(funcObj).special) {
frame.push(call(state, ctx, funcObj, null, callArgs));
}
else {
var proto = Values.getMember(ctx, funcObj, "prototype");
var obj = new ObjectValue();
obj.setPrototype(ctx, proto);
call(state, ctx, funcObj, obj, callArgs);
frame.push(obj);
}
frame.codePtr++;
return NO_RETURN;
}
public static Object execMakeVar(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var name = (String)instr.get(0);
frame.function.globals.define(name);
frame.codePtr++;
return NO_RETURN;
}
public static Object execDefProp(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var setter = frame.pop();
var getter = frame.pop();
var name = frame.pop();
var obj = frame.pop();
if (getter != null && !Values.isFunction(getter)) throw EngineException.ofType("Getter must be a function or undefined.");
if (setter != null && !Values.isFunction(setter)) throw EngineException.ofType("Setter must be a function or undefined.");
if (!Values.isObject(obj)) throw EngineException.ofType("Property apply target must be an object.");
Values.object(obj).defineProperty(name, Values.function(getter), Values.function(setter), false, false);
frame.push(obj);
frame.codePtr++;
return NO_RETURN;
}
public static Object execInstanceof(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var type = frame.pop();
var obj = frame.pop();
if (!Values.isPrimitive(type)) {
var proto = Values.getMember(ctx, type, "prototype");
frame.push(Values.isInstanceOf(ctx, obj, proto));
}
else {
frame.push(false);
}
frame.codePtr++;
return NO_RETURN;
}
public static Object execKeys(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var val = frame.pop();
var arr = new ObjectValue();
var i = 0;
var members = Values.getMembers(ctx, val, false, false);
Collections.reverse(members);
for (var el : members) {
if (el instanceof Symbol) continue;
arr.defineProperty(i++, el);
}
arr.defineProperty("length", i);
frame.push(arr);
frame.codePtr++;
return NO_RETURN;
}
public static Object execTry(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var finallyFunc = (boolean)instr.get(1) ? frame.pop() : null;
var catchFunc = (boolean)instr.get(0) ? frame.pop() : null;
var func = frame.pop();
if (
!Values.isFunction(func) ||
catchFunc != null && !Values.isFunction(catchFunc) ||
finallyFunc != null && !Values.isFunction(finallyFunc)
) throw EngineException.ofType("TRY instruction can be applied only upon functions.");
Object res = new SignalValue("no_return");
EngineException exception = null;
Values.function(func).name = frame.function.name + "::try";
if (catchFunc != null) Values.function(catchFunc).name = frame.function.name + "::catch";
if (finallyFunc != null) Values.function(finallyFunc).name = frame.function.name + "::finally";
try {
ctx.setData(CodeFrame.STOP_AT_START_KEY, state != DebugCommand.NORMAL);
res = Values.call(ctx, func, frame.thisArg);
}
catch (EngineException e) {
exception = e.setCause(exception);
}
if (exception != null && catchFunc != null) {
var exc = exception;
exception = null;
try {
ctx.setData(CodeFrame.STOP_AT_START_KEY, state != DebugCommand.NORMAL);
var _res = Values.call(ctx, catchFunc, frame.thisArg, exc);
if (!SignalValue.isSignal(_res, "no_return")) res = _res;
}
catch (EngineException e) {
exception = e.setCause(exc);
}
}
if (finallyFunc != null) {
try {
ctx.setData(CodeFrame.STOP_AT_START_KEY, state != DebugCommand.NORMAL);
var _res = Values.call(ctx, finallyFunc, frame.thisArg);
if (!SignalValue.isSignal(_res, "no_return")) {
res = _res;
exception = null;
}
}
catch (EngineException e) {
exception = e.setCause(exception);
}
}
if (exception != null) throw exception;
if (SignalValue.isSignal(res, "no_return")) {
frame.codePtr++;
return NO_RETURN;
}
else if (SignalValue.isSignal(res, "jmp_*")) {
frame.codePtr += Integer.parseInt(((SignalValue)res).data.substring(4));
return NO_RETURN;
}
else return res;
}
public static Object execDup(Instruction instr, CodeFrame frame, CallContext ctx) {
var val = frame.peek(instr.get(1));
for (int i = 0; i < (int)instr.get(0); i++) {
frame.push(val);
}
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadUndefined(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.push(null);
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadValue(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.push(instr.get(0));
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadVar(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var i = instr.get(0);
if (i instanceof String) frame.push(frame.function.globals.get(ctx, (String)i));
else frame.push(frame.scope.get((int)i).get(ctx));
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadObj(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.push(new ObjectValue());
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadGlob(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.push(frame.function.globals.obj);
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadArr(Instruction instr, CodeFrame frame, CallContext ctx) {
var res = new ArrayValue();
res.setSize(instr.get(0));
frame.push(res);
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadFunc(Instruction instr, CodeFrame frame, CallContext ctx) {
int n = (Integer)instr.get(0);
int localsN = (Integer)instr.get(1);
int len = (Integer)instr.get(2);
var captures = new ValueVariable[instr.params.length - 3];
for (var i = 3; i < instr.params.length; i++) {
captures[i - 3] = frame.scope.get(instr.get(i));
}
var start = frame.codePtr + 1;
var end = start + n - 1;
var body = new Instruction[end - start];
System.arraycopy(frame.function.body, start, body, 0, end - start);
var func = new CodeFunction("", localsN, len, frame.function.globals, captures, body);
frame.push(func);
frame.codePtr += n;
return NO_RETURN;
}
public static Object execLoadMember(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var key = frame.pop();
var obj = frame.pop();
try {
ctx.setData(CodeFrame.STOP_AT_START_KEY, state == DebugCommand.STEP_INTO);
frame.push(Values.getMember(ctx, obj, key));
}
catch (IllegalArgumentException e) {
throw EngineException.ofType(e.getMessage());
}
frame.codePtr++;
return NO_RETURN;
}
public static Object execLoadKeyMember(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.push(instr.get(0));
return execLoadMember(state, instr, frame, ctx);
}
public static Object execLoadRegEx(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.push(ctx.engine().makeRegex(instr.get(0), instr.get(1)));
frame.codePtr++;
return NO_RETURN;
}
public static Object execDiscard(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.pop();
frame.codePtr++;
return NO_RETURN;
}
public static Object execStoreMember(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var val = frame.pop();
var key = frame.pop();
var obj = frame.pop();
ctx.setData(CodeFrame.STOP_AT_START_KEY, state == DebugCommand.STEP_INTO);
if (!Values.setMember(ctx, obj, key, val)) throw EngineException.ofSyntax("Can't set member '" + key + "'.");
if ((boolean)instr.get(0)) frame.push(val);
frame.codePtr++;
return NO_RETURN;
}
public static Object execStoreVar(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var val = (boolean)instr.get(1) ? frame.peek() : frame.pop();
var i = instr.get(0);
if (i instanceof String) frame.function.globals.set(ctx, (String)i, val);
else frame.scope.get((int)i).set(ctx, val);
frame.codePtr++;
return NO_RETURN;
}
public static Object execStoreSelfFunc(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.scope.locals[(int)instr.get(0)].set(ctx, frame.function);
frame.codePtr++;
return NO_RETURN;
}
public static Object execJmp(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.codePtr += (int)instr.get(0);
return NO_RETURN;
}
public static Object execJmpIf(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.codePtr += Values.toBoolean(frame.pop()) ? (int)instr.get(0) : 1;
return NO_RETURN;
}
public static Object execJmpIfNot(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.codePtr += Values.not(frame.pop()) ? (int)instr.get(0) : 1;
return NO_RETURN;
}
public static Object execIn(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var obj = frame.pop();
var index = frame.pop();
frame.push(Values.hasMember(ctx, obj, index, false));
frame.codePtr++;
return NO_RETURN;
}
public static Object execTypeof(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
String name = instr.get(0);
Object obj;
if (name != null) {
if (frame.function.globals.has(ctx, name)) {
obj = frame.function.globals.get(ctx, name);
}
else obj = null;
}
else obj = frame.pop();
frame.push(Values.type(obj));
frame.codePtr++;
return NO_RETURN;
}
public static Object execNop(Instruction instr, CodeFrame frame, CallContext ctx) {
if (instr.is(0, "dbg_names")) {
var names = new String[instr.params.length - 1];
for (var i = 0; i < instr.params.length - 1; i++) {
if (!(instr.params[i + 1] instanceof String)) throw EngineException.ofSyntax("NOP dbg_names instruction must specify only string parameters.");
names[i] = (String)instr.params[i + 1];
}
frame.scope.setNames(names);
}
frame.codePtr++;
return NO_RETURN;
}
public static Object execDelete(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
var key = frame.pop();
var val = frame.pop();
if (!Values.deleteMember(ctx, val, key)) throw EngineException.ofSyntax("Can't delete member '" + key + "'.");
frame.push(true);
frame.codePtr++;
return NO_RETURN;
}
public static Object execAdd(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.add(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execSubtract(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.subtract(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execMultiply(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.multiply(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execDivide(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.divide(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execModulo(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.modulo(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execAnd(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.and(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execOr(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.or(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execXor(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.xor(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execLeftShift(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.shiftLeft(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execRightShift(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.shiftRight(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execUnsignedRightShift(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.unsignedShiftRight(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execNot(Instruction instr, CodeFrame frame, CallContext ctx) {
frame.push(Values.not(frame.pop()));
frame.codePtr++;
return NO_RETURN;
}
public static Object execNeg(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.push(Values.negative(ctx, frame.pop()));
frame.codePtr++;
return NO_RETURN;
}
public static Object execPos(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.push(Values.toNumber(ctx, frame.pop()));
frame.codePtr++;
return NO_RETURN;
}
public static Object execInverse(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
frame.push(Values.bitwiseNot(ctx, frame.pop()));
frame.codePtr++;
return NO_RETURN;
}
public static Object execGreaterThan(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.compare(ctx, a, b) > 0);
frame.codePtr++;
return NO_RETURN;
}
public static Object execLessThan(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.compare(ctx, a, b) < 0);
frame.codePtr++;
return NO_RETURN;
}
public static Object execGreaterThanEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.compare(ctx, a, b) >= 0);
frame.codePtr++;
return NO_RETURN;
}
public static Object execLessThanEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.compare(ctx, a, b) <= 0);
frame.codePtr++;
return NO_RETURN;
}
public static Object execLooseEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.looseEqual(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execLooseNotEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(!Values.looseEqual(ctx, a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(Values.strictEquals(a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object execNotEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
Object b = frame.pop(), a = frame.pop();
frame.push(!Values.strictEquals(a, b));
frame.codePtr++;
return NO_RETURN;
}
public static Object exec(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException {
switch (instr.type) {
case NOP: return execNop(instr, frame, ctx);
case RETURN: return execReturn(instr, frame, ctx);
case SIGNAL: return execSignal(instr, frame, ctx);
case THROW: return execThrow(instr, frame, ctx);
case THROW_SYNTAX: return execThrowSyntax(instr, frame, ctx);
case CALL: return execCall(state, instr, frame, ctx);
case CALL_NEW: return execCallNew(state, instr, frame, ctx);
case TRY: return execTry(state, instr, frame, ctx);
case DUP: return execDup(instr, frame, ctx);
case LOAD_VALUE: return execLoadValue(instr, frame, ctx);
case LOAD_VAR: return execLoadVar(instr, frame, ctx);
case LOAD_OBJ: return execLoadObj(instr, frame, ctx);
case LOAD_ARR: return execLoadArr(instr, frame, ctx);
case LOAD_FUNC: return execLoadFunc(instr, frame, ctx);
case LOAD_MEMBER: return execLoadMember(state, instr, frame, ctx);
case LOAD_VAL_MEMBER: return execLoadKeyMember(state, instr, frame, ctx);
case LOAD_REGEX: return execLoadRegEx(instr, frame, ctx);
case LOAD_GLOB: return execLoadGlob(instr, frame, ctx);
case DISCARD: return execDiscard(instr, frame, ctx);
case STORE_MEMBER: return execStoreMember(state, instr, frame, ctx);
case STORE_VAR: return execStoreVar(instr, frame, ctx);
case STORE_SELF_FUNC: return execStoreSelfFunc(instr, frame, ctx);
case MAKE_VAR: return execMakeVar(instr, frame, ctx);
case IN: return execIn(instr, frame, ctx);
case KEYS: return execKeys(instr, frame, ctx);
case DEF_PROP: return execDefProp(instr, frame, ctx);
case TYPEOF: return execTypeof(instr, frame, ctx);
case DELETE: return execDelete(instr, frame, ctx);
case INSTANCEOF: return execInstanceof(instr, frame, ctx);
case JMP: return execJmp(instr, frame, ctx);
case JMP_IF: return execJmpIf(instr, frame, ctx);
case JMP_IFN: return execJmpIfNot(instr, frame, ctx);
case ADD: return execAdd(instr, frame, ctx);
case SUBTRACT: return execSubtract(instr, frame, ctx);
case MULTIPLY: return execMultiply(instr, frame, ctx);
case DIVIDE: return execDivide(instr, frame, ctx);
case MODULO: return execModulo(instr, frame, ctx);
case AND: return execAnd(instr, frame, ctx);
case OR: return execOr(instr, frame, ctx);
case XOR: return execXor(instr, frame, ctx);
case SHIFT_LEFT: return execLeftShift(instr, frame, ctx);
case SHIFT_RIGHT: return execRightShift(instr, frame, ctx);
case USHIFT_RIGHT: return execUnsignedRightShift(instr, frame, ctx);
case NOT: return execNot(instr, frame, ctx);
case NEG: return execNeg(instr, frame, ctx);
case POS: return execPos(instr, frame, ctx);
case INVERSE: return execInverse(instr, frame, ctx);
case GREATER: return execGreaterThan(instr, frame, ctx);
case GREATER_EQUALS: return execGreaterThanEquals(instr, frame, ctx);
case LESS: return execLessThan(instr, frame, ctx);
case LESS_EQUALS: return execLessThanEquals(instr, frame, ctx);
case LOOSE_EQUALS: return execLooseEquals(instr, frame, ctx);
case LOOSE_NOT_EQUALS: return execLooseNotEquals(instr, frame, ctx);
case EQUALS: return execEquals(instr, frame, ctx);
case NOT_EQUALS: return execNotEquals(instr, frame, ctx);
default: throw EngineException.ofSyntax("Invalid instruction " + instr.type.name() + ".");
}
}
}

View File

@@ -0,0 +1,50 @@
package me.topchetoeu.jscript.engine.modules;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import me.topchetoeu.jscript.polyfills.PolyfillEngine;
public class FileModuleProvider implements ModuleProvider {
public File root;
public final boolean allowOutside;
private boolean checkInside(Path modFile) {
return modFile.toAbsolutePath().startsWith(root.toPath().toAbsolutePath());
}
@Override
public Module getModule(File cwd, String name) {
var realName = getRealName(cwd, name);
if (realName == null) return null;
var path = Path.of(realName + ".js").normalize();
try {
var res = PolyfillEngine.streamToString(new FileInputStream(path.toFile()));
return new Module(realName, path.toString(), res);
}
catch (IOException e) {
return null;
}
}
@Override
public String getRealName(File cwd, String name) {
var path = Path.of(".", Path.of(cwd.toString(), name).normalize().toString());
var fileName = path.getFileName().toString();
if (fileName == null) return null;
if (!fileName.equals("index") && path.toFile().isDirectory()) return getRealName(cwd, name + "/index");
path = Path.of(path.toString() + ".js");
if (!allowOutside && !checkInside(path)) return null;
if (!path.toFile().isFile() || !path.toFile().canRead()) return null;
var res = path.toString().replace('\\', '/');
var i = res.lastIndexOf('.');
return res.substring(0, i);
}
public FileModuleProvider(File root, boolean allowOutside) {
this.root = root.toPath().normalize().toFile();
this.allowOutside = allowOutside;
}
}

View File

@@ -0,0 +1,57 @@
package me.topchetoeu.jscript.engine.modules;
import java.io.File;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.CallContext.DataKey;
import me.topchetoeu.jscript.engine.scope.Variable;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.interop.NativeGetter;
import me.topchetoeu.jscript.interop.NativeSetter;
public class Module {
public class ExportsVariable implements Variable {
@Override
public boolean readonly() { return false; }
@Override
public Object get(CallContext ctx) { return exports; }
@Override
public void set(CallContext ctx, Object val) { exports = val; }
}
public static DataKey<Module> KEY = new DataKey<>();
public final String filename;
public final String source;
public final String name;
private Object exports = new ObjectValue();
private boolean executing = false;
@NativeGetter("name")
public String name() { return name; }
@NativeGetter("exports")
public Object exports() { return exports; }
@NativeSetter("exports")
public void setExports(Object val) { exports = val; }
public void execute(CallContext ctx) throws InterruptedException {
if (executing) return;
executing = true;
var scope = ctx.engine().scope().globalChild();
scope.define("module", true, this);
scope.define("exports", new ExportsVariable());
var parent = new File(filename).getParentFile();
if (parent == null) parent = new File(".");
ctx.engine().compile(scope, filename, source).call(ctx.copy().setData(KEY, this), null);
executing = false;
}
public Module(String name, String filename, String source) {
this.name = name;
this.filename = filename;
this.source = source;
}
}

View File

@@ -0,0 +1,80 @@
package me.topchetoeu.jscript.engine.modules;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import me.topchetoeu.jscript.engine.CallContext;
public class ModuleManager {
private final List<ModuleProvider> providers = new ArrayList<>();
private final HashMap<String, Module> cache = new HashMap<>();
public final FileModuleProvider files;
public void addProvider(ModuleProvider provider) {
this.providers.add(provider);
}
public boolean isCached(File cwd, String name) {
name = name.replace("\\", "/");
// Absolute paths are forbidden
if (name.startsWith("/")) return false;
// Look for files if we have a relative path
if (name.startsWith("../") || name.startsWith("./")) {
var realName = files.getRealName(cwd, name);
if (cache.containsKey(realName)) return true;
else return false;
}
for (var provider : providers) {
var realName = provider.getRealName(cwd, name);
if (realName == null) continue;
if (cache.containsKey(realName)) return true;
}
return false;
}
public Module tryLoad(CallContext ctx, String name) throws InterruptedException {
name = name.replace('\\', '/');
var pcwd = Path.of(".");
if (ctx.hasData(Module.KEY)) {
pcwd = Path.of(((Module)ctx.getData(Module.KEY)).filename).getParent();
if (pcwd == null) pcwd = Path.of(".");
}
var cwd = pcwd.toFile();
if (name.startsWith("/")) return null;
if (name.startsWith("../") || name.startsWith("./")) {
var realName = files.getRealName(cwd, name);
if (realName == null) return null;
if (cache.containsKey(realName)) return cache.get(realName);
var mod = files.getModule(cwd, name);
// cache.put(mod.name(), mod);
mod.execute(ctx);
return mod;
}
for (var provider : providers) {
var realName = provider.getRealName(cwd, name);
if (realName == null) continue;
if (cache.containsKey(realName)) return cache.get(realName);
var mod = provider.getModule(cwd, name);
// cache.put(mod.name(), mod);
mod.execute(ctx);
return mod;
}
return null;
}
public ModuleManager(File root) {
files = new FileModuleProvider(root, false);
}
}

View File

@@ -0,0 +1,9 @@
package me.topchetoeu.jscript.engine.modules;
import java.io.File;
public interface ModuleProvider {
Module getModule(File cwd, String name);
String getRealName(File cwd, String name);
default boolean hasModule(File cwd, String name) { return getRealName(cwd, name) != null; }
}

View File

@@ -0,0 +1,83 @@
package me.topchetoeu.jscript.engine.scope;
import java.util.HashSet;
import java.util.Set;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.NativeFunction;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.exceptions.EngineException;
public class GlobalScope implements ScopeRecord {
public final ObjectValue obj;
public boolean has(CallContext ctx, String name) throws InterruptedException {
return obj.hasMember(ctx, name, false);
}
public Object getKey(String name) {
return name;
}
public GlobalScope globalChild() {
return new GlobalScope(this);
}
public LocalScopeRecord child() {
return new LocalScopeRecord(this);
}
public Object define(String name) {
try {
if (obj.hasMember(null, name, true)) return name;
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return name;
}
obj.defineProperty(name, null);
return name;
}
public void define(String name, Variable val) {
obj.defineProperty(name,
new NativeFunction("get " + name, (ctx, th, a) -> val.get(ctx)),
new NativeFunction("set " + name, (ctx, th, args) -> { val.set(ctx, args.length > 0 ? args[0] : null); return null; }),
true, true
);
}
public void define(String name, boolean readonly, Object val) {
obj.defineProperty(name, val, readonly, true, true);
}
public void define(String... names) {
for (var n : names) define(n);
}
public void define(boolean readonly, FunctionValue val) {
define(val.name, readonly, val);
}
public Object get(CallContext ctx, String name) throws InterruptedException {
if (!obj.hasMember(ctx, name, false)) throw EngineException.ofSyntax("The variable '" + name + "' doesn't exist.");
else return obj.getMember(ctx, name);
}
public void set(CallContext ctx, String name, Object val) throws InterruptedException {
if (!obj.hasMember(ctx, name, false)) throw EngineException.ofSyntax("The variable '" + name + "' doesn't exist.");
if (!obj.setMember(ctx, name, val, false)) throw EngineException.ofSyntax("The global '" + name + "' is readonly.");
}
public Set<String> keys() {
var res = new HashSet<String>();
for (var key : keys()) {
if (key instanceof String) res.add((String)key);
}
return res;
}
public GlobalScope() {
this.obj = new ObjectValue();
}
public GlobalScope(GlobalScope parent) {
this.obj = new ObjectValue();
this.obj.setPrototype(null, parent.obj);
}
}

View File

@@ -0,0 +1,58 @@
package me.topchetoeu.jscript.engine.scope;
public class LocalScope {
private String[] names;
private LocalScope parent;
public final ValueVariable[] captures;
public final ValueVariable[] locals;
public ValueVariable get(int i) {
if (i >= 0) return locals[i];
else return captures[~i];
}
public String[] getNames() {
var res = new String[locals.length];
for (var i = 0; i < locals.length; i++) {
if (names == null || i >= names.length) res[i] = "local_" + i;
else res[i] = names[i];
}
return res;
}
public void setNames(String[] names) {
this.names = names;
}
public int size() {
return captures.length + locals.length;
}
public GlobalScope toGlobal(GlobalScope global) {
GlobalScope res;
if (parent == null) res = new GlobalScope(global);
else res = new GlobalScope(parent.toGlobal(global));
var names = getNames();
for (var i = 0; i < names.length; i++) {
res.define(names[i], locals[i]);
}
return res;
}
public LocalScope(int n, ValueVariable[] captures) {
locals = new ValueVariable[n];
this.captures = captures;
for (int i = 0; i < n; i++) {
locals[i] = new ValueVariable(false, null);
}
}
public LocalScope(int n, ValueVariable[] captures, LocalScope parent) {
this(n, captures);
this.parent = parent;
}
}

View File

@@ -0,0 +1,79 @@
package me.topchetoeu.jscript.engine.scope;
import java.util.ArrayList;
import me.topchetoeu.jscript.engine.CallContext;
public class LocalScopeRecord implements ScopeRecord {
public final LocalScopeRecord parent;
public final GlobalScope global;
private final ArrayList<String> captures = new ArrayList<>();
private final ArrayList<String> locals = new ArrayList<>();
public String[] locals() {
return locals.toArray(String[]::new);
}
public LocalScopeRecord child() {
return new LocalScopeRecord(this, global);
}
public int localsCount() {
return locals.size();
}
public int capturesCount() {
return captures.size();
}
public int[] getCaptures() {
var buff = new int[captures.size()];
var i = 0;
for (var name : captures) {
var index = parent.getKey(name);
if (index instanceof Integer) buff[i++] = (int)index;
}
var res = new int[i];
System.arraycopy(buff, 0, res, 0, i);
return res;
}
public Object getKey(String name) {
var capI = captures.indexOf(name);
var locI = locals.indexOf(name);
if (locI >= 0) return locI;
if (capI >= 0) return ~capI;
if (parent != null) {
var res = parent.getKey(name);
if (res != null && res instanceof Integer) {
captures.add(name);
return -captures.size();
}
}
return name;
}
public boolean has(CallContext ctx, String name) throws InterruptedException {
return
global.has(ctx, name) ||
locals.contains(name) ||
parent != null && parent.has(ctx, name);
}
public Object define(String name) {
if (locals.contains(name)) return locals.indexOf(name);
locals.add(name);
return locals.size() - 1;
}
public LocalScopeRecord(GlobalScope global) {
this.parent = null;
this.global = global;
}
public LocalScopeRecord(LocalScopeRecord parent, GlobalScope global) {
this.parent = parent;
this.global = global;
}
}

View File

@@ -0,0 +1,7 @@
package me.topchetoeu.jscript.engine.scope;
public interface ScopeRecord {
public Object getKey(String name);
public Object define(String name);
public LocalScopeRecord child();
}

View File

@@ -0,0 +1,28 @@
package me.topchetoeu.jscript.engine.scope;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.values.Values;
public class ValueVariable implements Variable {
public boolean readonly;
public Object value;
@Override
public boolean readonly() { return readonly; }
@Override
public Object get(CallContext ctx) {
return value;
}
@Override
public void set(CallContext ctx, Object val) {
if (readonly) return;
this.value = Values.normalize(val);
}
public ValueVariable(boolean readonly, Object val) {
this.readonly = readonly;
this.value = val;
}
}

View File

@@ -0,0 +1,9 @@
package me.topchetoeu.jscript.engine.scope;
import me.topchetoeu.jscript.engine.CallContext;
public interface Variable {
Object get(CallContext ctx) throws InterruptedException;
default boolean readonly() { return true; }
default void set(CallContext ctx, Object val) throws InterruptedException { }
}

View File

@@ -0,0 +1,160 @@
package me.topchetoeu.jscript.engine.values;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import me.topchetoeu.jscript.engine.CallContext;
public class ArrayValue extends ObjectValue {
private static final Object EMPTY = new Object();
private final ArrayList<Object> values = new ArrayList<>();
public int size() { return values.size(); }
public boolean setSize(int val) {
if (val < 0) return false;
while (size() > val) {
values.remove(values.size() - 1);
}
while (size() < val) {
values.add(EMPTY);
}
return true;
}
public Object get(int i) {
if (i < 0 || i >= values.size()) return null;
var res = values.get(i);
if (res == EMPTY) return null;
else return res;
}
public void set(int i, Object val) {
if (i < 0) return;
while (values.size() <= i) {
values.add(EMPTY);
}
values.set(i, Values.normalize(val));
}
public boolean has(int i) {
return i >= 0 && i < values.size() && values.get(i) != EMPTY;
}
public void remove(int i) {
if (i < 0 || i >= values.size()) return;
values.set(i, EMPTY);
}
public void shrink(int n) {
if (n > values.size()) values.clear();
else {
for (int i = 0; i < n && values.size() > 0; i++) {
values.remove(values.size() - 1);
}
}
}
public void sort(Comparator<Object> comparator) {
values.sort((a, b) -> {
var _a = 0;
var _b = 0;
if (a == null) _a = 1;
if (a == EMPTY) _a = 2;
if (b == null) _b = 1;
if (b == EMPTY) _b = 2;
if (Integer.compare(_a, _b) != 0) return Integer.compare(_a, _b);
return comparator.compare(a, b);
});
}
public Object[] toArray() {
Object[] res = new Object[values.size()];
for (var i = 0; i < values.size(); i++) {
if (values.get(i) == EMPTY) res[i] = null;
else res[i] = values.get(i);
}
return res;
}
@Override
protected Object getField(CallContext ctx, Object key) throws InterruptedException {
if (key.equals("length")) return values.size();
if (key instanceof Number) {
var i = ((Number)key).doubleValue();
if (i >= 0 && i - Math.floor(i) == 0) {
return get((int)i);
}
}
return super.getField(ctx, key);
}
@Override
protected boolean setField(CallContext ctx, Object key, Object val) throws InterruptedException {
if (key.equals("length")) {
return setSize((int)Values.toNumber(ctx, val));
}
if (key instanceof Number) {
var i = Values.number(key);
if (i >= 0 && i - Math.floor(i) == 0) {
set((int)i, val);
return true;
}
}
return super.setField(ctx, key, val);
}
@Override
protected boolean hasField(CallContext ctx, Object key) throws InterruptedException {
if (key.equals("length")) return true;
if (key instanceof Number) {
var i = Values.number(key);
if (i >= 0 && i - Math.floor(i) == 0) {
return has((int)i);
}
}
return super.hasField(ctx, key);
}
@Override
protected void deleteField(CallContext ctx, Object key) throws InterruptedException {
if (key instanceof Number) {
var i = Values.number(key);
if (i >= 0 && i - Math.floor(i) == 0) {
remove((int)i);
return;
}
}
super.deleteField(ctx, key);
}
@Override
public List<Object> keys(boolean includeNonEnumerable) {
var res = super.keys(includeNonEnumerable);
for (var i = 0; i < size(); i++) {
if (has(i)) res.add(i);
}
if (includeNonEnumerable) res.add("length");
return res;
}
public ArrayValue() {
super(PlaceholderProto.ARRAY);
nonEnumerableSet.add("length");
nonConfigurableSet.add("length");
}
public ArrayValue(Object ...values) {
this();
for (var i = 0; i < values.length; i++) this.values.add(values[i]);
}
public static ArrayValue of(Collection<Object> values) {
return new ArrayValue(values.toArray(Object[]::new));
}
}

View File

@@ -0,0 +1,50 @@
package me.topchetoeu.jscript.engine.values;
import java.util.LinkedHashMap;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.frame.CodeFrame;
import me.topchetoeu.jscript.engine.scope.GlobalScope;
import me.topchetoeu.jscript.engine.scope.ValueVariable;
public class CodeFunction extends FunctionValue {
public final int localsN;
public final int length;
public final Instruction[] body;
public final LinkedHashMap<Location, Integer> breakableLocToIndex = new LinkedHashMap<>();
public final LinkedHashMap<Integer, Location> breakableIndexToLoc = new LinkedHashMap<>();
public final ValueVariable[] captures;
public final GlobalScope globals;
public Location loc() {
for (var instr : body) {
if (instr.location != null) return instr.location;
}
return null;
}
@Override
public Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException {
return new CodeFrame(thisArg, args, this).run(ctx);
}
public CodeFunction(String name, int localsN, int length, GlobalScope globals, ValueVariable[] captures, Instruction[] body) {
super(name, length);
this.captures = captures;
this.globals = globals;
this.localsN = localsN;
this.length = length;
this.body = body;
for (var i = 0; i < body.length; i++) {
if (body[i].type == Type.LOAD_FUNC) i += (int)body[i].get(0);
if (body[i].debugged && body[i].location != null) {
breakableLocToIndex.put(body[i].location, i);
breakableIndexToLoc.put(i, body[i].location);
}
}
}
}

View File

@@ -0,0 +1,70 @@
package me.topchetoeu.jscript.engine.values;
import java.util.List;
import me.topchetoeu.jscript.engine.CallContext;
public abstract class FunctionValue extends ObjectValue {
public String name = "";
public boolean special = false;
public int length;
@Override
public String toString() {
return "function(...) { ... }";
}
public abstract Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException;
public Object call(CallContext ctx) throws InterruptedException {
return call(ctx, null);
}
@Override
protected Object getField(CallContext ctx, Object key) throws InterruptedException {
if (key.equals("name")) return name;
if (key.equals("length")) return length;
return super.getField(ctx, key);
}
@Override
protected boolean setField(CallContext ctx, Object key, Object val) throws InterruptedException {
if (key.equals("name")) name = Values.toString(ctx, val);
else if (key.equals("length")) length = (int)Values.toNumber(ctx, val);
else return super.setField(ctx, key, val);
return true;
}
@Override
protected boolean hasField(CallContext ctx, Object key) throws InterruptedException {
if (key.equals("name")) return true;
if (key.equals("length")) return true;
return super.hasField(ctx, key);
}
@Override
public List<Object> keys(boolean includeNonEnumerable) {
var res = super.keys(includeNonEnumerable);
if (includeNonEnumerable) {
res.add("name");
res.add("length");
}
return res;
}
public FunctionValue(String name, int length) {
super(PlaceholderProto.FUNCTION);
if (name == null) name = "";
this.length = length;
this.name = name;
nonConfigurableSet.add("name");
nonEnumerableSet.add("name");
nonWritableSet.add("length");
nonConfigurableSet.add("length");
nonEnumerableSet.add("length");
var proto = new ObjectValue();
proto.defineProperty("constructor", this, true, false, false);
this.defineProperty("prototype", proto, true, false, false);
}
}

View File

@@ -0,0 +1,21 @@
package me.topchetoeu.jscript.engine.values;
import me.topchetoeu.jscript.engine.CallContext;
public class NativeFunction extends FunctionValue {
public static interface NativeFunctionRunner {
Object run(CallContext ctx, Object thisArg, Object[] values) throws InterruptedException;
}
public final NativeFunctionRunner action;
@Override
public Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException {
return action.run(ctx, thisArg, args);
}
public NativeFunction(String name, NativeFunctionRunner action) {
super(name, 0);
this.action = action;
}
}

View File

@@ -0,0 +1,19 @@
package me.topchetoeu.jscript.engine.values;
import me.topchetoeu.jscript.engine.CallContext;
public class NativeWrapper extends ObjectValue {
private static final Object NATIVE_PROTO = new Object();
public final Object wrapped;
@Override
public ObjectValue getPrototype(CallContext ctx) throws InterruptedException {
if (prototype == NATIVE_PROTO) return ctx.engine.getPrototype(wrapped.getClass());
else return super.getPrototype(ctx);
}
public NativeWrapper(Object wrapped) {
this.wrapped = wrapped;
prototype = NATIVE_PROTO;
}
}

View File

@@ -0,0 +1,331 @@
package me.topchetoeu.jscript.engine.values;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import me.topchetoeu.jscript.engine.CallContext;
public class ObjectValue {
public static enum PlaceholderProto {
NONE,
OBJECT,
ARRAY,
FUNCTION,
ERROR,
SYNTAX_ERROR,
TYPE_ERROR,
RANGE_ERROR,
}
public static enum State {
NORMAL,
NO_EXTENSIONS,
SEALED,
FROZEN,
}
public static record Property(FunctionValue getter, FunctionValue setter) {}
private static final Object OBJ_PROTO = new Object();
private static final Object ARR_PROTO = new Object();
private static final Object FUNC_PROTO = new Object();
private static final Object ERR_PROTO = new Object();
private static final Object SYNTAX_ERR_PROTO = new Object();
private static final Object TYPE_ERR_PROTO = new Object();
private static final Object RANGE_ERR_PROTO = new Object();
protected Object prototype;
public State state = State.NORMAL;
public HashMap<Object, Object> values = new HashMap<>();
public HashMap<Object, Property> properties = new HashMap<>();
public HashSet<Object> nonWritableSet = new HashSet<>();
public HashSet<Object> nonConfigurableSet = new HashSet<>();
public HashSet<Object> nonEnumerableSet = new HashSet<>();
public final boolean memberWritable(Object key) {
if (state == State.FROZEN) return false;
return !nonWritableSet.contains(key);
}
public final boolean memberConfigurable(Object key) {
if (state == State.SEALED || state == State.FROZEN) return false;
return !nonConfigurableSet.contains(key);
}
public final boolean memberEnumerable(Object key) {
return !nonEnumerableSet.contains(key);
}
public final boolean extensible() {
return state == State.NORMAL;
}
public final void preventExtensions() {
if (state == State.NORMAL) state = State.NO_EXTENSIONS;
}
public final void seal() {
if (state == State.NORMAL || state == State.NO_EXTENSIONS) state = State.SEALED;
}
public final void freeze() {
state = State.FROZEN;
}
public final boolean defineProperty(Object key, Object val, boolean writable, boolean configurable, boolean enumerable) {
key = Values.normalize(key); val = Values.normalize(val);
boolean reconfigured =
writable != memberWritable(key) ||
configurable != memberConfigurable(key) ||
enumerable != memberEnumerable(key);
if (!reconfigured) {
if (!memberWritable(key)) {
var a = values.get(key);
var b = val;
if (a == null || b == null) return a == null && b == null;
return a == b || a.equals(b);
}
values.put(key, val);
return true;
}
if (
properties.containsKey(key) &&
values.get(key) == val &&
!reconfigured
) return true;
if (!extensible() && !values.containsKey(key) && !properties.containsKey(key)) return false;
if (!memberConfigurable(key))
return false;
nonWritableSet.remove(key);
nonEnumerableSet.remove(key);
properties.remove(key);
values.remove(key);
if (!writable) nonWritableSet.add(key);
if (!configurable) nonConfigurableSet.add(key);
if (!enumerable) nonEnumerableSet.add(key);
values.put(key, val);
return true;
}
public final boolean defineProperty(Object key, Object val) {
return defineProperty(Values.normalize(key), Values.normalize(val), true, true, true);
}
public final boolean defineProperty(Object key, FunctionValue getter, FunctionValue setter, boolean configurable, boolean enumerable) {
key = Values.normalize(key);
if (
properties.containsKey(key) &&
properties.get(key).getter == getter &&
properties.get(key).setter == setter &&
!configurable == nonConfigurableSet.contains(key) &&
!enumerable == nonEnumerableSet.contains(key)
) return true;
if (!extensible() && !values.containsKey(key) && !properties.containsKey(key)) return false;
if (!memberConfigurable(key)) return false;
nonWritableSet.remove(key);
nonEnumerableSet.remove(key);
properties.remove(key);
values.remove(key);
if (!configurable) nonConfigurableSet.add(key);
if (!enumerable) nonEnumerableSet.add(key);
properties.put(key, new Property(getter, setter));
return true;
}
public ObjectValue getPrototype(CallContext ctx) throws InterruptedException {
try {
if (prototype == OBJ_PROTO) return ctx.engine().objectProto();
if (prototype == ARR_PROTO) return ctx.engine().arrayProto();
if (prototype == FUNC_PROTO) return ctx.engine().functionProto();
if (prototype == ERR_PROTO) return ctx.engine().errorProto();
if (prototype == RANGE_ERR_PROTO) return ctx.engine().rangeErrorProto();
if (prototype == SYNTAX_ERR_PROTO) return ctx.engine().syntaxErrorProto();
if (prototype == TYPE_ERR_PROTO) return ctx.engine().typeErrorProto();
}
catch (NullPointerException e) {
return null;
}
return (ObjectValue)prototype;
}
public final boolean setPrototype(CallContext ctx, Object val) {
val = Values.normalize(val);
if (!extensible()) return false;
if (val == null || val == Values.NULL) prototype = null;
else if (Values.isObject(val)) {
var obj = Values.object(val);
if (ctx != null && ctx.engine() != null) {
if (obj == ctx.engine().objectProto()) prototype = OBJ_PROTO;
else if (obj == ctx.engine().arrayProto()) prototype = ARR_PROTO;
else if (obj == ctx.engine().functionProto()) prototype = FUNC_PROTO;
else if (obj == ctx.engine().errorProto()) prototype = ERR_PROTO;
else if (obj == ctx.engine().syntaxErrorProto()) prototype = SYNTAX_ERR_PROTO;
else if (obj == ctx.engine().typeErrorProto()) prototype = TYPE_ERR_PROTO;
else if (obj == ctx.engine().rangeErrorProto()) prototype = RANGE_ERR_PROTO;
else prototype = obj;
}
else prototype = obj;
return true;
}
return false;
}
public final boolean setPrototype(PlaceholderProto val) {
if (!extensible()) return false;
switch (val) {
case OBJECT: prototype = OBJ_PROTO; break;
case FUNCTION: prototype = FUNC_PROTO; break;
case ARRAY: prototype = ARR_PROTO; break;
case ERROR: prototype = ERR_PROTO; break;
case SYNTAX_ERROR: prototype = SYNTAX_ERR_PROTO; break;
case TYPE_ERROR: prototype = TYPE_ERR_PROTO; break;
case RANGE_ERROR: prototype = RANGE_ERR_PROTO; break;
case NONE: prototype = null; break;
}
return true;
}
protected Property getProperty(CallContext ctx, Object key) throws InterruptedException {
if (properties.containsKey(key)) return properties.get(key);
var proto = getPrototype(ctx);
if (proto != null) return proto.getProperty(ctx, key);
else return null;
}
protected Object getField(CallContext ctx, Object key) throws InterruptedException {
if (values.containsKey(key)) return values.get(key);
var proto = getPrototype(ctx);
if (proto != null) return proto.getField(ctx, key);
else return null;
}
protected boolean setField(CallContext ctx, Object key, Object val) throws InterruptedException {
values.put(key, val);
return true;
}
protected void deleteField(CallContext ctx, Object key) throws InterruptedException {
values.remove(key);
}
protected boolean hasField(CallContext ctx, Object key) throws InterruptedException {
return values.containsKey(key);
}
public final Object getMember(CallContext ctx, Object key, Object thisArg) throws InterruptedException {
key = Values.normalize(key);
if (key.equals("__proto__")) {
var res = getPrototype(ctx);
return res == null ? Values.NULL : res;
}
var prop = getProperty(ctx, key);
if (prop != null) {
if (prop.getter == null) return null;
else return prop.getter.call(ctx, Values.normalize(thisArg));
}
else return getField(ctx, key);
}
public final Object getMember(CallContext ctx, Object key) throws InterruptedException {
return getMember(ctx, key, this);
}
public final boolean setMember(CallContext ctx, Object key, Object val, Object thisArg, boolean onlyProps) throws InterruptedException {
key = Values.normalize(key); val = Values.normalize(val);
var prop = getProperty(ctx, key);
if (prop != null) {
if (prop.setter == null) return false;
prop.setter.call(ctx, Values.normalize(thisArg), val);
return true;
}
else if (onlyProps) return false;
else if (!extensible() && !values.containsKey(key)) return false;
else if (key == null) {
values.put(key, val);
return true;
}
else if (key.equals("__proto__")) return setPrototype(ctx, val);
else if (nonWritableSet.contains(key)) return false;
else return setField(ctx, key, val);
}
public final boolean setMember(CallContext ctx, Object key, Object val, boolean onlyProps) throws InterruptedException {
return setMember(ctx, Values.normalize(key), Values.normalize(val), this, onlyProps);
}
public final boolean hasMember(CallContext ctx, Object key, boolean own) throws InterruptedException {
key = Values.normalize(key);
if (key != null && key.equals("__proto__")) return true;
if (hasField(ctx, key)) return true;
if (properties.containsKey(key)) return true;
if (own) return false;
return prototype != null && getPrototype(ctx).hasMember(ctx, key, own);
}
public final boolean deleteMember(CallContext ctx, Object key) throws InterruptedException {
key = Values.normalize(key);
if (!memberConfigurable(key)) return false;
properties.remove(key);
nonWritableSet.remove(key);
nonEnumerableSet.remove(key);
deleteField(ctx, key);
return true;
}
public final ObjectValue getMemberDescriptor(CallContext ctx, Object key) throws InterruptedException {
key = Values.normalize(key);
var prop = properties.get(key);
var res = new ObjectValue();
res.defineProperty("configurable", memberConfigurable(key));
res.defineProperty("enumerable", memberEnumerable(key));
if (prop != null) {
res.defineProperty("get", prop.getter);
res.defineProperty("set", prop.setter);
}
else if (hasField(ctx, key)) {
res.defineProperty("value", values.get(key));
res.defineProperty("writable", memberWritable(key));
}
else return null;
return res;
}
public List<Object> keys(boolean includeNonEnumerable) {
var res = new ArrayList<Object>();
for (var key : values.keySet()) {
if (nonEnumerableSet.contains(key) && !includeNonEnumerable) continue;
res.add(key);
}
for (var key : properties.keySet()) {
if (nonEnumerableSet.contains(key) && !includeNonEnumerable) continue;
res.add(key);
}
return res;
}
public ObjectValue(Map<?, ?> values) {
this(PlaceholderProto.OBJECT);
for (var el : values.entrySet()) {
defineProperty(el.getKey(), el.getValue());
}
}
public ObjectValue(PlaceholderProto proto) {
nonConfigurableSet.add("__proto__");
nonEnumerableSet.add("__proto__");
setPrototype(proto);
}
public ObjectValue() {
this(PlaceholderProto.OBJECT);
}
}

View File

@@ -0,0 +1,17 @@
package me.topchetoeu.jscript.engine.values;
public final class SignalValue {
public final String data;
public SignalValue(String data) {
this.data = data;
}
public static boolean isSignal(Object signal, String value) {
if (!(signal instanceof SignalValue)) return false;
var val = ((SignalValue)signal).data;
if (value.endsWith("*")) return val.startsWith(value.substring(0, value.length() - 1));
else return val.equals(value);
}
}

View File

@@ -0,0 +1,15 @@
package me.topchetoeu.jscript.engine.values;
public final class Symbol {
public final String value;
public Symbol(String value) {
this.value = value;
}
@Override
public String toString() {
if (value == null) return "Symbol";
else return "Symbol(" + value + ")";
}
}

View File

@@ -0,0 +1,608 @@
package me.topchetoeu.jscript.engine.values;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.frame.ConvertHint;
import me.topchetoeu.jscript.exceptions.EngineException;
public class Values {
public static final Object NULL = new Object();
public static boolean isObject(Object val) { return val instanceof ObjectValue; }
public static boolean isFunction(Object val) { return val instanceof FunctionValue; }
public static boolean isArray(Object val) { return val instanceof ArrayValue; }
public static boolean isWrapper(Object val) { return val instanceof NativeWrapper; }
public static boolean isWrapper(Object val, Class<?> clazz) {
if (!isWrapper(val)) return false;
var res = (NativeWrapper)val;
return res != null && clazz.isInstance(res.wrapped);
}
public static boolean isNan(Object val) { return val instanceof Number && Double.isNaN(number(val)); }
public static ObjectValue object(Object val) {
if (val instanceof ObjectValue) return (ObjectValue)val;
else return null;
}
public static ArrayValue array(Object val) {
if (val instanceof ArrayValue) return (ArrayValue)val;
else return null;
}
public static FunctionValue function(Object val) {
if (val instanceof FunctionValue) return (FunctionValue)val;
else return null;
}
public static double number(Object val) {
if (val instanceof Number) return ((Number)val).doubleValue();
else return Double.NaN;
}
@SuppressWarnings("unchecked")
public static <T> T wrapper(Object val, Class<T> clazz) {
if (!isWrapper(val)) return null;
var res = (NativeWrapper)val;
if (res != null && clazz.isInstance(res.wrapped)) return (T)res.wrapped;
else return null;
}
public static String type(Object val) {
if (val == null) return "undefined";
if (val instanceof String) return "string";
if (val instanceof Number) return "number";
if (val instanceof Boolean) return "boolean";
if (val instanceof Symbol) return "symbol";
if (val instanceof FunctionValue) return "function";
return "object";
}
private static Object tryCallConvertFunc(CallContext ctx, Object obj, String name) throws InterruptedException {
var func = getMember(ctx, obj, name);
if (func != null) {
var res = ((FunctionValue)func).call(ctx, obj);
if (isPrimitive(res)) return res;
}
throw EngineException.ofType("Value couldn't be converted to a primitive.");
}
public static boolean isPrimitive(Object obj) {
return
obj instanceof Number ||
obj instanceof String ||
obj instanceof Boolean ||
obj instanceof Symbol ||
obj instanceof SignalValue ||
obj == null ||
obj == NULL;
}
public static Object toPrimitive(CallContext ctx, Object obj, ConvertHint hint) throws InterruptedException {
obj = normalize(obj);
if (isPrimitive(obj)) return obj;
var first = hint == ConvertHint.VALUEOF ? "valueOf" : "toString";
var second = hint == ConvertHint.VALUEOF ? "toString" : "valueOf";
if (ctx != null) {
try {
return tryCallConvertFunc(ctx, obj, first);
}
catch (EngineException unused) {
return tryCallConvertFunc(ctx, obj, second);
}
}
throw EngineException.ofType("Value couldn't be converted to a primitive.");
}
public static boolean toBoolean(Object obj) {
if (obj == NULL || obj == null) return false;
if (obj instanceof Number && number(obj) == 0) return false;
if (obj instanceof String && ((String)obj).equals("")) return false;
if (obj instanceof Boolean) return (Boolean)obj;
return true;
}
public static double toNumber(CallContext ctx, Object obj) throws InterruptedException {
var val = toPrimitive(ctx, obj, ConvertHint.VALUEOF);
if (val instanceof Number) return number(obj);
if (val instanceof Boolean) return ((Boolean)obj) ? 1 : 0;
if (val instanceof String) {
try {
return Double.parseDouble((String)val);
}
catch (NumberFormatException e) { }
}
return Double.NaN;
}
public static String toString(CallContext ctx, Object obj) throws InterruptedException {
var val = toPrimitive(ctx, obj, ConvertHint.VALUEOF);
if (val == null) return "undefined";
if (val == NULL) return "null";
if (val instanceof Number) {
var d = number(obj);
if (d == Double.NEGATIVE_INFINITY) return "-Infinity";
if (d == Double.POSITIVE_INFINITY) return "Infinity";
if (Double.isNaN(d)) return "NaN";
return BigDecimal.valueOf(d).stripTrailingZeros().toPlainString();
}
if (val instanceof Boolean) return (Boolean)val ? "true" : "false";
if (val instanceof String) return (String)val;
if (val instanceof Symbol) return ((Symbol)val).toString();
if (val instanceof SignalValue) return "[signal '" + ((SignalValue)val).data + "']";
return "Unknown value";
}
public static Object add(CallContext ctx, Object a, Object b) throws InterruptedException {
if (a instanceof String || b instanceof String) return toString(ctx, a) + toString(ctx, b);
else return toNumber(ctx, a) + toNumber(ctx, b);
}
public static double subtract(CallContext ctx, Object a, Object b) throws InterruptedException {
return toNumber(ctx, a) - toNumber(ctx, b);
}
public static double multiply(CallContext ctx, Object a, Object b) throws InterruptedException {
return toNumber(ctx, a) * toNumber(ctx, b);
}
public static double divide(CallContext ctx, Object a, Object b) throws InterruptedException {
return toNumber(ctx, a) / toNumber(ctx, b);
}
public static double modulo(CallContext ctx, Object a, Object b) throws InterruptedException {
return toNumber(ctx, a) % toNumber(ctx, b);
}
public static double negative(CallContext ctx, Object obj) throws InterruptedException {
return -toNumber(ctx, obj);
}
public static int and(CallContext ctx, Object a, Object b) throws InterruptedException {
return (int)toNumber(ctx, a) & (int)toNumber(ctx, b);
}
public static int or(CallContext ctx, Object a, Object b) throws InterruptedException {
return (int)toNumber(ctx, a) | (int)toNumber(ctx, b);
}
public static int xor(CallContext ctx, Object a, Object b) throws InterruptedException {
return (int)toNumber(ctx, a) ^ (int)toNumber(ctx, b);
}
public static int bitwiseNot(CallContext ctx, Object obj) throws InterruptedException {
return ~(int)toNumber(ctx, obj);
}
public static int shiftLeft(CallContext ctx, Object a, Object b) throws InterruptedException {
return (int)toNumber(ctx, a) << (int)toNumber(ctx, b);
}
public static int shiftRight(CallContext ctx, Object a, Object b) throws InterruptedException {
return (int)toNumber(ctx, a) >> (int)toNumber(ctx, b);
}
public static long unsignedShiftRight(CallContext ctx, Object a, Object b) throws InterruptedException {
long _a = (long)toNumber(ctx, a);
long _b = (long)toNumber(ctx, b);
if (_a < 0) _a += 0x100000000l;
if (_b < 0) _b += 0x100000000l;
return _a >>> _b;
}
public static int compare(CallContext ctx, Object a, Object b) throws InterruptedException {
a = toPrimitive(ctx, a, ConvertHint.VALUEOF);
b = toPrimitive(ctx, b, ConvertHint.VALUEOF);
if (a instanceof String && b instanceof String) return ((String)a).compareTo((String)b);
else return Double.compare(toNumber(ctx, a), toNumber(ctx, b));
}
public static boolean not(Object obj) {
return !toBoolean(obj);
}
public static boolean isInstanceOf(CallContext ctx, Object obj, Object proto) throws InterruptedException {
if (obj == null || obj == NULL || proto == null || proto == NULL) return false;
var val = getPrototype(ctx, obj);
while (val != null) {
if (val.equals(proto)) return true;
val = val.getPrototype(ctx);
}
return false;
}
public static Object getMember(CallContext ctx, Object obj, Object key) throws InterruptedException {
obj = normalize(obj); key = normalize(key);
if (obj == null) throw new IllegalArgumentException("Tried to access member of undefined.");
if (obj == NULL) throw new IllegalArgumentException("Tried to access member of null.");
if (isObject(obj)) return object(obj).getMember(ctx, key);
if (obj instanceof String && key instanceof Number) {
var i = number(key);
var s = (String)obj;
if (i >= 0 && i < s.length() && i - Math.floor(i) == 0) {
return s.charAt((int)i) + "";
}
}
var proto = getPrototype(ctx, obj);
if (proto == null) return key.equals("__proto__") ? NULL : null;
else if (key != null && key.equals("__proto__")) return proto;
else return proto.getMember(ctx, key, obj);
}
public static boolean setMember(CallContext ctx, Object obj, Object key, Object val) throws InterruptedException {
obj = normalize(obj); key = normalize(key); val = normalize(val);
if (obj == null) throw EngineException.ofType("Tried to access member of undefined.");
if (obj == NULL) throw EngineException.ofType("Tried to access member of null.");
if (key.equals("__proto__")) return setPrototype(ctx, obj, val);
if (isObject(obj)) return object(obj).setMember(ctx, key, val, false);
var proto = getPrototype(ctx, obj);
return proto.setMember(ctx, key, val, obj, true);
}
public static boolean hasMember(CallContext ctx, Object obj, Object key, boolean own) throws InterruptedException {
if (obj == null || obj == NULL) return false;
obj = normalize(obj); key = normalize(key);
if (key.equals("__proto__")) return true;
if (isObject(obj)) return object(obj).hasMember(ctx, key, own);
if (obj instanceof String && key instanceof Number) {
var i = number(key);
var s = (String)obj;
if (i >= 0 && i < s.length() && i - Math.floor(i) == 0) return true;
}
if (own) return false;
var proto = getPrototype(ctx, obj);
return proto != null && proto.hasMember(ctx, key, own);
}
public static boolean deleteMember(CallContext ctx, Object obj, Object key) throws InterruptedException {
if (obj == null || obj == NULL) return false;
obj = normalize(obj); key = normalize(key);
if (isObject(obj)) return object(obj).deleteMember(ctx, key);
else return false;
}
public static ObjectValue getPrototype(CallContext ctx, Object obj) throws InterruptedException {
if (obj == null || obj == NULL) return null;
obj = normalize(obj);
if (isObject(obj)) return object(obj).getPrototype(ctx);
if (ctx == null) return null;
if (obj instanceof String) return ctx.engine().stringProto();
else if (obj instanceof Number) return ctx.engine().numberProto();
else if (obj instanceof Boolean) return ctx.engine().booleanProto();
else if (obj instanceof Symbol) return ctx.engine().symbolProto();
return null;
}
public static boolean setPrototype(CallContext ctx, Object obj, Object proto) throws InterruptedException {
obj = normalize(obj); proto = normalize(proto);
return isObject(obj) && object(obj).setPrototype(ctx, proto);
}
public static List<Object> getMembers(CallContext ctx, Object obj, boolean own, boolean includeNonEnumerable) throws InterruptedException {
List<Object> res = new ArrayList<>();
if (isObject(obj)) res = object(obj).keys(includeNonEnumerable);
if (obj instanceof String) {
for (var i = 0; i < ((String)obj).length(); i++) res.add((double)i);
}
if (!own) {
var proto = getPrototype(ctx, obj);
while (proto != null) {
res.addAll(proto.keys(includeNonEnumerable));
proto = proto.getPrototype(ctx);
}
}
return res;
}
public static Object call(CallContext ctx, Object func, Object thisArg, Object ...args) throws InterruptedException {
if (!isFunction(func)) throw EngineException.ofType("Attempted to call a non-function value.");
return function(func).call(ctx, thisArg, args);
}
public static boolean strictEquals(Object a, Object b) {
a = normalize(a); b = normalize(b);
if (a == null || b == null) return a == null && b == null;
if (isNan(a) || isNan(b)) return false;
if (a instanceof Number && number(a) == -0.) a = 0.;
if (b instanceof Number && number(b) == -0.) b = 0.;
return a == b || a.equals(b);
}
public static boolean looseEqual(CallContext ctx, Object a, Object b) throws InterruptedException {
a = normalize(a); b = normalize(b);
// In loose equality, null is equivalent to undefined
if (a == NULL) a = null;
if (b == NULL) b = null;
if (a == null || b == null) return a == null && b == null;
// If both are objects, just compare their references
if (!isPrimitive(a) && !isPrimitive(b)) return a == b;
// Convert values to primitives
a = toPrimitive(ctx, a, ConvertHint.VALUEOF);
b = toPrimitive(ctx, b, ConvertHint.VALUEOF);
// Compare symbols by reference
if (a instanceof Symbol || b instanceof Symbol) return a == b;
if (a instanceof Boolean || b instanceof Boolean) return toBoolean(a) == toBoolean(b);
if (a instanceof Number || b instanceof Number) return strictEquals(toNumber(ctx, a), toNumber(ctx, b));
// Default to strings
return toString(ctx, a).equals(toString(ctx, b));
}
public static Object normalize(Object val) {
if (val instanceof Number) return number(val);
if (isPrimitive(val) || val instanceof ObjectValue) return val;
if (val instanceof Character) return val + "";
if (val instanceof Map) {
var res = new ObjectValue();
for (var entry : ((Map<?, ?>)val).entrySet()) {
res.defineProperty(entry.getKey(), entry.getValue());
}
return res;
}
if (val instanceof Iterable) {
var res = new ArrayValue();
for (var entry : ((Iterable<?>)val)) {
res.set(res.size(), entry);
}
return res;
}
return new NativeWrapper(val);
}
@SuppressWarnings("unchecked")
public static <T> T convert(CallContext ctx, Object obj, Class<T> clazz) throws InterruptedException {
if (clazz == Void.class) return null;
if (clazz == null || clazz == Object.class) return (T)obj;
var err = new IllegalArgumentException("Cannot convert '%s' to '%s'.".formatted(type(obj), clazz.getName()));
if (obj instanceof NativeWrapper) {
var res = ((NativeWrapper)obj).wrapped;
if (clazz.isInstance(res)) return (T)res;
}
if (obj instanceof ArrayValue) {
var raw = array(obj).toArray();
if (clazz.isAssignableFrom(ArrayList.class)) {
var res = new ArrayList<>();
for (var i = 0; i < raw.length; i++) res.add(convert(ctx, raw[i], Object.class));
return (T)new ArrayList<>(res);
}
if (clazz.isAssignableFrom(HashSet.class)) {
var res = new HashSet<>();
for (var i = 0; i < raw.length; i++) res.add(convert(ctx, raw[i], Object.class));
return (T)new HashSet<>(res);
}
if (clazz.isArray()) {
Object res = Array.newInstance(clazz.arrayType(), raw.length);
for (var i = 0; i < raw.length; i++) Array.set(res, i, convert(ctx, raw[i], Object.class));
return (T)res;
}
}
if (obj instanceof ObjectValue && clazz.isAssignableFrom(HashMap.class)) {
var res = new HashMap<>();
for (var el : object(obj).values.entrySet()) res.put(
convert(ctx, el.getKey(), null),
convert(ctx, el.getValue(), null)
);
return (T)res;
}
if (clazz == String.class) return (T)toString(ctx, obj);
if (clazz == Boolean.class || clazz == Boolean.TYPE) return (T)(Boolean)toBoolean(obj);
if (clazz == Byte.class || clazz == byte.class) return (T)(Byte)(byte)toNumber(ctx, obj);
if (clazz == Integer.class || clazz == int.class) return (T)(Integer)(int)toNumber(ctx, obj);
if (clazz == Long.class || clazz == long.class) return (T)(Long)(long)toNumber(ctx, obj);
if (clazz == Short.class || clazz == short.class) return (T)(Short)(short)toNumber(ctx, obj);
if (clazz == Float.class || clazz == float.class) return (T)(Float)(float)toNumber(ctx, obj);
if (clazz == Double.class || clazz == double.class) return (T)(Double)toNumber(ctx, obj);
if (clazz == Character.class || clazz == char.class) {
if (obj instanceof Number) return (T)(Character)(char)number(obj);
else if (obj == NULL) throw new IllegalArgumentException("Cannot convert null to character.");
else if (obj == null) throw new IllegalArgumentException("Cannot convert undefined to character.");
else {
var res = toString(ctx, obj);
if (res.length() == 0) throw new IllegalArgumentException("Cannot convert empty string to character.");
else return (T)(Character)res.charAt(0);
}
}
if (obj == null) return null;
if (clazz.isInstance(obj)) return (T)obj;
throw err;
}
public static Iterable<Object> toJavaIterable(CallContext ctx, Object obj) throws InterruptedException {
return () -> {
try {
var constr = getMember(ctx, ctx.engine().symbolProto(), "constructor");
var symbol = getMember(ctx, constr, "iterator");
var iteratorFunc = getMember(ctx, obj, symbol);
if (!isFunction(iteratorFunc)) return Collections.emptyIterator();
var iterator = getMember(ctx, call(ctx, iteratorFunc, obj), "next");
if (!isFunction(iterator)) return Collections.emptyIterator();
var iterable = obj;
return new Iterator<Object>() {
private Object value = null;
public boolean consumed = true;
private FunctionValue next = function(iterator);
private void loadNext() throws InterruptedException {
if (next == null) value = null;
else if (consumed) {
var curr = object(next.call(ctx, iterable));
if (curr == null) { next = null; value = null; }
if (toBoolean(curr.getMember(ctx, "done"))) { next = null; value = null; }
else {
this.value = curr.getMember(ctx, "value");
consumed = false;
}
}
}
@Override
public boolean hasNext() {
try {
loadNext();
return next != null;
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
@Override
public Object next() {
try {
loadNext();
var res = value;
value = null;
consumed = true;
return res;
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
};
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
catch (IllegalArgumentException | NullPointerException e) {
return Collections.emptyIterator();
}
};
}
public static ObjectValue fromJavaIterable(CallContext ctx, Iterable<?> iterable) throws InterruptedException {
var res = new ObjectValue();
var it = iterable.iterator();
try {
var key = getMember(ctx, getMember(ctx, ctx.engine().symbolProto(), "constructor"), "iterable");
res.defineProperty(key, new NativeFunction("", (_ctx, thisArg, args) -> fromJavaIterable(ctx, iterable)));
}
catch (IllegalArgumentException | NullPointerException e) { }
res.defineProperty("next", new NativeFunction("", (_ctx, _th, _args) -> {
if (!it.hasNext()) return new ObjectValue(Map.of("done", true));
else return new ObjectValue(Map.of("value", it.next()));
}));
return res;
}
private static void printValue(CallContext ctx, Object val, HashSet<Object> passed, int tab) throws InterruptedException {
if (passed.contains(val)) {
System.out.print("[circular]");
return;
}
var printed = true;
if (val instanceof FunctionValue) {
System.out.print("function ");
var name = Values.getMember(ctx, val, "name");
if (name != null) System.out.print(Values.toString(ctx, name));
System.out.print("(...)");
var loc = val instanceof CodeFunction ? ((CodeFunction)val).loc() : null;
if (loc != null) System.out.print(" @ " + loc);
}
else if (val instanceof ArrayValue) {
System.out.print("[");
var obj = ((ArrayValue)val);
for (int i = 0; i < obj.size(); i++) {
if (i != 0) System.out.print(", ");
else System.out.print(" ");
if (obj.has(i)) printValue(ctx, obj.get(i), passed, tab);
else System.out.print(", ");
}
System.out.print(" ] ");
}
else if (val instanceof NativeWrapper) {
var obj = ((NativeWrapper)val).wrapped;
System.out.print("Native " + obj.toString() + " ");
}
else printed = false;
if (val instanceof ObjectValue) {
if (tab > 3) {
System.out.print("{...}");
return;
}
passed.add(val);
var obj = (ObjectValue)val;
if (obj.values.size() + obj.properties.size() == 0) {
if (!printed) System.out.println("{}");
}
else {
System.out.println("{");
for (var el : obj.values.entrySet()) {
for (int i = 0; i < tab + 1; i++) System.out.print(" ");
printValue(ctx, el.getKey(), passed, tab + 1);
System.out.print(": ");
printValue(ctx, el.getValue(), passed, tab + 1);
System.out.println(",");
}
for (var el : obj.properties.entrySet()) {
for (int i = 0; i < tab + 1; i++) System.out.print(" ");
printValue(ctx, el.getKey(), passed, tab + 1);
System.out.println(": [prop],");
}
for (int i = 0; i < tab; i++) System.out.print(" ");
System.out.print("}");
passed.remove(val);
}
}
else if (val == null) System.out.print("undefined");
else if (val == Values.NULL) System.out.print("null");
else if (val instanceof String) System.out.print("'" + val + "'");
else System.out.print(Values.toString(ctx, val));
}
public static void printValue(CallContext ctx, Object val) throws InterruptedException {
printValue(ctx, val, new HashSet<>(), 0);
}
}

View File

@@ -0,0 +1,25 @@
package me.topchetoeu.jscript.events;
public interface Awaitable<T> {
T await() throws FinishedException, InterruptedException;
default Observable<T> toObservable() {
return sub -> {
var thread = new Thread(() -> {
try {
sub.next(await());
sub.finish();
}
catch (InterruptedException | FinishedException e) {
sub.finish();
}
catch (RuntimeException e) {
sub.error(e);
}
}, "Awaiter");
thread.start();
return () -> thread.interrupt();
};
}
}

View File

@@ -0,0 +1,34 @@
package me.topchetoeu.jscript.events;
public class DataNotifier<T> implements Awaitable<T> {
private Notifier notifier = new Notifier();
private boolean isErr;
private T val;
private RuntimeException err;
public void error(RuntimeException t) {
err = t;
isErr = true;
notifier.next();
}
public void error(Throwable t) {
error(new RuntimeException(t));
}
public void next(T val) {
this.val = val;
isErr = false;
notifier.next();
}
public T await() throws InterruptedException {
notifier.await();
try {
if (isErr) throw err;
else return val;
}
finally {
this.err = null;
this.val = null;
}
}
}

View File

@@ -0,0 +1,49 @@
package me.topchetoeu.jscript.events;
import java.util.HashSet;
public class Event<T> implements Observer<T>, Observable<T> {
private HashSet<Observer<T>> handlers = new HashSet<>();
public Handle on(Observer<T> handler) {
if (handlers == null) {
handler.finish();
return () -> {};
}
handlers.add(handler);
return () -> {
if (handlers == null) return;
handlers.remove(handler);
};
}
public boolean isFinished() {
return handlers == null;
}
public void next(T value) {
if (handlers == null) throw new IllegalStateException("Cannot use a finished event.");
for (var handler : handlers) {
handler.next(value);
}
}
public void error(RuntimeException value) {
if (handlers == null) throw new IllegalStateException("Cannot use a finished event.");
for (var handler : handlers) {
handler.error(value);
}
handlers.clear();
handlers = null;
}
public void finish() {
if (handlers == null) throw new IllegalStateException("Cannot use a finished event.");
for (var handler : handlers) {
handler.finish();
}
handlers.clear();
handlers = null;
}
}

View File

@@ -0,0 +1,7 @@
package me.topchetoeu.jscript.events;
public class FinishedException extends RuntimeException {
public FinishedException() {
super("The observable has ended.");
}
}

View File

@@ -0,0 +1,5 @@
package me.topchetoeu.jscript.events;
public interface Handle {
void free();
}

View File

@@ -0,0 +1,14 @@
package me.topchetoeu.jscript.events;
public class Notifier {
private boolean ok = false;
public synchronized void next() {
ok = true;
notifyAll();
}
public synchronized void await() throws InterruptedException {
while (!ok) wait();
ok = false;
}
}

View File

@@ -0,0 +1,75 @@
package me.topchetoeu.jscript.events;
public interface Observable<T> {
Handle on(Observer<T> val);
default Handle once(Observer<T> observer) {
// Java is fucking retarded
var unhandler = new Handle[1];
var shouldUnsub = new boolean[1];
unhandler[0] = on(new Observer<>() {
public void next(T data) {
observer.next(data);
if (unhandler[0] == null) shouldUnsub[0] = true;
else unhandler[0].free();
}
public void error(RuntimeException err) {
observer.error(err);
if (unhandler[0] == null) shouldUnsub[0] = true;
else unhandler[0].free();
}
public void finish() {
observer.finish();
if (unhandler[0] == null) shouldUnsub[0] = true;
else unhandler[0].free();
}
});
if (shouldUnsub[0]) {
unhandler[0].free();
return () -> {};
}
else return unhandler[0];
}
@SuppressWarnings("unchecked")
default Awaitable<T> toAwaitable() {
return () -> {
var notifier = new Notifier();
var valRef = new Object[1];
var isErrRef = new boolean[1];
once(new Observer<>() {
public void next(T data) {
valRef[0] = data;
notifier.next();
}
public void error(RuntimeException err) {
isErrRef[0] = true;
valRef[0] = err;
notifier.next();
}
public void finish() {
isErrRef[0] = true;
valRef[0] = new FinishedException();
notifier.next();
}
});
notifier.await();
if (isErrRef[0]) throw (RuntimeException)valRef[0];
else return (T)valRef[0];
};
}
default Observable<T> encapsulate() {
return val -> on(val);
}
default <T2> Observable<T2> pipe(Pipe<T, T2> pipe) {
return sub -> on(pipe.apply(sub));
}
default WarmObservable<T> warmUp() {
return new WarmObservable<>(this);
}
}

View File

@@ -0,0 +1,7 @@
package me.topchetoeu.jscript.events;
public interface Observer<T> {
public void next(T data);
public default void error(RuntimeException err) {}
public default void finish() { }
}

View File

@@ -0,0 +1,59 @@
package me.topchetoeu.jscript.events;
public interface Pipe<T, T2> {
Observer<T> apply(Observer<T2> obs);
// void next(T val, Observer<T2> target);
// default void error(RuntimeException err, Observer<T2> target) {
// target.error(err);
// }
// default void finish(Observer<T2> target) {
// target.finish();
// }
public static interface MapFunc<T1, T2> {
T2 map(T1 val);
}
public static <T1, T2> Pipe<T1, T2> map(MapFunc<T1, T2> func) {
return o -> val -> o.next(func.map(val));
}
public static <T> Pipe<T, T> filter(MapFunc<T, Boolean> func) {
return o -> val -> {
if (func.map(val)) o.next(val);
};
}
public static <T> Pipe<T, T> skip(int n) {
var i = new int[1];
return target -> val -> {
if (i[0] >= n) target.next(val);
else i[0]++;
};
}
public static <T> Pipe<T, T> limit(int n) {
return target -> new Observer<T>() {
private int i;
public void next(T val) {
if (i >= n) target.finish();
else {
target.next(val);
i++;
}
}
public void error(RuntimeException err) {
if (i < n) target.error(err);
}
public void finish() {
if (i < n) target.finish();
}
};
}
public static <T> Pipe<T, T> first() {
return limit(1);
}
public static <T> Pipe<Observable<T>, T> merge() {
return target -> val -> val.on(target);
}
}

View File

@@ -0,0 +1,46 @@
package me.topchetoeu.jscript.events;
import java.util.HashSet;
public class WarmObservable<T> implements Observable<T>, Handle {
private HashSet<Observer<T>> observers = new HashSet<>();
private Handle handle;
@Override
public Handle on(Observer<T> val) {
if (observers == null) return () -> {};
observers.add(val);
return () -> observers.remove(val);
}
@Override
public void free() {
if (observers == null) return;
handle.free();
handle = null;
observers = null;
}
public WarmObservable(Observable<T> observable) {
observable.on(new Observer<>() {
public void next(T data) {
for (var obs : observers) obs.next(data);
}
public void error(RuntimeException err) {
for (var obs : observers) obs.error(err);
handle = null;
observers = null;
}
public void finish() {
for (var obs : observers) obs.finish();
handle = null;
observers = null;
}
});
}
@Override
public WarmObservable<T> warmUp() {
return this;
}
}

View File

@@ -0,0 +1,70 @@
package me.topchetoeu.jscript.exceptions;
import java.util.ArrayList;
import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.engine.CallContext;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.engine.values.ObjectValue.PlaceholderProto;
public class EngineException extends RuntimeException {
public final Object value;
public EngineException cause;
public final List<String> stackTrace = new ArrayList<>();
public EngineException add(String name, Location location) {
var res = "";
if (location != null) res += "at " + location.toString() + " ";
if (name != null && !name.equals("")) res += "in " + name + " ";
this.stackTrace.add(res.trim());
return this;
}
public EngineException setCause(EngineException cause) {
this.cause = cause;
return this;
}
public String toString(CallContext ctx) throws InterruptedException {
var ss = new StringBuilder();
ss.append(Values.toString(ctx, value)).append('\n');
for (var line : stackTrace) {
ss.append(" ").append(line).append('\n');
}
if (cause != null) ss.append("Caused by ").append(cause.toString(ctx)).append('\n');
ss.deleteCharAt(ss.length() - 1);
return ss.toString();
}
private static Object err(String msg, PlaceholderProto proto) {
var res = new ObjectValue(proto);
res.defineProperty("message", msg);
return res;
}
public EngineException(Object error) {
super(error == null ? "null" : error.toString());
this.value = error;
this.cause = null;
}
public static EngineException ofError(String msg) {
return new EngineException(err(msg, PlaceholderProto.ERROR));
}
public static EngineException ofSyntax(SyntaxException e) {
return new EngineException(err(e.msg, PlaceholderProto.SYNTAX_ERROR)).add(null, e.loc);
}
public static EngineException ofSyntax(String msg) {
return new EngineException(err(msg, PlaceholderProto.SYNTAX_ERROR));
}
public static EngineException ofType(String msg) {
return new EngineException(err(msg, PlaceholderProto.TYPE_ERROR));
}
public static EngineException ofRange(String msg) {
return new EngineException(err(msg, PlaceholderProto.RANGE_ERROR));
}
}

View File

@@ -0,0 +1,14 @@
package me.topchetoeu.jscript.exceptions;
import me.topchetoeu.jscript.Location;
public class SyntaxException extends RuntimeException {
public final Location loc;
public final String msg;
public SyntaxException(Location loc, String msg) {
super("Syntax error (at %s): %s".formatted(loc, msg));
this.loc = loc;
this.msg = msg;
}
}

View File

@@ -0,0 +1,22 @@
package me.topchetoeu.jscript.filesystem;
import java.io.IOException;
public interface File {
int read(byte[] buff) throws IOException;
void write(byte[] buff) throws IOException;
long tell() throws IOException;
void seek(long offset, int pos) throws IOException;
void close() throws IOException;
Permissions perms();
default String readToString() throws IOException {
seek(0, 2);
long len = tell();
if (len < 0) return null;
seek(0, 0);
byte[] res = new byte[(int)len];
if (read(res) < 0) return null;
return new String(res);
}
}

View File

@@ -0,0 +1,17 @@
package me.topchetoeu.jscript.filesystem;
import java.io.IOException;
import java.nio.file.Path;
public interface Filesystem extends PermissionsProvider {
public static enum EntryType {
NONE,
FILE,
FOLDER,
}
File open(Path path) throws IOException;
EntryType type(Path path);
boolean mkdir(Path path);
boolean rm(Path path) throws IOException;
}

View File

@@ -0,0 +1,27 @@
package me.topchetoeu.jscript.filesystem;
import java.io.IOException;
public class InaccessibleFile implements File {
@Override
public int read(byte[] buff) throws IOException {
return -1;
}
@Override
public void write(byte[] buff) throws IOException {
}
@Override
public long tell() throws IOException {
return -1;
}
@Override
public void seek(long offset, int pos) throws IOException {
}
@Override
public void close() throws IOException {
}
@Override
public Permissions perms() {
return Permissions.NONE;
}
}

View File

@@ -0,0 +1,51 @@
package me.topchetoeu.jscript.filesystem;
import java.io.IOException;
public class MemoryFile implements File {
public byte[] data;
private int ptr = 0;
@Override
public void close() {
data = null;
ptr = -1;
}
@Override
public int read(byte[] buff) throws IOException {
if (data == null) return -1;
if (ptr == data.length) return -1;
int n = Math.min(buff.length, data.length - ptr);
System.arraycopy(data, ptr, buff, 0, n);
return n;
}
@Override
public void seek(long offset, int pos) throws IOException {
if (data == null) return;
if (pos == 0) ptr = (int)offset;
else if (pos == 1) ptr += (int)offset;
else ptr = data.length - (int)offset;
ptr = (int)Math.max(Math.min(ptr, data.length), 0);
}
@Override
public long tell() throws IOException {
if (data == null) return -1;
return ptr;
}
@Override
public void write(byte[] buff) throws IOException { }
@Override
public Permissions perms() {
if (data == null) return Permissions.NONE;
else return Permissions.READ;
}
public MemoryFile(byte[] data) {
this.data = data;
}
}

View File

@@ -0,0 +1,17 @@
package me.topchetoeu.jscript.filesystem;
public enum Permissions {
NONE("", false, false),
READ("r", true, false),
READ_WRITE("rw", true, true);
public final String readMode;
public final boolean readable;
public final boolean writable;
private Permissions(String mode, boolean r, boolean w) {
this.readMode = mode;
this.readable = r;
this.writable = w;
}
}

View File

@@ -0,0 +1,7 @@
package me.topchetoeu.jscript.filesystem;
import java.nio.file.Path;
public interface PermissionsProvider {
Permissions perms(Path file);
}

Some files were not shown because too many files have changed in this diff Show More