feat: fully implement local and capture scope object wrappers

feat: implement object sending and receiving
This commit is contained in:
TopchetoEU 2023-10-28 16:58:33 +03:00
parent 4b84309df6
commit edb71daef4
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
27 changed files with 535 additions and 236 deletions

View File

@ -16,14 +16,15 @@
<p> <p>
Here are the available entrypoints: Here are the available entrypoints:
<ul> <ul>
<li><a href="json/version">/json/version</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></li> <li><a href="json/list">/json/list</a> - a list of all entrypoints</li>
<li>/(any target)</a></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> </ul>
</p> </p>
<p> <p>
Developed by TopchetoEU, MIT License Running ${NAME} v${VERSION} by ${AUTHOR}
</p> </p>
</body> </body>
</html> </html>

View File

@ -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"
}
]
}
]
} }
] ]
} }

View File

@ -23,6 +23,7 @@ public class Main {
static Thread engineTask, debugTask; static Thread engineTask, debugTask;
static Engine engine; static Engine engine;
static Environment env; static Environment env;
static int j = 0;
private static Observer<Object> valuePrinter = new Observer<Object>() { private static Observer<Object> valuePrinter = new Observer<Object>() {
public void next(Object data) { public void next(Object data) {
@ -59,7 +60,7 @@ public class Main {
env.global.define("go", _ctx -> { env.global.define("go", _ctx -> {
try { try {
var f = Path.of("do.js"); 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); return func.call(_ctx);
} }
catch (IOException e) { catch (IOException e) {

View File

@ -8,7 +8,7 @@ import me.topchetoeu.jscript.Location;
public class CompileTarget { public class CompileTarget {
public final Vector<Instruction> target = new Vector<>(); 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 final TreeSet<Location> breakpoints;
public Instruction add(Instruction instr) { public Instruction add(Instruction instr) {
@ -31,7 +31,7 @@ public class CompileTarget {
public Instruction[] array() { return target.toArray(Instruction[]::new); } 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.functions = functions;
this.breakpoints = breakpoints; this.breakpoints = breakpoints;
} }

View File

@ -11,6 +11,7 @@ import me.topchetoeu.jscript.engine.scope.ScopeRecord;
public class CompoundStatement extends Statement { public class CompoundStatement extends Statement {
public final Statement[] statements; public final Statement[] statements;
public Location end;
@Override @Override
public void declare(ScopeRecord varsScope) { public void declare(ScopeRecord varsScope) {
@ -32,11 +33,16 @@ public class CompoundStatement extends Statement {
for (var i = 0; i < statements.length; i++) { for (var i = 0; i < statements.length; i++) {
var stm = statements[i]; var stm = statements[i];
if (stm instanceof FunctionStatement) continue; if (stm instanceof FunctionStatement) continue;
if (i != statements.length - 1) stm.compileWithDebug(target, scope, false); if (i != statements.length - 1) stm.compileWithDebug(target, scope, false);
else stm.compileWithDebug(target, scope, pollute); else stm.compileWithDebug(target, scope, pollute);
} }
if (end != null) {
target.add(Instruction.nop().locate(end));
target.setDebug();
}
} }
@Override @Override
@ -59,6 +65,11 @@ public class CompoundStatement extends Statement {
else return new CompoundStatement(loc(), res.toArray(Statement[]::new)); 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) { public CompoundStatement(Location loc, Statement ...statements) {
super(loc); super(loc);
this.statements = statements; this.statements = statements;

View 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];
}
}

View File

@ -147,14 +147,6 @@ public class Instruction {
public static Instruction debug() { public static Instruction debug() {
return new Instruction(null, Type.NOP, "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) { public static Instruction nop(Object ...params) {
for (var param : params) { for (var param : params) {

View File

@ -10,10 +10,12 @@ public class VariableDeclareStatement extends Statement {
public static class Pair { public static class Pair {
public final String name; public final String name;
public final Statement value; 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.name = name;
this.value = value; this.value = value;
this.location = location;
} }
} }
@ -30,16 +32,20 @@ public class VariableDeclareStatement extends Statement {
for (var entry : values) { for (var entry : values) {
if (entry.name == null) continue; if (entry.name == null) continue;
var key = scope.getKey(entry.name); 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) { if (entry.value instanceof FunctionStatement) {
((FunctionStatement)entry.value).compile(target, scope, entry.name, false); ((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) { else if (entry.value != null) {
entry.value.compile(target, scope, true); 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())); if (pollute) target.add(Instruction.loadValue(null).locate(loc()));

View File

@ -16,7 +16,7 @@ public class CallStatement extends Statement {
((IndexStatement)func).compile(target, scope, true, true); ((IndexStatement)func).compile(target, scope, true, true);
} }
else { else {
target.add(Instruction.loadValue(null).locate(loc())); target.add(Instruction.loadValue(null).locate(loc()));
func.compile(target, scope, true); func.compile(target, scope, true);
} }

View File

@ -5,6 +5,7 @@ import java.util.Random;
import me.topchetoeu.jscript.Location; import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.CompileTarget; import me.topchetoeu.jscript.compilation.CompileTarget;
import me.topchetoeu.jscript.compilation.CompoundStatement; import me.topchetoeu.jscript.compilation.CompoundStatement;
import me.topchetoeu.jscript.compilation.FunctionBody;
import me.topchetoeu.jscript.compilation.Instruction; import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Statement; import me.topchetoeu.jscript.compilation.Statement;
import me.topchetoeu.jscript.compilation.Instruction.Type; import me.topchetoeu.jscript.compilation.Instruction.Type;
@ -69,7 +70,6 @@ public class FunctionStatement extends Statement {
} }
body.declare(subscope); body.declare(subscope);
funcTarget.add(Instruction.debugVarNames(subscope.locals()));
body.compile(funcTarget, subscope, false); body.compile(funcTarget, subscope, false);
funcTarget.add(Instruction.ret().locate(loc())); funcTarget.add(Instruction.ret().locate(loc()));
checkBreakAndCont(funcTarget, start); checkBreakAndCont(funcTarget, start);
@ -77,7 +77,7 @@ public class FunctionStatement extends Statement {
var id = rand.nextLong(); var id = rand.nextLong();
target.add(Instruction.loadFunc(id, subscope.localsCount(), args.length, subscope.getCaptures()).locate(loc())); 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; if (name == null) name = this.name;

View File

@ -20,17 +20,17 @@ public class IndexStatement extends AssignableStatement {
return new IndexAssignStatement(loc(), object, index, val, operation); return new IndexAssignStatement(loc(), object, index, val, operation);
} }
public void compile(CompileTarget target, ScopeRecord scope, boolean dupObj, boolean pollute) { public void compile(CompileTarget target, ScopeRecord scope, boolean dupObj, boolean pollute) {
int start = 0;
object.compile(target, scope, true); object.compile(target, scope, true);
if (dupObj) target.add(Instruction.dup().locate(loc())); if (dupObj) target.add(Instruction.dup().locate(loc()));
if (index instanceof ConstantStatement) { if (index instanceof ConstantStatement) {
target.add(Instruction.loadMember(((ConstantStatement)index).value).locate(loc())); target.add(Instruction.loadMember(((ConstantStatement)index).value).locate(loc()));
target.setDebug();
return; return;
} }
index.compile(target, scope, true); index.compile(target, scope, true);
target.add(Instruction.loadMember().locate(loc())); target.add(Instruction.loadMember().locate(loc()));
target.setDebug(start); target.setDebug();
if (!pollute) target.add(Instruction.discard().locate(loc())); if (!pollute) target.add(Instruction.discard().locate(loc()));
} }
@Override @Override

View File

@ -4,7 +4,7 @@ import java.util.HashMap;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingDeque;
import me.topchetoeu.jscript.Filename; 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.engine.values.FunctionValue;
import me.topchetoeu.jscript.events.Awaitable; import me.topchetoeu.jscript.events.Awaitable;
import me.topchetoeu.jscript.events.DataNotifier; import me.topchetoeu.jscript.events.DataNotifier;
@ -51,7 +51,7 @@ public class Engine {
private LinkedBlockingDeque<Task> microTasks = new LinkedBlockingDeque<>(); private LinkedBlockingDeque<Task> microTasks = new LinkedBlockingDeque<>();
public final int id = ++nextId; 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); public final Data data = new Data().set(StackData.MAX_FRAMES, 10000);
private void runTask(Task task) { private void runTask(Task task) {
@ -63,8 +63,8 @@ public class Engine {
task.notifier.error(e); task.notifier.error(e);
} }
} }
private void run() { public void run(boolean untilEmpty) {
while (true) { while (!untilEmpty || !macroTasks.isEmpty()) {
try { try {
runTask(macroTasks.take()); runTask(macroTasks.take());
@ -83,7 +83,7 @@ public class Engine {
public Thread start() { public Thread start() {
if (this.thread == null) { 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(); this.thread.start();
} }
return this.thread; return this.thread;

View File

@ -20,4 +20,16 @@ public interface DebugHandler {
void stepOver(V8Message msg); void stepOver(V8Message msg);
void setPauseOnExceptions(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);
} }

View File

@ -8,6 +8,7 @@ import java.security.MessageDigest;
import java.util.Base64; import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import me.topchetoeu.jscript.Metadata;
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type; import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
import me.topchetoeu.jscript.exceptions.SyntaxException; import me.topchetoeu.jscript.exceptions.SyntaxException;
import me.topchetoeu.jscript.exceptions.UncheckedException; import me.topchetoeu.jscript.exceptions.UncheckedException;
@ -17,11 +18,11 @@ import me.topchetoeu.jscript.json.JSONList;
import me.topchetoeu.jscript.json.JSONMap; import me.topchetoeu.jscript.json.JSONMap;
public class DebugServer { public class DebugServer {
public static String browserDisplayName = "jscript"; public static String browserDisplayName = Metadata.NAME + "/" + Metadata.VERSION;
public final HashMap<String, DebuggerProvider> targets = new HashMap<>(); 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 { private static void send(HttpRequest req, String val) throws IOException {
req.writeResponse(200, "OK", "application/json", val.getBytes()); req.writeResponse(200, "OK", "application/json", val.getBytes());
@ -58,7 +59,7 @@ public class DebugServer {
try { try {
msg = new V8Message(raw.textData()); 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) { catch (SyntaxException e) {
ws.send(new V8Error(e.getMessage())); ws.send(new V8Error(e.getMessage()));
@ -86,6 +87,13 @@ public class DebugServer {
case "Debugger.stepOver": debugger.stepOver(msg); continue; case "Debugger.stepOver": debugger.stepOver(msg); continue;
case "Debugger.setPauseOnExceptions": debugger.setPauseOnExceptions(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 ( if (
@ -96,8 +104,7 @@ public class DebugServer {
msg.name.startsWith("Network.") || msg.name.startsWith("Network.") ||
msg.name.startsWith("Page.") msg.name.startsWith("Page.")
) ws.send(new V8Error("This isn't a browser...")); ) ws.send(new V8Error("This isn't a browser..."));
else ws.send(new V8Error("This API is not supported yet."));
if (msg.name.startsWith("Runtime.") || msg.name.startsWith("Profiler.")) ws.send(new V8Error("This API is not supported yet."));
} }
catch (Throwable e) { catch (Throwable e) {
e.printStackTrace(); e.printStackTrace();
@ -158,7 +165,7 @@ public class DebugServer {
switch (req.path) { switch (req.path) {
case "/json/version": case "/json/version":
send(req, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.2\"}"); send(req, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.1\"}");
break; break;
case "/json/list": case "/json/list":
case "/json": { case "/json": {
@ -176,9 +183,11 @@ public class DebugServer {
send(req, JSON.stringify(res)); send(req, JSON.stringify(res));
break; break;
} }
case "/json/protocol":
req.writeResponse(200, "OK", "application/json", protocol);
break;
case "/json/new": case "/json/new":
case "/json/activate": case "/json/activate":
case "/json/protocol":
case "/json/close": case "/json/close":
case "/devtools/inspector.html": case "/devtools/inspector.html":
req.writeResponse( req.writeResponse(
@ -216,7 +225,13 @@ public class DebugServer {
public DebugServer() { public DebugServer() {
try { try {
this.favicon = getClass().getClassLoader().getResourceAsStream("assets/favicon.png").readAllBytes(); 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); } catch (IOException e) { throw new UncheckedIOException(e); }
} }

View File

@ -10,8 +10,6 @@ import java.util.HashMap;
import java.util.IllegalFormatException; import java.util.IllegalFormatException;
import java.util.Map; import java.util.Map;
import me.topchetoeu.jscript.exceptions.UncheckedIOException;
public class HttpRequest { public class HttpRequest {
public final String method; public final String method;
public final String path; public final String path;
@ -21,19 +19,19 @@ public class HttpRequest {
public void writeCode(int code, String name) { public void writeCode(int code, String name) {
try { out.write(("HTTP/1.1 " + code + " " + name + "\r\n").getBytes()); } 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) { public void writeHeader(String name, String value) {
try { out.write((name + ": " + value + "\r\n").getBytes()); } 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) { public void writeLastHeader(String name, String value) {
try { out.write((name + ": " + value + "\r\n\r\n").getBytes()); } try { out.write((name + ": " + value + "\r\n\r\n").getBytes()); }
catch (IOException e) { throw new UncheckedIOException(e); } catch (IOException e) { }
} }
public void writeHeadersEnd() { public void writeHeadersEnd() {
try { out.write("\n".getBytes()); } 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) { public void writeResponse(int code, String name, String type, byte[] data) {
@ -44,13 +42,13 @@ public class HttpRequest {
out.write(data); out.write(data);
out.close(); out.close();
} }
catch (IOException e) { throw new UncheckedIOException(e); } catch (IOException e) { }
} }
public void writeResponse(int code, String name, String type, InputStream data) { public void writeResponse(int code, String name, String type, InputStream data) {
try { try {
writeResponse(code, name, type, data.readAllBytes()); 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) { 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()); return new HttpRequest(method, path, headers, socket.getOutputStream());
} }
catch (IOException e) { throw new UncheckedIOException(e); } catch (IOException | NullPointerException e) { return null; }
} }
} }

View File

@ -3,9 +3,11 @@ package me.topchetoeu.jscript.engine.debug;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import me.topchetoeu.jscript.Filename; import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.Location; 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.StackData;
import me.topchetoeu.jscript.engine.frame.CodeFrame; import me.topchetoeu.jscript.engine.frame.CodeFrame;
import me.topchetoeu.jscript.engine.frame.Runners; 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.ArrayValue;
import me.topchetoeu.jscript.engine.values.CodeFunction; import me.topchetoeu.jscript.engine.values.CodeFunction;
import me.topchetoeu.jscript.engine.values.FunctionValue; 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.engine.values.Values;
import me.topchetoeu.jscript.events.Notifier; import me.topchetoeu.jscript.events.Notifier;
import me.topchetoeu.jscript.exceptions.EngineException; import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONElement; import me.topchetoeu.jscript.json.JSONElement;
import me.topchetoeu.jscript.json.JSONList; import me.topchetoeu.jscript.json.JSONList;
import me.topchetoeu.jscript.json.JSONMap; import me.topchetoeu.jscript.json.JSONMap;
@ -35,6 +39,8 @@ import me.topchetoeu.jscript.lib.SetLib;
import me.topchetoeu.jscript.lib.GeneratorLib.Generator; import me.topchetoeu.jscript.lib.GeneratorLib.Generator;
public class SimpleDebugger implements Debugger { 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 { private static enum State {
RESUMED, RESUMED,
STEPPING_IN, STEPPING_IN,
@ -84,6 +90,7 @@ public class SimpleDebugger implements Debugger {
this.pattern = pattern; this.pattern = pattern;
this.line = line; this.line = line;
this.start = start; this.start = start;
if (condition != null && condition.trim().equals("")) condition = null;
this.condition = condition; this.condition = condition;
} }
} }
@ -91,9 +98,10 @@ public class SimpleDebugger implements Debugger {
public CodeFrame frame; public CodeFrame frame;
public CodeFunction func; public CodeFunction func;
public int id; public int id;
public ObjectValue local = new ObjectValue(), capture = new ObjectValue(), global; public ObjectValue local, capture, global;
public JSONMap serialized; public JSONMap serialized;
public Location location; public Location location;
public boolean debugData = false;
public void updateLoc(Location loc) { public void updateLoc(Location loc) {
serialized.set("location", serializeLocation(loc)); serialized.set("location", serializeLocation(loc));
@ -108,22 +116,40 @@ public class SimpleDebugger implements Debugger {
this.location = frame.function.loc(); this.location = frame.function.loc();
this.global = frame.function.environment.global.obj; 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);
this.serialized = new JSONMap() if (location != null) {
.set("callFrameId", id + "") debugData = true;
.set("functionName", func.name) this.serialized = new JSONMap()
.set("location", serializeLocation(func.loc())) .set("callFrameId", id + "")
.set("scopeChain", new JSONList() .set("functionName", func.name)
.add(new JSONMap().set("type", "local").set("name", "Local Scope").set("object", serializeObj(ctx, local))) .set("location", serializeLocation(location))
.add(new JSONMap().set("type", "closure").set("name", "Closure").set("object", serializeObj(ctx, capture))) .set("scopeChain", new JSONList()
.add(new JSONMap().set("type", "global").set("name", "Global Scope").set("object", serializeObj(ctx, global))) .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)))
.setNull("this"); .add(new JSONMap().set("type", "global").set("name", "Global Scope").set("object", serializeObj(ctx, global)))
)
.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 CatchType execptionType = CatchType.ALL;
public State state = State.RESUMED; public State state = State.RESUMED;
@ -145,6 +171,8 @@ public class SimpleDebugger implements Debugger {
private HashMap<Integer, ObjectValue> idToObject = new HashMap<>(); private HashMap<Integer, ObjectValue> idToObject = new HashMap<>();
private HashMap<ObjectValue, Integer> objectToId = 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(); private Notifier updateNotifier = new Notifier();
@ -191,19 +219,19 @@ public class SimpleDebugger implements Debugger {
return tail.first(); 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."); if (!el.isMap()) throw new RuntimeException("Expected location to be a map.");
var id = Integer.parseInt(el.map().string("scriptId")); var id = Integer.parseInt(el.map().string("scriptId"));
var line = (int)el.map().number("lineNumber") + 1; var line = (int)el.map().number("lineNumber") + 1;
var column = (int)el.map().number("columnNumber") + 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); var res = new Location(line, column, idToSource.get(id).filename);
if (correct) res = correctLocation(idToSource.get(id), res); if (correct) res = correctLocation(idToSource.get(id), res);
return res; return res;
} }
public JSONMap serializeLocation(Location loc) { private JSONMap serializeLocation(Location loc) {
var source = filenameToId.get(loc.filename()); var source = filenameToId.get(loc.filename());
return new JSONMap() return new JSONMap()
.set("scriptId", source + "") .set("scriptId", source + "")
@ -211,21 +239,21 @@ public class SimpleDebugger implements Debugger {
.set("columnNumber", loc.start() - 1); .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); if (objectToId.containsKey(obj)) return objectToId.get(obj);
else { else {
int id = nextId(); int id = nextId();
objectToId.put(obj, id); objectToId.put(obj, id);
if (ctx != null) objectToCtx.put(obj, ctx);
idToObject.put(id, obj); idToObject.put(id, obj);
return id; return id;
} }
} }
private JSONMap serializeObj(Context ctx, Object val) { private JSONMap serializeObj(Context ctx, Object val, boolean recurse) {
val = Values.normalize(null, val); val = Values.normalize(null, val);
if (val == Values.NULL) { if (val == Values.NULL) {
return new JSONMap() return new JSONMap()
.set("objectId", objectId(null) + "")
.set("type", "object") .set("type", "object")
.set("subtype", "null") .set("subtype", "null")
.setNull("value") .setNull("value")
@ -234,13 +262,14 @@ public class SimpleDebugger implements Debugger {
if (val instanceof ObjectValue) { if (val instanceof ObjectValue) {
var obj = (ObjectValue)val; var obj = (ObjectValue)val;
var id = objectId(obj); var id = objectId(ctx, obj);
var type = "object"; var type = "object";
String subtype = null; String subtype = null;
String className = null; String className = null;
if (obj instanceof FunctionValue) type = "function"; if (obj instanceof FunctionValue) type = "function";
if (obj instanceof ArrayValue) subtype = "array"; if (obj instanceof ArrayValue) subtype = "array";
if (Values.isWrapper(val, RegExpLib.class)) subtype = "regexp"; if (Values.isWrapper(val, RegExpLib.class)) subtype = "regexp";
if (Values.isWrapper(val, DateLib.class)) subtype = "date"; if (Values.isWrapper(val, DateLib.class)) subtype = "date";
if (Values.isWrapper(val, MapLib.class)) subtype = "map"; if (Values.isWrapper(val, MapLib.class)) subtype = "map";
@ -253,10 +282,13 @@ public class SimpleDebugger implements Debugger {
var res = new JSONMap() var res = new JSONMap()
.set("type", type) .set("type", type)
.set("objetId", id + ""); .set("objectId", id + "");
if (subtype != null) res.set("subtype", subtype); 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; return res;
} }
@ -271,7 +303,7 @@ public class SimpleDebugger implements Debugger {
if (Double.POSITIVE_INFINITY == num) res.set("unserializableValue", "Infinity"); if (Double.POSITIVE_INFINITY == num) res.set("unserializableValue", "Infinity");
else if (Double.NEGATIVE_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 if (Double.isNaN(num)) res.set("unserializableValue", "NaN");
else res.set("value", num); else res.set("value", num);
@ -280,6 +312,37 @@ public class SimpleDebugger implements Debugger {
throw new IllegalArgumentException("Unexpected JS object."); 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) { private void resume(State state) {
this.state = state; this.state = state;
@ -296,7 +359,7 @@ public class SimpleDebugger implements Debugger {
ws.send(new V8Event("Debugger.paused", map)); ws.send(new V8Event("Debugger.paused", map));
} }
private void pauseException(Context ctx) { private void pauseException(Context ctx) {
state = State.PAUSED_NORMAL; state = State.PAUSED_EXCEPTION;
var map = new JSONMap() var map = new JSONMap()
.set("callFrames", serializeFrames(ctx)) .set("callFrames", serializeFrames(ctx))
.set("reason", "exception"); .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) { @Override public void enable(V8Message msg) {
enabled = true; enabled = true;
ws.send(msg.respond()); 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))); ws.send(msg.respond(new JSONMap().set("scriptSource", idToSource.get(id).source)));
} }
@Override public void getPossibleBreakpoints(V8Message msg) { @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 start = deserializeLocation(msg.params.get("start"), false);
var end = msg.params.isMap("end") ? deserializeLocation(msg.params.get("end"), false) : null; 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(); 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) { @Override public void onSource(Filename filename, String source, TreeSet<Location> locations) {
int id = nextId(); int id = nextId();
var src = new Source(id, filename, source, locations); var src = new Source(id, filename, source, locations);
@ -488,6 +667,9 @@ public class SimpleDebugger implements Debugger {
updateFrames(ctx); updateFrames(ctx);
var frame = codeFrameToFrame.get(cf); var frame = codeFrameToFrame.get(cf);
if (!frame.debugData) return false;
if (instruction.location != null) frame.updateLoc(instruction.location); if (instruction.location != null) frame.updateLoc(instruction.location);
var loc = frame.location; var loc = frame.location;
var isBreakpointable = loc != null && ( var isBreakpointable = loc != null && (
@ -501,15 +683,12 @@ public class SimpleDebugger implements Debugger {
pauseException(ctx); pauseException(ctx);
} }
else if (isBreakpointable && locToBreakpoint.containsKey(loc)) { else if (isBreakpointable && locToBreakpoint.containsKey(loc)) {
pauseDebug(ctx, locToBreakpoint.get(loc)); var bp = locToBreakpoint.get(loc);
} var ok = bp.condition == null ? true : Values.toBoolean(run(currFrame, bp.condition));
else if (isBreakpointable && tmpBreakpts.contains(loc)) { if (ok) pauseDebug(ctx, locToBreakpoint.get(loc));
pauseDebug(ctx, null);
tmpBreakpts.remove(loc);
}
else if (instruction.type == Type.NOP && instruction.match("debug")) {
pauseDebug(ctx, null);
} }
else if (isBreakpointable && tmpBreakpts.remove(loc)) pauseDebug(ctx, null);
else if (instruction.type == Type.NOP && instruction.match("debug")) pauseDebug(ctx, null);
while (enabled) { while (enabled) {
switch (state) { switch (state) {
@ -527,12 +706,17 @@ public class SimpleDebugger implements Debugger {
else return false; else return false;
break; break;
case STEPPING_OVER: case STEPPING_OVER:
if ( if (stepOutFrame.frame == frame.frame) {
stepOutFrame == frame && ( if (returnVal != Runners.NO_RETURN) {
state = State.STEPPING_OUT;
return false;
}
else if (isBreakpointable && (
!loc.filename().equals(prevLocation.filename()) || !loc.filename().equals(prevLocation.filename()) ||
loc.line() != prevLocation.line() loc.line() != prevLocation.line()
) )) pauseDebug(ctx, null);
) pauseDebug(ctx, null); else return false;
}
else return false; else return false;
break; break;
} }
@ -544,14 +728,12 @@ public class SimpleDebugger implements Debugger {
@Override public void onFramePop(Context ctx, CodeFrame frame) { @Override public void onFramePop(Context ctx, CodeFrame frame) {
updateFrames(ctx); updateFrames(ctx);
try { try { idToFrame.remove(codeFrameToFrame.remove(frame).id); }
idToFrame.remove(codeFrameToFrame.remove(frame).id);
}
catch (NullPointerException e) { } catch (NullPointerException e) { }
if (StackData.frames(ctx).size() == 0) resume(State.RESUMED); if (StackData.frames(ctx).size() == 0) resume(State.RESUMED);
else if (stepOutFrame != null && stepOutFrame.frame == frame && 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); pauseDebug(ctx, null);
updateNotifier.await(); updateNotifier.await();

View File

@ -7,7 +7,6 @@ import java.io.OutputStream;
import java.net.Socket; import java.net.Socket;
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type; import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
import me.topchetoeu.jscript.exceptions.UncheckedException;
import me.topchetoeu.jscript.exceptions.UncheckedIOException; import me.topchetoeu.jscript.exceptions.UncheckedIOException;
public class WebSocket implements AutoCloseable { public class WebSocket implements AutoCloseable {

View File

@ -10,6 +10,8 @@ import me.topchetoeu.jscript.engine.scope.LocalScope;
import me.topchetoeu.jscript.engine.scope.ValueVariable; import me.topchetoeu.jscript.engine.scope.ValueVariable;
import me.topchetoeu.jscript.engine.values.ArrayValue; import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.CodeFunction; 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.engine.values.Values;
import me.topchetoeu.jscript.exceptions.EngineException; import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.exceptions.InterruptException; import me.topchetoeu.jscript.exceptions.InterruptException;
@ -56,6 +58,33 @@ public class CodeFrame {
public boolean jumpFlag = false; public boolean jumpFlag = false;
private Location prevLoc = null; 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) { public void addTry(int n, int catchN, int finallyN) {
var res = new TryCtx(codePtr + 1, n, catchN, finallyN); var res = new TryCtx(codePtr + 1, n, catchN, finallyN);
@ -144,7 +173,7 @@ public class CodeFrame {
} }
else if (returnValue != Runners.NO_RETURN) { else if (returnValue != Runners.NO_RETURN) {
if (tryCtx.hasFinally) { if (tryCtx.hasFinally) {
tryCtx.retVal = error; tryCtx.retVal = returnValue;
newState = TryCtx.STATE_FINALLY_RETURNED; newState = TryCtx.STATE_FINALLY_RETURNED;
} }
break; break;
@ -215,6 +244,7 @@ public class CodeFrame {
case TryCtx.STATE_CATCH: case TryCtx.STATE_CATCH:
scope.catchVars.add(new ValueVariable(false, tryCtx.err.value)); scope.catchVars.add(new ValueVariable(false, tryCtx.err.value));
codePtr = tryCtx.catchStart; codePtr = tryCtx.catchStart;
if (debugger != null) debugger.onInstruction(ctx, this, function.body[codePtr], null, error, true);
break; break;
default: default:
codePtr = tryCtx.finallyStart; codePtr = tryCtx.finallyStart;

View File

@ -301,15 +301,6 @@ public class Runners {
return NO_RETURN; return NO_RETURN;
} }
public static Object execNop(Context ctx, Instruction instr, CodeFrame frame) { 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++; frame.codePtr++;
return NO_RETURN; return NO_RETURN;
} }

View File

@ -2,12 +2,7 @@ package me.topchetoeu.jscript.engine.scope;
import java.util.ArrayList; 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 { public class LocalScope {
private String[] names;
public final ValueVariable[] captures; public final ValueVariable[] captures;
public final ValueVariable[] locals; public final ValueVariable[] locals;
public final ArrayList<ValueVariable> catchVars = new ArrayList<>(); public final ArrayList<ValueVariable> catchVars = new ArrayList<>();
@ -19,70 +14,11 @@ public class LocalScope {
else return captures[~i]; 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() { public int size() {
return captures.length + locals.length; 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) { public LocalScope(int n, ValueVariable[] captures) {
locals = new ValueVariable[n]; locals = new ValueVariable[n];
this.captures = captures; this.captures = captures;

View File

@ -11,6 +11,9 @@ public class LocalScopeRecord implements ScopeRecord {
private final ArrayList<String> captures = new ArrayList<>(); private final ArrayList<String> captures = new ArrayList<>();
private final ArrayList<String> locals = new ArrayList<>(); private final ArrayList<String> locals = new ArrayList<>();
public String[] captures() {
return captures.toArray(String[]::new);
}
public String[] locals() { public String[] locals() {
return locals.toArray(String[]::new); return locals.toArray(String[]::new);
} }

View File

@ -1,6 +1,7 @@
package me.topchetoeu.jscript.engine.values; package me.topchetoeu.jscript.engine.values;
import me.topchetoeu.jscript.Location; import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.FunctionBody;
import me.topchetoeu.jscript.compilation.Instruction; import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.Context; import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.Environment; import me.topchetoeu.jscript.engine.Environment;
@ -13,6 +14,7 @@ public class CodeFunction extends FunctionValue {
public final int localsN; public final int localsN;
public final int length; public final int length;
public final Instruction[] body; public final Instruction[] body;
public final String[] captureNames, localNames;
public final ValueVariable[] captures; public final ValueVariable[] captures;
public Environment environment; 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); super(name, length);
this.captures = captures; this.captures = captures;
this.captureNames = body.captureNames;
this.localNames = body.localNames;
this.environment = environment; this.environment = environment;
this.localsN = localsN; this.localsN = localsN;
this.length = length; this.length = length;
this.body = body; this.body = body.instructions;
} }
} }

View 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]);
}
}
}

View File

@ -1,9 +1,15 @@
package me.topchetoeu.jscript.json; package me.topchetoeu.jscript.json;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.topchetoeu.jscript.Filename; 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.exceptions.SyntaxException;
import me.topchetoeu.jscript.parsing.Operator; import me.topchetoeu.jscript.parsing.Operator;
import me.topchetoeu.jscript.parsing.ParseRes; import me.topchetoeu.jscript.parsing.ParseRes;
@ -11,6 +17,63 @@ import me.topchetoeu.jscript.parsing.Parsing;
import me.topchetoeu.jscript.parsing.Token; import me.topchetoeu.jscript.parsing.Token;
public class JSON { 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) { public static ParseRes<String> parseIdentifier(List<Token> tokens, int i) {
return Parsing.parseIdentifier(tokens, i); return Parsing.parseIdentifier(tokens, i);
} }

View File

@ -10,6 +10,9 @@ public class JSONList extends ArrayList<JSONElement> {
public JSONList(JSONElement ...els) { public JSONList(JSONElement ...els) {
super(List.of(els)); super(List.of(els));
} }
public JSONList(Collection<JSONElement> els) {
super(els);
}
public JSONList addNull() { this.add(JSONElement.NULL); return this; } public JSONList addNull() { this.add(JSONElement.NULL); return this; }
public JSONList add(String val) { this.add(JSONElement.of(val)); return this; } public JSONList add(String val) { this.add(JSONElement.of(val)); return this; }

View File

@ -1,84 +1,22 @@
package me.topchetoeu.jscript.lib; package me.topchetoeu.jscript.lib;
import java.util.HashSet;
import java.util.stream.Collectors;
import me.topchetoeu.jscript.Filename; import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.engine.Context; 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.EngineException;
import me.topchetoeu.jscript.exceptions.SyntaxException; import me.topchetoeu.jscript.exceptions.SyntaxException;
import me.topchetoeu.jscript.interop.Native; import me.topchetoeu.jscript.interop.Native;
import me.topchetoeu.jscript.json.JSONElement; import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONList;
import me.topchetoeu.jscript.json.JSONMap;
public class JSONLib { 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 @Native
public static Object parse(Context ctx, String val) { public static Object parse(Context ctx, String val) {
try { 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); } catch (SyntaxException e) { throw EngineException.ofSyntax(e.msg); }
} }
@Native @Native
public static String stringify(Context ctx, Object val) { 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));
} }
} }

View File

@ -1367,6 +1367,7 @@ public class Parsing {
} }
while (true) { while (true) {
var nameLoc = getLoc(filename, tokens, i + n);
var nameRes = parseIdentifier(tokens, i + n++); var nameRes = parseIdentifier(tokens, i + n++);
if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a variable name."); if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a variable name.");
@ -1384,7 +1385,7 @@ public class Parsing {
val = valRes.result; 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)) { if (isOperator(tokens, i + n, Operator.COMMA)) {
n++; n++;
@ -1512,7 +1513,7 @@ public class Parsing {
statements.add(res.result); 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) { public static ParseRes<String> parseLabel(List<Token> tokens, int i) {
int n = 0; int n = 0;
@ -1876,7 +1877,7 @@ public class Parsing {
return list.toArray(Statement[]::new); 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 target = environment.global.globalChild();
var subscope = target.child(); var subscope = target.child();
var res = new CompileTarget(funcs, breakpoints); var res = new CompileTarget(funcs, breakpoints);
@ -1903,14 +1904,14 @@ public class Parsing {
} }
else res.add(Instruction.ret()); 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 { try {
return compile(funcs, breakpoints, environment, parse(filename, raw)); return compile(funcs, breakpoints, environment, parse(filename, raw));
} }
catch (SyntaxException e) { 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) }));
} }
} }
} }