Debugging support #7

Merged
TopchetoEU merged 7 commits from TopcehtoEU/debugging into master 2023-10-28 14:11:55 +00:00
54 changed files with 3779 additions and 213 deletions
Showing only changes of commit 4b84309df6 - Show all commits

View File

@ -10,6 +10,8 @@ const conf = {
version: argv[3]
};
console.log(conf)
if (conf.version.startsWith('refs/tags/')) conf.version = conf.version.substring(10);
if (conf.version.startsWith('v')) conf.version = conf.version.substring(1);

BIN
src/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

29
src/assets/index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JScript Debugger</title>
</head>
<body>
<p>
This is the debugger of JScript. It implement the <a href="https://chromedevtools.github.io/devtools-protocol/1-2/">V8 Debugging protocol</a>,
so you can use the devtools in chrome. <br>
The debugger is still in early development, so please report any issues to
<a href="https://github.com/TopchetoEU/java-jscript/issues">the github repo</a>.
</p>
<p>
Here are the available entrypoints:
<ul>
<li><a href="json/version">/json/version</a></li>
<li><a href="json/list">/json/list</a></li>
<li>/(any target)</a></li>
</ul>
</p>
<p>
Developed by TopchetoEU, MIT License
</p>
</body>
</html>

View File

@ -0,0 +1,52 @@
package me.topchetoeu.jscript;
import java.io.File;
public class Filename {
public final String protocol;
public final String path;
public String toString() {
return protocol + "://" + path;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + protocol.hashCode();
result = prime * result + path.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
var other = (Filename) obj;
if (protocol == null) {
if (other.protocol != null) return false;
}
else if (!protocol.equals(other.protocol)) return false;
if (path == null) {
if (other.path != null) return false;
}
else if (!path.equals(other.path)) return false;
return true;
}
public static Filename fromFile(File file) {
return new Filename("file", file.getAbsolutePath());
}
public Filename(String protocol, String path) {
this.protocol = protocol;
this.path = path;
}
}

View File

