diff --git a/gradle.properties b/gradle.properties index 0181a94..065560f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ project_group = me.topchetoeu project_name = jscript project_version = 0.9.41-beta -main_class = me.topchetoeu.jscript.runtime.SimpleRepl +main_class = me.topchetoeu.jscript.repl.SimpleRepl diff --git a/src/main/java/me/topchetoeu/jscript/runtime/SimpleRepl.java b/src/main/java/me/topchetoeu/jscript/repl/SimpleRepl.java similarity index 85% rename from src/main/java/me/topchetoeu/jscript/runtime/SimpleRepl.java rename to src/main/java/me/topchetoeu/jscript/repl/SimpleRepl.java index 702ada4..90d497b 100644 --- a/src/main/java/me/topchetoeu/jscript/runtime/SimpleRepl.java +++ b/src/main/java/me/topchetoeu/jscript/repl/SimpleRepl.java @@ -1,8 +1,12 @@ -package me.topchetoeu.jscript.runtime; +package me.topchetoeu.jscript.repl; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; @@ -10,6 +14,7 @@ import java.util.Optional; import java.util.WeakHashMap; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -20,6 +25,17 @@ import me.topchetoeu.jscript.common.environment.Environment; import me.topchetoeu.jscript.common.environment.Key; import me.topchetoeu.jscript.common.json.JSON; import me.topchetoeu.jscript.common.parsing.Filename; +import me.topchetoeu.jscript.common.parsing.Location; +import me.topchetoeu.jscript.repl.debug.DebugServer; +import me.topchetoeu.jscript.repl.debug.Debugger; +import me.topchetoeu.jscript.repl.debug.SimpleDebugger; +import me.topchetoeu.jscript.repl.mapping.NativeMapper; +import me.topchetoeu.jscript.repl.mapping.SourceMap; +import me.topchetoeu.jscript.runtime.ArgumentsValue; +import me.topchetoeu.jscript.runtime.Engine; +import me.topchetoeu.jscript.runtime.EventLoop; +import me.topchetoeu.jscript.runtime.Frame; +import me.topchetoeu.jscript.runtime.JSONConverter; import me.topchetoeu.jscript.runtime.debug.DebugContext; import me.topchetoeu.jscript.runtime.exceptions.EngineException; import me.topchetoeu.jscript.runtime.values.Value; @@ -33,11 +49,17 @@ import me.topchetoeu.jscript.runtime.values.primitives.SymbolValue; import me.topchetoeu.jscript.runtime.values.primitives.UserValue; import me.topchetoeu.jscript.runtime.values.primitives.VoidValue; import me.topchetoeu.jscript.runtime.values.primitives.numbers.NumberValue; +import me.topchetoeu.jscript.runtime.Compiler; public class SimpleRepl { - static Thread engineTask; + static Thread engineTask, debugTask; static Engine engine = new Engine(); - static Environment environment = Environment.empty(); + static Environment environment = Environment.empty(), tsEnvironment; + static DebugServer server; + static Debugger debugger; + static Key STDOUT = new Key<>(); + static Key STDERR = new Key<>(); + static Key STDIN = new Key<>(); static int j = 0; static String[] args; @@ -49,6 +71,13 @@ public class SimpleRepl { } catch (EngineException | SyntaxException e) { System.err.println(Value.errorToReadable(environment, e, null)); } + server = new DebugServer(); + debugTask = server.start(new InetSocketAddress("127.0.0.1", 9229), true); + server.targets.put("default", (socket, req) -> new SimpleDebugger(socket) + .attach(DebugContext.get(environment)) + .attach(DebugContext.get(tsEnvironment)) + ); + System.out.println(String.format("Running %s v%s by %s", Metadata.name(), Metadata.version(), Metadata.author())); for (var arg : args) { @@ -142,6 +171,9 @@ public class SimpleRepl { getArgs.self(Map.class).clear(); return Value.UNDEFINED; })); + mapConstr.prototype.defineOwnField(env, "size", new NativeFunction(getArgs -> { + return NumberValue.of(getArgs.self(Map.class).size()); + })); prototype[0] = (ObjectValue)mapConstr.prototype; return mapConstr; @@ -286,6 +318,10 @@ public class SimpleRepl { res.defineOwnField(env, "NaN", NumberValue.NAN); res.defineOwnField(env, "Infinity", NumberValue.of(Double.POSITIVE_INFINITY)); + res.defineOwnField(env, "pow", new NativeFunction(args -> { + return NumberValue.of(Math.pow(args.get(0).toNumber(args.env).getDouble(), args.get(1).toNumber(args.env).getDouble())); + })); + return res; } @@ -453,6 +489,16 @@ public class SimpleRepl { return VoidValue.UNDEFINED; })); + res.defineOwnField(env, "sort", new NativeFunction(args -> { + var arr = (ArrayValue)args.get(0); + var func = (FunctionValue)args.get(1); + + arr.sort((a, b) -> { + return func.apply(args.env, Value.UNDEFINED, a, b).toNumber(args.env).getInt(); + }); + + return arr; + })); return res; } @@ -513,7 +559,12 @@ public class SimpleRepl { return StringValue.of(JSON.stringify(JSONConverter.fromJs(env, args.get(0)))); })); res.defineOwnField(env, "parse", new NativeFunction(args -> { - return JSONConverter.toJs(JSON.parse(null, args.get(0).toString(env))); + try { + return JSONConverter.toJs(JSON.parse(null, args.get(0).toString(env))); + } + catch (SyntaxException e) { + throw EngineException.ofSyntax(e.msg).add(env, e.loc.filename() + "", e.loc); + } })); return res; @@ -633,19 +684,66 @@ public class SimpleRepl { } private static void initGlobals() throws InterruptedException, ExecutionException { environment = createESEnv(); - var tsEnv = createESEnv(); var res = new FunctionValue[1]; var setter = new NativeFunction(args -> { res[0] = (FunctionValue)args.get(0); return Value.UNDEFINED; }); + tsEnvironment = createESEnv(); + var tsGlob = Value.global(tsEnvironment); + var tsCompilerFactory = new FunctionValue[1]; + + tsGlob.defineOwnField(tsEnvironment, "getResource", new NativeFunction(args -> { + var name = args.get(0).toString(args.env); + var src = Reading.resourceToString("lib/" + name); + + if (src == null) return Value.UNDEFINED; + else return StringValue.of(src); + })); + tsGlob.defineOwnField(tsEnvironment, "register", new NativeFunction(args -> { + var func = (FunctionValue)args.get(0); + tsCompilerFactory[0] = func; + return Value.UNDEFINED; + })); + tsGlob.defineOwnField(tsEnvironment, "parseVLQ", new NativeFunction(args -> { + var compiled = Filename.parse(args.get(0).toString(args.env)); + var original = Filename.parse(args.get(1).toString(args.env)); + var map = args.get(2).toString(args.env); + + var mapper = SourceMap.parse(compiled, original, map); + return new NativeMapper(mapper::toOriginal); + })); + tsGlob.defineOwnField(tsEnvironment, "chainMaps", new NativeFunction(args -> { + var list = new ArrayList>(); + for (var arg : args.args) { + list.add(NativeMapper.unwrap(args.env, (FunctionValue)arg)); + } + + return new NativeMapper(v -> { + for (var el : list) { + v = el.apply(v); + } + + return v; + }); + })); + tsGlob.defineOwnField(tsEnvironment, "registerSource", new NativeFunction(args -> { + var filename = Filename.parse(args.get(0).toString(args.env)); + var src = args.get(1).toString(args.env); + DebugContext.get(environment).onSource(filename, src); + return Value.UNDEFINED; + })); + var ts = Reading.resourceToString("lib/ts.js"); - if (ts != null) EventLoop.get(tsEnv).pushMsg( - false, tsEnv, + if (ts != null) EventLoop.get(tsEnvironment).pushMsg( + false, tsEnvironment, Filename.parse("jscript://ts.js"), ts, Value.UNDEFINED, setter ).get(); + + var tsCompiler = Compiler.get(environment).wrap(tsEnvironment, environment, tsCompilerFactory[0]); + environment.add(Compiler.KEY, tsCompiler); } public static void main(String args[]) throws InterruptedException { @@ -653,6 +751,7 @@ public class SimpleRepl { var reader = new Thread(SimpleRepl::reader); environment = initEnv(); + initEngine(); reader.setDaemon(true); @@ -661,5 +760,6 @@ public class SimpleRepl { engine.thread().join(); engineTask.interrupt(); + debugTask.interrupt(); } } diff --git a/src/main/java/me/topchetoeu/jscript/repl/V8Error.java b/src/main/java/me/topchetoeu/jscript/repl/V8Error.java new file mode 100644 index 0000000..646bb3b --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/V8Error.java @@ -0,0 +1,19 @@ +package me.topchetoeu.jscript.repl; + +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONMap; + +public class V8Error { + public final String message; + + public V8Error(String message) { + this.message = message; + } + + @Override + public String toString() { + return JSON.stringify(new JSONMap().set("error", new JSONMap() + .set("message", message) + )); + } +} \ No newline at end of file diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/DebugServer.java b/src/main/java/me/topchetoeu/jscript/repl/debug/DebugServer.java new file mode 100644 index 0000000..92fa9a2 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/DebugServer.java @@ -0,0 +1,249 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.HashMap; + +import me.topchetoeu.jscript.common.Metadata; +import me.topchetoeu.jscript.common.Reading; +import me.topchetoeu.jscript.common.SyntaxException; +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONList; +import me.topchetoeu.jscript.common.json.JSONMap; +import me.topchetoeu.jscript.repl.debug.WebSocketMessage.Type; + +public class DebugServer { + public static String browserDisplayName = Metadata.name() + "/" + Metadata.version(); + + public final HashMap targets = new HashMap<>(); + + private final byte[] favicon, index, protocol; + private final Object connNotifier = new Object(); + + private static void send(HttpRequest req, String val) throws IOException { + req.writeResponse(200, "OK", "application/json", val.getBytes()); + } + + // SILENCE JAVA + private MessageDigest getDigestInstance() { + try { + return MessageDigest.getInstance("sha1"); + } + catch (Throwable e) { throw new RuntimeException(e); } + } + + private static Thread runAsync(Runnable func, String name) { + var res = new Thread(func); + res.setName(name); + res.start(); + return res; + } + + private void handle(WebSocket ws, Debugger debugger) throws IOException { + WebSocketMessage raw; + + while ((raw = ws.receive()) != null) { + if (raw.type != Type.Text) { + ws.send(new V8Error("Expected a text message.")); + continue; + } + + V8Message msg; + + try { + msg = new V8Message(raw.textData()); + } + catch (SyntaxException e) { + ws.send(new V8Error(e.getMessage())); + return; + } + + System.out.println(msg); + + switch (msg.name) { + case "Debugger.enable": + synchronized (connNotifier) { + connNotifier.notify(); + } + debugger.enable(msg); + continue; + case "Debugger.disable": debugger.close(); continue; + + case "Debugger.setBreakpointByUrl": debugger.setBreakpointByUrl(msg); continue; + case "Debugger.removeBreakpoint": debugger.removeBreakpoint(msg); continue; + case "Debugger.continueToLocation": debugger.continueToLocation(msg); continue; + + case "Debugger.getScriptSource": debugger.getScriptSource(msg); continue; + case "Debugger.getPossibleBreakpoints": debugger.getPossibleBreakpoints(msg); continue; + + case "Debugger.resume": debugger.resume(msg); continue; + case "Debugger.pause": debugger.pause(msg); continue; + + case "Debugger.stepInto": debugger.stepInto(msg); continue; + case "Debugger.stepOut": debugger.stepOut(msg); continue; + 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.releaseObject": debugger.releaseObject(msg); continue; + case "Runtime.getProperties": debugger.getProperties(msg); continue; + case "Runtime.callFunctionOn": debugger.callFunctionOn(msg); continue; + case "Runtime.enable": debugger.runtimeEnable(msg); continue; + } + + if ( + msg.name.startsWith("DOM.") || + msg.name.startsWith("DOMDebugger.") || + msg.name.startsWith("Emulation.") || + msg.name.startsWith("Input.") || + msg.name.startsWith("Network.") || + msg.name.startsWith("Page.") + ) ws.send(new V8Error("This isn't a browser...")); + else ws.send(new V8Error("This API is not supported yet.")); + } + + debugger.close(); + } + private void onWsConnect(HttpRequest req, Socket socket, DebuggerProvider debuggerProvider) { + var key = req.headers.get("sec-websocket-key"); + + if (key == null) { + req.writeResponse( + 426, "Upgrade Required", "text/txt", + "Expected a WS upgrade".getBytes() + ); + return; + } + + var resKey = Base64.getEncoder().encodeToString(getDigestInstance().digest( + (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes() + )); + + req.writeCode(101, "Switching Protocols"); + req.writeHeader("Connection", "Upgrade"); + req.writeHeader("Sec-WebSocket-Accept", resKey); + req.writeLastHeader("Upgrade", "WebSocket"); + + var ws = new WebSocket(socket); + var debugger = debuggerProvider.getDebugger(ws, req); + + if (debugger == null) { + ws.close(); + return; + } + + runAsync(() -> { + var handle = new Thread(() -> { + System.out.println("test"); + debugger.close(); + }); + + Runtime.getRuntime().addShutdownHook(handle); + + try { handle(ws, debugger); } + catch (RuntimeException | IOException e) { + try { + e.printStackTrace(); + ws.send(new V8Error(e.getMessage())); + } + catch (IOException e2) { /* Shit outta luck */ } + } + finally { + Runtime.getRuntime().removeShutdownHook(handle); + ws.close(); + debugger.close(); + } + }, "Debug Handler"); + } + + public void awaitConnection() throws InterruptedException { + connNotifier.wait(); + } + + public void run(InetSocketAddress address) { + try { + ServerSocket server = new ServerSocket(); + server.bind(address); + + try { + while (true) { + var socket = server.accept(); + var req = HttpRequest.read(socket); + + if (req == null) continue; + switch (req.path) { + case "/json/version": + send(req, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.1\"}"); + break; + case "/json/list": + case "/json": { + var res = new JSONList(); + + for (var el : targets.entrySet()) { + res.add(new JSONMap() + .set("description", "JScript debugger") + .set("favicon", "/favicon.ico") + .set("id", el.getKey()) + .set("type", "node") + .set("webSocketDebuggerUrl", "ws://" + address.getHostString() + ":" + address.getPort() + "/" + el.getKey()) + ); + } + 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/close": + case "/devtools/inspector.html": + req.writeResponse( + 501, "Not Implemented", "text/txt", + "This feature isn't (and probably won't be) implemented.".getBytes() + ); + break; + case "/": + case "/index.html": + req.writeResponse(200, "OK", "text/html", index); + break; + case "/favicon.ico": + req.writeResponse(200, "OK", "image/png", favicon); + break; + default: + if (req.path.length() > 1 && targets.containsKey(req.path.substring(1))) { + onWsConnect(req, socket, targets.get(req.path.substring(1))); + } + break; + } + } + } + finally { server.close(); } + } + catch (IOException e) { throw new UncheckedIOException(e); } + } + + public Thread start(InetSocketAddress address, boolean daemon) { + var res = new Thread(() -> run(address), "Debug Server"); + res.setDaemon(daemon); + res.start(); + return res; + } + + public DebugServer() { + this.favicon = Reading.resourceToBytes("debugger/favicon.png"); + this.protocol = Reading.resourceToBytes("debugger/protocol.json"); + this.index = Reading.resourceToString("debugger/index.html") + .replace("${NAME}", Metadata.name()) + .replace("${VERSION}", Metadata.version()) + .replace("${AUTHOR}", Metadata.author()) + .getBytes(); + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/Debugger.java b/src/main/java/me/topchetoeu/jscript/repl/debug/Debugger.java new file mode 100644 index 0000000..3092565 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/Debugger.java @@ -0,0 +1,37 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.io.IOException; + +import me.topchetoeu.jscript.runtime.debug.DebugHandler; + +public interface Debugger extends DebugHandler { + void close(); + + void enable(V8Message msg) throws IOException; + void disable(V8Message msg) throws IOException; + + void setBreakpointByUrl(V8Message msg) throws IOException; + void removeBreakpoint(V8Message msg) throws IOException; + void continueToLocation(V8Message msg) throws IOException; + + void getScriptSource(V8Message msg) throws IOException; + void getPossibleBreakpoints(V8Message msg) throws IOException; + + void resume(V8Message msg) throws IOException; + void pause(V8Message msg) throws IOException; + + void stepInto(V8Message msg) throws IOException; + void stepOut(V8Message msg) throws IOException; + void stepOver(V8Message msg) throws IOException; + + void setPauseOnExceptions(V8Message msg) throws IOException; + + void evaluateOnCallFrame(V8Message msg) throws IOException; + + void getProperties(V8Message msg) throws IOException; + void releaseObjectGroup(V8Message msg) throws IOException; + void releaseObject(V8Message msg) throws IOException; + void callFunctionOn(V8Message msg) throws IOException; + + void runtimeEnable(V8Message msg) throws IOException; +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/DebuggerProvider.java b/src/main/java/me/topchetoeu/jscript/repl/debug/DebuggerProvider.java new file mode 100644 index 0000000..1ec4769 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/DebuggerProvider.java @@ -0,0 +1,5 @@ +package me.topchetoeu.jscript.repl.debug; + +public interface DebuggerProvider { + Debugger getDebugger(WebSocket socket, HttpRequest req); +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/HttpRequest.java b/src/main/java/me/topchetoeu/jscript/repl/debug/HttpRequest.java new file mode 100644 index 0000000..62aaba6 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/HttpRequest.java @@ -0,0 +1,101 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.util.HashMap; +import java.util.IllegalFormatException; +import java.util.Map; + +import me.topchetoeu.jscript.common.Reading; + +public class HttpRequest { + public final String method; + public final String path; + public final Map headers; + public final OutputStream out; + + + public void writeCode(int code, String name) { + try { out.write(("HTTP/1.1 " + code + " " + name + "\r\n").getBytes()); } + catch (IOException e) { } + } + public void writeHeader(String name, String value) { + try { out.write((name + ": " + value + "\r\n").getBytes()); } + catch (IOException e) { } + } + public void writeLastHeader(String name, String value) { + try { out.write((name + ": " + value + "\r\n\r\n").getBytes()); } + catch (IOException e) { } + } + public void writeHeadersEnd() { + try { out.write("\n".getBytes()); } + catch (IOException e) { } + } + + public void writeResponse(int code, String name, String type, byte[] data) { + writeCode(code, name); + writeHeader("Content-Type", type); + writeLastHeader("Content-Length", data.length + ""); + try { + out.write(data); + out.close(); + } + catch (IOException e) { } + } + public void writeResponse(int code, String name, String type, InputStream data) { + writeResponse(code, name, type, Reading.streamToBytes(data)); + } + + public HttpRequest(String method, String path, Map headers, OutputStream out) { + this.method = method; + this.path = path; + this.headers = headers; + this.out = out; + } + + // We dont need no http library + public static HttpRequest read(Socket socket) { + try { + var str = socket.getInputStream(); + var lines = new BufferedReader(new InputStreamReader(str)); + var line = lines.readLine(); + var i1 = line.indexOf(" "); + var i2 = line.indexOf(" ", i1 + 1); + + if (i1 < 0 || i2 < 0) { + socket.close(); + return null; + } + + var method = line.substring(0, i1).trim().toUpperCase(); + var path = line.substring(i1 + 1, i2).trim(); + var headers = new HashMap(); + + while (!(line = lines.readLine()).isEmpty()) { + var i = line.indexOf(":"); + if (i < 0) continue; + var name = line.substring(0, i).trim().toLowerCase(); + var value = line.substring(i + 1).trim(); + + if (name.length() == 0) continue; + headers.put(name, value); + } + + if (headers.containsKey("content-length")) { + try { + var i = Integer.parseInt(headers.get("content-length")); + str.skip(i); + } + catch (IllegalFormatException e) { /* ¯\_(ツ)_/¯ */ } + } + + return new HttpRequest(method, path, headers, socket.getOutputStream()); + } + catch (IOException | NullPointerException e) { return null; } + } +} + diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/ScopeObject.java b/src/main/java/me/topchetoeu/jscript/repl/debug/ScopeObject.java new file mode 100644 index 0000000..fcc5922 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/ScopeObject.java @@ -0,0 +1,216 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.IntUnaryOperator; + +import me.topchetoeu.jscript.common.environment.Environment; +import me.topchetoeu.jscript.runtime.Frame; +import me.topchetoeu.jscript.runtime.exceptions.EngineException; +import me.topchetoeu.jscript.runtime.values.KeyCache; +import me.topchetoeu.jscript.runtime.values.Member; +import me.topchetoeu.jscript.runtime.values.Value; +import me.topchetoeu.jscript.runtime.values.Member.FieldMember; +import me.topchetoeu.jscript.runtime.values.functions.FunctionValue; +import me.topchetoeu.jscript.runtime.values.objects.ObjectValue; +import me.topchetoeu.jscript.runtime.values.primitives.StringValue; +import me.topchetoeu.jscript.runtime.values.primitives.SymbolValue; +import me.topchetoeu.jscript.runtime.values.primitives.numbers.NumberValue; + +public class ScopeObject extends Value { + public static final class ScopeMember extends FieldMember { + public final Frame frame; + public final int i; + + @Override public Value get(Environment env, Value self) { + return frame.getVar(i); + } + + @Override public boolean set(Environment env, Value val, Value self) { + frame.setVar(i, val); + return true; + } + + public ScopeMember(Value self, Frame frame, int i) { + super(self, false, true, true); + + this.frame = frame; + this.i = i; + } + } + + private final Map fields = new HashMap<>(); + public final ObjectValue proto; + + @Override public StringValue type() { + return StringValue.of("object"); + } + @Override public boolean isPrimitive() { + return false; + } + @Override public Value toPrimitive(Environment env) { + throw EngineException.ofType("Value couldn't be converted to a primitive."); + } + @Override public NumberValue toNumber(Environment env) { + return NumberValue.NAN; + } + @Override public String toString(Environment env) { + return "[Scope]"; + } + @Override public boolean toBoolean() { + return true; + } + @Override public Member getOwnMember(Environment env, KeyCache key) { + if (key.isSymbol()) return null; + var strKey = key.toString(env); + return fields.get(strKey); + } + @Override public Set getOwnMembers(Environment env, boolean onlyEnumerable) { + return fields.keySet(); + } + @Override public Set getOwnSymbolMembers(Environment env, boolean onlyEnumerable) { + return new HashSet<>(); + } + @Override public boolean defineOwnField(Environment env, KeyCache key, Value val, Boolean writable, Boolean enumerable, Boolean configurable) { + if (key.isSymbol()) return false; + var strKey = key.toString(env); + var field = fields.get(strKey); + if (field == null) return false; + return field.reconfigure(env, this, val, writable, enumerable, configurable); + } + @Override public boolean defineOwnProperty(Environment env, KeyCache key, Optional get, Optional set, Boolean enumerable, Boolean configurable) { + return false; + } + @Override public boolean deleteOwnMember(Environment env, KeyCache key) { + return key.isSymbol() || !fields.containsKey(key.toString(env)); + } + @Override public boolean setPrototype(Environment env, ObjectValue val) { + return false; + } + @Override public State getState() { + return State.SEALED; + } + + @Override public void preventExtensions() { } + @Override public void seal() { } + @Override public void freeze() { } + + @Override public ObjectValue getPrototype(Environment env) { + return proto; + } + + public void add(String name, Value val) { + fields.put(name, FieldMember.of(this, val, false)); + } + public void remove(String name) { + fields.remove(name); + } + + private ScopeObject(ObjectValue proto) { + this.proto = proto; + } + public ScopeObject(Frame frame, String[] names, IntUnaryOperator transformer, ObjectValue proto) { + this.proto = proto; + + for (var i = 0; i < names.length; i++) { + fields.put(names[i], new ScopeMember(this, frame, transformer.applyAsInt(i))); + } + } + + private static String[] fixCaptures(Frame frame, String[] names) { + if (names == null) { + names = new String[frame.captures.length]; + for (var i = 0; i < names.length; i++) { + names[i] = "var_" + i; + } + } + else if (names.length > frame.captures.length) { + var newNames = new String[frame.captures.length]; + System.arraycopy(names, 0, newNames, 0, frame.captures.length); + names = newNames; + } + else if (names.length < frame.captures.length) { + var newNames = new String[frame.captures.length]; + System.arraycopy(names, 0, newNames, 0, names.length); + for (var i = names.length; i < frame.captures.length; i++) { + names[i] = "cap_" + i; + } + names = newNames; + } + + return names; + } + private static String[] fixLocals(Frame frame, String[] names) { + if (names == null) { + names = new String[frame.locals.length]; + for (var i = 0; i < names.length; i++) { + names[i] = "var_" + i; + } + } + else if (names.length > frame.locals.length) { + var newNames = new String[frame.locals.length]; + System.arraycopy(names, 0, newNames, 0, frame.locals.length); + names = newNames; + } + else if (names.length < frame.locals.length) { + var newNames = new String[frame.locals.length]; + System.arraycopy(names, 0, newNames, 0, names.length); + for (var i = names.length; i < frame.locals.length; i++) { + names[i] = "var_" + i; + } + names = newNames; + } + + return names; + } + private static String[] fixCapturables(Frame frame, String[] names) { + if (names == null) { + names = new String[frame.capturables.length]; + for (var i = 0; i < names.length; i++) { + names[i] = "var_" + (frame.locals.length + i); + } + } + else if (names.length > frame.capturables.length) { + var newNames = new String[frame.capturables.length]; + System.arraycopy(names, 0, newNames, 0, frame.capturables.length); + names = newNames; + } + else if (names.length < frame.capturables.length) { + var newNames = new String[frame.capturables.length]; + System.arraycopy(names, 0, newNames, 0, names.length); + for (var i = names.length; i < frame.capturables.length; i++) { + names[i] = "var_" + (frame.locals.length + i); + } + names = newNames; + } + + return names; + } + + public static ScopeObject locals(Frame frame, String[] names) { + return new ScopeObject(frame, fixLocals(frame, names), v -> v, null); + } + public static ScopeObject capturables(Frame frame, String[] names) { + return new ScopeObject(frame, fixCapturables(frame, names), v -> v + frame.locals.length, null); + } + public static ScopeObject captures(Frame frame, String[] names) { + return new ScopeObject(frame, fixCaptures(frame, names), v -> ~v, null); + } + + public static ScopeObject combine(ObjectValue proto, ScopeObject ...objs) { + var res = new ScopeObject(proto); + + for (var el : objs) { + res.fields.putAll(el.fields); + } + + return res; + } + + public static ScopeObject all(Frame frame, String[] local, String[] capturables, String[] captures) { + return combine((ObjectValue)Value.global(frame.env), locals(frame, local), capturables(frame, capturables), captures(frame, captures)); + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/SimpleDebugger.java b/src/main/java/me/topchetoeu/jscript/repl/debug/SimpleDebugger.java new file mode 100644 index 0000000..d1dca69 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/SimpleDebugger.java @@ -0,0 +1,1155 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutionException; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import me.topchetoeu.jscript.common.FunctionBody; +import me.topchetoeu.jscript.common.Instruction; +import me.topchetoeu.jscript.common.Instruction.BreakpointType; +import me.topchetoeu.jscript.common.Instruction.Type; +import me.topchetoeu.jscript.common.environment.Environment; +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONElement; +import me.topchetoeu.jscript.common.json.JSONList; +import me.topchetoeu.jscript.common.json.JSONMap; +import me.topchetoeu.jscript.common.mapping.FunctionMap; +import me.topchetoeu.jscript.common.parsing.Filename; +import me.topchetoeu.jscript.common.parsing.Location; +import me.topchetoeu.jscript.runtime.Compiler; +import me.topchetoeu.jscript.runtime.Engine; +import me.topchetoeu.jscript.runtime.EventLoop; +import me.topchetoeu.jscript.runtime.Frame; +import me.topchetoeu.jscript.runtime.JSONConverter; +import me.topchetoeu.jscript.runtime.debug.DebugContext; +import me.topchetoeu.jscript.runtime.exceptions.EngineException; +import me.topchetoeu.jscript.runtime.values.Value; +import me.topchetoeu.jscript.runtime.values.Member.FieldMember; +import me.topchetoeu.jscript.runtime.values.Member.PropertyMember; +import me.topchetoeu.jscript.runtime.values.functions.FunctionValue; +import me.topchetoeu.jscript.runtime.values.objects.ArrayValue; +import me.topchetoeu.jscript.runtime.values.objects.ObjectValue; +import me.topchetoeu.jscript.runtime.values.primitives.BoolValue; +import me.topchetoeu.jscript.runtime.values.primitives.StringValue; +import me.topchetoeu.jscript.runtime.values.primitives.SymbolValue; +import me.topchetoeu.jscript.runtime.values.primitives.numbers.NumberValue; + +// very simple indeed +public class SimpleDebugger implements Debugger { + public static final Set VSCODE_EMPTY = new HashSet<>(Arrays.asList( + "function(...runtimeArgs){\n let t = 1024; let e = null;\n if(e)try{let r=\"<>\",i=e.call(this,r);if(i!==r)return String(i)}catch(r){return`<>${JSON.stringify([String(r),\"object\"])}`}if(typeof this==\"object\"&&this){let r;for(let i of[Symbol.for(\"debug.description\"),Symbol.for(\"nodejs.util.inspect.custom\")])try{r=this[i]();break}catch{}if(!r&&!String(this.toString).includes(\"[native code]\")&&(r=String(this)),r&&!r.startsWith(\"[object \"))return r.length>=t?r.slice(0,t)+\"\\u2026\":r}\n ;\n\n}", + "function(...runtimeArgs){\n let r = 1024; let e = null;\n if(e)try{let t=\"<>\",n=e.call(this,t);if(n!==t)return String(n)}catch(t){return`<>${JSON.stringify([String(t),\"object\"])}`}if(typeof this==\"object\"&&this){let t;for(let n of[Symbol.for(\"debug.description\"),Symbol.for(\"nodejs.util.inspect.custom\")])if(typeof this[n]==\"function\")try{t=this[n]();break}catch{}if(!t&&!String(this.toString).includes(\"[native code]\")&&(t=String(this)),t&&!t.startsWith(\"[object\"))return t.length>=r?t.slice(0,r)+\"\\u2026\":t};}", + "function(...runtimeArgs){\n let r = 1024; let e = null;\n if(e)try{let t=\"<>\",n=e.call(this,t);if(n!==t)return String(n)}catch(t){return`<>${JSON.stringify([String(t),\"object\"])}`}if(typeof this==\"object\"&&this){let t;for(let n of[Symbol.for(\"debug.description\"),Symbol.for(\"nodejs.util.inspect.custom\")])if(typeof this[n]==\"function\")try{t=this[n](2);break}catch{}if(!t&&!String(this.toString).includes(\"[native code]\")&&(t=String(this)),t&&!t.startsWith(\"[object \"))return t.length>=r?t.slice(0,r)+\"\\u2026\":t};}", + "function(...runtimeArgs){\n let t = 1024; let e = null;\n let r={},i=\"<>\";if(typeof this!=\"object\"||!this)return r;for(let[n,s]of Object.entries(this)){if(e)try{let o=e.call(s,i);if(o!==i){r[n]=String(o);continue}}catch(o){r[n]=`<>${JSON.stringify([String(o),n])}`;continue}if(typeof s==\"object\"&&s){let o;for(let a of runtimeArgs[0])try{o=s[a]();break}catch{}!o&&!String(s.toString).includes(\"[native code]\")&&(o=String(s)),o&&!o.startsWith(\"[object \")&&(r[n]=o.length>=t?o.slice(0,t)+\"\\u2026\":o)}}return r\n ;\n\n}", + "function(...runtimeArgs){\n let r = 1024; let e = null;\n let t={},n=\"<>\";if(typeof this!=\"object\"||!this)return t;for(let[i,o]of Object.entries(this)){if(e)try{let s=e.call(o,n);if(s!==n){t[i]=String(s);continue}}catch(s){t[i]=`<>${JSON.stringify([String(s),i])}`;continue}if(typeof o==\"object\"&&o){let s;for(let a of runtimeArgs[0])if(typeof o[a]==\"function\")try{s=o[a]();break}catch{}!s&&!String(o.toString).includes(\"[native code]\")&&(s=String(o)),s&&!s.startsWith(\"[object \")&&(t[i]=s.length>=r?s.slice(0,r)+\"\\u2026\":s)}}return t\n ;\n\n}", + "function(...runtimeArgs){\n let r = 1024; let e = null;\n let t={},n=\"<>\";if(typeof this!=\"object\"||!this)return t;for(let[i,o]of Object.entries(this)){if(e)try{let s=e.call(o,n);if(s!==n){t[i]=String(s);continue}}catch(s){t[i]=`<>${JSON.stringify([String(s),i])}`;continue}if(typeof o==\"object\"&&o){let s;for(let a of runtimeArgs[0])if(typeof o[a]==\"function\")try{s=o[a](2);break}catch{}!s&&!String(o.toString).includes(\"[native code]\")&&(s=String(o)),s&&!s.startsWith(\"[object \")&&(t[i]=s.length>=r?s.slice(0,r)+\"\\u2026\":s)}}return t\n ;\n\n}", + "function(){let t={__proto__:this.__proto__\n},e=Object.getOwnPropertyNames(this);for(let r=0;r>>0;if(String(n>>>0)===i&&n>>>0!==4294967295)continue;let s=Object.getOwnPropertyDescriptor(this,i);s&&Object.defineProperty(t,i,s)}return t}", + "function(){return[Symbol.for(\"debug.description\"),Symbol.for(\"nodejs.util.inspect.custom\")]\n}" + )); + public static final Set VSCODE_SELF = new HashSet<>(Arrays.asList( + "function(t,e){let r={\n},i=t===-1?0:t,n=e===-1?this.length:t+e;for(let s=i;s{if(p!==\"function\")return n;if(l===\"constructor\")return\"class\";let m=String(f);return m.startsWith(\"class \")||m.includes(\"[native code]\")&&/^[A-Z]/.test(l)?\"class\":r?\"function\":\"method\"\n},o=l=>{switch(typeof l){case\"number\":case\"boolean\":return`${l}`;case\"object\":return l===null?\"null\":l.constructor.name||\"object\";case\"function\":return`fn(${new Array(l.length).fill(\"?\").join(\", \")})`;default:return typeof l}},s=[],a=new Set,u=\"~\",c=t===void 0?this:t;for(;c!=null;c=c.__proto__){u+=\"~\";let l=Object.getOwnPropertyNames(c).filter(p=>p.startsWith(e)&&!p.match(/^\\d+$/));for(let p of l){if(a.has(p))continue;a.add(p);let f=Object.getOwnPropertyDescriptor(c,p),m=n,h;try{let H=c[p];m=i(p,typeof f?.value,H),h=o(H)}catch{}s.push({label:p,sortText:u+p.replace(/^_+/,H=>\"{\".repeat(H.length)),type:m,detail:h})}r=!1}return{result:s,isArray:this instanceof Array}}"; + + private static enum State { + RESUMED, + STEPPING_IN, + STEPPING_OUT, + STEPPING_OVER, + PAUSED_NORMAL, + PAUSED_EXCEPTION, + } + private static enum CatchType { + NONE, + UNCAUGHT, + ALL, + } + private static class DebugSource { + public final int id; + public final Filename filename; + public final String source; + + public DebugSource(int id, Filename filename, String source) { + this.id = id; + this.filename = filename; + this.source = source; + } + } + + private class Breakpoint { + public final int id; + public final String condition; + public final Pattern pattern; + public final int line, start; + public final long locNum; + public final HashMap resolvedLocations = new HashMap<>(); + public final HashMap resolvedDistances = new HashMap<>(); + + public Breakpoint(int id, Pattern pattern, int line, int start, String condition) { + this.id = id; + this.condition = condition; + this.pattern = pattern; + this.line = line; + this.start = start; + this.locNum = start | ((long)line << 32); + + if (condition != null && condition.trim().equals("")) condition = null; + } + + // TODO: Figure out how to unload a breakpoint + // TODO: Do location resolution with function boundaries + public void addFunc(FunctionBody body, FunctionMap map) { + try { + for (var loc : map.correctBreakpoint(pattern, line, start)) { + var currNum = loc.start() + ((long)loc.line() << 32); + long currDist = 0; + if (currNum > locNum) currDist = currNum - locNum; + else currDist = locNum - currNum; + + if (currDist > resolvedDistances.getOrDefault(loc.filename(), Long.MAX_VALUE)) continue; + + resolvedLocations.put(loc.filename(), loc); + resolvedDistances.put(loc.filename(), currDist); + } + + for (var loc : resolvedLocations.values()) { + ws.send(new V8Event("Debugger.breakpointResolved", new JSONMap() + .set("breakpointId", id) + .set("location", serializeLocation(loc)) + )); + } + + updateBreakpoints(); + } + catch (IOException e) { + ws.close(); + close(); + } + } + } + private class DebugFrame { + public final Frame frame; + public final int id; + public final ScopeObject variables; + public final ScopeObject locals, capturables, captures; + public final StackObject valstack; + public final Value globals; + public Location location; + + public void updateLoc(Location loc) { + if (loc == null) return; + this.location = loc; + } + + public JSONMap serialize(Value returnValue) { + var chain = new JSONList(); + + if (returnValue != null) { + locals.add("Return Value", returnValue); + } + else { + locals.remove("Return Value"); + } + + chain.add(new JSONMap() + .set("type", "local") + .set("name", "Locals") + .set("object", serializeObj(frame.env, locals)) + ); + chain.add(new JSONMap() + .set("type", "local") + .set("name", "Capturables") + .set("object", serializeObj(frame.env, capturables)) + ); + chain.add(new JSONMap() + .set("type", "closure") + .set("name", "Captures") + .set("object", serializeObj(frame.env, captures)) + ); + chain.add(new JSONMap() + .set("type", "global") + .set("name", "Globals") + .set("object", serializeObj(frame.env, globals)) + ); + chain.add(new JSONMap() + .set("type", "other") + .set("name", "Stack") + .set("object", serializeObj(frame.env, valstack)) + ); + + return new JSONMap() + .set("callFrameId", id + "") + .set("functionName", frame.function.name) + .set("location", serializeLocation(location)) + .set("scopeChain", chain); + } + + public DebugFrame(Frame frame, int id) { + this.frame = frame; + this.id = id; + + var map = DebugContext.get(frame.env).getMap(frame.function); + this.globals = Value.global(frame.env); + this.locals = ScopeObject.locals(frame, map.localNames); + this.capturables = ScopeObject.capturables(frame, map.capturableNames); + this.captures = ScopeObject.captures(frame, map.captureNames); + this.variables = ScopeObject.combine((ObjectValue)this.globals, locals, capturables, captures); + this.valstack = new StackObject(frame); + } + } + private class ObjRef { + public final Value obj; + public final Environment env; + public final HashSet heldGroups = new HashSet<>(); + public boolean held = true; + + public boolean shouldRelease() { + return !held && heldGroups.size() == 0; + } + + public ObjRef(Environment env, Value obj) { + this.env = env; + this.obj = obj; + } + } + + private static class RunResult { + public final Environment ext; + public final Value result; + public final EngineException error; + + public RunResult(Environment ext, Value result, EngineException error) { + this.ext = ext; + this.result = result; + this.error = error; + } + } + + public boolean enabled = true; + public CatchType execptionType = CatchType.NONE; + public State state = State.RESUMED; + + public final WebSocket ws; + + private ObjectValue emptyObject = new ObjectValue(); + + private WeakHashMap contexts = new WeakHashMap<>(); + private WeakHashMap mappings = new WeakHashMap<>(); + private HashMap> bpLocs = new HashMap<>(); + + private HashMap idToBreakpoint = new HashMap<>(); + + private HashMap filenameToId = new HashMap<>(); + private HashMap idToSource = new HashMap<>(); + private ArrayList pendingSources = new ArrayList<>(); + + private HashMap idToFrame = new HashMap<>(); + private HashMap codeFrameToFrame = new HashMap<>(); + + private HashMap idToObject = new HashMap<>(); + private HashMap objectToId = new HashMap<>(); + private HashMap> objectGroups = new HashMap<>(); + + private Object updateNotifier = new Object(); + private boolean pendingPause = false; + + private int nextId = 0; + private DebugFrame stepOutFrame = null, currFrame = null; + private int stepOutPtr = 0; + + private boolean compare(String src, String target) { + src = src.replaceAll("\\s", ""); + target = target.replaceAll("\\s", ""); + if (src.length() != target.length()) return false; + var diff = 0; + var all = 0; + + for (var i = 0; i < src.length(); i++) { + var a = src.charAt(i); + var b = target.charAt(i); + var letter = Character.isLetter(a) && Character.isLetter(b); + + if (a != b) { + if (letter) diff++; + else return false; + } + + if (letter) all++; + } + + return diff / (float)all < .5f; + } + private boolean compare(String src, Set target) { + for (var el : target) { + if (compare(src, el)) return true; + } + return false; + } + + private int nextId() { + return nextId++; + } + + private synchronized DebugFrame getFrame(Frame frame) { + if (!codeFrameToFrame.containsKey(frame)) { + var id = nextId(); + var fr = new DebugFrame(frame, id); + + idToFrame.put(id, fr); + codeFrameToFrame.put(frame, fr); + + return fr; + } + else return codeFrameToFrame.get(frame); + } + private synchronized void updateFrames(Environment env, int skipN) { + var frame = Frame.get(env, skipN); + if (frame == null) return; + + currFrame = getFrame(frame); + } + private JSONList serializeFrames(Environment env, Value returnValue) { + var res = new JSONList(); + var frames = Frame.get(env); + for (var i = frames.size() - 1; i >= 0; i--) { + var el = frames.get(i); + + var frame = getFrame(el); + if (frame.location == null) continue; + + res.add(frame.serialize(returnValue)); + } + + return res; + } + + private void updateBreakpoints() { + bpLocs.clear(); + + for (var bp : idToBreakpoint.values()) { + for (var loc : bp.resolvedLocations.values()) { + bpLocs.putIfAbsent(loc, new HashSet<>()); + var set = bpLocs.get(loc); + + set.add(bp); + } + } + } + + private Location deserializeLocation(JSONElement el) { + 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"); + var column = (int)el.map().number("columnNumber"); + + if (!idToSource.containsKey(id)) throw new RuntimeException(String.format("The specified source %s doesn't exist.", id)); + + var res = Location.of(idToSource.get(id).filename, line, column); + return res; + } + private JSONMap serializeLocation(Location loc) { + var source = filenameToId.get(loc.filename()); + return new JSONMap() + .set("scriptId", source + "") + .set("lineNumber", loc.line()) + .set("columnNumber", loc.start()); + } + + private JSONMap serializeObj(Environment env, Value val, boolean byValue) { + if (val == Value.NULL) { + return new JSONMap() + .set("type", "object") + .set("subtype", "null") + .setNull("value") + .set("description", "null"); + } + + var type = val.type().value; + + if (type.equals("object") || type.equals("function")) { + int id; + + if (objectToId.containsKey(val)) id = objectToId.get(val); + else { + id = nextId(); + var ref = new ObjRef(env, val); + objectToId.put(val, id); + idToObject.put(id, ref); + } + + String subtype = null; + String className = null; + + if (val instanceof ArrayValue) subtype = "array"; + + try { className = val.getMemberPath(env, "constructor", "name").toString(env); } + catch (Exception e) { } + + var res = new JSONMap() + .set("type", type) + .set("objectId", id + ""); + + if (subtype != null) res.set("subtype", subtype); + if (className != null) { + res.set("className", className); + res.set("description", className); + } + + if (val instanceof ArrayValue arr) res.set("description", "Array(" + arr.size() + ")"); + else if (val instanceof FunctionValue) res.set("description", val.toString()); + else { + var defaultToString = false; + + try { + defaultToString = + val.getMember(env, "toString") == + env.get(Value.OBJECT_PROTO).getMember(env, "toString"); + } + catch (Exception e) { } + + try { res.set("description", className + (defaultToString ? "" : " { " + val.toString(env) + " }")); } + catch (Exception e) { } + } + + + if (byValue) try { res.put("value", JSONConverter.fromJs(env, val)); } + catch (Exception e) { } + + return res; + } + + if (val == Value.UNDEFINED) return new JSONMap().set("type", "undefined"); + if (val instanceof StringValue str) return new JSONMap().set("type", "string").set("value", str.value); + if (val instanceof BoolValue bool) return new JSONMap().set("type", "boolean").set("value", bool.value); + if (val instanceof SymbolValue symbol) return new JSONMap().set("type", "symbol").set("description", symbol.value); + if (val instanceof NumberValue numVal) { + var num = numVal.getDouble(); + var res = new JSONMap().set("type", "number"); + + if (Double.POSITIVE_INFINITY == num) res.set("unserializableValue", "Infinity"); + else if (Double.NEGATIVE_INFINITY == num) res.set("unserializableValue", "-Infinity"); + else if (Double.doubleToRawLongBits(num) == Double.doubleToRawLongBits(-0d)) 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); + + return res; + } + + throw new IllegalArgumentException("Unexpected JS object."); + } + private JSONMap serializeObj(Environment env, Value val) { + return serializeObj(env, val, false); + } + private void addObjectGroup(String name, Value val) { + var id = objectToId.getOrDefault(val, -1); + if (id < 0) return; + + var ref = idToObject.get(id); + + if (objectGroups.containsKey(name)) objectGroups.get(name).add(ref); + else objectGroups.put(name, new ArrayList<>(Arrays.asList(ref))); + + ref.heldGroups.add(name); + } + private void releaseGroup(String name) { + var objs = objectGroups.remove(name); + + if (objs != null) for (var obj : objs) { + if (obj.heldGroups.remove(name) && obj.shouldRelease()) { + var id = objectToId.remove(obj.obj); + if (id != null) idToObject.remove(id); + } + } + } + private Value deserializeArgument(JSONMap val) { + if (val.isString("objectId")) return idToObject.get(Integer.parseInt(val.string("objectId"))).obj; + else if (val.isString("unserializableValue")) switch (val.string("unserializableValue")) { + case "NaN": return NumberValue.NAN; + case "-Infinity": return NumberValue.of(Double.NEGATIVE_INFINITY); + case "Infinity": return NumberValue.of(Double.POSITIVE_INFINITY); + case "-0": return NumberValue.of(-0.); + } + var res = val.get("value"); + if (res == null) return null; + else return JSONConverter.toJs(res); + } + + private JSONMap serializeException(Environment env, EngineException err) { + String text = null; + + try { + text = err.value.toString(env); + } + catch (EngineException e) { + text = "[error while stringifying]"; + } + + var res = new JSONMap() + .set("exceptionId", nextId()) + .set("exception", serializeObj(env, err.value)) + .set("text", text); + + return res; + } + + private void resume(State state) { + try { + this.state = state; + ws.send(new V8Event("Debugger.resumed", new JSONMap())); + synchronized (updateNotifier) { + updateNotifier.notifyAll(); + } + } + catch (IOException e) { + ws.close(); + close(); + } + } + private void pauseDebug(Environment env, Breakpoint bp) { + try { + state = State.PAUSED_NORMAL; + var map = new JSONMap() + .set("callFrames", serializeFrames(env, null)) + .set("reason", "debugCommand"); + + if (bp != null) map.set("hitBreakpoints", new JSONList().add(bp.id + "")); + ws.send(new V8Event("Debugger.paused", map)); + } + catch (IOException e) { + ws.close(); + close(); + } + } + private void pauseException(Environment env, EngineException exception) { + try { + state = State.PAUSED_EXCEPTION; + var map = new JSONMap() + .set("callFrames", serializeFrames(env, null)) + .set("data", serializeObj(env, exception.value)) + .set("reason", "exception"); + + ws.send(new V8Event("Debugger.paused", map)); + } + catch (IOException e) { + ws.close(); + close(); + } + } + private void pauseReturn(Environment env, Value value) { + try { + state = State.PAUSED_NORMAL; + var map = new JSONMap() + .set("callFrames", serializeFrames(env, value)) + .set("reason", "debugCommand"); + + ws.send(new V8Event("Debugger.paused", map)); + } + catch (IOException e) { + ws.close(); + close(); + } + } + + private void sendSource(DebugSource src) { + try { + ws.send(new V8Event("Debugger.scriptParsed", new JSONMap() + .set("scriptId", src.id + "") + .set("hash", src.source.hashCode()) + .set("url", src.filename + "") + )); + } + catch (IOException e) { + ws.close(); + close(); + } + } + + // private Environment sanitizeEnvironment(Environment env) { + // var res = env.child(); + + // res.remove(EventLoop.KEY); + // res.remove(DebugContext.KEY); + // res.add(DebugContext.IGNORE); + + // return res; + // } + + private RunResult run(DebugFrame codeFrame, String code) { + if (codeFrame == null) return new RunResult(null, null, EngineException.ofError("Invalid code frame!")); + var engine = new Engine(); + var env = codeFrame.frame.env.child(); + + env.remove(DebugContext.KEY); + env.remove(EventLoop.KEY); + env.remove(Value.GLOBAL); + env.add(Compiler.KEY, Compiler.DEFAULT); + env.add(EventLoop.KEY, engine); + env.add(Value.GLOBAL, codeFrame.variables); + + var awaiter = engine.pushMsg(false, env, new Filename("jscript", "eval"), code, codeFrame.frame.self, codeFrame.frame.args); + + try { + engine.run(true); + try { + return new RunResult(env, awaiter.get(), null); + } + catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException runtime) throw runtime; + else throw new RuntimeException(e.getCause()); + } + } + catch (EngineException e) { return new RunResult(env, null, e); } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new RunResult(env, null, EngineException.ofError("Thread interrupted!")); + } + } + + private ObjectValue vscodeAutoSuggest(Environment env, Value target, String query, boolean variable) { + var res = new ArrayValue(); + var passed = new HashSet(); + var tildas = "~"; + if (target == null) target = Value.global(env); + + for (var proto = target; proto != null && proto != Value.NULL; proto = proto.getPrototype(env)) { + for (var key : proto.getMembers(env, true, true)) { + if (passed.contains(key)) continue; + passed.add(key); + + var member = proto.getOwnMember(env, key); + var val = member instanceof FieldMember field ? field.get(env, target) : Value.UNDEFINED; + var desc = new ObjectValue(); + var sortText = ""; + + if (key.startsWith(query)) sortText += "0@"; + else if (key.toLowerCase().startsWith(query.toLowerCase())) sortText += "1@"; + else if (key.contains(query)) sortText += "2@"; + else if (key.toLowerCase().contains(query.toLowerCase())) sortText += "3@"; + else sortText += "4@"; + + sortText += tildas + key; + + desc.defineOwnField(env, "label", StringValue.of(key)); + desc.defineOwnField(env, "sortText", StringValue.of(sortText)); + + if (val instanceof FunctionValue) { + if (key.equals("constructor")) desc.defineOwnField(env, "type", StringValue.of("name")); + else desc.defineOwnField(env, "type", StringValue.of(variable ? "function" : "method")); + } + else desc.defineOwnField(env, "type", StringValue.of(variable ? "variable" : "property")); + + switch (val.type().toString()) { + case "number": + case "boolean": + desc.defineOwnField(env, "detail", StringValue.of(val.toString(env))); + break; + case "object": + if (val == Value.NULL) desc.defineOwnField(env, "detail", StringValue.of("null")); + else try { + desc.defineOwnField(env, "detail", target.getMemberPath(env, "constructor", "name")); + } + catch (IllegalArgumentException e) { + desc.defineOwnField(env, "detail", StringValue.of("object")); + } + break; + case "function": { + var type = "fn("; + for (var i = 0; i < ((FunctionValue)val).length; i++) { + if (i != 0) type += ","; + type += "?"; + } + type += ")"; + desc.defineOwnField(env, "detail", StringValue.of(type)); + break; + } + default: + desc.defineOwnField(env, "type", val.type()); + break; + } + + res.set(env, res.size(), desc); + } + + tildas += "~"; + variable = true; + } + + var resObj = new ObjectValue(); + resObj.defineOwnField(env, "result", res); + resObj.defineOwnField(env, "isArray", BoolValue.of(target instanceof ArrayValue)); + return resObj; + } + + @Override public synchronized void enable(V8Message msg) throws IOException { + enabled = true; + ws.send(msg.respond()); + + for (var el : pendingSources) sendSource(el); + pendingSources.clear(); + + synchronized (updateNotifier) { + updateNotifier.notifyAll(); + } + } + @Override public synchronized void disable(V8Message msg) throws IOException { + close(); + ws.send(msg.respond()); + } + @Override public synchronized void close() { + if (state != State.RESUMED) { + try { + resume(State.RESUMED); + } + catch (Throwable e) { + // don't care, didn't ask + } + } + + enabled = false; + execptionType = CatchType.NONE; + state = State.RESUMED; + + mappings.clear(); + bpLocs.clear(); + + idToBreakpoint.clear(); + + filenameToId.clear(); + idToSource.clear(); + pendingSources.clear(); + + idToFrame.clear(); + codeFrameToFrame.clear(); + + idToObject.clear(); + objectToId.clear(); + objectGroups.clear(); + + pendingPause = false; + + stepOutFrame = currFrame = null; + stepOutPtr = 0; + + for (var ctx : contexts.keySet()) ctx.detachDebugger(this); + contexts.clear(); + + synchronized (updateNotifier) { + updateNotifier.notifyAll(); + } + } + + @Override public synchronized void getScriptSource(V8Message msg) throws IOException { + int id = Integer.parseInt(msg.params.string("scriptId")); + ws.send(msg.respond(new JSONMap().set("scriptSource", idToSource.get(id).source))); + } + @Override public synchronized void getPossibleBreakpoints(V8Message msg) throws IOException { + var start = deserializeLocation(msg.params.get("start")); + var end = msg.params.isMap("end") ? deserializeLocation(msg.params.get("end")) : null; + var res = new JSONList(); + + for (var el : mappings.values()) { + for (var bp : el.breakpoints(start, end)) { + res.add(serializeLocation(bp)); + } + } + + ws.send(msg.respond(new JSONMap().set("locations", res))); + } + + @Override public synchronized void pause(V8Message msg) throws IOException { + pendingPause = true; + ws.send(msg.respond()); + } + @Override public synchronized void resume(V8Message msg) throws IOException { + resume(State.RESUMED); + ws.send(msg.respond(new JSONMap())); + } + + @Override public synchronized void setBreakpointByUrl(V8Message msg) throws IOException { + var line = (int)msg.params.number("lineNumber"); + var col = (int)msg.params.number("columnNumber", 0); + var cond = msg.params.string("condition", "").trim(); + + if (cond.equals("")) cond = null; + if (cond != null) cond = "(" + cond + ")"; + + Pattern regex; + + if (msg.params.isString("url")) regex = Pattern.compile(Pattern.quote(msg.params.string("url"))); + else if (msg.params.isString("urlRegex")) regex = Pattern.compile(msg.params.string("urlRegex")); + else { + ws.send(msg.respond(new JSONMap() + .set("breakpointId", "john-doe") + .set("locations", new JSONList()) + )); + return; + } + + var bpt = new Breakpoint(nextId(), regex, line, col, cond); + idToBreakpoint.put(bpt.id, bpt); + + + for (var el : mappings.entrySet()) { + bpt.addFunc(el.getKey(), el.getValue()); + } + + var locs = new JSONList(); + + for (var loc : bpt.resolvedLocations.values()) { + locs.add(serializeLocation(loc)); + } + + ws.send(msg.respond(new JSONMap() + .set("breakpointId", bpt.id + "") + .set("locations", locs) + )); + } + @Override public synchronized void removeBreakpoint(V8Message msg) throws IOException { + var id = Integer.parseInt(msg.params.string("breakpointId")); + + idToBreakpoint.remove(id); + updateBreakpoints(); + ws.send(msg.respond()); + } + @Override public synchronized void continueToLocation(V8Message msg) throws IOException { + // TODO: Figure out if we need this + + // var loc = correctLocation(deserializeLocation(msg.params.get("location"))); + + // tmpBreakpts.add(loc); + + // resume(State.RESUMED); + // ws.send(msg.respond()); + } + + @Override public synchronized void setPauseOnExceptions(V8Message msg) throws IOException { + switch (msg.params.string("state")) { + case "none": execptionType = CatchType.NONE; break; + case "all": execptionType = CatchType.ALL; break; + case "uncaught": execptionType = CatchType.UNCAUGHT; break; + default: + ws.send(new V8Error("Invalid exception pause type.")); + return; + } + + ws.send(msg.respond()); + } + + @Override public synchronized void stepInto(V8Message msg) throws IOException { + if (state == State.RESUMED) ws.send(new V8Error("Debugger is resumed.")); + else { + stepOutFrame = currFrame; + stepOutPtr = currFrame.frame.codePtr; + resume(State.STEPPING_IN); + ws.send(msg.respond()); + } + } + @Override public synchronized void stepOut(V8Message msg) throws IOException { + if (state == State.RESUMED) ws.send(new V8Error("Debugger is resumed.")); + else { + stepOutFrame = currFrame; + stepOutPtr = currFrame.frame.codePtr; + resume(State.STEPPING_OUT); + ws.send(msg.respond()); + } + } + @Override public synchronized void stepOver(V8Message msg) throws IOException { + if (state == State.RESUMED) ws.send(new V8Error("Debugger is resumed.")); + else { + stepOutFrame = currFrame; + stepOutPtr = currFrame.frame.codePtr; + resume(State.STEPPING_OVER); + ws.send(msg.respond()); + } + } + + @Override public synchronized void evaluateOnCallFrame(V8Message msg) throws IOException { + 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) addObjectGroup(group, res.result); + + if (res.error != null) ws.send(msg.respond(new JSONMap().set("exceptionDetails", serializeException(res.ext, res.error)))); + else ws.send(msg.respond(new JSONMap().set("result", serializeObj(res.ext, res.result)))); + } + + @Override public synchronized void releaseObjectGroup(V8Message msg) throws IOException { + var group = msg.params.string("objectGroup"); + releaseGroup(group); + ws.send(msg.respond()); + } + @Override public synchronized void releaseObject(V8Message msg) throws IOException { + var id = Integer.parseInt(msg.params.string("objectId")); + var ref = idToObject.get(id); + ref.held = false; + + if (ref.shouldRelease()) { + objectToId.remove(ref.obj); + idToObject.remove(id); + } + + ws.send(msg.respond()); + } + @Override public synchronized void getProperties(V8Message msg) throws IOException { + var ref = idToObject.get(Integer.parseInt(msg.params.string("objectId"))); + var obj = ref.obj; + var env = ref.env; + var res = new JSONList(); + var own = true; + + if (obj != emptyObject && obj != null) { + while (obj != null) { + for (var key : obj.getMembers(env, true, false)) { + var propDesc = new JSONMap(); + + var member = obj.getOwnMember(env, key); + + if (member instanceof PropertyMember prop) { + propDesc.set("name", key); + if (prop.getter != null) propDesc.set("get", serializeObj(env, prop.getter)); + if (prop.setter != null) propDesc.set("set", serializeObj(env, prop.setter)); + propDesc.set("enumerable", member.enumerable()); + propDesc.set("configurable", member.configurable()); + propDesc.set("isOwn", true); + res.add(propDesc); + } + else { + propDesc.set("name", key); + propDesc.set("value", serializeObj(env, member.get(env, obj))); + propDesc.set("writable", member instanceof FieldMember field ? field.writable() : false); + propDesc.set("enumerable", member.enumerable()); + propDesc.set("configurable", member.configurable()); + propDesc.set("isOwn", own); + res.add(propDesc); + } + } + + var proto = obj.getPrototype(env); + + if (own) { + var protoDesc = new JSONMap(); + protoDesc.set("name", "[[Prototype]]"); + protoDesc.set("value", serializeObj(env, proto == null ? Value.NULL : proto)); + protoDesc.set("writable", false); + protoDesc.set("enumerable", false); + protoDesc.set("configurable", false); + protoDesc.set("isOwn", own); + res.add(protoDesc); + } + + obj = proto; + own = false; + } + } + + ws.send(msg.respond(new JSONMap().set("result", res))); + } + @Override public synchronized void callFunctionOn(V8Message msg) throws IOException { + 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 byValue = msg.params.bool("returnByValue", false); + + var selfRef = idToObject.get(Integer.parseInt(msg.params.string("objectId"))); + var self = selfRef.obj; + var env = selfRef.env; + + while (true) { + var start = src.lastIndexOf("//# sourceURL="); + if (start < 0) break; + var end = src.indexOf("\n", start); + if (end < 0) src = src.substring(0, start); + else src = src.substring(0, start) + src.substring(end + 1); + } + + try { + Value res = null; + if (compare(src, VSCODE_EMPTY)) res = emptyObject; + else if (compare(src, VSCODE_SELF)) res = self; + else if (compare(src, CHROME_GET_PROP_FUNC)) { + res = self; + for (var el : JSON.parse(null, args.get(0).toString(env)).list()) res = res.getMember(env, JSONConverter.toJs(el)); + } + else if (compare(src, CHROME_GET_PROP_FUNC_2)) { + res = args.get(0).apply(env, self); + } + else if (compare(src, VSCODE_CALL)) { + var func = (FunctionValue)(args.size() < 1 ? null : args.get(0)); + ws.send(msg.respond(new JSONMap().set("result", serializeObj(env, func.apply(env, self))))); + } + else if (compare(src, VSCODE_AUTOCOMPLETE)) { + var target = args.get(0); + if (target == null) target = self; + res = vscodeAutoSuggest(env, target, args.get(1).toString(env), args.get(2).toBoolean()); + } + else { + ws.send(new V8Error("Please use well-known functions with callFunctionOn")); + return; + } + ws.send(msg.respond(new JSONMap().set("result", serializeObj(env, res, byValue)))); + } + catch (EngineException e) { ws.send(msg.respond(new JSONMap().set("exceptionDetails", serializeException(env, e)))); } + } + + @Override public synchronized void runtimeEnable(V8Message msg) throws IOException { + ws.send(msg.respond()); + } + + @Override public void onSourceLoad(Filename filename, String source) { + int id = nextId(); + var src = new DebugSource(id, filename, source); + + idToSource.put(id, src); + filenameToId.put(filename, id); + + if (!enabled) pendingSources.add(src); + else sendSource(src); + } + @Override public void onFunctionLoad(FunctionBody body, FunctionMap map) { + for (var bpt : idToBreakpoint.values()) { + bpt.addFunc(body, map); + } + mappings.put(body, map); + } + @Override public boolean onInstruction(Environment env, Frame cf, Instruction instruction, Value returnVal, EngineException error, boolean caught) { + if (!enabled) return false; + + boolean isBreakpointable; + Location loc; + DebugFrame frame; + BreakpointType bptType; + + synchronized (this) { + frame = getFrame(cf); + + var map = DebugContext.get(env).getMap(frame.frame.function); + + frame.updateLoc(map.toLocation(frame.frame.codePtr)); + loc = frame.location; + bptType = map.getBreakpoint(frame.frame.codePtr); + isBreakpointable = loc != null && (bptType.shouldStepIn()); + + if (error != null && (execptionType == CatchType.ALL || execptionType == CatchType.UNCAUGHT && !caught)) { + pauseException(env, error); + } + else if ( + loc != null && + (state == State.STEPPING_IN || state == State.STEPPING_OVER) && + returnVal != null && stepOutFrame == frame + ) { + pauseReturn(env, returnVal); + } + else if (isBreakpointable && bpLocs.containsKey(loc)) { + for (var bp : bpLocs.get(loc)) { + var ok = bp.condition == null ? true : run(currFrame, bp.condition).result.toBoolean(); + if (ok) pauseDebug(env, bp); + } + } + // else if (isBreakpointable && tmpBreakpts.remove(loc)) pauseDebug(ctx, null); + else if (isBreakpointable && pendingPause) { + pauseDebug(env, null); + pendingPause = false; + } + else if ( + instruction != null && + instruction.type == Type.NOP && + instruction.params.length == 1 && + instruction.get(0).equals("debug") + ) pauseDebug(env, null); + } + + while (enabled) { + synchronized (this) { + switch (state) { + case PAUSED_EXCEPTION: + case PAUSED_NORMAL: break; + + case STEPPING_OUT: + case RESUMED: return false; + + case STEPPING_IN: + case STEPPING_OVER: + if (stepOutFrame.frame == frame.frame) { + if (returnVal != null || error != null) { + state = State.STEPPING_OUT; + continue; + } + else if (stepOutPtr != frame.frame.codePtr) { + + if (state == State.STEPPING_IN && bptType.shouldStepIn()) { + pauseDebug(env, null); + break; + } + else if (state == State.STEPPING_OVER && bptType.shouldStepOver()) { + pauseDebug(env, null); + break; + } + } + } + return false; + } + } + + try { + synchronized (updateNotifier) { + updateNotifier.wait(); + } + } + catch (InterruptedException e) { Thread.currentThread().interrupt(); } + } + + return false; + } + @Override public void onFramePush(Environment env, Frame frame) { + var prevFrame = currFrame; + updateFrames(env, 0); + + if (stepOutFrame != null && stepOutFrame.frame == prevFrame.frame && state == State.STEPPING_IN) { + stepOutFrame = currFrame; + } + } + @Override public void onFramePop(Environment env, Frame frame) { + updateFrames(env, 1); + + try { idToFrame.remove(codeFrameToFrame.remove(frame).id); } + catch (NullPointerException e) { } + + if (Frame.get(env).size() == 0) { + if (state == State.PAUSED_EXCEPTION || state == State.PAUSED_NORMAL) resume(State.RESUMED); + } + else if (stepOutFrame != null && stepOutFrame.frame == frame && state == State.STEPPING_OUT) { + state = State.STEPPING_IN; + stepOutFrame = currFrame; + } + } + + public SimpleDebugger attach(DebugContext ctx) { + ctx.attachDebugger(this); + contexts.put(ctx, ctx); + return this; + } + + public SimpleDebugger(WebSocket ws) { + this.ws = ws; + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/StackObject.java b/src/main/java/me/topchetoeu/jscript/repl/debug/StackObject.java new file mode 100644 index 0000000..7976647 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/StackObject.java @@ -0,0 +1,39 @@ +package me.topchetoeu.jscript.repl.debug; + +import me.topchetoeu.jscript.common.environment.Environment; +import me.topchetoeu.jscript.runtime.Frame; +import me.topchetoeu.jscript.runtime.values.Value; +import me.topchetoeu.jscript.runtime.values.objects.ArrayLikeValue; + +public class StackObject extends ArrayLikeValue { + public final Frame frame; + + @Override public Value get(int i) { + if (!has(i)) return null; + return frame.stack[i]; + } + @Override public boolean set(Environment env, int i, Value val) { + if (!has(i)) return false; + frame.stack[i] = val; + return true; + } + @Override public boolean has(int i) { + return i >= 0 && i < frame.stackPtr; + } + @Override public boolean remove(int i) { + return false; + } + @Override public boolean setSize(int val) { + return false; + } + @Override public int size() { + return frame.stackPtr; + } + // @Override public void set(int i, Value val) { + // } + + public StackObject(Frame frame) { + super(); + this.frame = frame; + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/V8Error.java b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Error.java new file mode 100644 index 0000000..b77354c --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Error.java @@ -0,0 +1,19 @@ +package me.topchetoeu.jscript.repl.debug; + +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONMap; + +public class V8Error { + public final String message; + + public V8Error(String message) { + this.message = message; + } + + @Override + public String toString() { + return JSON.stringify(new JSONMap().set("error", new JSONMap() + .set("message", message) + )); + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/V8Event.java b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Event.java new file mode 100644 index 0000000..64805ad --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Event.java @@ -0,0 +1,22 @@ +package me.topchetoeu.jscript.repl.debug; + +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONMap; + +public class V8Event { + public final String name; + public final JSONMap params; + + public V8Event(String name, JSONMap params) { + this.name = name; + this.params = params; + } + + @Override + public String toString() { + return JSON.stringify(new JSONMap() + .set("method", name) + .set("params", params) + ); + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/V8Message.java b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Message.java new file mode 100644 index 0000000..45a6d7b --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Message.java @@ -0,0 +1,50 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.util.Map; + +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONElement; +import me.topchetoeu.jscript.common.json.JSONMap; + +public class V8Message { + public final String name; + public final int id; + public final JSONMap params; + + public V8Message(String name, int id, Map params) { + this.name = name; + this.params = new JSONMap(params); + this.id = id; + } + public V8Result respond(JSONMap result) { + return new V8Result(id, result); + } + public V8Result respond() { + return new V8Result(id, new JSONMap()); + } + + public V8Message(JSONMap raw) { + if (!raw.isNumber("id")) throw new IllegalArgumentException("Expected number property 'id'."); + if (!raw.isString("method")) throw new IllegalArgumentException("Expected string property 'method'."); + + this.name = raw.string("method"); + this.id = (int)raw.number("id"); + this.params = raw.contains("params") ? raw.map("params") : new JSONMap(); + } + public V8Message(String raw) { + this(JSON.parse(null, raw).map()); + } + + public JSONMap toMap() { + var res = new JSONMap(); + return res; + } + @Override + public String toString() { + return JSON.stringify(new JSONMap() + .set("method", name) + .set("params", params) + .set("id", id) + ); + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/V8Result.java b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Result.java new file mode 100644 index 0000000..99e7558 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/V8Result.java @@ -0,0 +1,22 @@ +package me.topchetoeu.jscript.repl.debug; + +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.json.JSONMap; + +public class V8Result { + public final int id; + public final JSONMap result; + + public V8Result(int id, JSONMap result) { + this.id = id; + this.result = result; + } + + @Override + public String toString() { + return JSON.stringify(new JSONMap() + .set("id", id) + .set("result", result) + ); + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/WebSocket.java b/src/main/java/me/topchetoeu/jscript/repl/debug/WebSocket.java new file mode 100644 index 0000000..94d269d --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/WebSocket.java @@ -0,0 +1,186 @@ +package me.topchetoeu.jscript.repl.debug; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +import me.topchetoeu.jscript.repl.debug.WebSocketMessage.Type; + +public class WebSocket implements AutoCloseable { + public long maxLength = 1 << 20; + + private Socket socket; + private boolean closed = false; + + private OutputStream out() throws IOException { + return socket.getOutputStream(); + } + private InputStream in() throws IOException { + return socket.getInputStream(); + } + + private long readLen(int byteLen) throws IOException { + long res = 0; + + if (byteLen == 126) { + res |= in().read() << 8; + res |= in().read(); + return res; + } + else if (byteLen == 127) { + res |= in().read() << 56; + res |= in().read() << 48; + res |= in().read() << 40; + res |= in().read() << 32; + res |= in().read() << 24; + res |= in().read() << 16; + res |= in().read() << 8; + res |= in().read(); + return res; + } + else return byteLen; + } + private byte[] readMask(boolean has) throws IOException { + if (has) { + return new byte[] { + (byte)in().read(), + (byte)in().read(), + (byte)in().read(), + (byte)in().read() + }; + } + else return new byte[4]; + } + + private void writeLength(int len) throws IOException { + if (len < 126) { + out().write((int)len); + } + else if (len <= 0xFFFF) { + out().write(126); + out().write((int)(len >> 8) & 0xFF); + out().write((int)len & 0xFF); + } + else { + out().write(127); + out().write(0); + out().write(0); + out().write(0); + out().write(0); + out().write((len >> 24) & 0xFF); + out().write((len >> 16) & 0xFF); + out().write((len >> 8) & 0xFF); + out().write(len & 0xFF); + } + } + private synchronized void write(int type, byte[] data) throws IOException { + out().write(type | 0x80); + writeLength(data.length); + out().write(data); + } + + public void send(String data) throws IOException { + if (closed) throw new IllegalStateException("Websocket is closed."); + write(1, data.getBytes()); + } + public void send(byte[] data) throws IOException { + if (closed) throw new IllegalStateException("Websocket is closed."); + write(2, data); + } + public void send(WebSocketMessage msg) throws IOException { + if (msg.type == Type.Binary) send(msg.binaryData()); + else send(msg.textData()); + } + public void send(Object data) throws IOException { + if (closed) throw new IllegalStateException("Websocket is closed."); + write(1, data.toString().getBytes()); + } + + public void close(String reason) { + if (socket != null) { + try { + write(8, reason.getBytes()); + socket.close(); + } + catch (Throwable e) { } + } + + socket = null; + closed = true; + } + public void close() { + close(""); + } + + private WebSocketMessage fail(String reason) { + System.out.println("WebSocket Error: " + reason); + close(reason); + return null; + } + + private byte[] readData() throws IOException { + var maskLen = in().read(); + var hasMask = (maskLen & 0x80) != 0; + var len = (int)readLen(maskLen & 0x7F); + var mask = readMask(hasMask); + + if (len > maxLength) fail("WebSocket Error: client exceeded configured max message size"); + else { + var buff = new byte[len]; + + if (in().read(buff) < len) fail("WebSocket Error: payload too short"); + else { + for (int i = 0; i < len; i++) { + buff[i] ^= mask[(int)(i % 4)]; + } + return buff; + } + } + + return null; + } + + public WebSocketMessage receive() throws IOException { + var data = new ByteArrayOutputStream(); + var type = 0; + + while (socket != null && !closed) { + var finId = in().read(); + if (finId < 0) break; + var fin = (finId & 0x80) != 0; + int id = finId & 0x0F; + + if (id == 0x8) { close(); return null; } + if (id >= 0x8) { + if (!fin) return fail("WebSocket Error: client-sent control frame was fragmented"); + if (id == 0x9) write(0xA, data.toByteArray()); + continue; + } + + if (type == 0) type = id; + if (type == 0) return fail("WebSocket Error: client used opcode 0x00 for first fragment"); + + var buff = readData(); + if (buff == null) break; + + if (data.size() + buff.length > maxLength) return fail("WebSocket Error: client exceeded configured max message size"); + data.write(buff); + + if (!fin) continue; + var raw = data.toByteArray(); + + if (type == 1) { + return new WebSocketMessage(new String(raw)); + } + else return new WebSocketMessage(raw); + } + + return null; + } + + public WebSocket(Socket socket) { + this.socket = socket; + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/debug/WebSocketMessage.java b/src/main/java/me/topchetoeu/jscript/repl/debug/WebSocketMessage.java new file mode 100644 index 0000000..cc655c7 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/debug/WebSocketMessage.java @@ -0,0 +1,29 @@ +package me.topchetoeu.jscript.repl.debug; + +public class WebSocketMessage { + public static enum Type { + Text, + Binary, + } + + public final Type type; + private final Object data; + + public final String textData() { + if (type != Type.Text) throw new IllegalStateException("Message is not text."); + return (String)data; + } + public final byte[] binaryData() { + if (type != Type.Binary) throw new IllegalStateException("Message is not binary."); + return (byte[])data; + } + + public WebSocketMessage(String data) { + this.type = Type.Text; + this.data = data; + } + public WebSocketMessage(byte[] data) { + this.type = Type.Binary; + this.data = data; + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java b/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java new file mode 100644 index 0000000..6dc9b23 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java @@ -0,0 +1,63 @@ +package me.topchetoeu.jscript.repl.mapping; + +import java.util.function.Function; + +import me.topchetoeu.jscript.common.environment.Environment; +import me.topchetoeu.jscript.common.parsing.Filename; +import me.topchetoeu.jscript.common.parsing.Location; +import me.topchetoeu.jscript.runtime.exceptions.EngineException; +import me.topchetoeu.jscript.runtime.values.Value; +import me.topchetoeu.jscript.runtime.values.functions.FunctionValue; +import me.topchetoeu.jscript.runtime.values.objects.ArrayLikeValue; +import me.topchetoeu.jscript.runtime.values.objects.ArrayValue; +import me.topchetoeu.jscript.runtime.values.primitives.StringValue; +import me.topchetoeu.jscript.runtime.values.primitives.numbers.NumberValue; + +public class NativeMapper extends FunctionValue { + public final Function mapper; + + @Override protected Value onApply(Environment env, Value thisArg, Value... args) { + var rawLoc = (ArrayLikeValue)args[0]; + var loc = Location.of( + Filename.parse(rawLoc.get(0).toString(env)), + rawLoc.get(1).toNumber(env).getInt(), + rawLoc.get(2).toNumber(env).getInt() + ); + + var res = mapper.apply(loc); + + return new ArrayValue( + StringValue.of(res.filename().toString()), + NumberValue.of(res.line()), + NumberValue.of(res.start()) + ); + } + + @Override protected Value onConstruct(Environment ext, Value target, Value... args) { + throw EngineException.ofType("Function cannot be constructed"); + } + + public NativeMapper(Function mapper) { + super("mapper", 1); + this.mapper = mapper; + } + + public static Function unwrap(Environment env, FunctionValue func) { + if (func instanceof NativeMapper nat) return nat.mapper; + + return loc -> { + var rawLoc = new ArrayValue( + StringValue.of(loc.filename().toString()), + NumberValue.of(loc.line()), + NumberValue.of(loc.start()) + ); + + var rawRes = (ArrayLikeValue)func.apply(env, Value.UNDEFINED, rawLoc); + return Location.of( + Filename.parse(rawRes.get(0).toString(env)), + rawRes.get(1).toNumber(env).getInt(), + rawRes.get(2).toNumber(env).getInt() + ); + }; + } +} diff --git a/src/main/java/me/topchetoeu/jscript/repl/mapping/SourceMap.java b/src/main/java/me/topchetoeu/jscript/repl/mapping/SourceMap.java new file mode 100644 index 0000000..00c847d --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/mapping/SourceMap.java @@ -0,0 +1,123 @@ +package me.topchetoeu.jscript.repl.mapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import me.topchetoeu.jscript.common.json.JSON; +import me.topchetoeu.jscript.common.parsing.Filename; +import me.topchetoeu.jscript.common.parsing.Location; + +public class SourceMap { + private final TreeMap origToComp = new TreeMap<>(); + private final TreeMap compToOrig = new TreeMap<>(); + private final Filename compiled, original; + + public Location toCompiled(Location loc) { return convert(original, compiled, loc, origToComp); } + public Location toOriginal(Location loc) { return convert(compiled, original, loc, compToOrig); } + + private void add(long orig, long comp) { + var a = origToComp.remove(orig); + var b = compToOrig.remove(comp); + + if (b != null) origToComp.remove(b); + if (a != null) compToOrig.remove(a); + + origToComp.put(orig, comp); + compToOrig.put(comp, orig); + } + + public SourceMap apply(SourceMap map) { + var res = new SourceMap(map.compiled, map.original); + + for (var el : new ArrayList<>(origToComp.entrySet())) { + var mapped = convert(el.getValue(), map.origToComp); + res.origToComp.put(el.getKey(), mapped); + } + for (var el : new ArrayList<>(compToOrig.entrySet())) { + var mapped = convert(el.getKey(), map.compToOrig); + res.compToOrig.put(mapped, el.getValue()); + res.add(el.getValue(), mapped); + } + + return res; + } + + public SourceMap clone() { + var res = new SourceMap(this.compiled, this.original); + res.origToComp.putAll(this.origToComp); + res.compToOrig.putAll(this.compToOrig); + return res; + } + + public SourceMap(Filename compiled, Filename original) { + this.compiled = compiled; + this.original = original; + } + + public static SourceMap parse(Filename compiled, Filename original, String raw) { + var mapping = VLQ.decodeMapping(raw); + var res = new SourceMap(compiled, original); + + var compRow = 0l; + var compCol = 0l; + + for (var origRow = 0; origRow < mapping.length; origRow++) { + var origCol = 0; + + for (var rawSeg : mapping[origRow]) { + if (rawSeg.length > 1 && rawSeg[1] != 0) throw new IllegalArgumentException("Source mapping is to more than one files."); + origCol += rawSeg.length > 0 ? rawSeg[0] : 0; + compRow += rawSeg.length > 2 ? rawSeg[2] : 0; + compCol += rawSeg.length > 3 ? rawSeg[3] : 0; + + var compPacked = ((long)compRow << 32) | compCol; + var origPacked = ((long)origRow << 32) | origCol; + + res.add(origPacked, compPacked); + } + } + + return res; + } + public static List getSources(String raw) { + var json = JSON.parse(null, raw).map(); + return json + .list("sourcesContent") + .stream() + .map(v -> v.string()) + .collect(Collectors.toList()); + } + + public static SourceMap chain(SourceMap ...maps) { + if (maps.length == 0) return null; + var res = maps[0]; + + for (var i = 1; i < maps.length; i++) res = res.apply(maps[i]); + + return res; + } + + private static Long convert(long packed, TreeMap map) { + if (map.containsKey(packed)) return map.get(packed); + var key = map.floorKey(packed); + if (key == null) return null; + else return map.get(key); + } + + private static Location convert(Filename src, Filename dst, Location loc, TreeMap map) { + if (!loc.filename().equals(src)) return loc; + + var packed = ((loc.line()) << 32) | (loc.start()); + var resPacked = convert(packed, map); + + if (resPacked == null) return null; + else return Location.of(dst, (int)(resPacked >> 32), (int)(resPacked & 0xFFFF)); + } + + // public static SourceMap of(String filename, String raw) { + // var json = JSON.parse(Filename.parse(filename), raw); + // return new SourceMap(); + // } +} \ No newline at end of file diff --git a/src/main/java/me/topchetoeu/jscript/repl/mapping/VLQ.java b/src/main/java/me/topchetoeu/jscript/repl/mapping/VLQ.java new file mode 100644 index 0000000..50d3c71 --- /dev/null +++ b/src/main/java/me/topchetoeu/jscript/repl/mapping/VLQ.java @@ -0,0 +1,95 @@ +package me.topchetoeu.jscript.repl.mapping; + +import java.util.ArrayList; +import java.util.List; + +public class VLQ { + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + private static long[] toArray(List list) { + var arr = new long[list.size()]; + for (var i = 0; i < list.size(); i++) arr[i] = list.get(i); + return arr; + } + + public static String encode(long... arr) { + var raw = new StringBuilder(); + + for (var data : arr) { + var b = data < 0 ? 1 : 0; + data = Math.abs(data); + b |= (int)(data & 0b1111) << 1; + data >>= 4; + b |= data > 0 ? 0x20 : 0;; + raw.append(ALPHABET.charAt(b)); + + while (data > 0) { + b = (int)(data & 0b11111); + data >>= 5; + b |= data > 0 ? 0x20 : 0; + raw.append(ALPHABET.charAt(b)); + } + } + + return raw.toString(); + } + public static long[] decode(String val) { + if (val.length() == 0) return new long[0]; + + var list = new ArrayList(); + + for (var i = 0; i < val.length();) { + var sign = 1; + var curr = ALPHABET.indexOf(val.charAt(i++)); + var cont = (curr & 0x20) == 0x20; + if ((curr & 1) == 1) sign = -1; + long res = (curr & 0b11110) >> 1; + var n = 4; + + for (; i < val.length() && cont;) { + curr = ALPHABET.indexOf(val.charAt(i++)); + cont = (curr & 0x20) == 0x20; + res |= (curr & 0b11111) << n; + n += 5; + if (!cont) break; + } + + list.add(res * sign); + } + + return toArray(list); + } + + public static String encodeMapping(long[][][] arr) { + var res = new StringBuilder(); + var semicolon = false; + + for (var line : arr) { + var comma = false; + + if (semicolon) res.append(";"); + semicolon = true; + + for (var el : line) { + if (comma) res.append(","); + comma = true; + res.append(encode(el)); + } + } + + return res.toString(); + } + public static long[][][] decodeMapping(String val) { + var lines = new ArrayList(); + + for (var line : val.split(";", -1)) { + var elements = new ArrayList(); + for (var el : line.split(",", -1)) { + elements.add(decode(el)); + } + lines.add(elements.toArray(new long[0][])); + } + + return lines.toArray(new long[0][][]); + } +} \ No newline at end of file diff --git a/src/main/java/me/topchetoeu/jscript/runtime/Compiler.java b/src/main/java/me/topchetoeu/jscript/runtime/Compiler.java index 852d160..16a649e 100644 --- a/src/main/java/me/topchetoeu/jscript/runtime/Compiler.java +++ b/src/main/java/me/topchetoeu/jscript/runtime/Compiler.java @@ -1,25 +1,32 @@ package me.topchetoeu.jscript.runtime; +import java.util.function.Function; + import me.topchetoeu.jscript.common.FunctionBody; import me.topchetoeu.jscript.common.SyntaxException; import me.topchetoeu.jscript.common.environment.Environment; import me.topchetoeu.jscript.common.environment.Key; import me.topchetoeu.jscript.common.parsing.Filename; +import me.topchetoeu.jscript.common.parsing.Location; import me.topchetoeu.jscript.compilation.CompileResult; import me.topchetoeu.jscript.compilation.JavaScript; +import me.topchetoeu.jscript.repl.mapping.NativeMapper; import me.topchetoeu.jscript.runtime.debug.DebugContext; import me.topchetoeu.jscript.runtime.exceptions.EngineException; import me.topchetoeu.jscript.runtime.values.Value; import me.topchetoeu.jscript.runtime.values.functions.CodeFunction; +import me.topchetoeu.jscript.runtime.values.functions.FunctionValue; +import me.topchetoeu.jscript.runtime.values.functions.NativeFunction; +import me.topchetoeu.jscript.runtime.values.primitives.StringValue; public interface Compiler { - public static final Compiler DEFAULT = (env, filename, raw) -> { + public static final Compiler DEFAULT = (env, filename, raw, mapper) -> { try { var res = JavaScript.compile(env, filename, raw, true); var body = res.body(); DebugContext.get(env).onSource(filename, raw); - registerFunc(env, body, res); - return body; + registerFunc(env, body, res, mapper); + return new CodeFunction(env, filename.toString(), body, new Value[0][]); } catch (SyntaxException e) { var res = EngineException.ofSyntax(e.msg); @@ -30,25 +37,45 @@ public interface Compiler { public Key KEY = new Key<>(); - public FunctionBody compile(Environment env, Filename filename, String source); + public FunctionValue compile(Environment env, Filename filename, String source, Function map); + + public default Compiler wrap(Environment compilerEnv, Environment targetEnv, FunctionValue factory) { + var curr = new NativeFunction(args -> { + var filename = Filename.parse(args.get(0).toString(args.env)); + var src = args.get(1).toString(args.env); + var mapper = (FunctionValue)args.get(2); + return this.compile(targetEnv, filename, src, NativeMapper.unwrap(args.env, mapper)); + }); + + var next = (FunctionValue)factory.apply(compilerEnv, Value.UNDEFINED, curr); + + return (env, filename, source, map) -> { + return (FunctionValue)next.apply( + compilerEnv, Value.UNDEFINED, + StringValue.of(filename.toString()), + StringValue.of(source), + new NativeMapper(map) + ); + }; + } public static Compiler get(Environment ext) { - return ext.get(KEY, (env, filename, src) -> { + return ext.get(KEY, (env, filename, src, map) -> { throw EngineException.ofError("No compiler attached to engine"); }); } - static void registerFunc(Environment env, FunctionBody body, CompileResult res) { - var map = res.map(); + static void registerFunc(Environment env, FunctionBody body, CompileResult res, Function mapper) { + var map = res.map(mapper); DebugContext.get(env).onFunctionLoad(body, map); for (var i = 0; i < body.children.length; i++) { - registerFunc(env, body.children[i], res.children.get(i)); + registerFunc(env, body.children[i], res.children.get(i), mapper); } } - public static CodeFunction compileFunc(Environment env, Filename filename, String raw) { - return new CodeFunction(env, filename.toString(), get(env).compile(env, filename, raw), new Value[0][]); + public static FunctionValue compileFunc(Environment env, Filename filename, String raw) { + return get(env).compile(env, filename, raw, v -> v); } }