diff --git a/src/me/topchetoeu/jscript/Main.java b/src/me/topchetoeu/jscript/Main.java index cfa303b..a603117 100644 --- a/src/me/topchetoeu/jscript/Main.java +++ b/src/me/topchetoeu/jscript/Main.java @@ -119,19 +119,18 @@ public class Main { } private static void initEngine() { var ctx = new DebugContext(); - engine.globalEnvironment.add(DebugContext.ENV_KEY, ctx); + engine.add(DebugContext.ENV_KEY, ctx); debugServer.targets.put("target", (ws, req) -> new SimpleDebugger(ws).attach(ctx)); engineTask = engine.start(); debugTask = debugServer.start(new InetSocketAddress("127.0.0.1", 9229), true); } private static void initTypescript() { + var tsEnv = Internals.apply(new Environment()); + var bsEnv = Internals.apply(new Environment()); + try { - var tsEnv = Internals.apply(new Environment()); - tsEnv.add(Environment.HIDE_STACK, true); tsEnv.global.define(null, "module", false, new ObjectValue()); - var bsEnv = Internals.apply(new Environment()); - bsEnv.add(Environment.HIDE_STACK, true); engine.pushMsg( false, tsEnv, @@ -152,6 +151,9 @@ public class Main { catch (EngineException e) { Values.printError(e, "(while initializing TS)"); } + + bsEnv.add(Environment.HIDE_STACK, true); + tsEnv.add(Environment.HIDE_STACK, true); } public static void main(String args[]) { diff --git a/src/me/topchetoeu/jscript/ResultRunnable.java b/src/me/topchetoeu/jscript/ResultRunnable.java new file mode 100644 index 0000000..7fa559d --- /dev/null +++ b/src/me/topchetoeu/jscript/ResultRunnable.java @@ -0,0 +1,5 @@ +package me.topchetoeu.jscript; + +public interface ResultRunnable { + T run(); +} diff --git a/src/me/topchetoeu/jscript/engine/Context.java b/src/me/topchetoeu/jscript/engine/Context.java index 0b08d6d..33f3b07 100644 --- a/src/me/topchetoeu/jscript/engine/Context.java +++ b/src/me/topchetoeu/jscript/engine/Context.java @@ -6,6 +6,8 @@ import java.util.Iterator; import java.util.List; import java.util.TreeSet; import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import me.topchetoeu.jscript.Filename; import me.topchetoeu.jscript.Location; @@ -28,33 +30,43 @@ public class Context implements Extensions { @Override public void add(Symbol key, T obj) { if (environment != null) environment.add(key, obj); - else if (engine != null) engine.globalEnvironment.add(key, obj); + else if (engine != null) engine.add(key, obj); } @Override public T get(Symbol key) { if (environment != null && environment.has(key)) return environment.get(key); - else if (engine != null && engine.globalEnvironment.has(key)) return engine.globalEnvironment.get(key); + else if (engine != null && engine.has(key)) return engine.get(key); return null; } @Override public boolean has(Symbol key) { return environment != null && environment.has(key) || - engine != null && engine.globalEnvironment.has(key); + engine != null && engine.has(key); } @Override public boolean remove(Symbol key) { var res = false; if (environment != null) res |= environment.remove(key); - else if (engine != null) res |= engine.globalEnvironment.remove(key); + else if (engine != null) res |= engine.remove(key); return res; } + @Override public Iterable keys() { + if (engine == null && environment == null) return List.of(); + if (engine == null) return environment.keys(); + if (environment == null) return engine.keys(); + + return () -> Stream.concat( + StreamSupport.stream(engine.keys().spliterator(), false), + StreamSupport.stream(environment.keys().spliterator(), false) + ).distinct().iterator(); + } public FunctionValue compile(Filename filename, String raw) { var env = environment; var result = Environment.compileFunc(this).call(this, null, raw, filename.toString(), new EnvironmentLib(env)); var function = (FunctionValue)Values.getMember(this, result, "function"); - if (!has(DebugContext.ENV_KEY)) return function; + if (!DebugContext.enabled(this)) return function; var rawMapChain = ((ArrayValue)Values.getMember(this, result, "mapChain")).toArray(); var breakpoints = new TreeSet<>( @@ -139,7 +151,9 @@ public class Context implements Extensions { this.engine = engine; this.stackSize = stackSize; - if (engine != null && stackSize > engine.maxStackFrames) throw EngineException.ofRange("Stack overflow!"); + if (hasNotNull(Environment.MAX_STACK_COUNT) && stackSize > (int)get(Environment.MAX_STACK_COUNT)) { + throw EngineException.ofRange("Stack overflow!"); + } } public Context(Engine engine) { diff --git a/src/me/topchetoeu/jscript/engine/Engine.java b/src/me/topchetoeu/jscript/engine/Engine.java index 9ee6498..847609d 100644 --- a/src/me/topchetoeu/jscript/engine/Engine.java +++ b/src/me/topchetoeu/jscript/engine/Engine.java @@ -1,113 +1,55 @@ package me.topchetoeu.jscript.engine; import java.util.HashMap; -import java.util.concurrent.PriorityBlockingQueue; import me.topchetoeu.jscript.Filename; import me.topchetoeu.jscript.compilation.FunctionBody; import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.Symbol; import me.topchetoeu.jscript.events.Awaitable; -import me.topchetoeu.jscript.events.DataNotifier; -import me.topchetoeu.jscript.exceptions.InterruptException; -public class Engine { - private class UncompiledFunction extends FunctionValue { - public final Filename filename; - public final String raw; - private FunctionValue compiled = null; - - @Override - public Object call(Context ctx, Object thisArg, Object ...args) { - if (compiled == null) compiled = ctx.compile(filename, raw); - return compiled.call(ctx, thisArg, args); - } - - public UncompiledFunction(Filename filename, String raw) { - super(filename + "", 0); - this.filename = filename; - this.raw = raw; - } - } - - private static class Task implements Comparable { - public final FunctionValue func; - public final Object thisArg; - public final Object[] args; - public final DataNotifier notifier = new DataNotifier<>(); - public final Context ctx; - public final boolean micro; - - public Task(Context ctx, FunctionValue func, Object thisArg, Object[] args, boolean micro) { - this.ctx = ctx; - this.func = func; - this.thisArg = thisArg; - this.args = args; - this.micro = micro; - } - - @Override - public int compareTo(Task other) { - return Integer.compare(this.micro ? 0 : 1, other.micro ? 0 : 1); - } - } - - private static int nextId = 0; +public class Engine extends EventLoop implements Extensions { public static final HashMap functions = new HashMap<>(); - public final Environment globalEnvironment = new Environment(); + private final Environment env = new Environment(); - public final int id = ++nextId; - public int maxStackFrames = 10000; - - private Thread thread; - private PriorityBlockingQueue tasks = new PriorityBlockingQueue<>(); - - private void runTask(Task task) { - try { - 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); - } + @Override + public void add(Symbol key, T obj) { + this.env.add(key, obj); } - public void run(boolean untilEmpty) { - while (!untilEmpty || !tasks.isEmpty()) { - try { - runTask(tasks.take()); - } - catch (InterruptedException | InterruptException e) { - for (var msg : tasks) msg.notifier.error(new InterruptException(e)); - break; - } - } + @Override + public T get(Symbol key) { + return this.env.get(key); + } + @Override + public boolean has(Symbol key) { + return this.env.has(key); + } + @Override + public boolean remove(Symbol key) { + return this.env.remove(key); + } + @Override + public Iterable keys() { + return env.keys(); } - public Thread start() { - if (this.thread == null) { - this.thread = new Thread(() -> run(false), "JavaScript Runner #" + id); - this.thread.start(); - } - return this.thread; - } - public void stop() { - thread.interrupt(); - thread = null; - } - public boolean inExecThread() { - return Thread.currentThread() == thread; - } - public synchronized boolean isRunning() { - return this.thread != null; + public Engine copy() { + var res = new Engine(); + res.env.addAll(env); + return res; } public Awaitable pushMsg(boolean micro, Environment env, FunctionValue func, Object thisArg, Object ...args) { - var msg = new Task(new Context(this, env), func, thisArg, args, micro); - tasks.add(msg); - return msg.notifier; + return pushMsg(() -> { + return func.call(new Context(this, env), thisArg, args); + }, micro); } public Awaitable pushMsg(boolean micro, Environment env, Filename filename, String raw, Object thisArg, Object ...args) { - return pushMsg(micro, env, new UncompiledFunction(filename, raw), thisArg, args); + return pushMsg(() -> { + var ctx = new Context(this, env); + return ctx.compile(filename, raw).call(new Context(this, env), thisArg, args); + }, micro); } public Engine() { diff --git a/src/me/topchetoeu/jscript/engine/Environment.java b/src/me/topchetoeu/jscript/engine/Environment.java index a8c9cf5..db7252c 100644 --- a/src/me/topchetoeu/jscript/engine/Environment.java +++ b/src/me/topchetoeu/jscript/engine/Environment.java @@ -27,6 +27,7 @@ public class Environment implements Extensions { public static final Symbol REGEX_CONSTR = Symbol.get("Environment.regexConstructor"); public static final Symbol STACK = Symbol.get("Environment.stack"); + public static final Symbol MAX_STACK_COUNT = Symbol.get("Environment.maxStackCount"); public static final Symbol HIDE_STACK = Symbol.get("Environment.hideStack"); public static final Symbol OBJECT_PROTO = Symbol.get("Environment.objectPrototype"); @@ -62,13 +63,16 @@ public class Environment implements Extensions { @Override public boolean has(Symbol key) { return data.containsKey(key); } + @Override public Iterable keys() { + return data.keySet(); + } public static FunctionValue compileFunc(Extensions ext) { return ext.init(COMPILE_FUNC, new NativeFunction("compile", args -> { var source = args.getString(0); var filename = args.getString(1); - var env = Values.wrapper(args.get(2, ObjectValue.class).getMember(args.ctx, Symbol.get("env")), Environment.class); - var isDebug = args.ctx.has(DebugContext.ENV_KEY); + var env = Values.wrapper(args.convert(2, ObjectValue.class).getMember(args.ctx, Symbol.get("env")), Environment.class); + var isDebug = DebugContext.enabled(args.ctx); var res = new ObjectValue(); var target = Parsing.compile(env, Filename.parse(filename), source); @@ -92,7 +96,7 @@ public class Environment implements Extensions { })); } - public Environment fork() { + public Environment copy() { var res = new Environment(null, global); res.wrappers = wrappers.fork(res); @@ -102,7 +106,7 @@ public class Environment implements Extensions { return res; } public Environment child() { - var res = fork(); + var res = copy(); res.global = res.global.globalChild(); return res; } diff --git a/src/me/topchetoeu/jscript/engine/EventLoop.java b/src/me/topchetoeu/jscript/engine/EventLoop.java new file mode 100644 index 0000000..412e264 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/EventLoop.java @@ -0,0 +1,81 @@ +package me.topchetoeu.jscript.engine; + +import java.util.concurrent.PriorityBlockingQueue; + +import me.topchetoeu.jscript.ResultRunnable; +import me.topchetoeu.jscript.events.Awaitable; +import me.topchetoeu.jscript.events.DataNotifier; +import me.topchetoeu.jscript.exceptions.InterruptException; + +public class EventLoop { + private static class Task implements Comparable { + public final ResultRunnable runnable; + public final DataNotifier notifier = new DataNotifier<>(); + public final boolean micro; + + public Task(ResultRunnable runnable, boolean micro) { + this.runnable = runnable; + this.micro = micro; + } + + @Override + public int compareTo(Task other) { + return Integer.compare(this.micro ? 0 : 1, other.micro ? 0 : 1); + } + } + + private PriorityBlockingQueue tasks = new PriorityBlockingQueue<>(); + private Thread thread; + + @SuppressWarnings("unchecked") + public Awaitable pushMsg(ResultRunnable runnable, boolean micro) { + var msg = new Task(runnable, micro); + tasks.add(msg); + return (Awaitable)msg.notifier; + } + public Awaitable pushMsg(Runnable runnable, boolean micro) { + return pushMsg(() -> { runnable.run(); return null; }, micro); + } + + public void run(boolean untilEmpty) { + while (!untilEmpty || !tasks.isEmpty()) { + try { + var task = tasks.take(); + + try { + task.notifier.next(task.runnable.run()); + } + catch (RuntimeException e) { + if (e instanceof InterruptException) throw e; + task.notifier.error(e); + } + } + catch (InterruptedException | InterruptException e) { + for (var msg : tasks) msg.notifier.error(new InterruptException(e)); + break; + } + } + } + + public Thread thread() { + return thread; + } + public Thread start() { + if (thread == null) { + thread = new Thread(() -> run(false), "Event loop #" + hashCode()); + thread.start(); + } + return thread; + } + public void stop() { + if (thread != null) thread.interrupt(); + thread = null; + } + + public boolean inLoopThread() { + return Thread.currentThread() == thread; + } + public boolean isRunning() { + return this.thread != null; + } +} diff --git a/src/me/topchetoeu/jscript/engine/Extensions.java b/src/me/topchetoeu/jscript/engine/Extensions.java index 83072c9..05d248a 100644 --- a/src/me/topchetoeu/jscript/engine/Extensions.java +++ b/src/me/topchetoeu/jscript/engine/Extensions.java @@ -5,10 +5,15 @@ import me.topchetoeu.jscript.engine.values.Symbol; public interface Extensions { T get(Symbol key); void add(Symbol key, T obj); + Iterable keys(); boolean has(Symbol key); boolean remove(Symbol key); + default boolean hasNotNull(Symbol key) { + return has(key) && get(key) != null; + } + default T get(Symbol key, T defaultVal) { if (has(key)) return get(key); else return defaultVal; @@ -21,4 +26,9 @@ public interface Extensions { return val; } } + default void addAll(Extensions source) { + for (var key : source.keys()) { + add(key, source.get(key)); + } + } } diff --git a/src/me/topchetoeu/jscript/engine/debug/DebugContext.java b/src/me/topchetoeu/jscript/engine/debug/DebugContext.java index 1906c65..58cac6b 100644 --- a/src/me/topchetoeu/jscript/engine/debug/DebugContext.java +++ b/src/me/topchetoeu/jscript/engine/debug/DebugContext.java @@ -15,6 +15,7 @@ import me.topchetoeu.jscript.mapping.SourceMap; public class DebugContext implements DebugController { public static final Symbol ENV_KEY = Symbol.get("Engine.debug"); + public static final Symbol IGNORE = Symbol.get("Engine.ignoreDebug"); private HashMap sources; private HashMap> bpts; @@ -89,8 +90,11 @@ public class DebugContext implements DebugController { this(true); } + public static boolean enabled(Extensions exts) { + return exts.hasNotNull(ENV_KEY) && !exts.has(IGNORE); + } public static DebugContext get(Extensions exts) { - if (exts.has(ENV_KEY)) return exts.get(ENV_KEY); + if (enabled(exts)) return exts.get(ENV_KEY); else return new DebugContext(false); } } diff --git a/src/me/topchetoeu/jscript/engine/debug/SimpleDebugger.java b/src/me/topchetoeu/jscript/engine/debug/SimpleDebugger.java index d72de6d..3b16b70 100644 --- a/src/me/topchetoeu/jscript/engine/debug/SimpleDebugger.java +++ b/src/me/topchetoeu/jscript/engine/debug/SimpleDebugger.java @@ -301,6 +301,8 @@ public class SimpleDebugger implements Debugger { } private JSONMap serializeObj(Context ctx, Object val, boolean byValue) { val = Values.normalize(null, val); + ctx = new Context(ctx.engine.copy(), ctx.environment); + ctx.engine.add(DebugContext.IGNORE, true); if (val == Values.NULL) { return new JSONMap() @@ -477,7 +479,7 @@ public class SimpleDebugger implements Debugger { private RunResult run(Frame codeFrame, String code) { if (codeFrame == null) return new RunResult(null, code, new EngineException("Invalid code frame!")); var engine = new Engine(); - var env = codeFrame.func.environment.fork(); + var env = codeFrame.func.environment.copy(); env.global = new GlobalScope(codeFrame.local); diff --git a/src/me/topchetoeu/jscript/engine/frame/Runners.java b/src/me/topchetoeu/jscript/engine/frame/Runners.java index 7a5d59c..1eb1845 100644 --- a/src/me/topchetoeu/jscript/engine/frame/Runners.java +++ b/src/me/topchetoeu/jscript/engine/frame/Runners.java @@ -198,7 +198,7 @@ public class Runners { return execLoadMember(ctx, instr, frame); } public static Object execLoadRegEx(Context ctx, Instruction instr, CodeFrame frame) { - if (ctx.has(Environment.REGEX_CONSTR)) { + if (ctx.hasNotNull(Environment.REGEX_CONSTR)) { frame.push(ctx, Values.callNew(ctx, ctx.get(Environment.REGEX_CONSTR), instr.get(0), instr.get(1))); } else { diff --git a/src/me/topchetoeu/jscript/engine/values/Values.java b/src/me/topchetoeu/jscript/engine/values/Values.java index 4e94819..cdc1244 100644 --- a/src/me/topchetoeu/jscript/engine/values/Values.java +++ b/src/me/topchetoeu/jscript/engine/values/Values.java @@ -523,9 +523,6 @@ public class Values { ); return (T)res; } - if (clazz.isAssignableFrom(NativeWrapper.class)) { - return (T)new NativeWrapper(obj); - } if (clazz == String.class) return (T)toString(ctx, obj); if (clazz == Boolean.class || clazz == Boolean.TYPE) return (T)(Boolean)toBoolean(obj); @@ -547,6 +544,9 @@ public class Values { if (obj == null) return null; if (clazz.isInstance(obj)) return (T)obj; + if (clazz.isAssignableFrom(NativeWrapper.class)) { + return (T)new NativeWrapper(obj); + } throw new ConvertException(type(obj), clazz.getSimpleName()); } @@ -609,7 +609,7 @@ public class Values { try { var key = getMember(ctx, getMember(ctx, ctx.get(Environment.SYMBOL_PROTO), "constructor"), "iterator"); - res.defineProperty(ctx, key, new NativeFunction("", args -> args.thisArg)); + res.defineProperty(ctx, key, new NativeFunction("", args -> args.self)); } catch (IllegalArgumentException | NullPointerException e) { } @@ -634,12 +634,12 @@ public class Values { try { var key = getMemberPath(ctx, ctx.get(Environment.SYMBOL_PROTO), "constructor", "asyncIterator"); - res.defineProperty(ctx, key, new NativeFunction("", args -> args.thisArg)); + res.defineProperty(ctx, key, new NativeFunction("", args -> args.self)); } catch (IllegalArgumentException | NullPointerException e) { } res.defineProperty(ctx, "next", new NativeFunction("", args -> { - return PromiseLib.await(ctx, () -> { + return PromiseLib.await(args.ctx, () -> { if (!it.hasNext()) return new ObjectValue(ctx, Map.of("done", true)); else { var obj = new ObjectValue(); diff --git a/src/me/topchetoeu/jscript/exceptions/EngineException.java b/src/me/topchetoeu/jscript/exceptions/EngineException.java index 3b784d1..7ecd386 100644 --- a/src/me/topchetoeu/jscript/exceptions/EngineException.java +++ b/src/me/topchetoeu/jscript/exceptions/EngineException.java @@ -66,6 +66,11 @@ public class EngineException extends RuntimeException { if (this.engine == null) this.engine = engine; return this; } + public EngineException setCtx(Context ctx) { + if (this.env == null) this.env = ctx.environment; + if (this.engine == null) this.engine = ctx.engine; + return this; + } public String toString(Context ctx) { var ss = new StringBuilder(); diff --git a/src/me/topchetoeu/jscript/interop/Arguments.java b/src/me/topchetoeu/jscript/interop/Arguments.java index 796e96a..952ec4f 100644 --- a/src/me/topchetoeu/jscript/interop/Arguments.java +++ b/src/me/topchetoeu/jscript/interop/Arguments.java @@ -7,17 +7,28 @@ import me.topchetoeu.jscript.engine.values.NativeWrapper; import me.topchetoeu.jscript.engine.values.Values; public class Arguments { - public final Object thisArg; + public final Object self; public final Object[] args; public final Context ctx; - public T get(int i, Class type) { + public int n() { + return args.length; + } + + public boolean has(int i) { + return i == -1 || i >= 0 && i < args.length; + } + + public T self(Class type) { + return convert(-1, type); + } + public T convert(int i, Class type) { return Values.convert(ctx, get(i), type); } public Object get(int i, boolean unwrap) { Object res = null; - if (i == -1) res = thisArg; + if (i == -1) res = self; if (i >= 0 && i < args.length) res = args[i]; if (unwrap && res instanceof NativeWrapper) res = ((NativeWrapper)res).wrapped; @@ -26,70 +37,85 @@ public class Arguments { public Object get(int i) { return get(i, false); } + public Object getOrDefault(int i, Object def) { + if (i < 0 || i >= args.length) return def; + else return get(i); + } + + public Arguments slice(int start) { + var res = new Object[Math.max(0, args.length - start)]; + for (int j = start; j < args.length; j++) res[j - start] = get(j); + return new Arguments(ctx, args, res); + } @SuppressWarnings("unchecked") - public T[] slice(int i, Class type) { - var res = Array.newInstance(type, Math.max(0, args.length - i)); - for (; i < args.length; i++) Array.set(res, i - args.length, get(i, type)); - return ((T[])res); + public T[] convert(Class type) { + var res = Array.newInstance(type, args.length); + for (int i = 0; i < args.length; i++) Array.set(res, i, convert(i, type)); + return (T[])res; } - public Object slice(int i, boolean unwrap) { - var res = new Object[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, unwrap); + public int[] convertInt() { + var res = new int[args.length]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Integer.class); return res; } - public Object slice(int i) { - return slice(i, false); - } - - public int[] sliceInt(int i) { - var res = new int[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Integer.class); + public long[] convertLong() { + var res = new long[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Long.class); return res; } - public long[] sliceLong(int i) { - var res = new long[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Long.class); + public short[] sliceShort() { + var res = new short[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Short.class); return res; } - public short[] sliceShort(int i) { - var res = new short[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Short.class); + public float[] sliceFloat() { + var res = new float[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Float.class); return res; } - public float[] sliceFloat(int i) { - var res = new float[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Float.class); + public double[] sliceDouble() { + var res = new double[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Double.class); return res; } - public double[] sliceDouble(int i) { - var res = new double[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Double.class); + public byte[] sliceByte() { + var res = new byte[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Byte.class); return res; } - public byte[] sliceByte(int i) { - var res = new byte[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Byte.class); + public char[] sliceChar() { + var res = new char[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Character.class); return res; } - public char[] sliceChar(int i) { - var res = new char[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Character.class); - return res; - } - public boolean[] sliceBool(int i) { - var res = new boolean[Math.max(0, args.length - i)]; - for (; i < args.length; i++) res[i - args.length] = get(i, Boolean.class); + public boolean[] sliceBool() { + var res = new boolean[Math.max(0, args.length)]; + for (int i = 0; i < args.length; i++) res[i] = convert(i, Boolean.class); return res; } public String getString(int i) { return Values.toString(ctx, get(i)); } public boolean getBoolean(int i) { return Values.toBoolean(get(i)); } public int getInt(int i) { return (int)Values.toNumber(ctx, get(i)); } + public long getLong(int i) { return (long)Values.toNumber(ctx, get(i)); } + public double getDouble(int i) { return Values.toNumber(ctx, get(i)); } + public float getFloat(int i) { return (float)Values.toNumber(ctx, get(i)); } + + public int getInt(int i, int def) { + var res = get(i); + if (res == null) return def; + else return (int)Values.toNumber(ctx, res); + } + public String getString(int i, String def) { + var res = get(i); + if (res == null) return def; + else return Values.toString(ctx, res); + } public Arguments(Context ctx, Object thisArg, Object... args) { this.ctx = ctx; this.args = args; - this.thisArg = thisArg; + this.self = thisArg; } } diff --git a/src/me/topchetoeu/jscript/interop/ExposeField.java b/src/me/topchetoeu/jscript/interop/ExposeField.java new file mode 100644 index 0000000..57db817 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/ExposeField.java @@ -0,0 +1,13 @@ +package me.topchetoeu.jscript.interop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExposeField { + public String value() default ""; + public ExposeTarget target() default ExposeTarget.MEMBER; +} diff --git a/src/me/topchetoeu/jscript/interop/ExposeType.java b/src/me/topchetoeu/jscript/interop/ExposeType.java index 1b48f6a..f7f5a48 100644 --- a/src/me/topchetoeu/jscript/interop/ExposeType.java +++ b/src/me/topchetoeu/jscript/interop/ExposeType.java @@ -3,7 +3,6 @@ package me.topchetoeu.jscript.interop; public enum ExposeType { INIT, METHOD, - FIELD, GETTER, SETTER, } \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java b/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java index 7544adf..0c98107 100644 --- a/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java +++ b/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java @@ -1,6 +1,7 @@ package me.topchetoeu.jscript.interop; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -50,7 +51,7 @@ public class NativeWrapperProvider implements WrappersProvider { } } private static FunctionValue create(String name, Method method) { - return new NativeFunction(name, args -> call(args.ctx, name, method, args.thisArg, args)); + return new NativeFunction(name, args -> call(args.ctx, name, method, args.self, args)); } private static void checkSignature(Method method, boolean forceStatic, Class ...params) { if (forceStatic && !Modifier.isStatic(method.getModifiers())) throw new IllegalArgumentException(String.format( @@ -82,6 +83,28 @@ public class NativeWrapperProvider implements WrappersProvider { else return clazz.getSimpleName(); } + private static void checkUnderscore(Member member) { + if (!member.getName().startsWith("__")) { + System.out.println("WARNING: The name of the exposed member '%s.%s' doesn't start with '__'.".formatted( + member.getDeclaringClass().getName(), + member.getName() + )); + } + } + private static String getName(Member member, String overrideName) { + if (overrideName == null) overrideName = ""; + if (overrideName.isBlank()) { + var res = member.getName(); + if (res.startsWith("__")) res = res.substring(2); + return res; + } + else return overrideName.trim(); + } + private static Object getKey(String name) { + if (name.startsWith("@@")) return Symbol.get(name.substring(2)); + else return name; + } + private static void apply(ObjectValue obj, Environment env, ExposeTarget target, Class clazz) { var getters = new HashMap(); var setters = new HashMap(); @@ -92,11 +115,9 @@ public class NativeWrapperProvider implements WrappersProvider { for (var annotation : method.getAnnotationsByType(Expose.class)) { if (!annotation.target().shouldApply(target)) continue; - Object key = annotation.value(); - if (key.toString().startsWith("@@")) key = Symbol.get(key.toString().substring(2)); - else if (key.equals("")) key = method.getName(); - var name = key.toString(); - + checkUnderscore(method); + var name = getName(method, annotation.value()); + var key = getKey(name); var repeat = false; switch (annotation.type()) { @@ -107,19 +128,11 @@ public class NativeWrapperProvider implements WrappersProvider { ); call(null, null, method, obj, null, env); break; - case FIELD: - if (props.contains(key) || nonProps.contains(key)) repeat = true; - else { - checkSignature(method, true, Environment.class); - obj.defineProperty(null, key, call(new Context(null, env), name, method, null, env)); - nonProps.add(key); - } - break; case METHOD: if (props.contains(key) || nonProps.contains(key)) repeat = true; else { checkSignature(method, false, Arguments.class); - obj.defineProperty(null, key, create(name, method)); + obj.defineProperty(null, key, create(name, method), true, true, false); nonProps.add(key); } break; @@ -141,6 +154,52 @@ public class NativeWrapperProvider implements WrappersProvider { break; } + if (repeat) + throw new IllegalArgumentException(String.format( + "A member '%s' in the wrapper for '%s' of type '%s' is already present.", + name, clazz.getName(), target.toString() + )); + } + for (var annotation : method.getAnnotationsByType(ExposeField.class)) { + if (!annotation.target().shouldApply(target)) continue; + + checkUnderscore(method); + var name = getName(method, annotation.value()); + var key = getKey(name); + var repeat = false; + + if (props.contains(key) || nonProps.contains(key)) repeat = true; + else { + checkSignature(method, true, Environment.class); + obj.defineProperty(null, key, call(new Context(null, env), name, method, null, env), true, true, false); + nonProps.add(key); + } + + if (repeat) + throw new IllegalArgumentException(String.format( + "A member '%s' in the wrapper for '%s' of type '%s' is already present.", + name, clazz.getName(), target.toString() + )); + } + } + for (var field : clazz.getDeclaredFields()) { + for (var annotation : field.getAnnotationsByType(ExposeField.class)) { + if (!annotation.target().shouldApply(target)) continue; + + checkUnderscore(field); + var name = getName(field, annotation.value()); + var key = getKey(name); + var repeat = false; + + if (props.contains(key) || nonProps.contains(key)) repeat = true; + else { + try { + obj.defineProperty(null, key, Values.normalize(new Context(null, env), field.get(null)), true, true, false); + nonProps.add(key); + } + catch (IllegalArgumentException | IllegalAccessException e) { } + } + if (repeat) throw new IllegalArgumentException(String.format( "A member '%s' in the wrapper for '%s' of type '%s' is already present.", @@ -149,7 +208,7 @@ public class NativeWrapperProvider implements WrappersProvider { } } - for (var key : props) obj.defineProperty(null, key, getters.get(key), setters.get(key), true, true); + for (var key : props) obj.defineProperty(null, key, getters.get(key), setters.get(key), true, false); } private static Method getConstructor(Environment env, Class clazz) { @@ -187,6 +246,8 @@ public class NativeWrapperProvider implements WrappersProvider { new NativeFunction(getName(clazz), args -> { throw EngineException.ofError("This constructor is not invokable."); }) : create(getName(clazz), constr); + res.special = true; + apply(res, ctx, ExposeTarget.CONSTRUCTOR, clazz); return res; @@ -267,11 +328,11 @@ public class NativeWrapperProvider implements WrappersProvider { private void initError() { var proto = new ObjectValue(); proto.defineProperty(null, "message", new NativeFunction("message", args -> { - if (args.thisArg instanceof Throwable) return ((Throwable)args.thisArg).getMessage(); + if (args.self instanceof Throwable) return ((Throwable)args.self).getMessage(); else return null; })); - proto.defineProperty(null, "name", new NativeFunction("name", args -> getName(args.thisArg.getClass()))); - proto.defineProperty(null, "toString", new NativeFunction("toString", args -> args.thisArg.toString())); + proto.defineProperty(null, "name", new NativeFunction("name", args -> getName(args.self.getClass()))); + proto.defineProperty(null, "toString", new NativeFunction("toString", args -> args.self.toString())); var constr = makeConstructor(null, Throwable.class); proto.defineProperty(null, "constructor", constr, true, false, false); diff --git a/src/me/topchetoeu/jscript/permissions/PermissionsProvider.java b/src/me/topchetoeu/jscript/permissions/PermissionsProvider.java index 352f2c3..2cf84d7 100644 --- a/src/me/topchetoeu/jscript/permissions/PermissionsProvider.java +++ b/src/me/topchetoeu/jscript/permissions/PermissionsProvider.java @@ -19,11 +19,11 @@ public interface PermissionsProvider { public static PermissionsProvider get(Extensions exts) { return new PermissionsProvider() { @Override public boolean hasPermission(Permission perm) { - if (exts.has(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm); + if (exts.hasNotNull(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm); else return true; } @Override public boolean hasPermission(Permission perm, char delim) { - if (exts.has(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm, delim); + if (exts.hasNotNull(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm, delim); else return true; } };