@ -1,18 +1,18 @@
package me.topchetoeu.jscript;
public class Location {
public static final Location INTERNAL = new Location(0, 0, "<internal>");
public class Location implements Comparable<Location> {
public static final Location INTERNAL = new Location(0, 0, new Filename("jscript", "internal"));
private int line;
private int start;
private String filename;
private Filename filename;
public int line() { return line; }
public int start() { return start; }
public String filename() { return filename; }
public Filename filename() { return filename; }
@Override
public String toString() {
return filename + ":" + line + ":" + start;
return filename.toString() + ":" + line + ":" + start;
}
public Location add(int n, boolean clone) {
@ -55,7 +55,18 @@ public class Location {
return true;
}
public Location(int line, int start, String filename) {
@Override
public int compareTo(Location other) {
int a = filename.toString().compareTo(other.filename.toString());
int b = Integer.compare(line, other.line);
int c = Integer.compare(start, other.start);
if (a != 0) return a;
if (b != 0) return b;
return c;
}
public Location(int line, int start, Filename filename) {
this.line = line;
this.start = start;
this.filename = filename;

View File

@ -1,15 +1,15 @@
package me.topchetoeu.jscript;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.Environment;
import me.topchetoeu.jscript.engine.debug.DebugServer;
import me.topchetoeu.jscript.engine.debug.SimpleDebugger;
import me.topchetoeu.jscript.engine.values.NativeFunction;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.events.Observer;
@ -20,30 +20,10 @@ import me.topchetoeu.jscript.exceptions.UncheckedException;
import me.topchetoeu.jscript.lib.Internals;
public class Main {
static Thread task;
static Thread engineTask, debugTask;
static Engine engine;
static Environment env;
public static String streamToString(InputStream in) {
try {
StringBuilder out = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
for(var line = br.readLine(); line != null; line = br.readLine()) {
out.append(line).append('\n');
}
br.close();
return out.toString();
}
catch (Throwable e) { throw new UncheckedException(e); }
}
public static String resourceToString(String name) {
var str = Main.class.getResourceAsStream("/me/topchetoeu/jscript/" + name);
if (str == null) return null;
return streamToString(str);
}
private static Observer<Object> valuePrinter = new Observer<Object>() {
public void next(Object data) {
Values.printValue(null, data);
@ -56,17 +36,18 @@ public class Main {
@Override
public void finish() {
task.interrupt();
engineTask.interrupt();
}
};
public static void main(String args[]) {
System.out.println(String.format("Running %s v%s by %s", Metadata.NAME, Metadata.VERSION, Metadata.AUTHOR));
var in = new BufferedReader(new InputStreamReader(System.in));
engine = new Engine();
env = new Environment(null, null, null);
var exited = new boolean[1];
var server = new DebugServer();
server.targets.put("target", (ws, req) -> SimpleDebugger.get(ws, engine));
engine.pushMsg(false, null, new NativeFunction((ctx, thisArg, _a) -> {
new Internals().apply(env);
@ -77,7 +58,8 @@ public class Main {
});
env.global.define("go", _ctx -> {
try {
var func = _ctx.compile("do.js", new String(Files.readAllBytes(Path.of("do.js"))));
var f = Path.of("do.js");
var func = _ctx.compile(Filename.fromFile(f.toFile()), new String(Files.readAllBytes(f)));
return func.call(_ctx);
}
catch (IOException e) {
@ -88,15 +70,17 @@ public class Main {
return null;
}), null);
task = engine.start();
engineTask = engine.start();
debugTask = server.start(new InetSocketAddress("127.0.0.1", 9229), true);
var reader = new Thread(() -> {
try {
while (true) {
for (var i = 0; ; i++) {
try {
var raw = in.readLine();
var raw = Reading.read();
if (raw == null) break;
engine.pushMsg(false, new Context(engine).pushEnv(env), "<stdio>", raw, null).toObservable().once(valuePrinter);
valuePrinter.next(engine.pushMsg(false, new Context(engine).pushEnv(env), new Filename("jscript", "repl/" + i + ".js"), raw, null).await());
}
catch (EngineException e) { Values.printError(e, ""); }
}
@ -107,12 +91,13 @@ public class Main {
System.out.println("Syntax error:" + ex.msg);
}
catch (RuntimeException ex) {
if (exited[0]) return;
System.out.println("Internal error ocurred:");
ex.printStackTrace();
if (!exited[0]) {
System.out.println("Internal error ocurred:");
ex.printStackTrace();
}
}
catch (Throwable e) { throw new UncheckedException(e); }
if (exited[0]) return;
if (exited[0]) debugTask.interrupt();
});
reader.setDaemon(true);
reader.setName("STD Reader");

View File

@ -0,0 +1,36 @@
package me.topchetoeu.jscript;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import me.topchetoeu.jscript.exceptions.UncheckedException;
public class Reading {
private static final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
public static synchronized String read() throws IOException {
return reader.readLine();
}
public static String streamToString(InputStream in) {
try {
StringBuilder out = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
for(var line = br.readLine(); line != null; line = br.readLine()) {
out.append(line).append('\n');
}
br.close();
return out.toString();
}
catch (Throwable e) { throw new UncheckedException(e); }
}
public static String resourceToString(String name) {
var str = Main.class.getResourceAsStream("/me/topchetoeu/jscript/" + name);
if (str == null) return null;
return streamToString(str);
}
}

View File

@ -1,11 +1,15 @@
package me.topchetoeu.jscript.compilation;
import java.util.Map;
import java.util.TreeSet;
import java.util.Vector;
import me.topchetoeu.jscript.Location;
public class CompileTarget {
public final Vector<Instruction> target = new Vector<>();
public final Map<Long, Instruction[]> functions;
public final TreeSet<Location> breakpoints;
public Instruction add(Instruction instr) {
target.add(instr);
@ -14,6 +18,12 @@ public class CompileTarget {
public Instruction set(int i, Instruction instr) {
return target.set(i, instr);
}
public void setDebug(int i) {
breakpoints.add(target.get(i).location);
}
public void setDebug() {
setDebug(target.size() - 1);
}
public Instruction get(int i) {
return target.get(i);
}
@ -21,7 +31,8 @@ public class CompileTarget {
public Instruction[] array() { return target.toArray(Instruction[]::new); }
public CompileTarget(Map<Long, Instruction[]> functions) {
public CompileTarget(Map<Long, Instruction[]> functions, TreeSet<Location> breakpoints) {
this.functions = functions;
this.breakpoints = breakpoints;
}
}

View File

@ -25,7 +25,7 @@ public class CompoundStatement extends Statement {
if (stm instanceof FunctionStatement) {
int start = target.size();
((FunctionStatement)stm).compile(target, scope, null, true);
target.get(start).setDebug(true);
target.setDebug(start);
target.add(Instruction.discard());
}
}

View File

@ -90,16 +90,11 @@ public class Instruction {
public final Type type;
public final Object[] params;
public Location location;
public boolean debugged;
public Instruction locate(Location loc) {
this.location = loc;
return this;
}
public Instruction setDebug(boolean debug) {
debugged = debug;
return this;
}
@SuppressWarnings("unchecked")
public <T> T get(int i) {

View File

@ -14,7 +14,7 @@ public abstract class Statement {
public void compileWithDebug(CompileTarget target, ScopeRecord scope, boolean pollute) {
int start = target.size();
compile(target, scope, pollute);
if (target.size() != start) target.get(start).setDebug(true);
if (target.size() != start) target.setDebug(start);
}
public Location loc() { return _loc; }

View File

@ -68,7 +68,8 @@ public class SwitchStatement extends Statement {
var loc = target.get(el.getKey()).location;
var i = stmIndexMap.get(el.getValue());
if (i == null) i = target.size();
target.set(el.getKey(), Instruction.jmpIf(i - el.getKey()).locate(loc).setDebug(true));
target.set(el.getKey(), Instruction.jmpIf(i - el.getKey()).locate(loc));
target.setDebug(el.getKey());
}
target.add(Instruction.discard().locate(loc()));

View File

@ -22,7 +22,8 @@ public class CallStatement extends Statement {
for (var arg : args) arg.compile(target, scope, true);
target.add(Instruction.call(args.length).locate(loc()).setDebug(true));
target.add(Instruction.call(args.length).locate(loc()));
target.setDebug();
if (!pollute) target.add(Instruction.discard().locate(loc()));
}

View File

@ -51,7 +51,7 @@ public class FunctionStatement extends Statement {
var subscope = scope.child();
int start = target.size();
var funcTarget = new CompileTarget(target.functions);
var funcTarget = new CompileTarget(target.functions, target.breakpoints);
subscope.define("this");
var argsVar = subscope.define("arguments");

View File

@ -24,14 +24,16 @@ public class IndexAssignStatement extends Statement {
value.compile(target, scope, true);
target.add(Instruction.operation(operation).locate(loc()));
target.add(Instruction.storeMember(pollute).locate(loc()).setDebug(true));
target.add(Instruction.storeMember(pollute).locate(loc()));
target.setDebug();
}
else {
object.compile(target, scope, true);
index.compile(target, scope, true);
value.compile(target, scope, true);
target.add(Instruction.storeMember(pollute).locate(loc()).setDebug(true));
target.add(Instruction.storeMember(pollute).locate(loc()));
target.setDebug();
}
}

View File

@ -30,7 +30,7 @@ public class IndexStatement extends AssignableStatement {
index.compile(target, scope, true);
target.add(Instruction.loadMember().locate(loc()));
target.get(start).setDebug(true);
target.setDebug(start);
if (!pollute) target.add(Instruction.discard().locate(loc()));
}
@Override

View File

@ -16,7 +16,8 @@ public class NewStatement extends Statement {
for (var arg : args) arg.compile(target, scope, true);
target.add(Instruction.callNew(args.length).locate(loc()).setDebug(true));
target.add(Instruction.callNew(args.length).locate(loc()));
target.setDebug();
}
public NewStatement(Location loc, Statement func, Statement ...args) {

View File

@ -1,7 +1,10 @@
package me.topchetoeu.jscript.engine;
import java.util.Stack;
import java.util.TreeSet;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.parsing.Parsing;
@ -23,9 +26,13 @@ public class Context {
if (!env.empty()) this.env.pop();
}
public FunctionValue compile(String filename, String raw) {
var res = Values.toString(this, environment().compile.call(this, null, raw, filename));
return Parsing.compile(engine.functions, environment(), filename, res);
public FunctionValue compile(Filename filename, String raw) {
var src = Values.toString(this, environment().compile.call(this, null, raw, filename));
var debugger = StackData.getDebugger(this);
var breakpoints = new TreeSet<Location>();
var res = Parsing.compile(engine.functions, breakpoints, environment(), filename, src);
if (debugger != null) debugger.onSource(filename, src, breakpoints);
return res;
}
public Context(Engine engine, Data data) {

View File

@ -35,8 +35,7 @@ public class Data {
public <T> T get(DataKey<T> key, T val) {
for (var it = this; it != null; it = it.parent) {
if (it.data.containsKey(key)) {
this.set(key, val);
return (T)data.get((DataKey<Object>)key);
return (T)it.data.get((DataKey<Object>)key);
}
}
@ -45,7 +44,7 @@ public class Data {
}
public <T> T get(DataKey<T> key) {
for (var it = this; it != null; it = it.parent) {
if (it.data.containsKey(key)) return (T)data.get((DataKey<Object>)key);
if (it.data.containsKey(key)) return (T)it.data.get((DataKey<Object>)key);
}
return null;
}

View File

@ -3,6 +3,7 @@ package me.topchetoeu.jscript.engine;
import java.util.HashMap;
import java.util.concurrent.LinkedBlockingDeque;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.events.Awaitable;
@ -11,7 +12,7 @@ import me.topchetoeu.jscript.exceptions.InterruptException;
public class Engine {
private class UncompiledFunction extends FunctionValue {
public final String filename;
public final Filename filename;
public final String raw;
private FunctionValue compiled = null;
@ -21,8 +22,8 @@ public class Engine {
return compiled.call(ctx, thisArg, args);
}
public UncompiledFunction(String filename, String raw) {
super(filename, 0);
public UncompiledFunction(Filename filename, String raw) {
super(filename + "", 0);
this.filename = filename;
this.raw = raw;
}
@ -58,6 +59,7 @@ public class Engine {
task.notifier.next(task.func.call(task.ctx, task.thisArg, task.args));
}
catch (RuntimeException e) {
if (e instanceof InterruptException) throw e;
task.notifier.error(e);
}
}
@ -70,7 +72,7 @@ public class Engine {
runTask(microTasks.take());
}
}
catch (InterruptedException e) {
catch (InterruptedException | InterruptException e) {
for (var msg : macroTasks) {
msg.notifier.error(new InterruptException(e));
}
@ -103,7 +105,7 @@ public class Engine {
else macroTasks.addLast(msg);
return msg.notifier;
}
public Awaitable<Object> pushMsg(boolean micro, Context ctx, String filename, String raw, Object thisArg, Object ...args) {
public Awaitable<Object> pushMsg(boolean micro, Context ctx, Filename filename, String raw, Object thisArg, Object ...args) {
return pushMsg(micro, ctx, new UncompiledFunction(filename, raw), thisArg, args);
}
}

View File

@ -24,7 +24,7 @@ public class Environment {
@Native public FunctionValue compile;
@Native public FunctionValue regexConstructor = new NativeFunction("RegExp", (ctx, thisArg, args) -> {
throw EngineException.ofError("Regular expressions not supported.").setContext(ctx);
throw EngineException.ofError("Regular expressions not supported.").setCtx(ctx.environment(), ctx.engine);
});
public Environment addData(Data data) {

View File

@ -4,14 +4,14 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.topchetoeu.jscript.engine.debug.DebugServer;
import me.topchetoeu.jscript.engine.debug.Debugger;
import me.topchetoeu.jscript.engine.frame.CodeFrame;
import me.topchetoeu.jscript.exceptions.EngineException;
public class StackData {
public static final DataKey<ArrayList<CodeFrame>> FRAMES = new DataKey<>();
public static final DataKey<Integer> MAX_FRAMES = new DataKey<>();
public static final DataKey<DebugServer> DEBUGGER = new DataKey<>();
public static final DataKey<Debugger> DEBUGGER = new DataKey<>();
public static void pushFrame(Context ctx, CodeFrame frame) {
var frames = ctx.data.get(FRAMES, new ArrayList<>());
@ -25,16 +25,25 @@ public class StackData {
if (frames.get(frames.size() - 1) != frame) return false;
frames.remove(frames.size() - 1);
ctx.popEnv();
var dbg = getDebugger(ctx);
if (dbg != null) dbg.onFramePop(ctx, frame);
return true;
}
public static CodeFrame peekFrame(Context ctx) {
var frames = ctx.data.get(FRAMES, new ArrayList<>());
if (frames.size() == 0) return null;
return frames.get(frames.size() - 1);
}
public static List<CodeFrame> frames(Context ctx) {
return Collections.unmodifiableList(ctx.data.get(FRAMES, new ArrayList<>()));
}
public static List<String> stackTrace(Context ctx) {
var res = new ArrayList<String>();
var frames = frames(ctx);
for (var el : frames(ctx)) {
for (var i = frames.size() - 1; i >= 0; i--) {
var el = frames.get(i);
var name = el.function.name;
var loc = el.function.loc();
var trace = "";
@ -49,4 +58,8 @@ public class StackData {
return res;
}
public static Debugger getDebugger(Context ctx) {
return ctx.data.get(DEBUGGER);
}
}

View File

@ -0,0 +1,43 @@
package me.topchetoeu.jscript.engine.debug;
import java.util.TreeSet;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.frame.CodeFrame;
import me.topchetoeu.jscript.exceptions.EngineException;
public interface DebugController {
/**
* Called when a script has been loaded
* @param breakpoints
*/
void onSource(Filename filename, String source, TreeSet<Location> breakpoints);
/**
* Called immediatly before an instruction is executed, as well as after an instruction, if it has threw or returned.
* This function might pause in order to await debugging commands.
* @param ctx The context of execution
* @param frame The frame in which execution is occuring
* @param instruction The instruction which was or will be executed
* @param loc The most recent location the code frame has been at
* @param returnVal The return value of the instruction, Runners.NO_RETURN if none
* @param error The error that the instruction threw, null if none
* @param caught Whether or not the error has been caught
* @return Whether or not the frame should restart
*/
boolean onInstruction(Context ctx, CodeFrame frame, Instruction instruction, Object returnVal, EngineException error, boolean caught);
/**
* Called immediatly after a frame has been popped out of the frame stack.
* This function might pause in order to await debugging commands.
* @param ctx The context of execution
* @param frame The code frame which was popped out
*/
void onFramePop(Context ctx, CodeFrame frame);
void connect();
void disconnect();
}

View File

@ -0,0 +1,23 @@
package me.topchetoeu.jscript.engine.debug;
public interface DebugHandler {
void enable(V8Message msg);
void disable(V8Message msg);
void setBreakpoint(V8Message msg);
void setBreakpointByUrl(V8Message msg);
void removeBreakpoint(V8Message msg);
void continueToLocation(V8Message msg);
void getScriptSource(V8Message msg);
void getPossibleBreakpoints(V8Message msg);
void resume(V8Message msg);
void pause(V8Message msg);
void stepInto(V8Message msg);
void stepOut(V8Message msg);
void stepOver(V8Message msg);
void setPauseOnExceptions(V8Message msg);
}

View File

@ -0,0 +1,223 @@
package me.topchetoeu.jscript.engine.debug;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HashMap;
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
import me.topchetoeu.jscript.exceptions.SyntaxException;
import me.topchetoeu.jscript.exceptions.UncheckedException;
import me.topchetoeu.jscript.exceptions.UncheckedIOException;
import me.topchetoeu.jscript.json.JSON;
import me.topchetoeu.jscript.json.JSONList;
import me.topchetoeu.jscript.json.JSONMap;
public class DebugServer {
public static String browserDisplayName = "jscript";
public final HashMap<String, DebuggerProvider> targets = new HashMap<>();
private final byte[] favicon, index;
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 UncheckedException(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) {
WebSocketMessage raw;
debugger.connect();
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());
System.out.println(msg.name + ": " + JSON.stringify(msg.params));
}
catch (SyntaxException e) {
ws.send(new V8Error(e.getMessage()));
return;
}
try {
switch (msg.name) {
case "Debugger.enable": debugger.enable(msg); continue;
case "Debugger.disable": debugger.disable(msg); continue;
case "Debugger.setBreakpoint": debugger.setBreakpoint(msg); 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;
}
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..."));
if (msg.name.startsWith("Runtime.") || msg.name.startsWith("Profiler.")) ws.send(new V8Error("This API is not supported yet."));
}
catch (Throwable e) {
e.printStackTrace();
throw new UncheckedException(e);
}
}
debugger.disconnect();
}
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(() -> {
try { handle(ws, debugger); }
catch (RuntimeException e) {
ws.send(new V8Error(e.getMessage()));
}
finally { ws.close(); debugger.disconnect(); }
}, "Debug Handler");
}
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.2\"}");
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/new":
case "/json/activate":
case "/json/protocol":
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() {
try {
this.favicon = getClass().getClassLoader().getResourceAsStream("assets/favicon.png").readAllBytes();
this.index = getClass().getClassLoader().getResourceAsStream("assets/index.html").readAllBytes();
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
}

View File

@ -0,0 +1,5 @@
package me.topchetoeu.jscript.engine.debug;
public interface Debugger extends DebugHandler, DebugController {
}

View File

@ -0,0 +1,5 @@
package me.topchetoeu.jscript.engine.debug;
public interface DebuggerProvider {
Debugger getDebugger(WebSocket socket, HttpRequest req);
}

View File

@ -0,0 +1,104 @@
package me.topchetoeu.jscript.engine.debug;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.IllegalFormatException;
import java.util.Map;
import me.topchetoeu.jscript.exceptions.UncheckedIOException;
public class HttpRequest {
public final String method;
public final String path;
public final Map<String, String> 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) { throw new UncheckedIOException(e); }
}
public void writeHeader(String name, String value) {
try { out.write((name + ": " + value + "\r\n").getBytes()); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
public void writeLastHeader(String name, String value) {
try { out.write((name + ": " + value + "\r\n\r\n").getBytes()); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
public void writeHeadersEnd() {
try { out.write("\n".getBytes()); }
catch (IOException e) { throw new UncheckedIOException(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) { throw new UncheckedIOException(e); }
}
public void writeResponse(int code, String name, String type, InputStream data) {
try {
writeResponse(code, name, type, data.readAllBytes());
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
public HttpRequest(String method, String path, Map<String, String> 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<String, String>();
while (!(line = lines.readLine()).isEmpty()) {
var i = line.indexOf(":");
if (i < 0) continue;
var name = line.substring(0, i).trim().toLowerCase();
var value = line.substring(i + 1).trim();
if (name.length() == 0) continue;
headers.put(name, value);
}
if (headers.containsKey("content-length")) {
try {
var i = Integer.parseInt(headers.get("content-length"));
str.skip(i);
}
catch (IllegalFormatException e) { /* ¯\_(ツ)_/¯ */ }
}
return new HttpRequest(method, path, headers, socket.getOutputStream());
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
}

View File

@ -0,0 +1,585 @@
package me.topchetoeu.jscript.engine.debug;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Random;
import java.util.TreeSet;
import java.util.regex.Pattern;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.compilation.Instruction.Type;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.StackData;
import me.topchetoeu.jscript.engine.frame.CodeFrame;
import me.topchetoeu.jscript.engine.frame.Runners;
import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.CodeFunction;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.Symbol;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.events.Notifier;
import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.json.JSONElement;
import me.topchetoeu.jscript.json.JSONList;
import me.topchetoeu.jscript.json.JSONMap;
import me.topchetoeu.jscript.lib.DateLib;
import me.topchetoeu.jscript.lib.MapLib;
import me.topchetoeu.jscript.lib.PromiseLib;
import me.topchetoeu.jscript.lib.RegExpLib;
import me.topchetoeu.jscript.lib.SetLib;
import me.topchetoeu.jscript.lib.GeneratorLib.Generator;
public class SimpleDebugger implements Debugger {
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 Source {
public final int id;
public final Filename filename;
public final String source;
public final TreeSet<Location> breakpoints;
public Source(int id, Filename filename, String source, TreeSet<Location> breakpoints) {
this.id = id;
this.filename = filename;
this.source = source;
this.breakpoints = breakpoints;
}
}
private static class Breakpoint {
public final int id;
public final Location location;
public final String condition;
public Breakpoint(int id, Location location, String condition) {
this.id = id;
this.location = location;
this.condition = condition;
}
}
private static class BreakpointCandidate {
public final int id;
public final String condition;
public final Pattern pattern;
public final int line, start;
public final HashSet<Breakpoint> resolvedBreakpoints = new HashSet<>();
public BreakpointCandidate(int id, Pattern pattern, int line, int start, String condition) {
this.id = id;
this.pattern = pattern;
this.line = line;
this.start = start;
this.condition = condition;
}
}
private class Frame {
public CodeFrame frame;
public CodeFunction func;
public int id;
public ObjectValue local = new ObjectValue(), capture = new ObjectValue(), global;
public JSONMap serialized;
public Location location;
public void updateLoc(Location loc) {
serialized.set("location", serializeLocation(loc));
this.location = loc;
}
public Frame(Context ctx, CodeFrame frame, int id) {
this.frame = frame;
this.func = frame.function;
this.id = id;
this.local = new ObjectValue();
this.location = frame.function.loc();
this.global = frame.function.environment.global.obj;
frame.scope.applyToObject(ctx, this.local, this.capture, true);
this.serialized = new JSONMap()
.set("callFrameId", id + "")
.set("functionName", func.name)
.set("location", serializeLocation(func.loc()))
.set("scopeChain", new JSONList()
.add(new JSONMap().set("type", "local").set("name", "Local Scope").set("object", serializeObj(ctx, local)))
.add(new JSONMap().set("type", "closure").set("name", "Closure").set("object", serializeObj(ctx, capture)))
.add(new JSONMap().set("type", "global").set("name", "Global Scope").set("object", serializeObj(ctx, global)))
)
.setNull("this");
}
}
public boolean enabled = false;
public CatchType execptionType = CatchType.ALL;
public State state = State.RESUMED;
public final WebSocket ws;
public final Engine target;
private HashMap<Integer, BreakpointCandidate> idToBptCand = new HashMap<>();
private HashMap<Integer, Breakpoint> idToBreakpoint = new HashMap<>();
private HashMap<Location, Breakpoint> locToBreakpoint = new HashMap<>();
private HashSet<Location> tmpBreakpts = new HashSet<>();
private HashMap<Filename, Integer> filenameToId = new HashMap<>();
private HashMap<Integer, Source> idToSource = new HashMap<>();
private ArrayList<Source> pendingSources = new ArrayList<>();
private HashMap<Integer, Frame> idToFrame = new HashMap<>();
private HashMap<CodeFrame, Frame> codeFrameToFrame = new HashMap<>();
private HashMap<Integer, ObjectValue> idToObject = new HashMap<>();
private HashMap<ObjectValue, Integer> objectToId = new HashMap<>();
private Notifier updateNotifier = new Notifier();
private int nextId = new Random().nextInt() & 0x7FFFFFFF;
private Location prevLocation = null;
private Frame stepOutFrame = null, currFrame = null;
private int nextId() {
return nextId++ ^ 1630022591 /* big prime */;
}
private void updateFrames(Context ctx) {
var frame = StackData.peekFrame(ctx);
if (frame == null) return;
if (!codeFrameToFrame.containsKey(frame)) {
var id = nextId();
var fr = new Frame(ctx, frame, id);
idToFrame.put(id, fr);
codeFrameToFrame.put(frame, fr);
}
currFrame = codeFrameToFrame.get(frame);
}
private JSONList serializeFrames(Context ctx) {
var res = new JSONList();
var frames = StackData.frames(ctx);
for (var i = frames.size() - 1; i >= 0; i--) {
res.add(codeFrameToFrame.get(frames.get(i)).serialized);
}
return res;
}
private Location correctLocation(Source source, Location loc) {
var set = source.breakpoints;
if (set.contains(loc)) return loc;
var tail = set.tailSet(loc);
if (tail.isEmpty()) return null;
return tail.first();
}
public Location deserializeLocation(JSONElement el, boolean correct) {
if (!el.isMap()) throw new RuntimeException("Expected location to be a map.");
var id = Integer.parseInt(el.map().string("scriptId"));
var line = (int)el.map().number("lineNumber") + 1;
var column = (int)el.map().number("columnNumber") + 1;
if (!idToSource.containsKey(id)) throw new RuntimeException("The specified source %s doesn't exist.".formatted(id));
var res = new Location(line, column, idToSource.get(id).filename);
if (correct) res = correctLocation(idToSource.get(id), res);
return res;
}
public JSONMap serializeLocation(Location loc) {
var source = filenameToId.get(loc.filename());
return new JSONMap()
.set("scriptId", source + "")
.set("lineNumber", loc.line() - 1)
.set("columnNumber", loc.start() - 1);
}
private Integer objectId(ObjectValue obj) {
if (objectToId.containsKey(obj)) return objectToId.get(obj);
else {
int id = nextId();
objectToId.put(obj, id);
idToObject.put(id, obj);
return id;
}
}
private JSONMap serializeObj(Context ctx, Object val) {
val = Values.normalize(null, val);
if (val == Values.NULL) {
return new JSONMap()
.set("objectId", objectId(null) + "")
.set("type", "object")
.set("subtype", "null")
.setNull("value")
.set("description", "null");
}
if (val instanceof ObjectValue) {
var obj = (ObjectValue)val;
var id = objectId(obj);
var type = "object";
String subtype = null;
String className = null;
if (obj instanceof FunctionValue) type = "function";
if (obj instanceof ArrayValue) subtype = "array";
if (Values.isWrapper(val, RegExpLib.class)) subtype = "regexp";
if (Values.isWrapper(val, DateLib.class)) subtype = "date";
if (Values.isWrapper(val, MapLib.class)) subtype = "map";
if (Values.isWrapper(val, SetLib.class)) subtype = "set";
if (Values.isWrapper(val, Generator.class)) subtype = "generator";
if (Values.isWrapper(val, PromiseLib.class)) subtype = "promise";
try { className = Values.toString(ctx, Values.getMember(ctx, obj.getMember(ctx, "constructor"), "name")); }
catch (Exception e) { }
var res = new JSONMap()
.set("type", type)
.set("objetId", id + "");
if (subtype != null) res.set("subtype", subtype);
if (className != null) res.set("className", className);
return res;
}
if (val == null) return new JSONMap().set("type", "undefined");
if (val instanceof String) return new JSONMap().set("type", "string").set("value", (String)val);
if (val instanceof Boolean) return new JSONMap().set("type", "boolean").set("value", (Boolean)val);
if (val instanceof Symbol) return new JSONMap().set("type", "symbol").set("description", val.toString());
if (val instanceof Number) {
var num = (double)(Number)val;
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 (num == -0.) 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 void resume(State state) {
this.state = state;
ws.send(new V8Event("Debugger.resumed", new JSONMap()));
updateNotifier.next();
}
private void pauseDebug(Context ctx, Breakpoint bp) {
state = State.PAUSED_NORMAL;
var map = new JSONMap()
.set("callFrames", serializeFrames(ctx))
.set("reason", "debugCommand");
if (bp != null) map.set("hitBreakpoints", new JSONList().add(bp.id + ""));
ws.send(new V8Event("Debugger.paused", map));
}
private void pauseException(Context ctx) {
state = State.PAUSED_NORMAL;
var map = new JSONMap()
.set("callFrames", serializeFrames(ctx))
.set("reason", "exception");
ws.send(new V8Event("Debugger.paused", map));
}
private void sendSource(Source src) {
ws.send(new V8Event("Debugger.scriptParsed", new JSONMap()
.set("scriptId", src.id + "")
.set("hash", src.source.hashCode())
.set("url", src.filename + "")
));
}
private void addBreakpoint(Breakpoint bpt) {
idToBreakpoint.put(bpt.id, bpt);
locToBreakpoint.put(bpt.location, bpt);
ws.send(new V8Event("Debugger.breakpointResolved", new JSONMap()
.set("breakpointId", bpt.id)
.set("location", serializeLocation(bpt.location))
));
}
@Override public void enable(V8Message msg) {
enabled = true;
ws.send(msg.respond());
for (var el : pendingSources) sendSource(el);
pendingSources.clear();
updateNotifier.next();
}
@Override public void disable(V8Message msg) {
enabled = false;
ws.send(msg.respond());
updateNotifier.next();
}
@Override public void getScriptSource(V8Message msg) {
int id = Integer.parseInt(msg.params.string("scriptId"));
ws.send(msg.respond(new JSONMap().set("scriptSource", idToSource.get(id).source)));
}
@Override public void getPossibleBreakpoints(V8Message msg) {
var start = deserializeLocation(msg.params.get("start"), false);
var end = msg.params.isMap("end") ? deserializeLocation(msg.params.get("end"), false) : null;
var src = idToSource.get(filenameToId.get(start.filename()));
var res = new JSONList();
for (var loc : src.breakpoints.tailSet(start, true)) {
if (end != null && loc.compareTo(end) > 0) break;
res.add(serializeLocation(loc));
}
ws.send(msg.respond(new JSONMap().set("locations", res)));
}
@Override public void pause(V8Message msg) {
}
@Override public void resume(V8Message msg) {
resume(State.RESUMED);
ws.send(msg.respond(new JSONMap()));
}
@Override public void setBreakpoint(V8Message msg) {
// int id = nextId();
// var loc = deserializeLocation(msg.params.get("location"), true);
// var bpt = new Breakpoint(id, loc, null);
// breakpoints.put(loc, bpt);
// idToBrpt.put(id, bpt);
// ws.send(msg.respond(new JSONMap()
// .set("breakpointId", id)
// .set("actualLocation", serializeLocation(loc))
// ));
}
@Override public void setBreakpointByUrl(V8Message msg) {
var line = (int)msg.params.number("lineNumber") + 1;
var col = (int)msg.params.number("columnNumber", 0) + 1;
Pattern regex;
if (msg.params.isString("url")) regex = Pattern.compile(Pattern.quote(msg.params.string("url")));
else regex = Pattern.compile(msg.params.string("urlRegex"));
var bpcd = new BreakpointCandidate(nextId(), regex, line, col, null);
idToBptCand.put(bpcd.id, bpcd);
var locs = new JSONList();
for (var src : idToSource.values()) {
if (regex.matcher(src.filename.toString()).matches()) {
var loc = correctLocation(src, new Location(line, col, src.filename));
if (loc == null) continue;
var bp = new Breakpoint(nextId(), loc, bpcd.condition);
bpcd.resolvedBreakpoints.add(bp);
locs.add(serializeLocation(loc));
addBreakpoint(bp);
}
}
ws.send(msg.respond(new JSONMap()
.set("breakpointId", bpcd.id + "")
.set("locations", locs)
));
}
@Override public void removeBreakpoint(V8Message msg) {
var id = Integer.parseInt(msg.params.string("breakpointId"));
if (idToBptCand.containsKey(id)) {
var bpcd = idToBptCand.get(id);
for (var bp : bpcd.resolvedBreakpoints) {
idToBreakpoint.remove(bp.id);
locToBreakpoint.remove(bp.location);
}
idToBptCand.remove(id);
}
else if (idToBreakpoint.containsKey(id)) {
var bp = idToBreakpoint.remove(id);
locToBreakpoint.remove(bp.location);
}
ws.send(msg.respond());
}
@Override public void continueToLocation(V8Message msg) {
var loc = deserializeLocation(msg.params.get("location"), true);
tmpBreakpts.add(loc);
resume(State.RESUMED);
ws.send(msg.respond());
}
@Override public void setPauseOnExceptions(V8Message msg) {
ws.send(new V8Error("i dont wanna to"));
}
@Override public void stepInto(V8Message msg) {
if (state == State.RESUMED) ws.send(new V8Error("Debugger is resumed."));
else {
prevLocation = currFrame.location;
stepOutFrame = currFrame;
resume(State.STEPPING_IN);
ws.send(msg.respond());
}
}
@Override public void stepOut(V8Message msg) {
if (state == State.RESUMED) ws.send(new V8Error("Debugger is resumed."));
else {
prevLocation = currFrame.location;
stepOutFrame = currFrame;
resume(State.STEPPING_OUT);
ws.send(msg.respond());
}
}
@Override public void stepOver(V8Message msg) {
if (state == State.RESUMED) ws.send(new V8Error("Debugger is resumed."));
else {
prevLocation = currFrame.location;
stepOutFrame = currFrame;
resume(State.STEPPING_OVER);
ws.send(msg.respond());
}
}
@Override public void onSource(Filename filename, String source, TreeSet<Location> locations) {
int id = nextId();
var src = new Source(id, filename, source, locations);
filenameToId.put(filename, id);
idToSource.put(id, src);
for (var bpcd : idToBptCand.values()) {
if (!bpcd.pattern.matcher(filename.toString()).matches()) continue;
var loc = correctLocation(src, new Location(bpcd.line, bpcd.start, filename));
var bp = new Breakpoint(nextId(), loc, bpcd.condition);
if (loc == null) continue;
bpcd.resolvedBreakpoints.add(bp);
addBreakpoint(bp);
}
if (!enabled) pendingSources.add(src);
else sendSource(src);
}
@Override public boolean onInstruction(Context ctx, CodeFrame cf, Instruction instruction, Object returnVal, EngineException error, boolean caught) {
if (!enabled) return false;
updateFrames(ctx);
var frame = codeFrameToFrame.get(cf);
if (instruction.location != null) frame.updateLoc(instruction.location);
var loc = frame.location;
var isBreakpointable = loc != null && (
idToSource.get(filenameToId.get(loc.filename())).breakpoints.contains(loc) ||
returnVal != Runners.NO_RETURN
);
if (error != null && !caught && StackData.frames(ctx).size() > 1) error = null;
if (error != null && (execptionType == CatchType.ALL || execptionType == CatchType.UNCAUGHT && !caught)) {
pauseException(ctx);
}
else if (isBreakpointable && locToBreakpoint.containsKey(loc)) {
pauseDebug(ctx, locToBreakpoint.get(loc));
}
else if (isBreakpointable && tmpBreakpts.contains(loc)) {
pauseDebug(ctx, null);
tmpBreakpts.remove(loc);
}
else if (instruction.type == Type.NOP && instruction.match("debug")) {
pauseDebug(ctx, null);
}
while (enabled) {
switch (state) {
case PAUSED_EXCEPTION:
case PAUSED_NORMAL: break;
case STEPPING_OUT:
case RESUMED: return false;
case STEPPING_IN:
if (!prevLocation.equals(loc)) {
if (isBreakpointable) pauseDebug(ctx, null);
else if (returnVal != Runners.NO_RETURN) pauseDebug(ctx, null);
else return false;
}
else return false;
break;
case STEPPING_OVER:
if (
stepOutFrame == frame && (
!loc.filename().equals(prevLocation.filename()) ||
loc.line() != prevLocation.line()
)
) pauseDebug(ctx, null);
else return false;
break;
}
updateNotifier.await();
}
return false;
}
@Override public void onFramePop(Context ctx, CodeFrame frame) {
updateFrames(ctx);
try {
idToFrame.remove(codeFrameToFrame.remove(frame).id);
}
catch (NullPointerException e) { }
if (StackData.frames(ctx).size() == 0) resume(State.RESUMED);
else if (stepOutFrame != null && stepOutFrame.frame == frame &&
(state == State.STEPPING_OUT || state == State.STEPPING_IN || state == State.STEPPING_OVER)
) {
pauseDebug(ctx, null);
updateNotifier.await();
}
}
@Override public void connect() {
target.data.set(StackData.DEBUGGER, this);
}
@Override public void disconnect() {
target.data.remove(StackData.DEBUGGER);
enabled = false;
updateNotifier.next();
}
private SimpleDebugger(WebSocket ws, Engine target) {
this.ws = ws;
this.target = target;
}
public static SimpleDebugger get(WebSocket ws, Engine target) {
if (target.data.has(StackData.DEBUGGER)) {
ws.send(new V8Error("A debugger is already attached to this engine."));
return null;
}
else {
var res = new SimpleDebugger(ws, target);
return res;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,208 @@
package me.topchetoeu.jscript.engine.debug;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type;
import me.topchetoeu.jscript.exceptions.UncheckedException;
import me.topchetoeu.jscript.exceptions.UncheckedIOException;
public class WebSocket implements AutoCloseable {
public long maxLength = 2000000;
private Socket socket;
private boolean closed = false;
private OutputStream out() {
try { return socket.getOutputStream(); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
private InputStream in() {
try { return socket.getInputStream(); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
private long readLen(int byteLen) {
long res = 0;
try {
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;
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
private byte[] readMask(boolean has) {
if (has) {
try { return new byte[] {
(byte)in().read(),
(byte)in().read(),
(byte)in().read(),
(byte)in().read()
}; }
catch (IOException e) { throw new UncheckedIOException(e); }
}
else return new byte[4];
}
private void writeLength(long len) {
try {
if (len < 126) {
out().write((int)len);
}
else if (len < 0xFFFF) {
out().write(126);
out().write((int)(len >> 8) & 0xFF);
out().write((int)len & 0xFF);
}
else {
out().write(127);
out().write((int)(len >> 56) & 0xFF);
out().write((int)(len >> 48) & 0xFF);
out().write((int)(len >> 40) & 0xFF);
out().write((int)(len >> 32) & 0xFF);
out().write((int)(len >> 24) & 0xFF);
out().write((int)(len >> 16) & 0xFF);
out().write((int)(len >> 8) & 0xFF);
out().write((int)len & 0xFF);
}
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
private synchronized void write(int type, byte[] data) {
try {
out().write(type | 0x80);
writeLength(data.length);
for (int i = 0; i < data.length; i++) {
out().write(data[i]);
}
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
public void send(String data) {
if (closed) throw new IllegalStateException("Object is closed.");
write(1, data.getBytes());
}
public void send(byte[] data) {
if (closed) throw new IllegalStateException("Object is closed.");
write(2, data);
}
public void send(WebSocketMessage msg) {
if (msg.type == Type.Binary) send(msg.binaryData());
else send(msg.textData());
}
public void send(Object data) {
if (closed) throw new IllegalStateException("Object is closed.");
write(1, data.toString().getBytes());
}
public void close(String reason) {
if (socket != null) {
try {
write(8, reason.getBytes());
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() {
try {
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;
}
catch (IOException e) { throw new UncheckedIOException(e); }
}
public WebSocketMessage receive() {
try {
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);
}
}
catch (IOException e) {
close();
}
return null;
}
public WebSocket(Socket socket) {
this.socket = socket;
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,13 @@ package me.topchetoeu.jscript.engine.frame;
import java.util.Stack;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.Instruction;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.StackData;
import me.topchetoeu.jscript.engine.scope.LocalScope;
import me.topchetoeu.jscript.engine.scope.ValueVariable;
import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.CodeFunction;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.exceptions.InterruptException;
@ -95,34 +96,32 @@ public class CodeFrame {
}
private void setCause(Context ctx, EngineException err, EngineException cause) {
if (err.value instanceof ObjectValue) {
Values.setMember(ctx, err, ctx.environment().symbol("Symbol.cause"), cause);
}
err.cause = cause;
}
private Object nextNoTry(Context ctx) {
private Object nextNoTry(Context ctx, Instruction instr) {
if (Thread.currentThread().isInterrupted()) throw new InterruptException();
if (codePtr < 0 || codePtr >= function.body.length) return null;
var instr = function.body[codePtr];
var loc = instr.location;
if (loc != null) prevLoc = loc;
try {
this.jumpFlag = false;
return Runners.exec(ctx, instr, this);
}
catch (EngineException e) {
throw e.add(function.name, prevLoc).setContext(ctx);
throw e.add(function.name, prevLoc).setCtx(function.environment, ctx.engine);
}
}
public Object next(Context ctx, Object value, Object returnValue, EngineException error) {
if (value != Runners.NO_RETURN) push(ctx, value);
var debugger = StackData.getDebugger(ctx);
if (returnValue == Runners.NO_RETURN && error == null) {
try { returnValue = nextNoTry(ctx); }
try {
var instr = function.body[codePtr];
if (debugger != null) debugger.onInstruction(ctx, this, instr, Runners.NO_RETURN, null, false);
returnValue = nextNoTry(ctx, instr);
}
catch (EngineException e) { error = e; }
}
@ -165,6 +164,7 @@ public class CodeFrame {
tryCtx.err = error;
newState = TryCtx.STATE_FINALLY_THREW;
}
setCause(ctx, error, tryCtx.err);
break;
}
else if (returnValue != Runners.NO_RETURN) {
@ -223,8 +223,15 @@ public class CodeFrame {
return Runners.NO_RETURN;
}
if (error != null) throw error.setContext(ctx);
if (returnValue != Runners.NO_RETURN) return returnValue;
if (error != null) {
if (debugger != null) debugger.onInstruction(ctx, this, function.body[codePtr], null, error, false);
throw error;
}
if (returnValue != Runners.NO_RETURN) {
if (debugger != null) debugger.onInstruction(ctx, this, function.body[codePtr], returnValue, null, false);
return returnValue;
}
return Runners.NO_RETURN;
}

View File

@ -2,6 +2,10 @@ package me.topchetoeu.jscript.engine.scope;
import java.util.ArrayList;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.values.NativeFunction;
import me.topchetoeu.jscript.engine.values.ObjectValue;
public class LocalScope {
private String[] names;
public final ValueVariable[] captures;
@ -15,11 +19,25 @@ public class LocalScope {
else return captures[~i];
}
public String[] getNames() {
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 (var i = 0; i < locals.length; i++) {
if (names == null || i >= names.length) res[i] = "local_" + i;
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];
}
@ -33,11 +51,36 @@ public class LocalScope {
return captures.length + locals.length;
}
public void toGlobal(GlobalScope global) {
var names = getNames();
for (var i = 0; i < names.length; i++) {
global.define(names[i], locals[i]);
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) {

View File

@ -212,7 +212,7 @@ public class ArrayValue extends ObjectValue implements Iterable<Object> {
for (var i = 0; i < size; i++) this.values[i] = Values.normalize(ctx, values[i]);
}
public static ArrayValue of(Context ctx, Collection<Object> values) {
public static ArrayValue of(Context ctx, Collection<?> values) {
return new ArrayValue(ctx, values.toArray(Object[]::new));
}
}

View File

@ -665,7 +665,8 @@ public class Values {
prefix = prefix == null ? "Uncaught" : "Uncaught " + prefix;
try {
if (err instanceof EngineException) {
System.out.println(prefix + " " + ((EngineException)err).toString(((EngineException)err).ctx));
var ee = ((EngineException)err);
System.out.println(prefix + " " + ee.toString(new Context(ee.engine).pushEnv(ee.env)));
}
else if (err instanceof SyntaxException) {
System.out.println("Syntax error:" + ((SyntaxException)err).msg);

View File

@ -5,6 +5,8 @@ import java.util.List;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.Engine;
import me.topchetoeu.jscript.engine.Environment;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.engine.values.ObjectValue.PlaceholderProto;
@ -12,7 +14,8 @@ import me.topchetoeu.jscript.engine.values.ObjectValue.PlaceholderProto;
public class EngineException extends RuntimeException {
public final Object value;
public EngineException cause;
public Context ctx = null;
public Environment env = null;
public Engine engine = null;
public final List<String> stackTrace = new ArrayList<>();
public EngineException add(String name, Location location) {
@ -28,8 +31,9 @@ public class EngineException extends RuntimeException {
this.cause = cause;
return this;
}
public EngineException setContext(Context ctx) {
this.ctx = ctx;
public EngineException setCtx(Environment env, Engine engine) {
if (this.env == null) this.env = env;
if (this.engine == null) this.engine = engine;
return this;
}

View File

@ -2,7 +2,7 @@ package me.topchetoeu.jscript.exceptions;
public class InterruptException extends RuntimeException {
public InterruptException() { }
public InterruptException(InterruptedException e) {
public InterruptException(Throwable e) {
super(e);
}
}

View File

@ -0,0 +1,9 @@
package me.topchetoeu.jscript.exceptions;
import java.io.IOException;
public class UncheckedIOException extends RuntimeException {
public UncheckedIOException(IOException e) {
super(e);
}
}

View File

@ -79,7 +79,7 @@ public class OverloadFunction extends FunctionValue {
catch (InstantiationException e) { throw EngineException.ofError("The class may not be instantiated."); }
catch (IllegalAccessException | IllegalArgumentException e) { continue; }
catch (InvocationTargetException e) {
var loc = new Location(0, 0, "<internal>");
var loc = Location.INTERNAL;
if (e.getTargetException() instanceof EngineException) {
throw ((EngineException)e.getTargetException()).add(name, loc);
}
@ -91,7 +91,7 @@ public class OverloadFunction extends FunctionValue {
}
}
catch (ReflectiveOperationException e) {
throw EngineException.ofError(e.getMessage()).add(name, new Location(0, 0, "<internal>"));
throw EngineException.ofError(e.getMessage()).add(name, Location.INTERNAL);
}
}

View File

@ -3,6 +3,7 @@ package me.topchetoeu.jscript.json;
import java.util.List;
import java.util.stream.Collectors;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.exceptions.SyntaxException;
import me.topchetoeu.jscript.parsing.Operator;
import me.topchetoeu.jscript.parsing.ParseRes;
@ -13,17 +14,17 @@ public class JSON {
public static ParseRes<String> parseIdentifier(List<Token> tokens, int i) {
return Parsing.parseIdentifier(tokens, i);
}
public static ParseRes<String> parseString(String filename, List<Token> tokens, int i) {
public static ParseRes<String> parseString(Filename filename, List<Token> tokens, int i) {
var res = Parsing.parseString(filename, tokens, i);
if (res.isSuccess()) return ParseRes.res((String)res.result.value, res.n);
else return res.transform();
}
public static ParseRes<Double> parseNumber(String filename, List<Token> tokens, int i) {
public static ParseRes<Double> parseNumber(Filename filename, List<Token> tokens, int i) {
var res = Parsing.parseNumber(filename, tokens, i);
if (res.isSuccess()) return ParseRes.res((Double)res.result.value, res.n);
else return res.transform();
}
public static ParseRes<Boolean> parseBool(String filename, List<Token> tokens, int i) {
public static ParseRes<Boolean> parseBool(Filename filename, List<Token> tokens, int i) {
var id = parseIdentifier(tokens, i);
if (!id.isSuccess()) return ParseRes.failed();
@ -32,7 +33,7 @@ public class JSON {
else return ParseRes.failed();
}
public static ParseRes<?> parseValue(String filename, List<Token> tokens, int i) {
public static ParseRes<?> parseValue(Filename filename, List<Token> tokens, int i) {
return ParseRes.any(
parseString(filename, tokens, i),
parseNumber(filename, tokens, i),
@ -42,7 +43,7 @@ public class JSON {
);
}
public static ParseRes<JSONMap> parseMap(String filename, List<Token> tokens, int i) {
public static ParseRes<JSONMap> parseMap(Filename filename, List<Token> tokens, int i) {
int n = 0;
if (!Parsing.isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.failed();
@ -82,7 +83,7 @@ public class JSON {
return ParseRes.res(values, n);
}
public static ParseRes<JSONList> parseList(String filename, List<Token> tokens, int i) {
public static ParseRes<JSONList> parseList(Filename filename, List<Token> tokens, int i) {
int n = 0;
if (!Parsing.isOperator(tokens, i + n++, Operator.BRACKET_OPEN)) return ParseRes.failed();
@ -109,7 +110,7 @@ public class JSON {
return ParseRes.res(values, n);
}
public static JSONElement parse(String filename, String raw) {
public static JSONElement parse(Filename filename, String raw) {
var res = parseValue(filename, Parsing.tokenize(filename, raw), 0);
if (res.isFailed()) throw new SyntaxException(null, "Invalid JSON given.");
else if (res.isError()) throw new SyntaxException(null, res.error);
@ -120,7 +121,12 @@ public class JSON {
if (el.isNumber()) return Double.toString(el.number());
if (el.isBoolean()) return el.bool() ? "true" : "false";
if (el.isNull()) return "null";
if (el.isString()) return "\"" + el.string().replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
if (el.isString()) return "\"" + el.string()
.replace("\\", "\\\\")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\"", "\\\"")
+ "\"";
if (el.isList()) {
var res = new StringBuilder().append("[");
for (int i = 0; i < el.list().size(); i++) {

View File

@ -65,10 +65,22 @@ public class JSONElement {
return (double)value;
}
public boolean bool() {
if (!isNumber()) throw new IllegalStateException("Element is not a boolean.");
if (!isBoolean()) throw new IllegalStateException("Element is not a boolean.");
return (boolean)value;
}
@Override
public String toString() {
if (isMap()) return "{...}";
if (isList()) return "[...]";
if (isString()) return (String)value;
if (isString()) return (String)value;
if (isNumber()) return (double)value + "";
if (isBoolean()) return (boolean)value + "";
if (isNull()) return "null";
return "";
}
private JSONElement(Type type, Object val) {
this.type = type;
this.value = val;

View File

@ -17,5 +17,7 @@ public class JSONList extends ArrayList<JSONElement> {
public JSONList add(boolean val) { this.add(JSONElement.of(val)); return this; }
public JSONList add(Map<String, JSONElement> val) { this.add(JSONElement.of(val)); return this; }
public JSONList add(Collection<JSONElement> val) { this.add(JSONElement.of(val)); return this; }
public JSONList add(JSONMap val) { this.add(JSONElement.of(val)); return this; }
public JSONList add(JSONList val) { this.add(JSONElement.of(val)); return this; }
}

View File

@ -51,7 +51,7 @@ public class JSONMap implements Map<String, JSONElement> {
public JSONMap map(String path) {
var el = get(path);
if (el == null) throw new IllegalStateException(String.format("'%s' doesn't exist.", path));
if (el == null) throw new RuntimeException(String.format("'%s' doesn't exist.", path));
return el.map();
}
public JSONMap map(String path, JSONMap defaultVal) {
@ -63,7 +63,7 @@ public class JSONMap implements Map<String, JSONElement> {
public JSONList list(String path) {
var el = get(path);
if (el == null) throw new IllegalStateException(String.format("'%s' doesn't exist.", path));
if (el == null) throw new RuntimeException(String.format("'%s' doesn't exist.", path));
return el.list();
}
public JSONList list(String path, JSONList defaultVal) {
@ -75,7 +75,7 @@ public class JSONMap implements Map<String, JSONElement> {
public String string(String path) {
var el = get(path);
if (el == null) throw new IllegalStateException(String.format("'%s' doesn't exist.", path));
if (el == null) throw new RuntimeException(String.format("'%s' doesn't exist.", path));
return el.string();
}
public String string(String path, String defaultVal) {
@ -87,7 +87,7 @@ public class JSONMap implements Map<String, JSONElement> {
public double number(String path) {
var el = get(path);
if (el == null) throw new IllegalStateException(String.format("'%s' doesn't exist.", path));
if (el == null) throw new RuntimeException(String.format("'%s' doesn't exist.", path));
return el.number();
}
public double number(String path, double defaultVal) {
@ -99,7 +99,7 @@ public class JSONMap implements Map<String, JSONElement> {
public boolean bool(String path) {
var el = get(path);
if (el == null) throw new IllegalStateException(String.format("'%s' doesn't exist.", path));
if (el == null) throw new RuntimeException(String.format("'%s' doesn't exist.", path));
return el.bool();
}
public boolean bool(String path, boolean defaultVal) {

View File

@ -12,7 +12,7 @@ import me.topchetoeu.jscript.interop.NativeConstructor;
import me.topchetoeu.jscript.interop.NativeInit;
public class ErrorLib {
private static String toString(Context ctx, Object cause, Object name, Object message, ArrayValue stack) {
private static String toString(Context ctx, boolean rethrown, Object cause, Object name, Object message, ArrayValue stack) {
if (name == null) name = "";
else name = Values.toString(ctx, name).trim();
if (message == null) message = "";
@ -30,7 +30,10 @@ public class ErrorLib {
}
}
if (cause instanceof ObjectValue) res.append(toString(ctx, cause));
if (cause instanceof ObjectValue) {
if (rethrown) res.append("\n (rethrown)");
else res.append("\nCaused by ").append(toString(ctx, cause));
}
return res.toString();
}
@ -39,8 +42,10 @@ public class ErrorLib {
if (thisArg instanceof ObjectValue) {
var stack = Values.getMember(ctx, thisArg, "stack");
if (!(stack instanceof ArrayValue)) stack = null;
var cause = Values.getMember(ctx, thisArg, ctx.environment().symbol("Symbol.cause"));
return toString(ctx,
Values.getMember(ctx, thisArg, "cause"),
thisArg == cause,
cause,
Values.getMember(ctx, thisArg, "name"),
Values.getMember(ctx, thisArg, "message"),
(ArrayValue)stack
@ -53,7 +58,7 @@ public class ErrorLib {
var target = new ObjectValue();
if (thisArg instanceof ObjectValue) target = (ObjectValue)thisArg;
target.defineProperty(ctx, "stack", new ArrayValue(ctx, StackData.stackTrace(ctx)));
target.defineProperty(ctx, "stack", ArrayValue.of(ctx, StackData.stackTrace(ctx)));
target.defineProperty(ctx, "name", "Error");
if (message == null) target.defineProperty(ctx, "message", "");
else target.defineProperty(ctx, "message", Values.toString(ctx, message));

View File

@ -1,7 +1,9 @@
package me.topchetoeu.jscript.lib;
import java.io.IOException;
import java.util.HashMap;
import me.topchetoeu.jscript.Reading;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.DataKey;
import me.topchetoeu.jscript.engine.Environment;
@ -14,12 +16,22 @@ public class Internals {
private static final DataKey<HashMap<Integer, Thread>> THREADS = new DataKey<>();
private static final DataKey<Integer> I = new DataKey<>();
@Native public static void log(Context ctx, Object ...args) {
for (var arg : args) {
Values.printValue(ctx, arg);
}
System.out.println();
}
@Native public static String readline(Context ctx) {
try {
return Reading.read();
}
catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Native public static int setTimeout(Context ctx, FunctionValue func, int delay, Object ...args) {
var thread = new Thread(() -> {

View File

@ -3,6 +3,7 @@ package me.topchetoeu.jscript.lib;
import java.util.HashSet;
import java.util.stream.Collectors;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.ObjectValue;
@ -72,7 +73,7 @@ public class JSONLib {
@Native
public static Object parse(Context ctx, String val) {
try {
return toJS(me.topchetoeu.jscript.json.JSON.parse("<value>", val));
return toJS(me.topchetoeu.jscript.json.JSON.parse(new Filename("jscript", "json"), val));
}
catch (SyntaxException e) { throw EngineException.ofSyntax(e.msg); }
}

View File

@ -234,12 +234,12 @@ public class PromiseLib {
private boolean handled = false;
private Object val;
private void resolve(Context ctx, Object val, int state) {
public void fulfill(Context ctx, Object val) {
if (this.state != STATE_PENDING) return;
if (val instanceof PromiseLib) ((PromiseLib)val).handle(ctx,
new NativeFunction(null, (e, th, a) -> { this.resolve(ctx, a[0], state); return null; }),
new NativeFunction(null, (e, th, a) -> { this.resolve(ctx, a[0], STATE_REJECTED); return null; })
new NativeFunction(null, (e, th, a) -> { this.fulfill(ctx, a[0]); return null; }),
new NativeFunction(null, (e, th, a) -> { this.reject(ctx, a[0]); return null; })
);
else {
Object next;
@ -248,29 +248,14 @@ public class PromiseLib {
try {
if (next instanceof FunctionValue) ((FunctionValue)next).call(ctx, val,
new NativeFunction((e, _thisArg, a) -> { this.resolve(ctx, a.length > 0 ? a[0] : null, state); return null; }),
new NativeFunction((e, _thisArg, a) -> { this.resolve(ctx, a.length > 0 ? a[0] : null, STATE_REJECTED); return null; })
new NativeFunction((e, _thisArg, a) -> { this.fulfill(ctx, a.length > 0 ? a[0] : null); return null; }),
new NativeFunction((e, _thisArg, a) -> { this.reject(ctx, a.length > 0 ? a[0] : null); return null; })
);
else {
this.val = val;
this.state = state;
this.state = STATE_FULFILLED;
if (state == STATE_FULFILLED) {
for (var handle : handles) handle.fulfilled.call(handle.ctx, null, val);
}
else if (state == STATE_REJECTED) {
for (var handle : handles) handle.rejected.call(handle.ctx, null, val);
if (handles.size() == 0) {
ctx.engine.pushMsg(true, ctx, new NativeFunction((_ctx, _thisArg, _args) -> {
if (!handled) {
Values.printError(new EngineException(val).setContext(ctx), "(in promise)");
throw new InterruptException();
}
return null;
}), null);
}
}
for (var handle : handles) handle.fulfilled.call(handle.ctx, null, val);
handles = null;
}
@ -280,18 +265,46 @@ public class PromiseLib {
}
}
}
/**
* Thread safe - call from any thread
*/
public void fulfill(Context ctx, Object val) {
resolve(ctx, val, STATE_FULFILLED);
}
/**
* Thread safe - call from any thread
*/
public void reject(Context ctx, Object val) {
resolve(ctx, val, STATE_REJECTED);
if (this.state != STATE_PENDING) return;
if (val instanceof PromiseLib) ((PromiseLib)val).handle(ctx,
new NativeFunction(null, (e, th, a) -> { this.reject(ctx, a[0]); return null; }),
new NativeFunction(null, (e, th, a) -> { this.reject(ctx, a[0]); return null; })
);
else {
Object next;
try { next = Values.getMember(ctx, val, "next"); }
catch (IllegalArgumentException e) { next = null; }
try {
if (next instanceof FunctionValue) ((FunctionValue)next).call(ctx, val,
new NativeFunction((e, _thisArg, a) -> { this.reject(ctx, a.length > 0 ? a[0] : null); return null; }),
new NativeFunction((e, _thisArg, a) -> { this.reject(ctx, a.length > 0 ? a[0] : null); return null; })
);
else {
this.val = val;
this.state = STATE_REJECTED;
for (var handle : handles) handle.rejected.call(handle.ctx, null, val);
if (handles.size() == 0) {
ctx.engine.pushMsg(true, ctx, new NativeFunction((_ctx, _thisArg, _args) -> {
if (!handled) {
Values.printError(new EngineException(val).setCtx(ctx.environment(), ctx.engine), "(in promise)");
throw new InterruptException();
}
return null;
}), null);
}
handles = null;
}
}
catch (EngineException err) {
this.reject(ctx, err.value);
}
}
}
private void handle(Context ctx, FunctionValue fulfill, FunctionValue reject) {

View File

@ -26,6 +26,7 @@ public class SymbolLib {
@NativeGetter public static Symbol search(Context ctx) { return ctx.environment().symbol("Symbol.search"); }
@NativeGetter public static Symbol iterator(Context ctx) { return ctx.environment().symbol("Symbol.iterator"); }
@NativeGetter public static Symbol asyncIterator(Context ctx) { return ctx.environment().symbol("Symbol.asyncIterator"); }
@NativeGetter public static Symbol cause(Context ctx) { return ctx.environment().symbol("Symbol.cause"); }
public final Symbol value;

View File

@ -6,7 +6,9 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.TreeSet;
import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.compilation.*;
import me.topchetoeu.jscript.compilation.Instruction.Type;
@ -25,7 +27,7 @@ import me.topchetoeu.jscript.parsing.ParseRes.State;
// TODO: this has to be rewritten
public class Parsing {
public static interface Parser<T> {
ParseRes<T> parse(String filename, List<Token> tokens, int i);
ParseRes<T> parse(Filename filename, List<Token> tokens, int i);
}
private static class ObjProp {
@ -69,7 +71,7 @@ public class Parsing {
reserved.add("delete");
reserved.add("break");
reserved.add("continue");
reserved.add("debug");
reserved.add("debugger");
reserved.add("implements");
reserved.add("interface");
reserved.add("package");
@ -122,7 +124,7 @@ public class Parsing {
private static final int CURR_MULTI_COMMENT = 8;
private static final int CURR_SINGLE_COMMENT = 9;
private static void addToken(StringBuilder currToken, int currStage, int line, int lastStart, String filename, List<RawToken> tokens) {
private static void addToken(StringBuilder currToken, int currStage, int line, int lastStart, Filename filename, List<RawToken> tokens) {
var res = currToken.toString();
switch (currStage) {
@ -143,7 +145,7 @@ public class Parsing {
// This method is so long because we're tokenizing the string using an iterative approach
// instead of a recursive descent parser. This is mainly done for performance reasons.
private static ArrayList<RawToken> splitTokens(String filename, String raw) {
private static ArrayList<RawToken> splitTokens(Filename filename, String raw) {
var tokens = new ArrayList<RawToken>();
var currToken = new StringBuilder(64);
@ -572,7 +574,7 @@ public class Parsing {
else return res;
}
private static List<Token> parseTokens(String filename, Collection<RawToken> tokens) {
private static List<Token> parseTokens(Filename filename, Collection<RawToken> tokens) {
var res = new ArrayList<Token>();
for (var el : tokens) {
@ -593,11 +595,11 @@ public class Parsing {
return res;
}
public static List<Token> tokenize(String filename, String raw) {
public static List<Token> tokenize(Filename filename, String raw) {
return parseTokens(filename, splitTokens(filename, raw));
}
public static Location getLoc(String filename, List<Token> tokens, int i) {
public static Location getLoc(Filename filename, List<Token> tokens, int i) {
if (tokens.size() == 0 || tokens.size() == 0) return new Location(1, 1, filename);
if (i >= tokens.size()) i = tokens.size() - 1;
return new Location(tokens.get(i).line, tokens.get(i).start, filename);
@ -663,7 +665,7 @@ public class Parsing {
return !reserved.contains(name);
}
public static ParseRes<ConstantStatement> parseString(String filename, List<Token> tokens, int i) {
public static ParseRes<ConstantStatement> parseString(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
try {
if (tokens.get(i).isString()) {
@ -675,7 +677,7 @@ public class Parsing {
return ParseRes.failed();
}
}
public static ParseRes<ConstantStatement> parseNumber(String filename, List<Token> tokens, int i) {
public static ParseRes<ConstantStatement> parseNumber(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
try {
if (tokens.get(i).isNumber()) {
@ -688,7 +690,7 @@ public class Parsing {
}
}
public static ParseRes<RegexStatement> parseRegex(String filename, List<Token> tokens, int i) {
public static ParseRes<RegexStatement> parseRegex(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
try {
if (tokens.get(i).isRegex()) {
@ -705,7 +707,7 @@ public class Parsing {
}
}
public static ParseRes<ArrayStatement> parseArray(String filename, List<Token> tokens, int i) {
public static ParseRes<ArrayStatement> parseArray(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isOperator(tokens, i + n++, Operator.BRACKET_OPEN)) return ParseRes.failed();
@ -744,7 +746,7 @@ public class Parsing {
return ParseRes.res(new ArrayStatement(loc, values.toArray(Statement[]::new)), n);
}
public static ParseRes<List<String>> parseParamList(String filename, List<Token> tokens, int i) {
public static ParseRes<List<String>> parseParamList(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -776,7 +778,7 @@ public class Parsing {
return ParseRes.res(args, n);
}
public static ParseRes<? extends Object> parsePropName(String filename, List<Token> tokens, int i) {
public static ParseRes<? extends Object> parsePropName(Filename filename, List<Token> tokens, int i) {
var idRes = parseIdentifier(tokens, i);
if (idRes.isSuccess()) return ParseRes.res(idRes.result, 1);
var strRes = parseString(null, tokens, i);
@ -786,7 +788,7 @@ public class Parsing {
return ParseRes.failed();
}
public static ParseRes<ObjProp> parseObjectProp(String filename, List<Token> tokens, int i) {
public static ParseRes<ObjProp> parseObjectProp(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -813,7 +815,7 @@ public class Parsing {
new FunctionStatement(loc, access + " " + name.toString(), argsRes.result.toArray(String[]::new), res.result)
), n);
}
public static ParseRes<ObjectStatement> parseObject(String filename, List<Token> tokens, int i) {
public static ParseRes<ObjectStatement> parseObject(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.failed();
@ -870,7 +872,7 @@ public class Parsing {
return ParseRes.res(new ObjectStatement(loc, values, getters, setters), n);
}
public static ParseRes<NewStatement> parseNew(String filename, List<Token> tokens, int i) {
public static ParseRes<NewStatement> parseNew(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
var n = 0;
if (!isIdentifier(tokens, i + n++, "new")) return ParseRes.failed();
@ -886,7 +888,7 @@ public class Parsing {
return ParseRes.res(new NewStatement(loc, call.func, call.args), n);
}
public static ParseRes<TypeofStatement> parseTypeof(String filename, List<Token> tokens, int i) {
public static ParseRes<TypeofStatement> parseTypeof(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
var n = 0;
if (!isIdentifier(tokens, i + n++, "typeof")) return ParseRes.failed();
@ -897,7 +899,7 @@ public class Parsing {
return ParseRes.res(new TypeofStatement(loc, valRes.result), n);
}
public static ParseRes<VoidStatement> parseVoid(String filename, List<Token> tokens, int i) {
public static ParseRes<VoidStatement> parseVoid(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
var n = 0;
if (!isIdentifier(tokens, i + n++, "void")) return ParseRes.failed();
@ -908,7 +910,7 @@ public class Parsing {
return ParseRes.res(new VoidStatement(loc, valRes.result), n);
}
public static ParseRes<? extends Statement> parseDelete(String filename, List<Token> tokens, int i) {
public static ParseRes<? extends Statement> parseDelete(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isIdentifier(tokens, i + n++, "delete")) return ParseRes.failed();
@ -929,7 +931,7 @@ public class Parsing {
}
}
public static ParseRes<FunctionStatement> parseFunction(String filename, List<Token> tokens, int i, boolean statement) {
public static ParseRes<FunctionStatement> parseFunction(Filename filename, List<Token> tokens, int i, boolean statement) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -972,7 +974,7 @@ public class Parsing {
else return ParseRes.error(loc, "Expected a compound statement for function.", res);
}
public static ParseRes<OperationStatement> parseUnary(String filename, List<Token> tokens, int i) {
public static ParseRes<OperationStatement> parseUnary(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -993,7 +995,7 @@ public class Parsing {
if (res.isSuccess()) return ParseRes.res(new OperationStatement(loc, operation, res.result), n + res.n);
else return ParseRes.error(loc, String.format("Expected a value after the unary operator '%s'.", op.value), res);
}
public static ParseRes<ChangeStatement> parsePrefixChange(String filename, List<Token> tokens, int i) {
public static ParseRes<ChangeStatement> parsePrefixChange(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1010,7 +1012,7 @@ public class Parsing {
if (!(res.result instanceof AssignableStatement)) return ParseRes.error(loc, "Expected assignable value after prefix operator.");
return ParseRes.res(new ChangeStatement(loc, (AssignableStatement)res.result, change, false), n + res.n);
}
public static ParseRes<? extends Statement> parseParens(String filename, List<Token> tokens, int i) {
public static ParseRes<? extends Statement> parseParens(Filename filename, List<Token> tokens, int i) {
int n = 0;
if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.failed();
@ -1023,7 +1025,7 @@ public class Parsing {
return ParseRes.res(res.result, n);
}
@SuppressWarnings("all")
public static ParseRes<? extends Statement> parseSimple(String filename, List<Token> tokens, int i, boolean statement) {
public static ParseRes<? extends Statement> parseSimple(Filename filename, List<Token> tokens, int i, boolean statement) {
var res = new ArrayList<>();
if (!statement) {
@ -1050,7 +1052,7 @@ public class Parsing {
return ParseRes.any(res.toArray(ParseRes[]::new));
}
public static ParseRes<VariableStatement> parseVariable(String filename, List<Token> tokens, int i) {
public static ParseRes<VariableStatement> parseVariable(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
var literal = parseIdentifier(tokens, i);
@ -1066,7 +1068,7 @@ public class Parsing {
return ParseRes.res(new VariableStatement(loc, literal.result), 1);
}
public static ParseRes<? extends Statement> parseLiteral(String filename, List<Token> tokens, int i) {
public static ParseRes<? extends Statement> parseLiteral(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
var id = parseIdentifier(tokens, i);
if (!id.isSuccess()) return id.transform();
@ -1094,7 +1096,7 @@ public class Parsing {
}
return ParseRes.failed();
}
public static ParseRes<IndexStatement> parseMember(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<IndexStatement> parseMember(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
var n = 0;
@ -1107,7 +1109,7 @@ public class Parsing {
return ParseRes.res(new IndexStatement(loc, prev, new ConstantStatement(loc, literal.result)), n);
}
public static ParseRes<IndexStatement> parseIndex(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<IndexStatement> parseIndex(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
var n = 0;
@ -1123,7 +1125,7 @@ public class Parsing {
return ParseRes.res(new IndexStatement(loc, prev, valRes.result), n);
}
public static ParseRes<? extends Statement> parseAssign(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<? extends Statement> parseAssign(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
int n = 0 ;
@ -1157,7 +1159,7 @@ public class Parsing {
return ParseRes.res(((AssignableStatement)prev).toAssign(res.result, operation), n);
}
public static ParseRes<CallStatement> parseCall(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<CallStatement> parseCall(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
var n = 0;
@ -1189,7 +1191,7 @@ public class Parsing {
return ParseRes.res(new CallStatement(loc, prev, args.toArray(Statement[]::new)), n);
}
public static ParseRes<ChangeStatement> parsePostfixChange(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<ChangeStatement> parsePostfixChange(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1207,7 +1209,7 @@ public class Parsing {
if (!(prev instanceof AssignableStatement)) return ParseRes.error(loc, "Expected assignable value before suffix operator.");
return ParseRes.res(new ChangeStatement(loc, (AssignableStatement)prev, change, true), n);
}
public static ParseRes<OperationStatement> parseInstanceof(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<OperationStatement> parseInstanceof(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1220,7 +1222,7 @@ public class Parsing {
return ParseRes.res(new OperationStatement(loc, Operation.INSTANCEOF, prev, valRes.result), n);
}
public static ParseRes<OperationStatement> parseIn(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<OperationStatement> parseIn(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1233,7 +1235,7 @@ public class Parsing {
return ParseRes.res(new OperationStatement(loc, Operation.IN, prev, valRes.result), n);
}
public static ParseRes<CompoundStatement> parseComma(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<CompoundStatement> parseComma(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
var n = 0;
@ -1246,7 +1248,7 @@ public class Parsing {
return ParseRes.res(new CompoundStatement(loc, prev, res.result), n);
}
public static ParseRes<IfStatement> parseTernary(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<IfStatement> parseTernary(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
var n = 0;
@ -1265,7 +1267,7 @@ public class Parsing {
return ParseRes.res(new IfStatement(loc, prev, a.result, b.result), n);
}
public static ParseRes<? extends Statement> parseOperator(String filename, List<Token> tokens, int i, Statement prev, int precedence) {
public static ParseRes<? extends Statement> parseOperator(Filename filename, List<Token> tokens, int i, Statement prev, int precedence) {
var loc = getLoc(filename, tokens, i);
var n = 0;
@ -1290,7 +1292,7 @@ public class Parsing {
return ParseRes.res(new OperationStatement(loc, op.operation, prev, res.result), n);
}
public static ParseRes<? extends Statement> parseValue(String filename, List<Token> tokens, int i, int precedence, boolean statement) {
public static ParseRes<? extends Statement> parseValue(Filename filename, List<Token> tokens, int i, int precedence, boolean statement) {
Statement prev = null;
int n = 0;
@ -1331,11 +1333,11 @@ public class Parsing {
if (prev == null) return ParseRes.failed();
else return ParseRes.res(prev, n);
}
public static ParseRes<? extends Statement> parseValue(String filename, List<Token> tokens, int i, int precedence) {
public static ParseRes<? extends Statement> parseValue(Filename filename, List<Token> tokens, int i, int precedence) {
return parseValue(filename, tokens, i, precedence, false);
}
public static ParseRes<? extends Statement> parseValueStatement(String filename, List<Token> tokens, int i) {
public static ParseRes<? extends Statement> parseValueStatement(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
var valRes = parseValue(filename, tokens, i, 0, true);
if (!valRes.isSuccess()) return valRes.transform();
@ -1352,7 +1354,7 @@ public class Parsing {
}
else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.", res);
}
public static ParseRes<VariableDeclareStatement> parseVariableDeclare(String filename, List<Token> tokens, int i) {
public static ParseRes<VariableDeclareStatement> parseVariableDeclare(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isIdentifier(tokens, i + n++, "var")) return ParseRes.failed();
@ -1396,7 +1398,7 @@ public class Parsing {
}
}
public static ParseRes<ReturnStatement> parseReturn(String filename, List<Token> tokens, int i) {
public static ParseRes<ReturnStatement> parseReturn(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isIdentifier(tokens, i + n++, "return")) return ParseRes.failed();
@ -1420,7 +1422,7 @@ public class Parsing {
else
return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.", valRes);
}
public static ParseRes<ThrowStatement> parseThrow(String filename, List<Token> tokens, int i) {
public static ParseRes<ThrowStatement> parseThrow(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isIdentifier(tokens, i + n++, "throw")) return ParseRes.failed();
@ -1438,7 +1440,7 @@ public class Parsing {
else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.", valRes);
}
public static ParseRes<BreakStatement> parseBreak(String filename, List<Token> tokens, int i) {
public static ParseRes<BreakStatement> parseBreak(Filename filename, List<Token> tokens, int i) {
if (!isIdentifier(tokens, i, "break")) return ParseRes.failed();
if (isStatementEnd(tokens, i + 1)) {
@ -1456,7 +1458,7 @@ public class Parsing {
}
else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.");
}
public static ParseRes<ContinueStatement> parseContinue(String filename, List<Token> tokens, int i) {
public static ParseRes<ContinueStatement> parseContinue(Filename filename, List<Token> tokens, int i) {
if (!isIdentifier(tokens, i, "continue")) return ParseRes.failed();
if (isStatementEnd(tokens, i + 1)) {
@ -1474,8 +1476,8 @@ public class Parsing {
}
else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.");
}
public static ParseRes<DebugStatement> parseDebug(String filename, List<Token> tokens, int i) {
if (!isIdentifier(tokens, i, "debug")) return ParseRes.failed();
public static ParseRes<DebugStatement> parseDebug(Filename filename, List<Token> tokens, int i) {
if (!isIdentifier(tokens, i, "debugger")) return ParseRes.failed();
if (isStatementEnd(tokens, i + 1)) {
if (isOperator(tokens, i + 1, Operator.SEMICOLON)) return ParseRes.res(new DebugStatement(getLoc(filename, tokens, i)), 2);
@ -1484,7 +1486,7 @@ public class Parsing {
else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.");
}
public static ParseRes<CompoundStatement> parseCompound(String filename, List<Token> tokens, int i) {
public static ParseRes<CompoundStatement> parseCompound(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
if (!isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.failed();
@ -1520,7 +1522,7 @@ public class Parsing {
return ParseRes.res(nameRes.result, n);
}
public static ParseRes<IfStatement> parseIf(String filename, List<Token> tokens, int i) {
public static ParseRes<IfStatement> parseIf(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1547,7 +1549,7 @@ public class Parsing {
return ParseRes.res(new IfStatement(loc, condRes.result, res.result, elseRes.result), n);
}
public static ParseRes<WhileStatement> parseWhile(String filename, List<Token> tokens, int i) {
public static ParseRes<WhileStatement> parseWhile(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1569,7 +1571,7 @@ public class Parsing {
return ParseRes.res(new WhileStatement(loc, labelRes.result, condRes.result, res.result), n);
}
public static ParseRes<Statement> parseSwitchCase(String filename, List<Token> tokens, int i) {
public static ParseRes<Statement> parseSwitchCase(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1589,7 +1591,7 @@ public class Parsing {
return ParseRes.res(null, 2);
}
public static ParseRes<SwitchStatement> parseSwitch(String filename, List<Token> tokens, int i) {
public static ParseRes<SwitchStatement> parseSwitch(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1649,7 +1651,7 @@ public class Parsing {
statements.toArray(Statement[]::new)
), n);
}
public static ParseRes<DoWhileStatement> parseDoWhile(String filename, List<Token> tokens, int i) {
public static ParseRes<DoWhileStatement> parseDoWhile(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1678,7 +1680,7 @@ public class Parsing {
}
else return ParseRes.error(getLoc(filename, tokens, i), "Expected a semicolon.");
}
public static ParseRes<Statement> parseFor(String filename, List<Token> tokens, int i) {
public static ParseRes<Statement> parseFor(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1736,7 +1738,7 @@ public class Parsing {
return ParseRes.res(new ForStatement(loc, labelRes.result, decl, cond, inc, res.result), n);
}
public static ParseRes<ForInStatement> parseForIn(String filename, List<Token> tokens, int i) {
public static ParseRes<ForInStatement> parseForIn(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1789,7 +1791,7 @@ public class Parsing {
return ParseRes.res(new ForInStatement(loc, labelRes.result, isDecl, nameRes.result, varVal, objRes.result, bodyRes.result), n);
}
public static ParseRes<TryStatement> parseCatch(String filename, List<Token> tokens, int i) {
public static ParseRes<TryStatement> parseCatch(Filename filename, List<Token> tokens, int i) {
var loc = getLoc(filename, tokens, i);
int n = 0;
@ -1830,7 +1832,7 @@ public class Parsing {
return ParseRes.res(new TryStatement(loc, res.result, catchBody, finallyBody, name), n);
}
public static ParseRes<? extends Statement> parseStatement(String filename, List<Token> tokens, int i) {
public static ParseRes<? extends Statement> parseStatement(Filename filename, List<Token> tokens, int i) {
if (isOperator(tokens, i, Operator.SEMICOLON)) return ParseRes.res(new CompoundStatement(getLoc(filename, tokens, i)), 1);
if (isIdentifier(tokens, i, "with")) return ParseRes.error(getLoc(filename, tokens, i), "'with' statements are not allowed.");
return ParseRes.any(
@ -1853,7 +1855,7 @@ public class Parsing {
);
}
public static Statement[] parse(String filename, String raw) {
public static Statement[] parse(Filename filename, String raw) {
var tokens = tokenize(filename, raw);
var list = new ArrayList<Statement>();
int i = 0;
@ -1874,10 +1876,10 @@ public class Parsing {
return list.toArray(Statement[]::new);
}
public static CodeFunction compile(HashMap<Long, Instruction[]> funcs, Environment environment, Statement ...statements) {
public static CodeFunction compile(HashMap<Long, Instruction[]> funcs, TreeSet<Location> breakpoints, Environment environment, Statement ...statements) {
var target = environment.global.globalChild();
var subscope = target.child();
var res = new CompileTarget(funcs);
var res = new CompileTarget(funcs, breakpoints);
var body = new CompoundStatement(null, statements);
if (body instanceof CompoundStatement) body = (CompoundStatement)body;
else body = new CompoundStatement(null, new Statement[] { body });
@ -1903,9 +1905,9 @@ public class Parsing {
return new CodeFunction(environment, "", subscope.localsCount(), 0, new ValueVariable[0], res.array());
}
public static CodeFunction compile(HashMap<Long, Instruction[]> funcs, Environment environment, String filename, String raw) {
public static CodeFunction compile(HashMap<Long, Instruction[]> funcs, TreeSet<Location> breakpoints, Environment environment, Filename filename, String raw) {
try {
return compile(funcs, environment, parse(filename, raw));
return compile(funcs, breakpoints, environment, parse(filename, raw));
}
catch (SyntaxException e) {
return new CodeFunction(environment, null, 2, 0, new ValueVariable[0], new Instruction[] { Instruction.throwSyntax(e) });