Debugging support #7
@ -16,14 +16,15 @@
|
||||
<p>
|
||||
Here are the available entrypoints:
|
||||
<ul>
|
||||
<li><a href="json/version">/json/version</a></li>
|
||||
<li><a href="json/list">/json/list</a></li>
|
||||
<li>/(any target)</a></li>
|
||||
<li><a href="json/version">/json/version</a> - version and other stuff about the JScript engine</li>
|
||||
<li><a href="json/list">/json/list</a> - a list of all entrypoints</li>
|
||||
<li><a href="json/protocol">/json/protocol</a> - documentation of the implemented V8 protocol</li>
|
||||
<li>/(any target) - websocket entrypoints for debugging</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Developed by TopchetoEU, MIT License
|
||||
Running ${NAME} v${VERSION} by ${AUTHOR}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
@ -1957,6 +1957,51 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"domain": "NodeWorker",
|
||||
"description": "Implemented partly just because of pesky vscode.",
|
||||
"deprecated": true,
|
||||
"commands": [
|
||||
{
|
||||
"name": "enable",
|
||||
"description": "Used to get the attachedToWorker event",
|
||||
"parameters": [ ]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "attachedToWorker",
|
||||
"description": "Issued when attached to a worker.",
|
||||
"parameters": [
|
||||
{ "name": "sessionId",
|
||||
"description": "Identifier assigned to the session used to send/receive messages.",
|
||||
"$ref": "string"
|
||||
},
|
||||
{
|
||||
"name": "workerInfo",
|
||||
"type": "object",
|
||||
"properties": [
|
||||
{ "name": "workerId",
|
||||
"$ref": "string"
|
||||
},
|
||||
{ "name": "type",
|
||||
"type": "string"
|
||||
},
|
||||
{ "name": "title",
|
||||
"type": "string"
|
||||
},
|
||||
{ "name": "url",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "name": "waitingForDebugger",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -23,6 +23,7 @@ public class Main {
|
||||
static Thread engineTask, debugTask;
|
||||
static Engine engine;
|
||||
static Environment env;
|
||||
static int j = 0;
|
||||
|
||||
private static Observer<Object> valuePrinter = new Observer<Object>() {
|
||||
public void next(Object data) {
|
||||
@ -59,7 +60,7 @@ public class Main {
|
||||
env.global.define("go", _ctx -> {
|
||||
try {
|
||||
var f = Path.of("do.js");
|
||||
var func = _ctx.compile(Filename.fromFile(f.toFile()), new String(Files.readAllBytes(f)));
|
||||
var func = _ctx.compile(new Filename("do", "do/" + j++ + ".js"), new String(Files.readAllBytes(f)));
|
||||
return func.call(_ctx);
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
@ -8,7 +8,7 @@ import me.topchetoeu.jscript.Location;
|
||||
|
||||
public class CompileTarget {
|
||||
public final Vector<Instruction> target = new Vector<>();
|
||||
public final Map<Long, Instruction[]> functions;
|
||||
public final Map<Long, FunctionBody> functions;
|
||||
public final TreeSet<Location> breakpoints;
|
||||
|
||||
public Instruction add(Instruction instr) {
|
||||
@ -31,7 +31,7 @@ public class CompileTarget {
|
||||
|
||||
public Instruction[] array() { return target.toArray(Instruction[]::new); }
|
||||
|
||||
public CompileTarget(Map<Long, Instruction[]> functions, TreeSet<Location> breakpoints) {
|
||||
public CompileTarget(Map<Long, FunctionBody> functions, TreeSet<Location> breakpoints) {
|
||||
this.functions = functions;
|
||||
this.breakpoints = breakpoints;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import me.topchetoeu.jscript.engine.scope.ScopeRecord;
|
||||
|
||||
public class CompoundStatement extends Statement {
|
||||
public final Statement[] statements;
|
||||
public Location end;
|
||||
|
||||
@Override
|
||||
public void declare(ScopeRecord varsScope) {
|
||||
@ -37,6 +38,11 @@ public class CompoundStatement extends Statement {
|
||||
if (i != statements.length - 1) stm.compileWithDebug(target, scope, false);
|
||||
else stm.compileWithDebug(target, scope, pollute);
|
||||
}
|
||||
|
||||
if (end != null) {
|
||||
target.add(Instruction.nop().locate(end));
|
||||
target.setDebug();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -59,6 +65,11 @@ public class CompoundStatement extends Statement {
|
||||
else return new CompoundStatement(loc(), res.toArray(Statement[]::new));
|
||||
}
|
||||
|
||||
public CompoundStatement setEnd(Location loc) {
|
||||
this.end = loc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CompoundStatement(Location loc, Statement ...statements) {
|
||||
super(loc);
|
||||
this.statements = statements;
|
||||
|
17
src/me/topchetoeu/jscript/compilation/FunctionBody.java
Normal file
17
src/me/topchetoeu/jscript/compilation/FunctionBody.java
Normal file
@ -0,0 +1,17 @@
|
||||
package me.topchetoeu.jscript.compilation;
|
||||
|
||||
public class FunctionBody {
|
||||
public final Instruction[] instructions;
|
||||
public final String[] captureNames, localNames;
|
||||
|
||||
public FunctionBody(Instruction[] instructions, String[] captureNames, String[] localNames) {
|
||||
this.instructions = instructions;
|
||||
this.captureNames = captureNames;
|
||||
this.localNames = localNames;
|
||||
}
|
||||
public FunctionBody(Instruction[] instructions) {
|
||||
this.instructions = instructions;
|
||||
this.captureNames = new String[0];
|
||||
this.localNames = new String[0];
|
||||
}
|
||||
}
|
@ -147,14 +147,6 @@ public class Instruction {
|
||||
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);
|
||||
}
|
||||
|
||||
public static Instruction nop(Object ...params) {
|
||||
for (var param : params) {
|
||||
|
@ -10,10 +10,12 @@ public class VariableDeclareStatement extends Statement {
|
||||
public static class Pair {
|
||||
public final String name;
|
||||
public final Statement value;
|
||||
public final Location location;
|
||||
|
||||
public Pair(String name, Statement value) {
|
||||
public Pair(String name, Statement value, Location location) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.location = location;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,16 +32,20 @@ public class VariableDeclareStatement extends Statement {
|
||||
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()));
|
||||
int start = target.size();
|
||||
|
||||
if (key instanceof String) target.add(Instruction.makeVar((String)key).locate(entry.location));
|
||||
|
||||
if (entry.value instanceof FunctionStatement) {
|
||||
((FunctionStatement)entry.value).compile(target, scope, entry.name, false);
|
||||
target.add(Instruction.storeVar(key).locate(loc()));
|
||||
target.add(Instruction.storeVar(key).locate(entry.location));
|
||||
}
|
||||
else if (entry.value != null) {
|
||||
entry.value.compile(target, scope, true);
|
||||
target.add(Instruction.storeVar(key).locate(loc()));
|
||||
target.add(Instruction.storeVar(key).locate(entry.location));
|
||||
}
|
||||
|
||||
if (target.size() != start) target.setDebug(start);
|
||||
}
|
||||
|
||||
if (pollute) target.add(Instruction.loadValue(null).locate(loc()));
|
||||
|
@ -5,6 +5,7 @@ import java.util.Random;
|
||||
import me.topchetoeu.jscript.Location;
|
||||
import me.topchetoeu.jscript.compilation.CompileTarget;
|
||||
import me.topchetoeu.jscript.compilation.CompoundStatement;
|
||||
import me.topchetoeu.jscript.compilation.FunctionBody;
|
||||
import me.topchetoeu.jscript.compilation.Instruction;
|
||||
import me.topchetoeu.jscript.compilation.Statement;
|
||||
import me.topchetoeu.jscript.compilation.Instruction.Type;
|
||||
@ -69,7 +70,6 @@ public class FunctionStatement extends Statement {
|
||||
}
|
||||
|
||||
body.declare(subscope);
|
||||
funcTarget.add(Instruction.debugVarNames(subscope.locals()));
|
||||
body.compile(funcTarget, subscope, false);
|
||||
funcTarget.add(Instruction.ret().locate(loc()));
|
||||
checkBreakAndCont(funcTarget, start);
|
||||
@ -77,7 +77,7 @@ public class FunctionStatement extends Statement {
|
||||
var id = rand.nextLong();
|
||||
|
||||
target.add(Instruction.loadFunc(id, subscope.localsCount(), args.length, subscope.getCaptures()).locate(loc()));
|
||||
target.functions.put(id, funcTarget.array());
|
||||
target.functions.put(id, new FunctionBody(funcTarget.array(), subscope.captures(), subscope.locals()));
|
||||
|
||||
if (name == null) name = this.name;
|
||||
|
||||
|
@ -20,17 +20,17 @@ public class IndexStatement extends AssignableStatement {
|
||||
return new IndexAssignStatement(loc(), object, index, val, operation);
|
||||
}
|
||||
public void compile(CompileTarget target, ScopeRecord scope, boolean dupObj, boolean pollute) {
|
||||
int start = 0;
|
||||
object.compile(target, scope, true);
|
||||
if (dupObj) target.add(Instruction.dup().locate(loc()));
|
||||
if (index instanceof ConstantStatement) {
|
||||
target.add(Instruction.loadMember(((ConstantStatement)index).value).locate(loc()));
|
||||
target.setDebug();
|
||||
return;
|
||||
}
|
||||
|
||||
index.compile(target, scope, true);
|
||||
target.add(Instruction.loadMember().locate(loc()));
|
||||
target.setDebug(start);
|
||||
target.setDebug();
|
||||
if (!pollute) target.add(Instruction.discard().locate(loc()));
|
||||
}
|
||||
@Override
|
||||
|
@ -4,7 +4,7 @@ import java.util.HashMap;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
import me.topchetoeu.jscript.Filename;
|
||||
import me.topchetoeu.jscript.compilation.Instruction;
|
||||
import me.topchetoeu.jscript.compilation.FunctionBody;
|
||||
import me.topchetoeu.jscript.engine.values.FunctionValue;
|
||||
import me.topchetoeu.jscript.events.Awaitable;
|
||||
import me.topchetoeu.jscript.events.DataNotifier;
|
||||
@ -51,7 +51,7 @@ public class Engine {
|
||||
private LinkedBlockingDeque<Task> microTasks = new LinkedBlockingDeque<>();
|
||||
|
||||
public final int id = ++nextId;
|
||||
public final HashMap<Long, Instruction[]> functions = new HashMap<>();
|
||||
public final HashMap<Long, FunctionBody> functions = new HashMap<>();
|
||||
public final Data data = new Data().set(StackData.MAX_FRAMES, 10000);
|
||||
|
||||
private void runTask(Task task) {
|
||||
@ -63,8 +63,8 @@ public class Engine {
|
||||
task.notifier.error(e);
|
||||
}
|
||||
}
|
||||
private void run() {
|
||||
while (true) {
|
||||
public void run(boolean untilEmpty) {
|
||||
while (!untilEmpty || !macroTasks.isEmpty()) {
|
||||
try {
|
||||
runTask(macroTasks.take());
|
||||
|
||||
@ -83,7 +83,7 @@ public class Engine {
|
||||
|
||||
public Thread start() {
|
||||
if (this.thread == null) {
|
||||
this.thread = new Thread(this::run, "JavaScript Runner #" + id);
|
||||
this.thread = new Thread(() -> run(false), "JavaScript Runner #" + id);
|
||||
this.thread.start();
|
||||
}
|
||||
return this.thread;
|
||||
|
@ -20,4 +20,16 @@ public interface DebugHandler {
|
||||
void stepOver(V8Message msg);
|
||||
|
||||
void setPauseOnExceptions(V8Message msg);
|
||||
|
||||
void evaluateOnCallFrame(V8Message msg);
|
||||
|
||||
void getProperties(V8Message msg);
|
||||
void releaseObjectGroup(V8Message msg);
|
||||
/**
|
||||
* This method might not execute the actual code for well-known requests
|
||||
*/
|
||||
void callFunctionOn(V8Message msg);
|
||||
|
||||
// void nodeWorkerEnable(V8Message msg);
|
||||
void runtimeEnable(V8Message msg);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
|
||||
import me.topchetoeu.jscript.Metadata;
|
||||
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
|
||||
import me.topchetoeu.jscript.exceptions.SyntaxException;
|
||||
import me.topchetoeu.jscript.exceptions.UncheckedException;
|
||||
@ -17,11 +18,11 @@ import me.topchetoeu.jscript.json.JSONList;
|
||||
import me.topchetoeu.jscript.json.JSONMap;
|
||||
|
||||
public class DebugServer {
|
||||
public static String browserDisplayName = "jscript";
|
||||
public static String browserDisplayName = Metadata.NAME + "/" + Metadata.VERSION;
|
||||
|
||||
public final HashMap<String, DebuggerProvider> targets = new HashMap<>();
|
||||
|
||||
private final byte[] favicon, index;
|
||||
private final byte[] favicon, index, protocol;
|
||||
|
||||
private static void send(HttpRequest req, String val) throws IOException {
|
||||
req.writeResponse(200, "OK", "application/json", val.getBytes());
|
||||
@ -58,7 +59,7 @@ public class DebugServer {
|
||||
|
||||
try {
|
||||
msg = new V8Message(raw.textData());
|
||||
System.out.println(msg.name + ": " + JSON.stringify(msg.params));
|
||||
// System.out.println(msg.name + ": " + JSON.stringify(msg.params));
|
||||
}
|
||||
catch (SyntaxException e) {
|
||||
ws.send(new V8Error(e.getMessage()));
|
||||
@ -86,6 +87,13 @@ public class DebugServer {
|
||||
case "Debugger.stepOver": debugger.stepOver(msg); continue;
|
||||
|
||||
case "Debugger.setPauseOnExceptions": debugger.setPauseOnExceptions(msg); continue;
|
||||
case "Debugger.evaluateOnCallFrame": debugger.evaluateOnCallFrame(msg); continue;
|
||||
|
||||
case "Runtime.releaseObjectGroup": debugger.releaseObjectGroup(msg); continue;
|
||||
case "Runtime.getProperties": debugger.getProperties(msg); continue;
|
||||
case "Runtime.callFunctionOn": debugger.callFunctionOn(msg); continue;
|
||||
// case "NodeWorker.enable": debugger.nodeWorkerEnable(msg); continue;
|
||||
case "Runtime.enable": debugger.runtimeEnable(msg); continue;
|
||||
}
|
||||
|
||||
if (
|
||||
@ -96,8 +104,7 @@ public class DebugServer {
|
||||
msg.name.startsWith("Network.") ||
|
||||
msg.name.startsWith("Page.")
|
||||
) ws.send(new V8Error("This isn't a browser..."));
|
||||
|
||||
if (msg.name.startsWith("Runtime.") || msg.name.startsWith("Profiler.")) ws.send(new V8Error("This API is not supported yet."));
|
||||
else ws.send(new V8Error("This API is not supported yet."));
|
||||
}
|
||||
catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
@ -158,7 +165,7 @@ public class DebugServer {
|
||||
|
||||
switch (req.path) {
|
||||
case "/json/version":
|
||||
send(req, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.2\"}");
|
||||
send(req, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.1\"}");
|
||||
break;
|
||||
case "/json/list":
|
||||
case "/json": {
|
||||
@ -176,9 +183,11 @@ public class DebugServer {
|
||||
send(req, JSON.stringify(res));
|
||||
break;
|
||||
}
|
||||
case "/json/protocol":
|
||||
req.writeResponse(200, "OK", "application/json", protocol);
|
||||
break;
|
||||
case "/json/new":
|
||||
case "/json/activate":
|
||||
case "/json/protocol":
|
||||
case "/json/close":
|
||||
case "/devtools/inspector.html":
|
||||
req.writeResponse(
|
||||
@ -216,7 +225,13 @@ public class DebugServer {
|
||||
public DebugServer() {
|
||||
try {
|
||||
this.favicon = getClass().getClassLoader().getResourceAsStream("assets/favicon.png").readAllBytes();
|
||||
this.index = getClass().getClassLoader().getResourceAsStream("assets/index.html").readAllBytes();
|
||||
this.protocol = getClass().getClassLoader().getResourceAsStream("assets/protocol.json").readAllBytes();
|
||||
var index = new String(getClass().getClassLoader().getResourceAsStream("assets/index.html").readAllBytes());
|
||||
this.index = index
|
||||
.replace("${NAME}", Metadata.NAME)
|
||||
.replace("${VERSION}", Metadata.VERSION)
|
||||
.replace("${AUTHOR}", Metadata.AUTHOR)
|
||||
.getBytes();
|
||||
}
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
}
|
||||
|
@ -10,8 +10,6 @@ import java.util.HashMap;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.Map;
|
||||
|
||||
import me.topchetoeu.jscript.exceptions.UncheckedIOException;
|
||||
|
||||
public class HttpRequest {
|
||||
public final String method;
|
||||
public final String path;
|
||||
@ -21,19 +19,19 @@ public class HttpRequest {
|
||||
|
||||
public void writeCode(int code, String name) {
|
||||
try { out.write(("HTTP/1.1 " + code + " " + name + "\r\n").getBytes()); }
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException e) { }
|
||||
}
|
||||
public void writeHeader(String name, String value) {
|
||||
try { out.write((name + ": " + value + "\r\n").getBytes()); }
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException e) { }
|
||||
}
|
||||
public void writeLastHeader(String name, String value) {
|
||||
try { out.write((name + ": " + value + "\r\n\r\n").getBytes()); }
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException e) { }
|
||||
}
|
||||
public void writeHeadersEnd() {
|
||||
try { out.write("\n".getBytes()); }
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException e) { }
|
||||
}
|
||||
|
||||
public void writeResponse(int code, String name, String type, byte[] data) {
|
||||
@ -44,13 +42,13 @@ public class HttpRequest {
|
||||
out.write(data);
|
||||
out.close();
|
||||
}
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException e) { }
|
||||
}
|
||||
public void writeResponse(int code, String name, String type, InputStream data) {
|
||||
try {
|
||||
writeResponse(code, name, type, data.readAllBytes());
|
||||
}
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException e) { }
|
||||
}
|
||||
|
||||
public HttpRequest(String method, String path, Map<String, String> headers, OutputStream out) {
|
||||
@ -98,7 +96,7 @@ public class HttpRequest {
|
||||
|
||||
return new HttpRequest(method, path, headers, socket.getOutputStream());
|
||||
}
|
||||
catch (IOException e) { throw new UncheckedIOException(e); }
|
||||
catch (IOException | NullPointerException e) { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,11 @@ 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.Random;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.topchetoeu.jscript.Filename;
|
||||
import me.topchetoeu.jscript.Location;
|
||||
@ -16,6 +18,7 @@ import me.topchetoeu.jscript.engine.Engine;
|
||||
import me.topchetoeu.jscript.engine.StackData;
|
||||
import me.topchetoeu.jscript.engine.frame.CodeFrame;
|
||||
import me.topchetoeu.jscript.engine.frame.Runners;
|
||||
import me.topchetoeu.jscript.engine.scope.GlobalScope;
|
||||
import me.topchetoeu.jscript.engine.values.ArrayValue;
|
||||
import me.topchetoeu.jscript.engine.values.CodeFunction;
|
||||
import me.topchetoeu.jscript.engine.values.FunctionValue;
|
||||
@ -24,6 +27,7 @@ import me.topchetoeu.jscript.engine.values.Symbol;
|
||||
import me.topchetoeu.jscript.engine.values.Values;
|
||||
import me.topchetoeu.jscript.events.Notifier;
|
||||
import me.topchetoeu.jscript.exceptions.EngineException;
|
||||
import me.topchetoeu.jscript.json.JSON;
|
||||
import me.topchetoeu.jscript.json.JSONElement;
|
||||
import me.topchetoeu.jscript.json.JSONList;
|
||||
import me.topchetoeu.jscript.json.JSONMap;
|
||||
@ -35,6 +39,8 @@ import me.topchetoeu.jscript.lib.SetLib;
|
||||
import me.topchetoeu.jscript.lib.GeneratorLib.Generator;
|
||||
|
||||
public class SimpleDebugger implements Debugger {
|
||||
public static final String CHROME_GET_PROP_FUNC = "function s(e){let t=this;const n=JSON.parse(e);for(let e=0,i=n.length;e<i;++e)t=t[n[e]];return t}";
|
||||
|
||||
private static enum State {
|
||||
RESUMED,
|
||||
STEPPING_IN,
|
||||
@ -84,6 +90,7 @@ public class SimpleDebugger implements Debugger {
|
||||
this.pattern = pattern;
|
||||
this.line = line;
|
||||
this.start = start;
|
||||
if (condition != null && condition.trim().equals("")) condition = null;
|
||||
this.condition = condition;
|
||||
}
|
||||
}
|
||||
@ -91,9 +98,10 @@ public class SimpleDebugger implements Debugger {
|
||||
public CodeFrame frame;
|
||||
public CodeFunction func;
|
||||
public int id;
|
||||
public ObjectValue local = new ObjectValue(), capture = new ObjectValue(), global;
|
||||
public ObjectValue local, capture, global;
|
||||
public JSONMap serialized;
|
||||
public Location location;
|
||||
public boolean debugData = false;
|
||||
|
||||
public void updateLoc(Location loc) {
|
||||
serialized.set("location", serializeLocation(loc));
|
||||
@ -108,12 +116,17 @@ public class SimpleDebugger implements Debugger {
|
||||
this.location = frame.function.loc();
|
||||
|
||||
this.global = frame.function.environment.global.obj;
|
||||
frame.scope.applyToObject(ctx, this.local, this.capture, true);
|
||||
this.local = frame.getLocalScope(ctx, true);
|
||||
this.capture = frame.getCaptureScope(ctx, true);
|
||||
this.local.setPrototype(ctx, capture);
|
||||
this.capture.setPrototype(ctx, global);
|
||||
|
||||
if (location != null) {
|
||||
debugData = true;
|
||||
this.serialized = new JSONMap()
|
||||
.set("callFrameId", id + "")
|
||||
.set("functionName", func.name)
|
||||
.set("location", serializeLocation(func.loc()))
|
||||
.set("location", serializeLocation(location))
|
||||
.set("scopeChain", new JSONList()
|
||||
.add(new JSONMap().set("type", "local").set("name", "Local Scope").set("object", serializeObj(ctx, local)))
|
||||
.add(new JSONMap().set("type", "closure").set("name", "Closure").set("object", serializeObj(ctx, capture)))
|
||||
@ -122,8 +135,21 @@ public class SimpleDebugger implements Debugger {
|
||||
.setNull("this");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean enabled = false;
|
||||
private class RunResult {
|
||||
public final Context ctx;
|
||||
public final Object result;
|
||||
public final EngineException error;
|
||||
|
||||
public RunResult(Context ctx, Object result, EngineException error) {
|
||||
this.ctx = ctx;
|
||||
this.result = result;
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean enabled = true;
|
||||
public CatchType execptionType = CatchType.ALL;
|
||||
public State state = State.RESUMED;
|
||||
|
||||
@ -145,6 +171,8 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
private HashMap<Integer, ObjectValue> idToObject = new HashMap<>();
|
||||
private HashMap<ObjectValue, Integer> objectToId = new HashMap<>();
|
||||
private HashMap<ObjectValue, Context> objectToCtx = new HashMap<>();
|
||||
private HashMap<String, ArrayList<ObjectValue>> objectGroups = new HashMap<>();
|
||||
|
||||
private Notifier updateNotifier = new Notifier();
|
||||
|
||||
@ -191,19 +219,19 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
return tail.first();
|
||||
}
|
||||
public Location deserializeLocation(JSONElement el, boolean correct) {
|
||||
private Location deserializeLocation(JSONElement el, boolean correct) {
|
||||
if (!el.isMap()) throw new RuntimeException("Expected location to be a map.");
|
||||
var id = Integer.parseInt(el.map().string("scriptId"));
|
||||
var line = (int)el.map().number("lineNumber") + 1;
|
||||
var column = (int)el.map().number("columnNumber") + 1;
|
||||
|
||||
if (!idToSource.containsKey(id)) throw new RuntimeException("The specified source %s doesn't exist.".formatted(id));
|
||||
if (!idToSource.containsKey(id)) throw new RuntimeException(String.format("The specified source %s doesn't exist.", id));
|
||||
|
||||
var res = new Location(line, column, idToSource.get(id).filename);
|
||||
if (correct) res = correctLocation(idToSource.get(id), res);
|
||||
return res;
|
||||
}
|
||||
public JSONMap serializeLocation(Location loc) {
|
||||
private JSONMap serializeLocation(Location loc) {
|
||||
var source = filenameToId.get(loc.filename());
|
||||
return new JSONMap()
|
||||
.set("scriptId", source + "")
|
||||
@ -211,21 +239,21 @@ public class SimpleDebugger implements Debugger {
|
||||
.set("columnNumber", loc.start() - 1);
|
||||
}
|
||||
|
||||
private Integer objectId(ObjectValue obj) {
|
||||
private Integer objectId(Context ctx, ObjectValue obj) {
|
||||
if (objectToId.containsKey(obj)) return objectToId.get(obj);
|
||||
else {
|
||||
int id = nextId();
|
||||
objectToId.put(obj, id);
|
||||
if (ctx != null) objectToCtx.put(obj, ctx);
|
||||
idToObject.put(id, obj);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
private JSONMap serializeObj(Context ctx, Object val) {
|
||||
private JSONMap serializeObj(Context ctx, Object val, boolean recurse) {
|
||||
val = Values.normalize(null, val);
|
||||
|
||||
if (val == Values.NULL) {
|
||||
return new JSONMap()
|
||||
.set("objectId", objectId(null) + "")
|
||||
.set("type", "object")
|
||||
.set("subtype", "null")
|
||||
.setNull("value")
|
||||
@ -234,13 +262,14 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
if (val instanceof ObjectValue) {
|
||||
var obj = (ObjectValue)val;
|
||||
var id = objectId(obj);
|
||||
var id = objectId(ctx, obj);
|
||||
var type = "object";
|
||||
String subtype = null;
|
||||
String className = null;
|
||||
|
||||
if (obj instanceof FunctionValue) type = "function";
|
||||
if (obj instanceof ArrayValue) subtype = "array";
|
||||
|
||||
if (Values.isWrapper(val, RegExpLib.class)) subtype = "regexp";
|
||||
if (Values.isWrapper(val, DateLib.class)) subtype = "date";
|
||||
if (Values.isWrapper(val, MapLib.class)) subtype = "map";
|
||||
@ -253,10 +282,13 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
var res = new JSONMap()
|
||||
.set("type", type)
|
||||
.set("objetId", id + "");
|
||||
.set("objectId", id + "");
|
||||
|
||||
if (subtype != null) res.set("subtype", subtype);
|
||||
if (className != null) res.set("className", className);
|
||||
if (className != null) {
|
||||
res.set("className", className);
|
||||
res.set("description", "{ " + className + " }");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@ -271,7 +303,7 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
if (Double.POSITIVE_INFINITY == num) res.set("unserializableValue", "Infinity");
|
||||
else if (Double.NEGATIVE_INFINITY == num) res.set("unserializableValue", "-Infinity");
|
||||
else if (num == -0.) res.set("unserializableValue", "-0");
|
||||
else if (Double.doubleToRawLongBits(num) == Double.doubleToRawLongBits(-0d)) res.set("unserializableValue", "-0");
|
||||
else if (Double.isNaN(num)) res.set("unserializableValue", "NaN");
|
||||
else res.set("value", num);
|
||||
|
||||
@ -280,6 +312,37 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
throw new IllegalArgumentException("Unexpected JS object.");
|
||||
}
|
||||
private JSONMap serializeObj(Context ctx, Object val) {
|
||||
return serializeObj(ctx, val, true);
|
||||
}
|
||||
private void setObjectGroup(String name, Object val) {
|
||||
if (val instanceof ObjectValue) {
|
||||
var obj = (ObjectValue)val;
|
||||
|
||||
if (objectGroups.containsKey(name)) objectGroups.get(name).add(obj);
|
||||
else objectGroups.put(name, new ArrayList<>(List.of(obj)));
|
||||
}
|
||||
}
|
||||
private void releaseGroup(String name) {
|
||||
var objs = objectGroups.remove(name);
|
||||
|
||||
if (objs != null) {
|
||||
for (var obj : objs) {
|
||||
var id = objectToId.remove(obj);
|
||||
if (id != null) idToObject.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
private Object deserializeArgument(JSONMap val) {
|
||||
if (val.isString("objectId")) return idToObject.get(Integer.parseInt(val.string("objectId")));
|
||||
else if (val.isString("unserializableValue")) switch (val.string("unserializableValue")) {
|
||||
case "NaN": return Double.NaN;
|
||||
case "-Infinity": return Double.NEGATIVE_INFINITY;
|
||||
case "Infinity": return Double.POSITIVE_INFINITY;
|
||||
case "-0": return -0.;
|
||||
}
|
||||
return JSON.toJs(val.get("value"));
|
||||
}
|
||||
|
||||
private void resume(State state) {
|
||||
this.state = state;
|
||||
@ -296,7 +359,7 @@ public class SimpleDebugger implements Debugger {
|
||||
ws.send(new V8Event("Debugger.paused", map));
|
||||
}
|
||||
private void pauseException(Context ctx) {
|
||||
state = State.PAUSED_NORMAL;
|
||||
state = State.PAUSED_EXCEPTION;
|
||||
var map = new JSONMap()
|
||||
.set("callFrames", serializeFrames(ctx))
|
||||
.set("reason", "exception");
|
||||
@ -322,6 +385,27 @@ public class SimpleDebugger implements Debugger {
|
||||
));
|
||||
}
|
||||
|
||||
private RunResult run(Frame codeFrame, String code) {
|
||||
var engine = new Engine();
|
||||
var env = codeFrame.func.environment.fork();
|
||||
|
||||
ObjectValue global = env.global.obj,
|
||||
capture = codeFrame.frame.getCaptureScope(null, enabled),
|
||||
local = codeFrame.frame.getLocalScope(null, enabled);
|
||||
|
||||
capture.setPrototype(null, global);
|
||||
local.setPrototype(null, capture);
|
||||
env.global = new GlobalScope(local);
|
||||
|
||||
var ctx = new Context(engine).pushEnv(env);
|
||||
var awaiter = engine.pushMsg(false, ctx, new Filename("temp", "exec"), code, codeFrame.frame.thisArg, codeFrame.frame.args);
|
||||
|
||||
engine.run(true);
|
||||
|
||||
try { return new RunResult(ctx, awaiter.await(), null); }
|
||||
catch (EngineException e) { return new RunResult(ctx, null, e); }
|
||||
}
|
||||
|
||||
@Override public void enable(V8Message msg) {
|
||||
enabled = true;
|
||||
ws.send(msg.respond());
|
||||
@ -342,9 +426,9 @@ public class SimpleDebugger implements Debugger {
|
||||
ws.send(msg.respond(new JSONMap().set("scriptSource", idToSource.get(id).source)));
|
||||
}
|
||||
@Override public void getPossibleBreakpoints(V8Message msg) {
|
||||
var src = idToSource.get(Integer.parseInt(msg.params.map("start").string("scriptId")));
|
||||
var start = deserializeLocation(msg.params.get("start"), false);
|
||||
var end = msg.params.isMap("end") ? deserializeLocation(msg.params.get("end"), false) : null;
|
||||
var src = idToSource.get(filenameToId.get(start.filename()));
|
||||
|
||||
var res = new JSONList();
|
||||
|
||||
@ -464,6 +548,101 @@ public class SimpleDebugger implements Debugger {
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void evaluateOnCallFrame(V8Message msg) {
|
||||
var cfId = Integer.parseInt(msg.params.string("callFrameId"));
|
||||
var expr = msg.params.string("expression");
|
||||
var group = msg.params.string("objectGroup", null);
|
||||
|
||||
var cf = idToFrame.get(cfId);
|
||||
var res = run(cf, expr);
|
||||
|
||||
if (group != null) setObjectGroup(group, res.result);
|
||||
|
||||
ws.send(msg.respond(new JSONMap().set("result", serializeObj(res.ctx, res.result))));
|
||||
}
|
||||
|
||||
@Override public void releaseObjectGroup(V8Message msg) {
|
||||
var group = msg.params.string("objectGroup");
|
||||
releaseGroup(group);
|
||||
ws.send(msg.respond());
|
||||
}
|
||||
@Override public void getProperties(V8Message msg) {
|
||||
var obj = idToObject.get(Integer.parseInt(msg.params.string("objectId")));
|
||||
var own = msg.params.bool("ownProperties") || true;
|
||||
var currOwn = true;
|
||||
|
||||
var res = new JSONList();
|
||||
|
||||
while (obj != null) {
|
||||
var ctx = objectToCtx.get(obj);
|
||||
|
||||
for (var key : obj.keys(true)) {
|
||||
var propDesc = new JSONMap();
|
||||
|
||||
if (obj.properties.containsKey(key)) {
|
||||
var prop = obj.properties.get(key);
|
||||
|
||||
propDesc.set("name", Values.toString(ctx, key));
|
||||
if (prop.getter != null) propDesc.set("get", serializeObj(ctx, prop.getter));
|
||||
if (prop.setter != null) propDesc.set("set", serializeObj(ctx, prop.setter));
|
||||
propDesc.set("enumerable", obj.memberEnumerable(key));
|
||||
propDesc.set("configurable", obj.memberConfigurable(key));
|
||||
propDesc.set("isOwn", currOwn);
|
||||
res.add(propDesc);
|
||||
}
|
||||
else {
|
||||
propDesc.set("name", Values.toString(ctx, key));
|
||||
propDesc.set("value", serializeObj(ctx, obj.getMember(ctx, key)));
|
||||
propDesc.set("writable", obj.memberWritable(key));
|
||||
propDesc.set("enumerable", obj.memberEnumerable(key));
|
||||
propDesc.set("configurable", obj.memberConfigurable(key));
|
||||
propDesc.set("isOwn", currOwn);
|
||||
res.add(propDesc);
|
||||
}
|
||||
}
|
||||
|
||||
obj = obj.getPrototype(ctx);
|
||||
|
||||
var protoDesc = new JSONMap();
|
||||
protoDesc.set("name", "__proto__");
|
||||
protoDesc.set("value", serializeObj(ctx, obj == null ? Values.NULL : obj));
|
||||
protoDesc.set("writable", true);
|
||||
protoDesc.set("enumerable", false);
|
||||
protoDesc.set("configurable", false);
|
||||
protoDesc.set("isOwn", currOwn);
|
||||
res.add(protoDesc);
|
||||
|
||||
currOwn = false;
|
||||
if (own) break;
|
||||
}
|
||||
|
||||
ws.send(msg.respond(new JSONMap().set("result", res)));
|
||||
}
|
||||
@Override public void callFunctionOn(V8Message msg) {
|
||||
var src = msg.params.string("functionDeclaration");
|
||||
var args = msg.params.list("arguments", new JSONList()).stream().map(v -> v.map()).map(this::deserializeArgument).collect(Collectors.toList());
|
||||
var thisArg = idToObject.get(Integer.parseInt(msg.params.string("objectId")));
|
||||
var ctx = objectToCtx.get(thisArg);
|
||||
|
||||
switch (src) {
|
||||
case CHROME_GET_PROP_FUNC: {
|
||||
var path = JSON.parse(new Filename("tmp", "json"), (String)args.get(0)).list();
|
||||
Object res = thisArg;
|
||||
for (var el : path) res = Values.getMember(ctx, res, JSON.toJs(el));
|
||||
ws.send(msg.respond(new JSONMap().set("result", serializeObj(ctx, res))));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
ws.send(new V8Error("A non well-known function was used with callFunctionOn."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runtimeEnable(V8Message msg) {
|
||||
ws.send(msg.respond());
|
||||
}
|
||||
|
||||
@Override public void onSource(Filename filename, String source, TreeSet<Location> locations) {
|
||||
int id = nextId();
|
||||
var src = new Source(id, filename, source, locations);
|
||||
@ -488,6 +667,9 @@ public class SimpleDebugger implements Debugger {
|
||||
|
||||
updateFrames(ctx);
|
||||
var frame = codeFrameToFrame.get(cf);
|
||||
|
||||
if (!frame.debugData) return false;
|
||||
|
||||
if (instruction.location != null) frame.updateLoc(instruction.location);
|
||||
var loc = frame.location;
|
||||
var isBreakpointable = loc != null && (
|
||||
@ -501,15 +683,12 @@ public class SimpleDebugger implements Debugger {
|
||||
pauseException(ctx);
|
||||
}
|
||||
else if (isBreakpointable && locToBreakpoint.containsKey(loc)) {
|
||||
pauseDebug(ctx, locToBreakpoint.get(loc));
|
||||
}
|
||||
else if (isBreakpointable && tmpBreakpts.contains(loc)) {
|
||||
pauseDebug(ctx, null);
|
||||
tmpBreakpts.remove(loc);
|
||||
}
|
||||
else if (instruction.type == Type.NOP && instruction.match("debug")) {
|
||||
pauseDebug(ctx, null);
|
||||
var bp = locToBreakpoint.get(loc);
|
||||
var ok = bp.condition == null ? true : Values.toBoolean(run(currFrame, bp.condition));
|
||||
if (ok) pauseDebug(ctx, locToBreakpoint.get(loc));
|
||||
}
|
||||
else if (isBreakpointable && tmpBreakpts.remove(loc)) pauseDebug(ctx, null);
|
||||
else if (instruction.type == Type.NOP && instruction.match("debug")) pauseDebug(ctx, null);
|
||||
|
||||
while (enabled) {
|
||||
switch (state) {
|
||||
@ -527,12 +706,17 @@ public class SimpleDebugger implements Debugger {
|
||||
else return false;
|
||||
break;
|
||||
case STEPPING_OVER:
|
||||
if (
|
||||
stepOutFrame == frame && (
|
||||
if (stepOutFrame.frame == frame.frame) {
|
||||
if (returnVal != Runners.NO_RETURN) {
|
||||
state = State.STEPPING_OUT;
|
||||
return false;
|
||||
}
|
||||
else if (isBreakpointable && (
|
||||
!loc.filename().equals(prevLocation.filename()) ||
|
||||
loc.line() != prevLocation.line()
|
||||
)
|
||||
) pauseDebug(ctx, null);
|
||||
)) pauseDebug(ctx, null);
|
||||
else return false;
|
||||
}
|
||||
else return false;
|
||||
break;
|
||||
}
|
||||
@ -544,14 +728,12 @@ public class SimpleDebugger implements Debugger {
|
||||
@Override public void onFramePop(Context ctx, CodeFrame frame) {
|
||||
updateFrames(ctx);
|
||||
|
||||
try {
|
||||
idToFrame.remove(codeFrameToFrame.remove(frame).id);
|
||||
}
|
||||
try { idToFrame.remove(codeFrameToFrame.remove(frame).id); }
|
||||
catch (NullPointerException e) { }
|
||||
|
||||
if (StackData.frames(ctx).size() == 0) resume(State.RESUMED);
|
||||
else if (stepOutFrame != null && stepOutFrame.frame == frame &&
|
||||
(state == State.STEPPING_OUT || state == State.STEPPING_IN || state == State.STEPPING_OVER)
|
||||
(state == State.STEPPING_OUT || state == State.STEPPING_IN)
|
||||
) {
|
||||
pauseDebug(ctx, null);
|
||||
updateNotifier.await();
|
||||
|
@ -7,7 +7,6 @@ import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
|
||||
import me.topchetoeu.jscript.exceptions.UncheckedException;
|
||||
import me.topchetoeu.jscript.exceptions.UncheckedIOException;
|
||||
|
||||
public class WebSocket implements AutoCloseable {
|
||||
|
@ -10,6 +10,8 @@ import me.topchetoeu.jscript.engine.scope.LocalScope;
|
||||
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.ScopeValue;
|
||||
import me.topchetoeu.jscript.engine.values.Values;
|
||||
import me.topchetoeu.jscript.exceptions.EngineException;
|
||||
import me.topchetoeu.jscript.exceptions.InterruptException;
|
||||
@ -56,6 +58,33 @@ public class CodeFrame {
|
||||
public boolean jumpFlag = false;
|
||||
private Location prevLoc = null;
|
||||
|
||||
public ObjectValue getLocalScope(Context ctx, boolean props) {
|
||||
var names = new String[scope.locals.length];
|
||||
|
||||
for (int i = 0; i < scope.locals.length; i++) {
|
||||
var name = "local_" + (i - 2);
|
||||
|
||||
if (i == 0) name = "this";
|
||||
else if (i == 1) name = "arguments";
|
||||
else if (i < function.localNames.length) name = function.localNames[i];
|
||||
|
||||
names[i] = name;
|
||||
}
|
||||
|
||||
return new ScopeValue(scope.locals, names);
|
||||
}
|
||||
public ObjectValue getCaptureScope(Context ctx, boolean props) {
|
||||
var names = new String[scope.captures.length];
|
||||
|
||||
for (int i = 0; i < scope.captures.length; i++) {
|
||||
var name = "capture_" + (i - 2);
|
||||
if (i < function.captureNames.length) name = function.captureNames[i];
|
||||
names[i] = name;
|
||||
}
|
||||
|
||||
return new ScopeValue(scope.captures, names);
|
||||
}
|
||||
|
||||
public void addTry(int n, int catchN, int finallyN) {
|
||||
var res = new TryCtx(codePtr + 1, n, catchN, finallyN);
|
||||
|
||||
@ -144,7 +173,7 @@ public class CodeFrame {
|
||||
}
|
||||
else if (returnValue != Runners.NO_RETURN) {
|
||||
if (tryCtx.hasFinally) {
|
||||
tryCtx.retVal = error;
|
||||
tryCtx.retVal = returnValue;
|
||||
newState = TryCtx.STATE_FINALLY_RETURNED;
|
||||
}
|
||||
break;
|
||||
@ -215,6 +244,7 @@ public class CodeFrame {
|
||||
case TryCtx.STATE_CATCH:
|
||||
scope.catchVars.add(new ValueVariable(false, tryCtx.err.value));
|
||||
codePtr = tryCtx.catchStart;
|
||||
if (debugger != null) debugger.onInstruction(ctx, this, function.body[codePtr], null, error, true);
|
||||
break;
|
||||
default:
|
||||
codePtr = tryCtx.finallyStart;
|
||||
|
@ -301,15 +301,6 @@ public class Runners {
|
||||
return NO_RETURN;
|
||||
}
|
||||
public static Object execNop(Context ctx, Instruction instr, CodeFrame frame) {
|
||||
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;
|
||||
}
|
||||
|
@ -2,12 +2,7 @@ package me.topchetoeu.jscript.engine.scope;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import me.topchetoeu.jscript.engine.Context;
|
||||
import me.topchetoeu.jscript.engine.values.NativeFunction;
|
||||
import me.topchetoeu.jscript.engine.values.ObjectValue;
|
||||
|
||||
public class LocalScope {
|
||||
private String[] names;
|
||||
public final ValueVariable[] captures;
|
||||
public final ValueVariable[] locals;
|
||||
public final ArrayList<ValueVariable> catchVars = new ArrayList<>();
|
||||
@ -19,70 +14,11 @@ public class LocalScope {
|
||||
else return captures[~i];
|
||||
}
|
||||
|
||||
public String[] getCaptureNames() {
|
||||
var res = new String[captures.length];
|
||||
|
||||
for (int i = 0; i < captures.length; i++) {
|
||||
if (names == null || i >= names.length) res[i] = "capture_" + (i);
|
||||
else res[i] = names[i];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
public String[] getLocalNames() {
|
||||
var res = new String[locals.length];
|
||||
|
||||
for (int i = captures.length, j = 0; i < locals.length; i++, j++) {
|
||||
if (names == null || i >= names.length) {
|
||||
if (j == 0) res[j] = "this";
|
||||
else if (j == 1) res[j] = "arguments";
|
||||
else res[i] = "local_" + (j - 2);
|
||||
}
|
||||
else res[i] = names[i];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
public void setNames(String[] names) {
|
||||
this.names = names;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return captures.length + locals.length;
|
||||
}
|
||||
|
||||
public void applyToObject(Context ctx, ObjectValue locals, ObjectValue captures, boolean props) {
|
||||
var localNames = getLocalNames();
|
||||
var captureNames = getCaptureNames();
|
||||
|
||||
for (var i = 0; i < this.locals.length; i++) {
|
||||
var name = localNames[i];
|
||||
var _i = i;
|
||||
|
||||
if (props) locals.defineProperty(ctx, name,
|
||||
new NativeFunction(name, (_ctx, thisArg, args) -> this.locals[_i].get(_ctx)),
|
||||
this.locals[i].readonly ? null :
|
||||
new NativeFunction(name, (_ctx, thisArg, args) -> { this.locals[_i].set(_ctx, args.length < 1 ? null : args[0]); return null; }),
|
||||
true, true
|
||||
);
|
||||
else locals.defineProperty(ctx, name, this.locals[i].get(ctx));
|
||||
}
|
||||
for (var i = 0; i < this.captures.length; i++) {
|
||||
var name = captureNames[i];
|
||||
var _i = i;
|
||||
|
||||
if (props) captures.defineProperty(ctx, name,
|
||||
new NativeFunction(name, (_ctx, thisArg, args) -> this.captures[_i].get(_ctx)),
|
||||
this.captures[i].readonly ? null :
|
||||
new NativeFunction(name, (_ctx, thisArg, args) -> { this.captures[_i].set(_ctx, args.length < 1 ? null : args[0]); return null; }),
|
||||
true, true
|
||||
);
|
||||
else captures.defineProperty(ctx, name, this.captures[i].get(ctx));
|
||||
}
|
||||
|
||||
captures.setPrototype(ctx, locals);
|
||||
}
|
||||
|
||||
public LocalScope(int n, ValueVariable[] captures) {
|
||||
locals = new ValueVariable[n];
|
||||
this.captures = captures;
|
||||
|
@ -11,6 +11,9 @@ public class LocalScopeRecord implements ScopeRecord {
|
||||
private final ArrayList<String> captures = new ArrayList<>();
|
||||
private final ArrayList<String> locals = new ArrayList<>();
|
||||
|
||||
public String[] captures() {
|
||||
return captures.toArray(String[]::new);
|
||||
}
|
||||
public String[] locals() {
|
||||
return locals.toArray(String[]::new);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package me.topchetoeu.jscript.engine.values;
|
||||
|
||||
import me.topchetoeu.jscript.Location;
|
||||
import me.topchetoeu.jscript.compilation.FunctionBody;
|
||||
import me.topchetoeu.jscript.compilation.Instruction;
|
||||
import me.topchetoeu.jscript.engine.Context;
|
||||
import me.topchetoeu.jscript.engine.Environment;
|
||||
@ -13,6 +14,7 @@ public class CodeFunction extends FunctionValue {
|
||||
public final int localsN;
|
||||
public final int length;
|
||||
public final Instruction[] body;
|
||||
public final String[] captureNames, localNames;
|
||||
public final ValueVariable[] captures;
|
||||
public Environment environment;
|
||||
|
||||
@ -45,12 +47,14 @@ public class CodeFunction extends FunctionValue {
|
||||
}
|
||||
}
|
||||
|
||||
public CodeFunction(Environment environment, String name, int localsN, int length, ValueVariable[] captures, Instruction[] body) {
|
||||
public CodeFunction(Environment environment, String name, int localsN, int length, ValueVariable[] captures, FunctionBody body) {
|
||||
super(name, length);
|
||||
this.captures = captures;
|
||||
this.captureNames = body.captureNames;
|
||||
this.localNames = body.localNames;
|
||||
this.environment = environment;
|
||||
this.localsN = localsN;
|
||||
this.length = length;
|
||||
this.body = body;
|
||||
this.body = body.instructions;
|
||||
}
|
||||
}
|
||||
|
51
src/me/topchetoeu/jscript/engine/values/ScopeValue.java
Normal file
51
src/me/topchetoeu/jscript/engine/values/ScopeValue.java
Normal file
@ -0,0 +1,51 @@
|
||||
package me.topchetoeu.jscript.engine.values;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import me.topchetoeu.jscript.engine.Context;
|
||||
import me.topchetoeu.jscript.engine.scope.ValueVariable;
|
||||
|
||||
public class ScopeValue extends ObjectValue {
|
||||
public final ValueVariable[] variables;
|
||||
public final HashMap<String, Integer> names = new HashMap<>();
|
||||
|
||||
@Override
|
||||
protected Object getField(Context ctx, Object key) {
|
||||
if (names.containsKey(key)) return variables[names.get(key)].get(ctx);
|
||||
return super.getField(ctx, key);
|
||||
}
|
||||
@Override
|
||||
protected boolean setField(Context ctx, Object key, Object val) {
|
||||
if (names.containsKey(key)) {
|
||||
variables[names.get(key)].set(ctx, val);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.setField(ctx, key, val);
|
||||
}
|
||||
@Override
|
||||
protected void deleteField(Context ctx, Object key) {
|
||||
if (names.containsKey(key)) return;
|
||||
super.deleteField(ctx, key);
|
||||
}
|
||||
@Override
|
||||
protected boolean hasField(Context ctx, Object key) {
|
||||
if (names.containsKey(key)) return true;
|
||||
return super.hasField(ctx, key);
|
||||
}
|
||||
@Override
|
||||
public List<Object> keys(boolean includeNonEnumerable) {
|
||||
var res = super.keys(includeNonEnumerable);
|
||||
res.addAll(names.keySet());
|
||||
return res;
|
||||
}
|
||||
|
||||
public ScopeValue(ValueVariable[] variables, String[] names) {
|
||||
this.variables = variables;
|
||||
for (var i = 0; i < names.length && i < variables.length; i++) {
|
||||
this.names.put(names[i], i);
|
||||
this.nonConfigurableSet.add(names[i]);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,15 @@
|
||||
package me.topchetoeu.jscript.json;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.topchetoeu.jscript.Filename;
|
||||
import me.topchetoeu.jscript.engine.Context;
|
||||
import me.topchetoeu.jscript.engine.values.ArrayValue;
|
||||
import me.topchetoeu.jscript.engine.values.ObjectValue;
|
||||
import me.topchetoeu.jscript.engine.values.Values;
|
||||
import me.topchetoeu.jscript.exceptions.EngineException;
|
||||
import me.topchetoeu.jscript.exceptions.SyntaxException;
|
||||
import me.topchetoeu.jscript.parsing.Operator;
|
||||
import me.topchetoeu.jscript.parsing.ParseRes;
|
||||
@ -11,6 +17,63 @@ import me.topchetoeu.jscript.parsing.Parsing;
|
||||
import me.topchetoeu.jscript.parsing.Token;
|
||||
|
||||
public class JSON {
|
||||
public static Object toJs(JSONElement val) {
|
||||
if (val.isBoolean()) return val.bool();
|
||||
if (val.isString()) return val.string();
|
||||
if (val.isNumber()) return val.number();
|
||||
if (val.isList()) return ArrayValue.of(null, val.list().stream().map(JSON::toJs).collect(Collectors.toList()));
|
||||
if (val.isMap()) {
|
||||
var res = new ObjectValue();
|
||||
for (var el : val.map().entrySet()) {
|
||||
res.defineProperty(null, el.getKey(), toJs(el.getValue()));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
if (val.isNull()) return Values.NULL;
|
||||
return null;
|
||||
}
|
||||
private static JSONElement fromJs(Context ctx, Object val, HashSet<Object> prev) {
|
||||
if (val instanceof Boolean) return JSONElement.bool((boolean)val);
|
||||
if (val instanceof Number) return JSONElement.number(((Number)val).doubleValue());
|
||||
if (val instanceof String) return JSONElement.string((String)val);
|
||||
if (val == Values.NULL) return JSONElement.NULL;
|
||||
if (val instanceof ObjectValue) {
|
||||
if (prev.contains(val)) throw new EngineException("Circular dependency in JSON.");
|
||||
prev.add(val);
|
||||
|
||||
var res = new JSONMap();
|
||||
|
||||
for (var el : ((ObjectValue)val).keys(false)) {
|
||||
var jsonEl = fromJs(ctx, ((ObjectValue)val).getMember(ctx, el), prev);
|
||||
if (jsonEl == null) continue;
|
||||
if (el instanceof String || el instanceof Number) res.put(el.toString(), jsonEl);
|
||||
}
|
||||
|
||||
prev.remove(val);
|
||||
return JSONElement.of(res);
|
||||
}
|
||||
if (val instanceof ArrayValue) {
|
||||
if (prev.contains(val)) throw new EngineException("Circular dependency in JSON.");
|
||||
prev.add(val);
|
||||
|
||||
var res = new JSONList();
|
||||
|
||||
for (var el : ((ArrayValue)val).toArray()) {
|
||||
var jsonEl = fromJs(ctx, el, prev);
|
||||
if (jsonEl == null) jsonEl = JSONElement.NULL;
|
||||
res.add(jsonEl);
|
||||
}
|
||||
|
||||
prev.remove(val);
|
||||
return JSONElement.of(res);
|
||||
}
|
||||
if (val == null) return null;
|
||||
return null;
|
||||
}
|
||||
public static JSONElement fromJs(Context ctx, Object val) {
|
||||
return fromJs(ctx, val, new HashSet<>());
|
||||
}
|
||||
|
||||
public static ParseRes<String> parseIdentifier(List<Token> tokens, int i) {
|
||||
return Parsing.parseIdentifier(tokens, i);
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ public class JSONList extends ArrayList<JSONElement> {
|
||||
public JSONList(JSONElement ...els) {
|
||||
super(List.of(els));
|
||||
}
|
||||
public JSONList(Collection<JSONElement> els) {
|
||||
super(els);
|
||||
}
|
||||
|
||||
public JSONList addNull() { this.add(JSONElement.NULL); return this; }
|
||||
public JSONList add(String val) { this.add(JSONElement.of(val)); return this; }
|
||||
|
@ -1,84 +1,22 @@
|
||||
package me.topchetoeu.jscript.lib;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.topchetoeu.jscript.Filename;
|
||||
import me.topchetoeu.jscript.engine.Context;
|
||||
import me.topchetoeu.jscript.engine.values.ArrayValue;
|
||||
import me.topchetoeu.jscript.engine.values.ObjectValue;
|
||||
import me.topchetoeu.jscript.engine.values.Values;
|
||||
import me.topchetoeu.jscript.exceptions.EngineException;
|
||||
import me.topchetoeu.jscript.exceptions.SyntaxException;
|
||||
import me.topchetoeu.jscript.interop.Native;
|
||||
import me.topchetoeu.jscript.json.JSONElement;
|
||||
import me.topchetoeu.jscript.json.JSONList;
|
||||
import me.topchetoeu.jscript.json.JSONMap;
|
||||
import me.topchetoeu.jscript.json.JSON;
|
||||
|
||||
public class JSONLib {
|
||||
private static Object toJS(JSONElement val) {
|
||||
if (val.isBoolean()) return val.bool();
|
||||
if (val.isString()) return val.string();
|
||||
if (val.isNumber()) return val.number();
|
||||
if (val.isList()) return ArrayValue.of(null, val.list().stream().map(JSONLib::toJS).collect(Collectors.toList()));
|
||||
if (val.isMap()) {
|
||||
var res = new ObjectValue();
|
||||
for (var el : val.map().entrySet()) {
|
||||
res.defineProperty(null, el.getKey(), toJS(el.getValue()));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
if (val.isNull()) return Values.NULL;
|
||||
return null;
|
||||
}
|
||||
private static JSONElement toJSON(Context ctx, Object val, HashSet<Object> prev) {
|
||||
if (val instanceof Boolean) return JSONElement.bool((boolean)val);
|
||||
if (val instanceof Number) return JSONElement.number(((Number)val).doubleValue());
|
||||
if (val instanceof String) return JSONElement.string((String)val);
|
||||
if (val == Values.NULL) return JSONElement.NULL;
|
||||
if (val instanceof ObjectValue) {
|
||||
if (prev.contains(val)) throw new EngineException("Circular dependency in JSON.");
|
||||
prev.add(val);
|
||||
|
||||
var res = new JSONMap();
|
||||
|
||||
for (var el : ((ObjectValue)val).keys(false)) {
|
||||
var jsonEl = toJSON(ctx, ((ObjectValue)val).getMember(ctx, el), prev);
|
||||
if (jsonEl == null) continue;
|
||||
if (el instanceof String || el instanceof Number) res.put(el.toString(), jsonEl);
|
||||
}
|
||||
|
||||
prev.remove(val);
|
||||
return JSONElement.of(res);
|
||||
}
|
||||
if (val instanceof ArrayValue) {
|
||||
if (prev.contains(val)) throw new EngineException("Circular dependency in JSON.");
|
||||
prev.add(val);
|
||||
|
||||
var res = new JSONList();
|
||||
|
||||
for (var el : ((ArrayValue)val).toArray()) {
|
||||
var jsonEl = toJSON(ctx, el, prev);
|
||||
if (jsonEl == null) jsonEl = JSONElement.NULL;
|
||||
res.add(jsonEl);
|
||||
}
|
||||
|
||||
prev.remove(val);
|
||||
return JSONElement.of(res);
|
||||
}
|
||||
if (val == null) return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Native
|
||||
public static Object parse(Context ctx, String val) {
|
||||
try {
|
||||
return toJS(me.topchetoeu.jscript.json.JSON.parse(new Filename("jscript", "json"), val));
|
||||
return JSON.toJs(JSON.parse(new Filename("jscript", "json"), val));
|
||||
}
|
||||
catch (SyntaxException e) { throw EngineException.ofSyntax(e.msg); }
|
||||
}
|
||||
@Native
|
||||
public static String stringify(Context ctx, Object val) {
|
||||
return me.topchetoeu.jscript.json.JSON.stringify(toJSON(ctx, val, new HashSet<>()));
|
||||
return me.topchetoeu.jscript.json.JSON.stringify(JSON.fromJs(ctx, val));
|
||||
}
|
||||
}
|
||||
|
@ -1367,6 +1367,7 @@ public class Parsing {
|
||||
}
|
||||
|
||||
while (true) {
|
||||
var nameLoc = getLoc(filename, tokens, i + n);
|
||||
var nameRes = parseIdentifier(tokens, i + n++);
|
||||
if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a variable name.");
|
||||
|
||||
@ -1384,7 +1385,7 @@ public class Parsing {
|
||||
val = valRes.result;
|
||||
}
|
||||
|
||||
res.add(new Pair(nameRes.result, val));
|
||||
res.add(new Pair(nameRes.result, val, nameLoc));
|
||||
|
||||
if (isOperator(tokens, i + n, Operator.COMMA)) {
|
||||
n++;
|
||||
@ -1512,7 +1513,7 @@ public class Parsing {
|
||||
statements.add(res.result);
|
||||
}
|
||||
|
||||
return ParseRes.res(new CompoundStatement(loc, statements.toArray(Statement[]::new)), n);
|
||||
return ParseRes.res(new CompoundStatement(loc, statements.toArray(Statement[]::new)).setEnd(getLoc(filename, tokens, i + n - 1)), n);
|
||||
}
|
||||
public static ParseRes<String> parseLabel(List<Token> tokens, int i) {
|
||||
int n = 0;
|
||||
@ -1876,7 +1877,7 @@ public class Parsing {
|
||||
return list.toArray(Statement[]::new);
|
||||
}
|
||||
|
||||
public static CodeFunction compile(HashMap<Long, Instruction[]> funcs, TreeSet<Location> breakpoints, Environment environment, Statement ...statements) {
|
||||
public static CodeFunction compile(HashMap<Long, FunctionBody> funcs, TreeSet<Location> breakpoints, Environment environment, Statement ...statements) {
|
||||
var target = environment.global.globalChild();
|
||||
var subscope = target.child();
|
||||
var res = new CompileTarget(funcs, breakpoints);
|
||||
@ -1903,14 +1904,14 @@ public class Parsing {
|
||||
}
|
||||
else res.add(Instruction.ret());
|
||||
|
||||
return new CodeFunction(environment, "", subscope.localsCount(), 0, new ValueVariable[0], res.array());
|
||||
return new CodeFunction(environment, "", subscope.localsCount(), 0, new ValueVariable[0], new FunctionBody(res.array(), subscope.captures(), subscope.locals()));
|
||||
}
|
||||
public static CodeFunction compile(HashMap<Long, Instruction[]> funcs, TreeSet<Location> breakpoints, Environment environment, Filename filename, String raw) {
|
||||
public static CodeFunction compile(HashMap<Long, FunctionBody> funcs, TreeSet<Location> breakpoints, Environment environment, Filename filename, String raw) {
|
||||
try {
|
||||
return compile(funcs, breakpoints, environment, parse(filename, raw));
|
||||
}
|
||||
catch (SyntaxException e) {
|
||||
return new CodeFunction(environment, null, 2, 0, new ValueVariable[0], new Instruction[] { Instruction.throwSyntax(e) });
|
||||
return new CodeFunction(environment, null, 2, 0, new ValueVariable[0], new FunctionBody(new Instruction[] { Instruction.throwSyntax(e).locate(e.loc) }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user