From 2858d685ade842bc0793611bce6048bdfa2124d5 Mon Sep 17 00:00:00 2001 From: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com> Date: Sat, 5 Aug 2023 18:37:18 +0300 Subject: [PATCH] initial commit --- .gitattributes | 9 + .gitignore | 6 + LICENESE | 21 + README.md | 34 + files.txt | 144 ++ lib/core.ts | 193 ++ lib/iterators.ts | 216 ++ lib/map.ts | 44 + lib/promise.ts | 43 + lib/regex.ts | 211 ++ lib/require.ts | 15 + lib/set.ts | 45 + lib/values/array.ts | 361 ++++ lib/values/boolean.ts | 22 + lib/values/errors.ts | 89 + lib/values/function.ts | 78 + lib/values/number.ts | 46 + lib/values/object.ts | 234 ++ lib/values/string.ts | 261 +++ lib/values/symbol.ts | 38 + src/me/topchetoeu/jscript/Location.java | 63 + src/me/topchetoeu/jscript/Main.java | 104 + .../topchetoeu/jscript/MessageReceiver.java | 6 + .../compilation/AssignableStatement.java | 12 + .../jscript/compilation/CompileOptions.java | 11 + .../compilation/CompoundStatement.java | 77 + .../jscript/compilation/DiscardStatement.java | 33 + .../jscript/compilation/Instruction.java | 285 +++ .../jscript/compilation/Statement.java | 42 + .../compilation/VariableDeclareStatement.java | 44 + .../compilation/control/BreakStatement.java | 25 + .../control/ContinueStatement.java | 25 + .../compilation/control/DebugStatement.java | 22 + .../compilation/control/DeleteStatement.java | 29 + .../compilation/control/DoWhileStatement.java | 82 + .../compilation/control/ForInStatement.java | 81 + .../compilation/control/ForStatement.java | 97 + .../compilation/control/IfStatement.java | 80 + .../compilation/control/ReturnStatement.java | 27 + .../compilation/control/SwitchStatement.java | 81 + .../compilation/control/ThrowStatement.java | 26 + .../compilation/control/TryStatement.java | 87 + .../compilation/control/WhileStatement.java | 104 + .../compilation/values/ArrayStatement.java | 37 + .../compilation/values/CallStatement.java | 44 + .../compilation/values/ChangeStatement.java | 35 + .../compilation/values/CommaStatement.java | 38 + .../compilation/values/ConstantStatement.java | 27 + .../compilation/values/FunctionStatement.java | 110 + .../values/GlobalThisStatement.java | 24 + .../values/IndexAssignStatement.java | 51 + .../compilation/values/IndexStatement.java | 53 + .../compilation/values/LazyAndStatement.java | 45 + .../compilation/values/LazyOrStatement.java | 45 + .../compilation/values/NewStatement.java | 32 + .../compilation/values/ObjectStatement.java | 57 + .../values/OperationStatement.java | 104 + .../compilation/values/RegexStatement.java | 28 + .../compilation/values/TernaryStatement.java | 58 + .../compilation/values/TypeofStatement.java | 55 + .../values/VariableAssignStatement.java | 42 + .../values/VariableIndexStatement.java | 27 + .../compilation/values/VariableStatement.java | 35 + .../compilation/values/VoidStatement.java | 34 + .../jscript/engine/BreakpointData.java | 5 + .../jscript/engine/CallContext.java | 58 + .../jscript/engine/DebugCommand.java | 8 + src/me/topchetoeu/jscript/engine/Engine.java | 195 ++ .../jscript/engine/debug/DebugServer.java | 154 ++ .../jscript/engine/debug/DebugState.java | 52 + .../topchetoeu/jscript/engine/debug/Http.java | 65 + .../jscript/engine/debug/HttpRequest.java | 6 + .../jscript/engine/debug/V8Error.java | 19 + .../jscript/engine/debug/V8Event.java | 22 + .../jscript/engine/debug/V8Message.java | 50 + .../jscript/engine/debug/V8Result.java | 22 + .../jscript/engine/debug/WebSocket.java | 185 ++ .../engine/debug/WebSocketMessage.java | 29 + .../debug/handlers/DebuggerHandles.java | 29 + .../jscript/engine/frame/CodeFrame.java | 177 ++ .../jscript/engine/frame/ConvertHint.java | 6 + .../engine/frame/InstructionResult.java | 9 + .../jscript/engine/frame/Runners.java | 605 ++++++ .../engine/modules/FileModuleProvider.java | 50 + .../jscript/engine/modules/Module.java | 57 + .../jscript/engine/modules/ModuleManager.java | 80 + .../engine/modules/ModuleProvider.java | 9 + .../jscript/engine/scope/GlobalScope.java | 83 + .../jscript/engine/scope/LocalScope.java | 58 + .../engine/scope/LocalScopeRecord.java | 79 + .../jscript/engine/scope/ScopeRecord.java | 7 + .../jscript/engine/scope/ValueVariable.java | 28 + .../jscript/engine/scope/Variable.java | 9 + .../jscript/engine/values/ArrayValue.java | 160 ++ .../jscript/engine/values/CodeFunction.java | 50 + .../jscript/engine/values/FunctionValue.java | 70 + .../jscript/engine/values/NativeFunction.java | 21 + .../jscript/engine/values/NativeWrapper.java | 19 + .../jscript/engine/values/ObjectValue.java | 331 +++ .../jscript/engine/values/SignalValue.java | 17 + .../jscript/engine/values/Symbol.java | 15 + .../jscript/engine/values/Values.java | 608 ++++++ .../topchetoeu/jscript/events/Awaitable.java | 25 + .../jscript/events/DataNotifier.java | 34 + src/me/topchetoeu/jscript/events/Event.java | 49 + .../jscript/events/FinishedException.java | 7 + src/me/topchetoeu/jscript/events/Handle.java | 5 + .../topchetoeu/jscript/events/Notifier.java | 14 + .../topchetoeu/jscript/events/Observable.java | 75 + .../topchetoeu/jscript/events/Observer.java | 7 + src/me/topchetoeu/jscript/events/Pipe.java | 59 + .../jscript/events/WarmObservable.java | 46 + .../jscript/exceptions/EngineException.java | 70 + .../jscript/exceptions/SyntaxException.java | 14 + .../topchetoeu/jscript/filesystem/File.java | 22 + .../jscript/filesystem/Filesystem.java | 17 + .../jscript/filesystem/InaccessibleFile.java | 27 + .../jscript/filesystem/MemoryFile.java | 51 + .../jscript/filesystem/Permissions.java | 17 + .../filesystem/PermissionsProvider.java | 7 + .../jscript/filesystem/PhysicalFile.java | 53 + .../filesystem/PhysicalFilesystem.java | 74 + src/me/topchetoeu/jscript/interop/Native.java | 12 + .../jscript/interop/NativeGetter.java | 12 + .../jscript/interop/NativeSetter.java | 12 + .../jscript/interop/NativeTypeRegister.java | 162 ++ .../topchetoeu/jscript/interop/Overload.java | 62 + .../jscript/interop/OverloadFunction.java | 85 + src/me/topchetoeu/jscript/js/bootstrap.js | 108 + src/me/topchetoeu/jscript/json/JSON.java | 167 ++ .../topchetoeu/jscript/json/JSONElement.java | 76 + src/me/topchetoeu/jscript/json/JSONList.java | 21 + src/me/topchetoeu/jscript/json/JSONMap.java | 150 ++ .../topchetoeu/jscript/parsing/Operator.java | 114 + .../topchetoeu/jscript/parsing/ParseRes.java | 97 + .../topchetoeu/jscript/parsing/Parsing.java | 1877 +++++++++++++++++ .../topchetoeu/jscript/parsing/RawToken.java | 15 + .../topchetoeu/jscript/parsing/TestRes.java | 45 + src/me/topchetoeu/jscript/parsing/Token.java | 55 + .../topchetoeu/jscript/parsing/TokenType.java | 9 + src/me/topchetoeu/jscript/polyfills/Date.java | 302 +++ .../jscript/polyfills/Internals.java | 254 +++ src/me/topchetoeu/jscript/polyfills/JSON.java | 38 + src/me/topchetoeu/jscript/polyfills/Map.java | 85 + src/me/topchetoeu/jscript/polyfills/Math.java | 211 ++ .../jscript/polyfills/PolyfillEngine.java | 110 + .../topchetoeu/jscript/polyfills/Promise.java | 350 +++ .../topchetoeu/jscript/polyfills/RegExp.java | 187 ++ src/me/topchetoeu/jscript/polyfills/Set.java | 111 + .../jscript/polyfills/TypescriptEngine.java | 51 + tsconfig.json | 16 + 151 files changed, 13448 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENESE create mode 100644 README.md create mode 100644 files.txt create mode 100644 lib/core.ts create mode 100644 lib/iterators.ts create mode 100644 lib/map.ts create mode 100644 lib/promise.ts create mode 100644 lib/regex.ts create mode 100644 lib/require.ts create mode 100644 lib/set.ts create mode 100644 lib/values/array.ts create mode 100644 lib/values/boolean.ts create mode 100644 lib/values/errors.ts create mode 100644 lib/values/function.ts create mode 100644 lib/values/number.ts create mode 100644 lib/values/object.ts create mode 100644 lib/values/string.ts create mode 100644 lib/values/symbol.ts create mode 100644 src/me/topchetoeu/jscript/Location.java create mode 100644 src/me/topchetoeu/jscript/Main.java create mode 100644 src/me/topchetoeu/jscript/MessageReceiver.java create mode 100644 src/me/topchetoeu/jscript/compilation/AssignableStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/CompileOptions.java create mode 100644 src/me/topchetoeu/jscript/compilation/CompoundStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/DiscardStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/Instruction.java create mode 100644 src/me/topchetoeu/jscript/compilation/Statement.java create mode 100644 src/me/topchetoeu/jscript/compilation/VariableDeclareStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/BreakStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/ContinueStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/DebugStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/DeleteStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/DoWhileStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/ForInStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/ForStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/IfStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/ReturnStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/SwitchStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/ThrowStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/TryStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/control/WhileStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/ArrayStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/CallStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/ChangeStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/CommaStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/ConstantStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/FunctionStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/GlobalThisStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/IndexAssignStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/IndexStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/LazyAndStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/LazyOrStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/NewStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/ObjectStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/OperationStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/RegexStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/TernaryStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/TypeofStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/VariableAssignStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/VariableIndexStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/VariableStatement.java create mode 100644 src/me/topchetoeu/jscript/compilation/values/VoidStatement.java create mode 100644 src/me/topchetoeu/jscript/engine/BreakpointData.java create mode 100644 src/me/topchetoeu/jscript/engine/CallContext.java create mode 100644 src/me/topchetoeu/jscript/engine/DebugCommand.java create mode 100644 src/me/topchetoeu/jscript/engine/Engine.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/DebugServer.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/DebugState.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/Http.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/HttpRequest.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/V8Error.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/V8Event.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/V8Message.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/V8Result.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/WebSocket.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/WebSocketMessage.java create mode 100644 src/me/topchetoeu/jscript/engine/debug/handlers/DebuggerHandles.java create mode 100644 src/me/topchetoeu/jscript/engine/frame/CodeFrame.java create mode 100644 src/me/topchetoeu/jscript/engine/frame/ConvertHint.java create mode 100644 src/me/topchetoeu/jscript/engine/frame/InstructionResult.java create mode 100644 src/me/topchetoeu/jscript/engine/frame/Runners.java create mode 100644 src/me/topchetoeu/jscript/engine/modules/FileModuleProvider.java create mode 100644 src/me/topchetoeu/jscript/engine/modules/Module.java create mode 100644 src/me/topchetoeu/jscript/engine/modules/ModuleManager.java create mode 100644 src/me/topchetoeu/jscript/engine/modules/ModuleProvider.java create mode 100644 src/me/topchetoeu/jscript/engine/scope/GlobalScope.java create mode 100644 src/me/topchetoeu/jscript/engine/scope/LocalScope.java create mode 100644 src/me/topchetoeu/jscript/engine/scope/LocalScopeRecord.java create mode 100644 src/me/topchetoeu/jscript/engine/scope/ScopeRecord.java create mode 100644 src/me/topchetoeu/jscript/engine/scope/ValueVariable.java create mode 100644 src/me/topchetoeu/jscript/engine/scope/Variable.java create mode 100644 src/me/topchetoeu/jscript/engine/values/ArrayValue.java create mode 100644 src/me/topchetoeu/jscript/engine/values/CodeFunction.java create mode 100644 src/me/topchetoeu/jscript/engine/values/FunctionValue.java create mode 100644 src/me/topchetoeu/jscript/engine/values/NativeFunction.java create mode 100644 src/me/topchetoeu/jscript/engine/values/NativeWrapper.java create mode 100644 src/me/topchetoeu/jscript/engine/values/ObjectValue.java create mode 100644 src/me/topchetoeu/jscript/engine/values/SignalValue.java create mode 100644 src/me/topchetoeu/jscript/engine/values/Symbol.java create mode 100644 src/me/topchetoeu/jscript/engine/values/Values.java create mode 100644 src/me/topchetoeu/jscript/events/Awaitable.java create mode 100644 src/me/topchetoeu/jscript/events/DataNotifier.java create mode 100644 src/me/topchetoeu/jscript/events/Event.java create mode 100644 src/me/topchetoeu/jscript/events/FinishedException.java create mode 100644 src/me/topchetoeu/jscript/events/Handle.java create mode 100644 src/me/topchetoeu/jscript/events/Notifier.java create mode 100644 src/me/topchetoeu/jscript/events/Observable.java create mode 100644 src/me/topchetoeu/jscript/events/Observer.java create mode 100644 src/me/topchetoeu/jscript/events/Pipe.java create mode 100644 src/me/topchetoeu/jscript/events/WarmObservable.java create mode 100644 src/me/topchetoeu/jscript/exceptions/EngineException.java create mode 100644 src/me/topchetoeu/jscript/exceptions/SyntaxException.java create mode 100644 src/me/topchetoeu/jscript/filesystem/File.java create mode 100644 src/me/topchetoeu/jscript/filesystem/Filesystem.java create mode 100644 src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java create mode 100644 src/me/topchetoeu/jscript/filesystem/MemoryFile.java create mode 100644 src/me/topchetoeu/jscript/filesystem/Permissions.java create mode 100644 src/me/topchetoeu/jscript/filesystem/PermissionsProvider.java create mode 100644 src/me/topchetoeu/jscript/filesystem/PhysicalFile.java create mode 100644 src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java create mode 100644 src/me/topchetoeu/jscript/interop/Native.java create mode 100644 src/me/topchetoeu/jscript/interop/NativeGetter.java create mode 100644 src/me/topchetoeu/jscript/interop/NativeSetter.java create mode 100644 src/me/topchetoeu/jscript/interop/NativeTypeRegister.java create mode 100644 src/me/topchetoeu/jscript/interop/Overload.java create mode 100644 src/me/topchetoeu/jscript/interop/OverloadFunction.java create mode 100644 src/me/topchetoeu/jscript/js/bootstrap.js create mode 100644 src/me/topchetoeu/jscript/json/JSON.java create mode 100644 src/me/topchetoeu/jscript/json/JSONElement.java create mode 100644 src/me/topchetoeu/jscript/json/JSONList.java create mode 100644 src/me/topchetoeu/jscript/json/JSONMap.java create mode 100644 src/me/topchetoeu/jscript/parsing/Operator.java create mode 100644 src/me/topchetoeu/jscript/parsing/ParseRes.java create mode 100644 src/me/topchetoeu/jscript/parsing/Parsing.java create mode 100644 src/me/topchetoeu/jscript/parsing/RawToken.java create mode 100644 src/me/topchetoeu/jscript/parsing/TestRes.java create mode 100644 src/me/topchetoeu/jscript/parsing/Token.java create mode 100644 src/me/topchetoeu/jscript/parsing/TokenType.java create mode 100644 src/me/topchetoeu/jscript/polyfills/Date.java create mode 100644 src/me/topchetoeu/jscript/polyfills/Internals.java create mode 100644 src/me/topchetoeu/jscript/polyfills/JSON.java create mode 100644 src/me/topchetoeu/jscript/polyfills/Map.java create mode 100644 src/me/topchetoeu/jscript/polyfills/Math.java create mode 100644 src/me/topchetoeu/jscript/polyfills/PolyfillEngine.java create mode 100644 src/me/topchetoeu/jscript/polyfills/Promise.java create mode 100644 src/me/topchetoeu/jscript/polyfills/RegExp.java create mode 100644 src/me/topchetoeu/jscript/polyfills/Set.java create mode 100644 src/me/topchetoeu/jscript/polyfills/TypescriptEngine.java create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c53344c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0520ab8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode +.gradle +out +build +bin +/*.js \ No newline at end of file diff --git a/LICENESE b/LICENESE new file mode 100644 index 0000000..1603cbe --- /dev/null +++ b/LICENESE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 TopchetoEU + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1664132 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# JScript + +**NOTE: This had nothing to do with Microsoft's dialect of EcmaScript** + +**WARNING: Currently, this code is mostly undocumented. Proceed with caution and a psychiatrist.** + +JScript is an engine, capable of running EcmaScript 5, written entirely in Java. This engine has been developed with the goal of being easy to integrate with your preexisting codebase, **THE GOAL OF THIS ENGINE IS NOT PERFORMANCE**. My crude experiments show that this engine is 50x-100x slower than V8, which, although bad, is acceptable for most simple scripting purposes. + +## Example + +The following will create a REPL using the engine as a backend. Not that this won't properly log errors. I recommend checking out the implementation in `Main.main`: + +```java +var engine = new PolyfillEngine(new File(".")); +var in = new BufferedReader(new InputStreamReader(System.in)); +engine.start(); + +while (true) { + try { + var raw = in.readLine(); + + var res = engine.pushMsg(false, engine.global(), Map.of(), "", raw, null).await(); + Values.printValue(engine.context(), res); + System.out.println(); + } + catch (EngineException e) { + try { + System.out.println("Uncaught " + e.toString(engine.context())); + } + catch (InterruptedException _e) { return; } + } + catch (IOException | InterruptedException e) { return; } +} +``` diff --git a/files.txt b/files.txt new file mode 100644 index 0000000..68c8009 --- /dev/null +++ b/files.txt @@ -0,0 +1,144 @@ +src/me/topchetoeu/jscript/compilation/AssignableStatement.java +src/me/topchetoeu/jscript/compilation/CompileOptions.java +src/me/topchetoeu/jscript/compilation/CompoundStatement.java +src/me/topchetoeu/jscript/compilation/control/BreakStatement.java +src/me/topchetoeu/jscript/compilation/control/ContinueStatement.java +src/me/topchetoeu/jscript/compilation/control/DebugStatement.java +src/me/topchetoeu/jscript/compilation/control/DeleteStatement.java +src/me/topchetoeu/jscript/compilation/control/DoWhileStatement.java +src/me/topchetoeu/jscript/compilation/control/ForInStatement.java +src/me/topchetoeu/jscript/compilation/control/ForStatement.java +src/me/topchetoeu/jscript/compilation/control/IfStatement.java +src/me/topchetoeu/jscript/compilation/control/ReturnStatement.java +src/me/topchetoeu/jscript/compilation/control/SwitchStatement.java +src/me/topchetoeu/jscript/compilation/control/ThrowStatement.java +src/me/topchetoeu/jscript/compilation/control/TryStatement.java +src/me/topchetoeu/jscript/compilation/control/WhileStatement.java +src/me/topchetoeu/jscript/compilation/DiscardStatement.java +src/me/topchetoeu/jscript/compilation/Instruction.java +src/me/topchetoeu/jscript/compilation/Statement.java +src/me/topchetoeu/jscript/compilation/values/LazyOrStatement.java +src/me/topchetoeu/jscript/compilation/values/ArrayStatement.java +src/me/topchetoeu/jscript/compilation/values/CallStatement.java +src/me/topchetoeu/jscript/compilation/values/ChangeStatement.java +src/me/topchetoeu/jscript/compilation/values/CommaStatement.java +src/me/topchetoeu/jscript/compilation/values/ConstantStatement.java +src/me/topchetoeu/jscript/compilation/values/FunctionStatement.java +src/me/topchetoeu/jscript/compilation/values/GlobalThisStatement.java +src/me/topchetoeu/jscript/compilation/values/IndexAssignStatement.java +src/me/topchetoeu/jscript/compilation/values/IndexStatement.java +src/me/topchetoeu/jscript/compilation/values/LazyAndStatement.java +src/me/topchetoeu/jscript/compilation/values/NewStatement.java +src/me/topchetoeu/jscript/compilation/values/ObjectStatement.java +src/me/topchetoeu/jscript/compilation/values/OperationStatement.java +src/me/topchetoeu/jscript/compilation/values/RegexStatement.java +src/me/topchetoeu/jscript/compilation/values/TernaryStatement.java +src/me/topchetoeu/jscript/compilation/values/TypeofStatement.java +src/me/topchetoeu/jscript/compilation/values/VariableAssignStatement.java +src/me/topchetoeu/jscript/compilation/values/VariableIndexStatement.java +src/me/topchetoeu/jscript/compilation/values/VariableStatement.java +src/me/topchetoeu/jscript/compilation/values/VoidStatement.java +src/me/topchetoeu/jscript/compilation/VariableDeclareStatement.java +src/me/topchetoeu/jscript/engine/frame/CodeFrame.java +src/me/topchetoeu/jscript/engine/frame/ConvertHint.java +src/me/topchetoeu/jscript/engine/frame/InstructionResult.java +src/me/topchetoeu/jscript/engine/frame/Runners.java +src/me/topchetoeu/jscript/engine/BreakpointData.java +src/me/topchetoeu/jscript/engine/CallContext.java +src/me/topchetoeu/jscript/engine/debug/DebugServer.java +src/me/topchetoeu/jscript/engine/debug/DebugState.java +src/me/topchetoeu/jscript/engine/debug/handlers/DebuggerHandles.java +src/me/topchetoeu/jscript/engine/debug/Http.java +src/me/topchetoeu/jscript/engine/debug/HttpRequest.java +src/me/topchetoeu/jscript/engine/debug/V8Error.java +src/me/topchetoeu/jscript/engine/debug/V8Event.java +src/me/topchetoeu/jscript/engine/debug/V8Message.java +src/me/topchetoeu/jscript/engine/debug/V8Result.java +src/me/topchetoeu/jscript/engine/debug/WebSocket.java +src/me/topchetoeu/jscript/engine/debug/WebSocketMessage.java +src/me/topchetoeu/jscript/engine/DebugCommand.java +src/me/topchetoeu/jscript/engine/Engine.java +src/me/topchetoeu/jscript/engine/modules/FileModuleProvider.java +src/me/topchetoeu/jscript/engine/modules/Module.java +src/me/topchetoeu/jscript/engine/modules/ModuleManager.java +src/me/topchetoeu/jscript/engine/modules/ModuleProvider.java +src/me/topchetoeu/jscript/engine/scope/GlobalScope.java +src/me/topchetoeu/jscript/engine/scope/LocalScope.java +src/me/topchetoeu/jscript/engine/scope/LocalScopeRecord.java +src/me/topchetoeu/jscript/engine/scope/ScopeRecord.java +src/me/topchetoeu/jscript/engine/scope/ValueVariable.java +src/me/topchetoeu/jscript/engine/scope/Variable.java +src/me/topchetoeu/jscript/engine/values/ArrayValue.java +src/me/topchetoeu/jscript/engine/values/CodeFunction.java +src/me/topchetoeu/jscript/engine/values/FunctionValue.java +src/me/topchetoeu/jscript/engine/values/NativeFunction.java +src/me/topchetoeu/jscript/engine/values/NativeWrapper.java +src/me/topchetoeu/jscript/engine/values/ObjectValue.java +src/me/topchetoeu/jscript/engine/values/SignalValue.java +src/me/topchetoeu/jscript/engine/values/Symbol.java +src/me/topchetoeu/jscript/engine/values/Values.java +src/me/topchetoeu/jscript/events/Awaitable.java +src/me/topchetoeu/jscript/events/DataNotifier.java +src/me/topchetoeu/jscript/events/Event.java +src/me/topchetoeu/jscript/events/FinishedException.java +src/me/topchetoeu/jscript/events/Handle.java +src/me/topchetoeu/jscript/events/Notifier.java +src/me/topchetoeu/jscript/events/Observable.java +src/me/topchetoeu/jscript/events/Observer.java +src/me/topchetoeu/jscript/events/Pipe.java +src/me/topchetoeu/jscript/events/WarmObservable.java +src/me/topchetoeu/jscript/exceptions/EngineException.java +src/me/topchetoeu/jscript/exceptions/SyntaxException.java +src/me/topchetoeu/jscript/filesystem/File.java +src/me/topchetoeu/jscript/filesystem/Filesystem.java +src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java +src/me/topchetoeu/jscript/filesystem/MemoryFile.java +src/me/topchetoeu/jscript/filesystem/Permissions.java +src/me/topchetoeu/jscript/filesystem/PermissionsProvider.java +src/me/topchetoeu/jscript/filesystem/PhysicalFile.java +src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java +src/me/topchetoeu/jscript/interop/Native.java +src/me/topchetoeu/jscript/interop/NativeGetter.java +src/me/topchetoeu/jscript/interop/NativeSetter.java +src/me/topchetoeu/jscript/interop/NativeTypeRegister.java +src/me/topchetoeu/jscript/interop/Overload.java +src/me/topchetoeu/jscript/interop/OverloadFunction.java +src/me/topchetoeu/jscript/json/JSON.java +src/me/topchetoeu/jscript/json/JSONElement.java +src/me/topchetoeu/jscript/json/JSONList.java +src/me/topchetoeu/jscript/json/JSONMap.java +src/me/topchetoeu/jscript/Location.java +src/me/topchetoeu/jscript/Main.java +src/me/topchetoeu/jscript/MessageReceiver.java +src/me/topchetoeu/jscript/parsing/Operator.java +src/me/topchetoeu/jscript/parsing/ParseRes.java +src/me/topchetoeu/jscript/parsing/Parsing.java +src/me/topchetoeu/jscript/parsing/RawToken.java +src/me/topchetoeu/jscript/parsing/TestRes.java +src/me/topchetoeu/jscript/parsing/Token.java +src/me/topchetoeu/jscript/parsing/TokenType.java +src/me/topchetoeu/jscript/polyfills/Date.java +src/me/topchetoeu/jscript/polyfills/Internals.java +src/me/topchetoeu/jscript/polyfills/JSON.java +src/me/topchetoeu/jscript/polyfills/Map.java +src/me/topchetoeu/jscript/polyfills/Math.java +src/me/topchetoeu/jscript/polyfills/PolyfillEngine.java +src/me/topchetoeu/jscript/polyfills/Promise.java +src/me/topchetoeu/jscript/polyfills/RegExp.java +src/me/topchetoeu/jscript/polyfills/Set.java +src/me/topchetoeu/jscript/polyfills/TypescriptEngine.java +lib/core.ts +lib/iterators.ts +lib/map.ts +lib/promise.ts +lib/regex.ts +lib/require.ts +lib/set.ts +lib/values/array.ts +lib/values/boolean.ts +lib/values/errors.ts +lib/values/function.ts +lib/values/number.ts +lib/values/object.ts +lib/values/string.ts +lib/values/symbol.ts diff --git a/lib/core.ts b/lib/core.ts new file mode 100644 index 0000000..e3e39d1 --- /dev/null +++ b/lib/core.ts @@ -0,0 +1,193 @@ +type PropertyDescriptor = { + value: any; + writable?: boolean; + enumerable?: boolean; + configurable?: boolean; +} | { + get?(this: ThisT): T; + set(this: ThisT, val: T): void; + enumerable?: boolean; + configurable?: boolean; +} | { + get(this: ThisT): T; + set?(this: ThisT, val: T): void; + enumerable?: boolean; + configurable?: boolean; +}; +type Exclude = T extends U ? never : T; +type Extract = T extends U ? T : never; +type Record = { [x in KeyT]: ValT } + +interface IArguments { + [i: number]: any; + length: number; +} + +interface MathObject { + readonly E: number; + readonly PI: number; + readonly SQRT2: number; + readonly SQRT1_2: number; + readonly LN2: number; + readonly LN10: number; + readonly LOG2E: number; + readonly LOG10E: number; + + asin(x: number): number; + acos(x: number): number; + atan(x: number): number; + atan2(y: number, x: number): number; + asinh(x: number): number; + acosh(x: number): number; + atanh(x: number): number; + sin(x: number): number; + cos(x: number): number; + tan(x: number): number; + sinh(x: number): number; + cosh(x: number): number; + tanh(x: number): number; + sqrt(x: number): number; + cbrt(x: number): number; + hypot(...vals: number[]): number; + imul(a: number, b: number): number; + exp(x: number): number; + expm1(x: number): number; + pow(x: number, y: number): number; + log(x: number): number; + log10(x: number): number; + log1p(x: number): number; + log2(x: number): number; + ceil(x: number): number; + floor(x: number): number; + round(x: number): number; + fround(x: number): number; + trunc(x: number): number; + abs(x: number): number; + max(...vals: number[]): number; + min(...vals: number[]): number; + sign(x: number): number; + random(): number; + clz32(x: number): number; +} + + +//@ts-ignore +declare const arguments: IArguments; +declare const Math: MathObject; +declare const NaN: number; +declare const Infinity: number; + +declare var setTimeout: (handle: (...args: [ ...T, ...any[] ]) => void, delay?: number, ...args: T) => number; +declare var setInterval: (handle: (...args: [ ...T, ...any[] ]) => void, delay?: number, ...args: T) => number; + +declare var clearTimeout: (id: number) => void; +declare var clearInterval: (id: number) => void; + +/** @internal */ +declare var internals: any; +/** @internal */ +declare function run(file: string, pollute?: boolean): void; + +/** @internal */ +type ReplaceThis = T extends ((...args: infer ArgsT) => infer RetT) ? + ((this: ThisT, ...args: ArgsT) => RetT) : + T; + +/** @internal */ +declare var setProps: < + TargetT extends object, + DescT extends { [x in Exclude ]?: ReplaceThis } +>(target: TargetT, desc: DescT) => void; +/** @internal */ +declare var setConstr: (target: T, constr: ConstrT) => void; + +declare function log(...vals: any[]): void; +/** @internal */ +declare var lgt: typeof globalThis, gt: typeof globalThis; + +declare function assert(condition: () => unknown, message?: string): boolean; + +gt.assert = (cond, msg) => { + try { + if (!cond()) throw 'condition not satisfied'; + log('Passed ' + msg); + return true; + } + catch (e) { + log('Failed ' + msg + ' because of: ' + e); + return false; + } +} + +try { + lgt.setProps = (target, desc) => { + var props = internals.keys(desc, false); + for (var i = 0; i < props.length; i++) { + var key = props[i]; + internals.defineField( + target, key, (desc as any)[key], + true, // writable + false, // enumerable + true // configurable + ); + } + } + lgt.setConstr = (target, constr) => { + internals.defineField( + target, 'constructor', constr, + true, // writable + false, // enumerable + true // configurable + ); + } + + run('values/object.js'); + run('values/symbol.js'); + run('values/function.js'); + run('values/errors.js'); + run('values/string.js'); + run('values/number.js'); + run('values/boolean.js'); + run('values/array.js'); + + internals.special(Object, Function, Error, Array); + + gt.setTimeout = (func, delay, ...args) => { + if (typeof func !== 'function') throw new TypeError("func must be a function."); + delay = (delay ?? 0) - 0; + return internals.setTimeout(() => func(...args), delay) + }; + gt.setInterval = (func, delay, ...args) => { + if (typeof func !== 'function') throw new TypeError("func must be a function."); + delay = (delay ?? 0) - 0; + return internals.setInterval(() => func(...args), delay) + }; + + gt.clearTimeout = (id) => { + id = id | 0; + internals.clearTimeout(id); + }; + gt.clearInterval = (id) => { + id = id | 0; + internals.clearInterval(id); + }; + + + run('iterators.js'); + run('promise.js'); + run('map.js', true); + run('set.js', true); + run('regex.js'); + run('require.js'); + + log('Loaded polyfills!'); +} +catch (e: any) { + var err = 'Uncaught error while loading polyfills: '; + if (typeof Error !== 'undefined' && e instanceof Error && e.toString !== {}.toString) err += e; + else if ('message' in e) { + if ('name' in e) err += e.name + ": " + e.message; + else err += 'Error: ' + e.message; + } + else err += e; +} diff --git a/lib/iterators.ts b/lib/iterators.ts new file mode 100644 index 0000000..bdcd11f --- /dev/null +++ b/lib/iterators.ts @@ -0,0 +1,216 @@ +interface SymbolConstructor { + readonly iterator: unique symbol; + readonly asyncIterator: unique symbol; +} + +type IteratorYieldResult = + { done?: false; } & + (TReturn extends undefined ? { value?: undefined; } : { value: TReturn; }); + +type IteratorReturnResult = + { done: true } & + (TReturn extends undefined ? { value?: undefined; } : { value: TReturn; }); + +type IteratorResult = IteratorYieldResult | IteratorReturnResult; + +interface Iterator { + next(...args: [] | [TNext]): IteratorResult; + return?(value?: TReturn): IteratorResult; + throw?(e?: any): IteratorResult; +} + +interface Iterable { + [Symbol.iterator](): Iterator; +} + +interface IterableIterator extends Iterator { + [Symbol.iterator](): IterableIterator; +} + +interface Generator extends Iterator { + // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places. + next(...args: [] | [TNext]): IteratorResult; + return(value: TReturn): IteratorResult; + throw(e: any): IteratorResult; + [Symbol.iterator](): Generator; +} + +interface GeneratorFunction { + /** + * Creates a new Generator object. + * @param args A list of arguments the function accepts. + */ + new (...args: any[]): Generator; + /** + * Creates a new Generator object. + * @param args A list of arguments the function accepts. + */ + (...args: any[]): Generator; + /** + * The length of the arguments. + */ + readonly length: number; + /** + * Returns the name of the function. + */ + readonly name: string; + /** + * A reference to the prototype. + */ + readonly prototype: Generator; +} + +interface GeneratorFunctionConstructor { + /** + * Creates a new Generator function. + * @param args A list of arguments the function accepts. + */ + new (...args: string[]): GeneratorFunction; + /** + * Creates a new Generator function. + * @param args A list of arguments the function accepts. + */ + (...args: string[]): GeneratorFunction; + /** + * The length of the arguments. + */ + readonly length: number; + /** + * Returns the name of the function. + */ + readonly name: string; + /** + * A reference to the prototype. + */ +} + +interface AsyncIterator { + // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places. + next(...args: [] | [TNext]): Promise>; + return?(value?: TReturn | Thenable): Promise>; + throw?(e?: any): Promise>; +} + +interface AsyncIterable { + [Symbol.asyncIterator](): AsyncIterator; +} + +interface AsyncIterableIterator extends AsyncIterator { + [Symbol.asyncIterator](): AsyncIterableIterator; +} + +interface AsyncGenerator extends AsyncIterator { + // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places. + next(...args: [] | [TNext]): Promise>; + return(value: TReturn | Thenable): Promise>; + throw(e: any): Promise>; + [Symbol.asyncIterator](): AsyncGenerator; +} + +interface AsyncGeneratorFunction { + /** + * Creates a new AsyncGenerator object. + * @param args A list of arguments the function accepts. + */ + new (...args: any[]): AsyncGenerator; + /** + * Creates a new AsyncGenerator object. + * @param args A list of arguments the function accepts. + */ + (...args: any[]): AsyncGenerator; + /** + * The length of the arguments. + */ + readonly length: number; + /** + * Returns the name of the function. + */ + readonly name: string; + /** + * A reference to the prototype. + */ + readonly prototype: AsyncGenerator; +} + +interface AsyncGeneratorFunctionConstructor { + /** + * Creates a new AsyncGenerator function. + * @param args A list of arguments the function accepts. + */ + new (...args: string[]): AsyncGeneratorFunction; + /** + * Creates a new AsyncGenerator function. + * @param args A list of arguments the function accepts. + */ + (...args: string[]): AsyncGeneratorFunction; + /** + * The length of the arguments. + */ + readonly length: number; + /** + * Returns the name of the function. + */ + readonly name: string; + /** + * A reference to the prototype. + */ + readonly prototype: AsyncGeneratorFunction; +} + + +interface Array extends IterableIterator { + entries(): IterableIterator<[number, T]>; + values(): IterableIterator; + keys(): IterableIterator; +} + +setProps(Symbol, { + iterator: Symbol("Symbol.iterator") as any, + asyncIterator: Symbol("Symbol.asyncIterator") as any, +}); + +setProps(Array.prototype, { + [Symbol.iterator]: function() { + return this.values(); + }, + + values() { + var i = 0; + + return { + next: () => { + while (i < this.length) { + if (i++ in this) return { done: false, value: this[i - 1] }; + } + return { done: true, value: undefined }; + }, + [Symbol.iterator]() { return this; } + }; + }, + keys() { + var i = 0; + + return { + next: () => { + while (i < this.length) { + if (i++ in this) return { done: false, value: i - 1 }; + } + return { done: true, value: undefined }; + }, + [Symbol.iterator]() { return this; } + }; + }, + entries() { + var i = 0; + + return { + next: () => { + while (i < this.length) { + if (i++ in this) return { done: false, value: [i - 1, this[i - 1]] }; + } + return { done: true, value: undefined }; + }, + [Symbol.iterator]() { return this; } + }; + }, +}); diff --git a/lib/map.ts b/lib/map.ts new file mode 100644 index 0000000..f48c235 --- /dev/null +++ b/lib/map.ts @@ -0,0 +1,44 @@ +declare class Map { + public [Symbol.iterator](): IterableIterator<[KeyT, ValueT]>; + + public clear(): void; + public delete(key: KeyT): boolean; + + public entries(): IterableIterator<[KeyT, ValueT]>; + public keys(): IterableIterator; + public values(): IterableIterator; + + public get(key: KeyT): ValueT; + public set(key: KeyT, val: ValueT): this; + public has(key: KeyT): boolean; + + public get size(): number; + + public forEach(func: (key: KeyT, val: ValueT, map: Map) => void, thisArg?: any): void; + + public constructor(); +} + +Map.prototype[Symbol.iterator] = function() { + return this.entries(); +}; + +var entries = Map.prototype.entries; +var keys = Map.prototype.keys; +var values = Map.prototype.values; + +Map.prototype.entries = function() { + var it = entries.call(this); + it[Symbol.iterator] = () => it; + return it; +}; +Map.prototype.keys = function() { + var it = keys.call(this); + it[Symbol.iterator] = () => it; + return it; +}; +Map.prototype.values = function() { + var it = values.call(this); + it[Symbol.iterator] = () => it; + return it; +}; diff --git a/lib/promise.ts b/lib/promise.ts new file mode 100644 index 0000000..99fb463 --- /dev/null +++ b/lib/promise.ts @@ -0,0 +1,43 @@ +type PromiseFulfillFunc = (val: T) => void; +type PromiseThenFunc = (val: T) => NextT; +type PromiseRejectFunc = (err: unknown) => void; +type PromiseFunc = (resolve: PromiseFulfillFunc, reject: PromiseRejectFunc) => void; + +type PromiseResult ={ type: 'fulfilled'; value: T; } | { type: 'rejected'; reason: any; } + +interface Thenable { + then(this: Promise, onFulfilled: PromiseThenFunc, onRejected?: PromiseRejectFunc): Promise>; + then(this: Promise, onFulfilled: undefined, onRejected?: PromiseRejectFunc): Promise; +} + +// wippidy-wine, this code is now mine :D +type Awaited = + T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode + T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped + F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument + Awaited : // recursively unwrap the value + never : // the argument to `then` was not callable + T; + +interface PromiseConstructor { + prototype: Promise; + + new (func: PromiseFunc): Promise>; + resolve(val: T): Promise>; + reject(val: any): Promise; + + any(promises: (Promise|T)[]): Promise; + race(promises: (Promise|T)[]): Promise; + all(promises: T): Promise<{ [Key in keyof T]: Awaited }>; + allSettled(...promises: T): Promise<[...{ [P in keyof T]: PromiseResult>}]>; +} + +interface Promise extends Thenable { + constructor: PromiseConstructor; + catch(func: PromiseRejectFunc): Promise; + finally(func: () => void): Promise; +} + +declare var Promise: PromiseConstructor; + +(Promise.prototype as any)[Symbol.typeName] = 'Promise'; diff --git a/lib/regex.ts b/lib/regex.ts new file mode 100644 index 0000000..4a1e6ff --- /dev/null +++ b/lib/regex.ts @@ -0,0 +1,211 @@ +interface RegExpResultIndices extends Array<[number, number]> { + groups?: { [name: string]: [number, number]; }; +} +interface RegExpResult extends Array { + groups?: { [name: string]: string; }; + index: number; + input: string; + indices?: RegExpResultIndices; + escape(raw: string, flags: string): RegExp; +} +interface SymbolConstructor { + readonly match: unique symbol; + readonly matchAll: unique symbol; + readonly split: unique symbol; + readonly replace: unique symbol; + readonly search: unique symbol; +} + +type ReplaceFunc = (match: string, ...args: any[]) => string; + +interface Matcher { + [Symbol.match](target: string): RegExpResult | string[] | null; + [Symbol.matchAll](target: string): IterableIterator; +} +interface Splitter { + [Symbol.split](target: string, limit?: number, sensible?: boolean): string[]; +} +interface Replacer { + [Symbol.replace](target: string, replacement: string): string; +} +interface Searcher { + [Symbol.search](target: string, reverse?: boolean, start?: number): number; +} + +declare class RegExp implements Matcher, Splitter, Replacer, Searcher { + static escape(raw: any, flags?: string): RegExp; + + prototype: RegExp; + + exec(val: string): RegExpResult | null; + test(val: string): boolean; + toString(): string; + + [Symbol.match](target: string): RegExpResult | string[] | null; + [Symbol.matchAll](target: string): IterableIterator; + [Symbol.split](target: string, limit?: number, sensible?: boolean): string[]; + [Symbol.replace](target: string, replacement: string | ReplaceFunc): string; + [Symbol.search](target: string, reverse?: boolean, start?: number): number; + + readonly dotAll: boolean; + readonly global: boolean; + readonly hasIndices: boolean; + readonly ignoreCase: boolean; + readonly multiline: boolean; + readonly sticky: boolean; + readonly unicode: boolean; + + readonly source: string; + readonly flags: string; + + lastIndex: number; + + constructor(pattern?: string, flags?: string); + constructor(pattern?: RegExp, flags?: string); +} + +(Symbol as any).replace = Symbol('Symbol.replace'); +(Symbol as any).match = Symbol('Symbol.match'); +(Symbol as any).matchAll = Symbol('Symbol.matchAll'); +(Symbol as any).split = Symbol('Symbol.split'); +(Symbol as any).search = Symbol('Symbol.search'); + +setProps(RegExp.prototype, { + [Symbol.typeName]: 'RegExp', + + test(val) { + return !!this.exec(val); + }, + toString() { + return '/' + this.source + '/' + this.flags; + }, + + [Symbol.match](target) { + if (this.global) { + const res: string[] = []; + let val; + while (val = this.exec(target)) { + res.push(val[0]); + } + this.lastIndex = 0; + return res; + } + else { + const res = this.exec(target); + if (!this.sticky) this.lastIndex = 0; + return res; + } + }, + [Symbol.matchAll](target) { + let pattern: RegExp | undefined = new this.constructor(this, this.flags + "g") as RegExp; + + return { + next: (): IteratorResult => { + const val = pattern?.exec(target); + + if (val === null || val === undefined) { + pattern = undefined; + return { done: true }; + } + else return { value: val }; + }, + [Symbol.iterator]() { return this; } + } + }, + [Symbol.split](target, limit, sensible) { + const pattern = new this.constructor(this, this.flags + "g") as RegExp; + let match: RegExpResult | null; + let lastEnd = 0; + const res: string[] = []; + + while ((match = pattern.exec(target)) !== null) { + let added: string[] = []; + + if (match.index >= target.length) break; + + if (match[0].length === 0) { + added = [ target.substring(lastEnd, pattern.lastIndex), ]; + if (pattern.lastIndex < target.length) added.push(...match.slice(1)); + } + else if (match.index - lastEnd > 0) { + added = [ target.substring(lastEnd, match.index), ...match.slice(1) ]; + } + else { + for (let i = 1; i < match.length; i++) { + res[res.length - match.length + i] = match[i]; + } + } + + if (sensible) { + if (limit !== undefined && res.length + added.length >= limit) break; + else res.push(...added); + } + else { + for (let i = 0; i < added.length; i++) { + if (limit !== undefined && res.length >= limit) return res; + else res.push(added[i]); + } + } + + lastEnd = pattern.lastIndex; + } + + if (lastEnd < target.length) { + res.push(target.substring(lastEnd)); + } + + return res; + }, + [Symbol.replace](target, replacement) { + const pattern = new this.constructor(this, this.flags + "d") as RegExp; + let match: RegExpResult | null; + let lastEnd = 0; + const res: string[] = []; + + // log(pattern.toString()); + + while ((match = pattern.exec(target)) !== null) { + const indices = match.indices![0]; + res.push(target.substring(lastEnd, indices[0])); + if (replacement instanceof Function) { + res.push(replacement(target.substring(indices[0], indices[1]), ...match.slice(1), indices[0], target)); + } + else { + res.push(replacement); + } + lastEnd = indices[1]; + if (!pattern.global) break; + } + + if (lastEnd < target.length) { + res.push(target.substring(lastEnd)); + } + + return res.join(''); + }, + [Symbol.search](target, reverse, start) { + const pattern: RegExp | undefined = new this.constructor(this, this.flags + "g") as RegExp; + + + if (!reverse) { + pattern.lastIndex = (start as any) | 0; + const res = pattern.exec(target); + if (res) return res.index; + else return -1; + } + else { + start ??= target.length; + start |= 0; + let res: RegExpResult | null = null; + + while (true) { + const tmp = pattern.exec(target); + if (tmp === null || tmp.index > start) break; + res = tmp; + } + + if (res && res.index <= start) return res.index; + else return -1; + } + }, +}); diff --git a/lib/require.ts b/lib/require.ts new file mode 100644 index 0000000..811d6a5 --- /dev/null +++ b/lib/require.ts @@ -0,0 +1,15 @@ +type RequireFunc = (path: string) => any; + +interface Module { + exports: any; + name: string; +} + +declare var require: RequireFunc; +declare var exports: any; +declare var module: Module; + +gt.require = function(path: string) { + if (typeof path !== 'string') path = path + ''; + return internals.require(path); +}; diff --git a/lib/set.ts b/lib/set.ts new file mode 100644 index 0000000..4ef9ab6 --- /dev/null +++ b/lib/set.ts @@ -0,0 +1,45 @@ +declare class Set { + public [Symbol.iterator](): IterableIterator; + + public entries(): IterableIterator<[T, T]>; + public keys(): IterableIterator; + public values(): IterableIterator; + + public clear(): void; + + public add(val: T): this; + public delete(val: T): boolean; + public has(key: T): boolean; + + public get size(): number; + + public forEach(func: (key: T, set: Set) => void, thisArg?: any): void; + + public constructor(); +} + +Set.prototype[Symbol.iterator] = function() { + return this.values(); +}; + +(() => { + var entries = Set.prototype.entries; + var keys = Set.prototype.keys; + var values = Set.prototype.values; + + Set.prototype.entries = function() { + var it = entries.call(this); + it[Symbol.iterator] = () => it; + return it; + }; + Set.prototype.keys = function() { + var it = keys.call(this); + it[Symbol.iterator] = () => it; + return it; + }; + Set.prototype.values = function() { + var it = values.call(this); + it[Symbol.iterator] = () => it; + return it; + }; +})(); \ No newline at end of file diff --git a/lib/values/array.ts b/lib/values/array.ts new file mode 100644 index 0000000..888c34d --- /dev/null +++ b/lib/values/array.ts @@ -0,0 +1,361 @@ +// god this is awful +type FlatArray = { + "done": Arr, + "recur": Arr extends Array + ? FlatArray + : Arr +}[Depth extends -1 ? "done" : "recur"]; + +interface Array { + [i: number]: T; + + constructor: ArrayConstructor; + length: number; + + toString(): string; + // toLocaleString(): string; + join(separator?: string): string; + fill(val: T, start?: number, end?: number): T[]; + pop(): T | undefined; + push(...items: T[]): number; + concat(...items: (T | T[])[]): T[]; + concat(...items: (T | T[])[]): T[]; + join(separator?: string): string; + reverse(): T[]; + shift(): T | undefined; + slice(start?: number, end?: number): T[]; + sort(compareFn?: (a: T, b: T) => number): this; + splice(start: number, deleteCount?: number | undefined, ...items: T[]): T[]; + unshift(...items: T[]): number; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + every(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; + some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + includes(el: any, start?: number): boolean; + + map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[]; + find(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[]; + findIndex(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): number; + findLast(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[]; + findLastIndex(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): number; + + flat(depth?: D): FlatArray; + flatMap(func: (val: T, i: number, arr: T[]) => T | T[], thisAarg?: any): FlatArray; + sort(func?: (a: T, b: T) => number): this; + + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; +} +interface ArrayConstructor { + new (arrayLength?: number): T[]; + new (...items: T[]): T[]; + (arrayLength?: number): T[]; + (...items: T[]): T[]; + isArray(arg: any): arg is any[]; + prototype: Array; +} + +declare var Array: ArrayConstructor; + +gt.Array = function(len?: number) { + var res = []; + + if (typeof len === 'number' && arguments.length === 1) { + if (len < 0) throw 'Invalid array length.'; + res.length = len; + } + else { + for (var i = 0; i < arguments.length; i++) { + res[i] = arguments[i]; + } + } + + return res; +} as ArrayConstructor; + +Array.prototype = ([] as any).__proto__ as Array; +setConstr(Array.prototype, Array); + +function wrapI(max: number, i: number) { + i |= 0; + if (i < 0) i = max + i; + return i; +} +function clampI(max: number, i: number) { + if (i < 0) i = 0; + if (i > max) i = max; + return i; +} + +lgt.wrapI = wrapI; +lgt.clampI = clampI; + +(Array.prototype as any)[Symbol.typeName] = "Array"; + +setProps(Array.prototype, { + concat() { + var res = [] as any[]; + res.push.apply(res, this); + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + if (arg instanceof Array) { + res.push.apply(res, arg); + } + else { + res.push(arg); + } + } + + return res; + }, + every(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument not a function."); + func = func.bind(thisArg); + + for (var i = 0; i < this.length; i++) { + if (!func(this[i], i, this)) return false; + } + + return true; + }, + some(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument not a function."); + func = func.bind(thisArg); + + for (var i = 0; i < this.length; i++) { + if (func(this[i], i, this)) return true; + } + + return false; + }, + fill(val, start, end) { + if (arguments.length < 3) end = this.length; + if (arguments.length < 2) start = 0; + + start = clampI(this.length, wrapI(this.length + 1, start ?? 0)); + end = clampI(this.length, wrapI(this.length + 1, end ?? this.length)); + + for (; start < end; start++) { + this[start] = val; + } + + return this; + }, + filter(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); + + var res = []; + for (var i = 0; i < this.length; i++) { + if (i in this && func.call(thisArg, this[i], i, this)) res.push(this[i]); + } + return res; + }, + find(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); + + for (var i = 0; i < this.length; i++) { + if (i in this && func.call(thisArg, this[i], i, this)) return this[i]; + } + + return undefined; + }, + findIndex(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); + + for (var i = 0; i < this.length; i++) { + if (i in this && func.call(thisArg, this[i], i, this)) return i; + } + + return -1; + }, + findLast(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); + + for (var i = this.length - 1; i >= 0; i--) { + if (i in this && func.call(thisArg, this[i], i, this)) return this[i]; + } + + return undefined; + }, + findLastIndex(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); + + for (var i = this.length - 1; i >= 0; i--) { + if (i in this && func.call(thisArg, this[i], i, this)) return i; + } + + return -1; + }, + flat(depth) { + var res = [] as any[]; + var buff = []; + res.push(...this); + + for (var i = 0; i < (depth ?? 1); i++) { + var anyArrays = false; + for (var el of res) { + if (el instanceof Array) { + buff.push(...el); + anyArrays = true; + } + else buff.push(el); + } + + res = buff; + buff = []; + if (!anyArrays) break; + } + + return res; + }, + flatMap(func, th) { + return this.map(func, th).flat(); + }, + forEach(func, thisArg) { + for (var i = 0; i < this.length; i++) { + if (i in this) func.call(thisArg, this[i], i, this); + } + }, + map(func, thisArg) { + if (typeof func !== 'function') throw new TypeError("Given argument is not a function."); + + var res = []; + for (var i = 0; i < this.length; i++) { + if (i in this) res[i] = func.call(thisArg, this[i], i, this); + } + return res; + }, + pop() { + if (this.length === 0) return undefined; + var val = this[this.length - 1]; + this.length--; + return val; + }, + push() { + for (var i = 0; i < arguments.length; i++) { + this[this.length] = arguments[i]; + } + return arguments.length; + }, + shift() { + if (this.length === 0) return undefined; + var res = this[0]; + + for (var i = 0; i < this.length - 1; i++) { + this[i] = this[i + 1]; + } + + this.length--; + + return res; + }, + unshift() { + for (var i = this.length - 1; i >= 0; i--) { + this[i + arguments.length] = this[i]; + } + for (var i = 0; i < arguments.length; i++) { + this[i] = arguments[i]; + } + + return arguments.length; + }, + slice(start, end) { + start = clampI(this.length, wrapI(this.length + 1, start ?? 0)); + end = clampI(this.length, wrapI(this.length + 1, end ?? this.length)); + + var res: any[] = []; + var n = end - start; + if (n <= 0) return res; + + for (var i = 0; i < n; i++) { + res[i] = this[start + i]; + } + + return res; + }, + toString() { + let res = ''; + for (let i = 0; i < this.length; i++) { + if (i > 0) res += ','; + if (i in this && this[i] !== undefined && this[i] !== null) res += this[i]; + } + + return res; + }, + indexOf(el, start) { + start = start! | 0; + for (var i = Math.max(0, start); i < this.length; i++) { + if (i in this && this[i] == el) return i; + } + + return -1; + }, + includes(el, start) { + return this.indexOf(el, start) >= 0; + }, + join(val = ',') { + let res = '', first = true; + + for (let i = 0; i < this.length; i++) { + if (!(i in this)) continue; + if (!first) res += val; + first = false; + res += this[i]; + } + return res; + }, + sort(func) { + func ??= (a, b) => { + const _a = a + ''; + const _b = b + ''; + + if (_a > _b) return 1; + if (_a < _b) return -1; + return 0; + }; + + if (typeof func !== 'function') throw new TypeError('Expected func to be undefined or a function.'); + + internals.sort(this, func); + return this; + }, + splice(start, deleteCount, ...items) { + start = clampI(this.length, wrapI(this.length, start ?? 0)); + deleteCount = (deleteCount ?? Infinity | 0); + if (start + deleteCount >= this.length) deleteCount = this.length - start; + + const res = this.slice(start, start + deleteCount); + const moveN = items.length - deleteCount; + const len = this.length; + + if (moveN < 0) { + for (let i = start - moveN; i < len; i++) { + this[i + moveN] = this[i]; + } + } + else if (moveN > 0) { + for (let i = len - 1; i >= start; i--) { + this[i + moveN] = this[i]; + } + } + + for (let i = 0; i < items.length; i++) { + this[i + start] = items[i]; + } + + this.length = len + moveN; + + return res; + } +}); + +setProps(Array, { + isArray(val: any) { return (val instanceof Array); } +}); diff --git a/lib/values/boolean.ts b/lib/values/boolean.ts new file mode 100644 index 0000000..104d194 --- /dev/null +++ b/lib/values/boolean.ts @@ -0,0 +1,22 @@ +interface Boolean { + valueOf(): boolean; + constructor: BooleanConstructor; +} +interface BooleanConstructor { + (val: any): boolean; + new (val: any): Boolean; + prototype: Boolean; +} + +declare var Boolean: BooleanConstructor; + +gt.Boolean = function (this: Boolean | undefined, arg) { + var val; + if (arguments.length === 0) val = false; + else val = !!arg; + if (this === undefined || this === null) return val; + else (this as any).value = val; +} as BooleanConstructor; + +Boolean.prototype = (false as any).__proto__ as Boolean; +setConstr(Boolean.prototype, Boolean); \ No newline at end of file diff --git a/lib/values/errors.ts b/lib/values/errors.ts new file mode 100644 index 0000000..0f16197 --- /dev/null +++ b/lib/values/errors.ts @@ -0,0 +1,89 @@ +interface Error { + constructor: ErrorConstructor; + name: string; + message: string; + stack: string[]; +} +interface ErrorConstructor { + (msg?: any): Error; + new (msg?: any): Error; + prototype: Error; +} + +interface TypeErrorConstructor extends ErrorConstructor { + (msg?: any): TypeError; + new (msg?: any): TypeError; + prototype: Error; +} +interface TypeError extends Error { + constructor: TypeErrorConstructor; + name: 'TypeError'; +} + +interface RangeErrorConstructor extends ErrorConstructor { + (msg?: any): RangeError; + new (msg?: any): RangeError; + prototype: Error; +} +interface RangeError extends Error { + constructor: RangeErrorConstructor; + name: 'RangeError'; +} + +interface SyntaxErrorConstructor extends ErrorConstructor { + (msg?: any): RangeError; + new (msg?: any): RangeError; + prototype: Error; +} +interface SyntaxError extends Error { + constructor: SyntaxErrorConstructor; + name: 'SyntaxError'; +} + + +declare var Error: ErrorConstructor; +declare var RangeError: RangeErrorConstructor; +declare var TypeError: TypeErrorConstructor; +declare var SyntaxError: SyntaxErrorConstructor; + +gt.Error = function Error(msg: string) { + if (msg === undefined) msg = ''; + else msg += ''; + + return Object.setPrototypeOf({ + message: msg, + stack: [] as string[], + }, Error.prototype); +} as ErrorConstructor; + +Error.prototype = internals.err ?? {}; +Error.prototype.name = 'Error'; +setConstr(Error.prototype, Error); + +Error.prototype.toString = function() { + if (!(this instanceof Error)) return ''; + + if (this.message === '') return this.name; + else return this.name + ': ' + this.message; +}; + +function makeError(name: string, proto: any): T { + var err = function (msg: string) { + var res = new Error(msg); + (res as any).__proto__ = err.prototype; + return res; + } as T; + + err.prototype = proto; + err.prototype.name = name; + setConstr(err.prototype, err as ErrorConstructor); + (err.prototype as any).__proto__ = Error.prototype; + (err as any).__proto__ = Error; + internals.special(err); + + return err; +} + +gt.RangeError = makeError('RangeError', internals.range ?? {}); +gt.TypeError = makeError('TypeError', internals.type ?? {}); +gt.SyntaxError = makeError('SyntaxError', internals.syntax ?? {}); \ No newline at end of file diff --git a/lib/values/function.ts b/lib/values/function.ts new file mode 100644 index 0000000..872ce83 --- /dev/null +++ b/lib/values/function.ts @@ -0,0 +1,78 @@ +interface Function { + apply(this: Function, thisArg: any, argArray?: any): any; + call(this: Function, thisArg: any, ...argArray: any[]): any; + bind(this: Function, thisArg: any, ...argArray: any[]): Function; + + toString(): string; + + prototype: any; + constructor: FunctionConstructor; + readonly length: number; + name: string; +} +interface FunctionConstructor extends Function { + (...args: string[]): (...args: any[]) => any; + new (...args: string[]): (...args: any[]) => any; + prototype: Function; +} + +interface CallableFunction extends Function { + (...args: any[]): any; + apply(this: (this: ThisArg, ...args: Args) => RetT, thisArg: ThisArg, argArray?: Args): RetT; + call(this: (this: ThisArg, ...args: Args) => RetT, thisArg: ThisArg, ...argArray: Args): RetT; + bind(this: (this: ThisArg, ...args: [ ...Args, ...Rest ]) => RetT, thisArg: ThisArg, ...argArray: Args): (this: void, ...args: Rest) => RetT; +} +interface NewableFunction extends Function { + new(...args: any[]): any; + apply(this: new (...args: Args) => RetT, thisArg: any, argArray?: Args): RetT; + call(this: new (...args: Args) => RetT, thisArg: any, ...argArray: Args): RetT; + bind(this: new (...args: Args) => RetT, thisArg: any, ...argArray: Args): new (...args: Args) => RetT; +} + +declare var Function: FunctionConstructor; + +gt.Function = function() { + throw 'Using the constructor Function() is forbidden.'; +} as unknown as FunctionConstructor; + +Function.prototype = (Function as any).__proto__ as Function; +setConstr(Function.prototype, Function); + +setProps(Function.prototype, { + apply(thisArg, args) { + if (typeof args !== 'object') throw 'Expected arguments to be an array-like object.'; + var len = args.length - 0; + var newArgs: any[] = []; + + while (len >= 0) { + len--; + newArgs[len] = args[len]; + } + + return internals.apply(this, thisArg, newArgs); + }, + call(thisArg, ...args) { + return this.apply(thisArg, args); + }, + bind(thisArg, ...args) { + var func = this; + + var res = function() { + var resArgs = []; + + for (var i = 0; i < args.length; i++) { + resArgs[i] = args[i]; + } + for (var i = 0; i < arguments.length; i++) { + resArgs[i + args.length] = arguments[i]; + } + + return func.apply(thisArg, resArgs); + }; + res.name = " " + func.name; + return res; + }, + toString() { + return 'function (...) { ... }'; + }, +}); \ No newline at end of file diff --git a/lib/values/number.ts b/lib/values/number.ts new file mode 100644 index 0000000..5a21c13 --- /dev/null +++ b/lib/values/number.ts @@ -0,0 +1,46 @@ +interface Number { + toString(): string; + valueOf(): number; + constructor: NumberConstructor; +} +interface NumberConstructor { + (val: any): number; + new (val: any): Number; + prototype: Number; + parseInt(val: unknown): number; + parseFloat(val: unknown): number; +} + +declare var Number: NumberConstructor; +declare const parseInt: typeof Number.parseInt; +declare const parseFloat: typeof Number.parseFloat; + +gt.Number = function(this: Number | undefined, arg: any) { + var val; + if (arguments.length === 0) val = 0; + else val = arg - 0; + if (this === undefined || this === null) return val; + else (this as any).value = val; +} as NumberConstructor; + +Number.prototype = (0 as any).__proto__ as Number; +setConstr(Number.prototype, Number); + +setProps(Number.prototype, { + valueOf() { + if (typeof this === 'number') return this; + else return (this as any).value; + }, + toString() { + if (typeof this === 'number') return this + ''; + else return (this as any).value + ''; + } +}); + +setProps(Number, { + parseInt(val) { return Math.trunc(Number.parseFloat(val)); }, + parseFloat(val) { return internals.parseFloat(val); }, +}); + +(gt as any).parseInt = Number.parseInt; +(gt as any).parseFloat = Number.parseFloat; \ No newline at end of file diff --git a/lib/values/object.ts b/lib/values/object.ts new file mode 100644 index 0000000..6d33f03 --- /dev/null +++ b/lib/values/object.ts @@ -0,0 +1,234 @@ + interface Object { + constructor: NewableFunction; + [Symbol.typeName]: string; + + valueOf(): this; + toString(): string; + hasOwnProperty(key: any): boolean; +} +interface ObjectConstructor extends Function { + (arg: string): String; + (arg: number): Number; + (arg: boolean): Boolean; + (arg?: undefined | null): {}; + (arg: T): T; + + new (arg: string): String; + new (arg: number): Number; + new (arg: boolean): Boolean; + new (arg?: undefined | null): {}; + new (arg: T): T; + + prototype: Object; + + assign(target: T, ...src: object[]): T; + create(proto: T, props?: { [key: string]: PropertyDescriptor }): T; + + keys(obj: T, onlyString?: true): (keyof T)[]; + keys(obj: T, onlyString: false): any[]; + entries(obj: T, onlyString?: true): [keyof T, T[keyof T]][]; + entries(obj: T, onlyString: false): [any, any][]; + values(obj: T, onlyString?: true): (T[keyof T])[]; + values(obj: T, onlyString: false): any[]; + + fromEntries(entries: Iterable<[any, any]>): object; + + defineProperty(obj: ThisT, key: any, desc: PropertyDescriptor): ThisT; + defineProperties(obj: ThisT, desc: { [key: string]: PropertyDescriptor }): ThisT; + + getOwnPropertyNames(obj: T): (keyof T)[]; + getOwnPropertySymbols(obj: T): (keyof T)[]; + hasOwn(obj: T, key: KeyT): boolean; + + getOwnPropertyDescriptor(obj: T, key: KeyT): PropertyDescriptor; + getOwnPropertyDescriptors(obj: T): { [x in keyof T]: PropertyDescriptor }; + + getPrototypeOf(obj: any): object | null; + setPrototypeOf(obj: T, proto: object | null): T; + + preventExtensions(obj: T): T; + seal(obj: T): T; + freeze(obj: T): T; + + isExtensible(obj: object): boolean; + isSealed(obj: object): boolean; + isFrozen(obj: object): boolean; +} + +declare var Object: ObjectConstructor; + +gt.Object = function(arg: any) { + if (arg === undefined || arg === null) return {}; + else if (typeof arg === 'boolean') return new Boolean(arg); + else if (typeof arg === 'number') return new Number(arg); + else if (typeof arg === 'string') return new String(arg); + return arg; +} as ObjectConstructor; + +Object.prototype = ({} as any).__proto__ as Object; +setConstr(Object.prototype, Object as any); + +function throwNotObject(obj: any, name: string) { + if (obj === null || typeof obj !== 'object' && typeof obj !== 'function') { + throw new TypeError(`Object.${name} may only be used for objects.`); + } +} +function check(obj: any) { + return typeof obj === 'object' && obj !== null || typeof obj === 'function'; +} + +setProps(Object, { + assign: function(dst, ...src) { + throwNotObject(dst, 'assign'); + for (let i = 0; i < src.length; i++) { + const obj = src[i]; + throwNotObject(obj, 'assign'); + for (const key of Object.keys(obj)) { + (dst as any)[key] = (obj as any)[key]; + } + } + return dst; + }, + create(obj, props) { + props ??= {}; + return Object.defineProperties({ __proto__: obj }, props as any) as any; + }, + + defineProperty(obj, key, attrib) { + throwNotObject(obj, 'defineProperty'); + if (typeof attrib !== 'object') throw new TypeError('Expected attributes to be an object.'); + + if ('value' in attrib) { + if ('get' in attrib || 'set' in attrib) throw new TypeError('Cannot specify a value and accessors for a property.'); + if (!internals.defineField( + obj, key, + attrib.value, + !!attrib.writable, + !!attrib.enumerable, + !!attrib.configurable + )) throw new TypeError('Can\'t define property \'' + key + '\'.'); + } + else { + if (typeof attrib.get !== 'function' && attrib.get !== undefined) throw new TypeError('Get accessor must be a function.'); + if (typeof attrib.set !== 'function' && attrib.set !== undefined) throw new TypeError('Set accessor must be a function.'); + + if (!internals.defineProp( + obj, key, + attrib.get, + attrib.set, + !!attrib.enumerable, + !!attrib.configurable + )) throw new TypeError('Can\'t define property \'' + key + '\'.'); + } + + return obj; + }, + defineProperties(obj, attrib) { + throwNotObject(obj, 'defineProperties'); + if (typeof attrib !== 'object' && typeof attrib !== 'function') throw 'Expected second argument to be an object.'; + + for (var key in attrib) { + Object.defineProperty(obj, key, attrib[key]); + } + + return obj; + }, + + keys(obj, onlyString) { + onlyString = !!(onlyString ?? true); + return internals.keys(obj, onlyString); + }, + entries(obj, onlyString) { + return Object.keys(obj, onlyString).map(v => [ v, (obj as any)[v] ]); + }, + values(obj, onlyString) { + return Object.keys(obj, onlyString).map(v => (obj as any)[v]); + }, + + getOwnPropertyDescriptor(obj, key) { + return internals.ownProp(obj, key); + }, + getOwnPropertyDescriptors(obj) { + return Object.fromEntries([ + ...Object.getOwnPropertyNames(obj), + ...Object.getOwnPropertySymbols(obj) + ].map(v => [ v, Object.getOwnPropertyDescriptor(obj, v) ])) as any; + }, + + getOwnPropertyNames(obj) { + return internals.ownPropKeys(obj, false); + }, + getOwnPropertySymbols(obj) { + return internals.ownPropKeys(obj, true); + }, + hasOwn(obj, key) { + if (Object.getOwnPropertyNames(obj).includes(key)) return true; + if (Object.getOwnPropertySymbols(obj).includes(key)) return true; + return false; + }, + + getPrototypeOf(obj) { + return obj.__proto__; + }, + setPrototypeOf(obj, proto) { + (obj as any).__proto__ = proto; + return obj; + }, + + fromEntries(iterable) { + const res = {} as any; + + for (const el of iterable) { + res[el[0]] = el[1]; + } + + return res; + }, + + preventExtensions(obj) { + throwNotObject(obj, 'preventExtensions'); + internals.preventExtensions(obj); + return obj; + }, + seal(obj) { + throwNotObject(obj, 'seal'); + internals.seal(obj); + return obj; + }, + freeze(obj) { + throwNotObject(obj, 'freeze'); + internals.freeze(obj); + return obj; + }, + + isExtensible(obj) { + if (!check(obj)) return false; + return internals.extensible(obj); + }, + isSealed(obj) { + if (!check(obj)) return true; + if (Object.isExtensible(obj)) return false; + return Object.getOwnPropertyNames(obj).every(v => !Object.getOwnPropertyDescriptor(obj, v).configurable); + }, + isFrozen(obj) { + if (!check(obj)) return true; + if (Object.isExtensible(obj)) return false; + return Object.getOwnPropertyNames(obj).every(v => { + var prop = Object.getOwnPropertyDescriptor(obj, v); + if ('writable' in prop && prop.writable) return false; + return !prop.configurable; + }); + } +}); + +setProps(Object.prototype, { + valueOf() { + return this; + }, + toString() { + return '[object ' + (this[Symbol.typeName] ?? 'Unknown') + ']'; + }, + hasOwnProperty(key) { + return Object.hasOwn(this, key); + }, +}); diff --git a/lib/values/string.ts b/lib/values/string.ts new file mode 100644 index 0000000..8ec19e3 --- /dev/null +++ b/lib/values/string.ts @@ -0,0 +1,261 @@ +interface Replacer { + [Symbol.replace](target: string, val: string | ((match: string, ...args: any[]) => string)): string; +} + +interface String { + [i: number]: string; + + toString(): string; + valueOf(): string; + + charAt(pos: number): string; + charCodeAt(pos: number): number; + substring(start?: number, end?: number): string; + slice(start?: number, end?: number): string; + substr(start?: number, length?: number): string; + + startsWith(str: string, pos?: number): string; + endsWith(str: string, pos?: number): string; + + replace(pattern: string | Replacer, val: string): string; + replaceAll(pattern: string | Replacer, val: string): string; + + match(pattern: string | Matcher): RegExpResult | string[] | null; + matchAll(pattern: string | Matcher): IterableIterator; + + split(pattern: string | Splitter, limit?: number, sensible?: boolean): string; + + concat(...others: string[]): string; + indexOf(term: string | Searcher, start?: number): number; + lastIndexOf(term: string | Searcher, start?: number): number; + + toLowerCase(): string; + toUpperCase(): string; + + trim(): string; + + includes(term: string, start?: number): boolean; + + length: number; + + constructor: StringConstructor; +} +interface StringConstructor { + (val: any): string; + new (val: any): String; + + fromCharCode(val: number): string; + + prototype: String; +} + +declare var String: StringConstructor; + +gt.String = function(this: String | undefined, arg: any) { + var val; + if (arguments.length === 0) val = ''; + else val = arg + ''; + if (this === undefined || this === null) return val; + else (this as any).value = val; +} as StringConstructor; + +String.prototype = ('' as any).__proto__ as String; +setConstr(String.prototype, String); + +setProps(String.prototype, { + toString() { + if (typeof this === 'string') return this; + else return (this as any).value; + }, + valueOf() { + if (typeof this === 'string') return this; + else return (this as any).value; + }, + + substring(start, end) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.substring(start, end); + else throw new Error('This function may be used only with primitive or object strings.'); + } + start = start ?? 0 | 0; + end = (end ?? this.length) | 0; + return internals.substring(this, start, end); + }, + substr(start, length) { + start = start ?? 0 | 0; + + if (start >= this.length) start = this.length - 1; + if (start < 0) start = 0; + + length = (length ?? this.length - start) | 0; + return this.substring(start, length + start); + }, + + toLowerCase() { + return internals.toLower(this + ''); + }, + toUpperCase() { + return internals.toUpper(this + ''); + }, + + charAt(pos) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.charAt(pos); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + pos = pos | 0; + if (pos < 0 || pos >= this.length) return ''; + return this[pos]; + }, + charCodeAt(pos) { + var res = this.charAt(pos); + if (res === '') return NaN; + else return internals.toCharCode(res); + }, + + startsWith(term, pos) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.startsWith(term, pos); + else throw new Error('This function may be used only with primitive or object strings.'); + } + pos = pos! | 0; + return internals.startsWith(this, term + '', pos); + }, + endsWith(term, pos) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.endsWith(term, pos); + else throw new Error('This function may be used only with primitive or object strings.'); + } + pos = (pos ?? this.length) | 0; + return internals.endsWith(this, term + '', pos); + }, + + indexOf(term: any, start) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.indexOf(term, start); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof term[Symbol.search] !== 'function') term = RegExp.escape(term); + + return term[Symbol.search](this, false, start); + }, + lastIndexOf(term: any, start) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.indexOf(term, start); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof term[Symbol.search] !== 'function') term = RegExp.escape(term); + + return term[Symbol.search](this, true, start); + }, + includes(term, start) { + return this.indexOf(term, start) >= 0; + }, + + replace(pattern: any, val) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.replace(pattern, val); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof pattern[Symbol.replace] !== 'function') pattern = RegExp.escape(pattern); + + return pattern[Symbol.replace](this, val); + }, + replaceAll(pattern: any, val) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.replace(pattern, val); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof pattern[Symbol.replace] !== 'function') pattern = RegExp.escape(pattern, "g"); + if (pattern instanceof RegExp && !pattern.global) pattern = new pattern.constructor(pattern.source, pattern.flags + "g"); + + return pattern[Symbol.replace](this, val); + }, + + match(pattern: any) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.match(pattern); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof pattern[Symbol.match] !== 'function') pattern = RegExp.escape(pattern); + + return pattern[Symbol.match](this); + }, + matchAll(pattern: any) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.matchAll(pattern); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof pattern[Symbol.match] !== 'function') pattern = RegExp.escape(pattern, "g"); + if (pattern instanceof RegExp && !pattern.global) pattern = new pattern.constructor(pattern.source, pattern.flags + "g"); + + return pattern[Symbol.match](this); + }, + + split(pattern: any, lim, sensible) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.split(pattern, lim, sensible); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + if (typeof pattern[Symbol.split] !== 'function') pattern = RegExp.escape(pattern, "g"); + + return pattern[Symbol.split](this, lim, sensible); + }, + slice(start, end) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.slice(start, end); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + start = wrapI(this.length, start ?? 0 | 0); + end = wrapI(this.length, end ?? this.length | 0); + + if (start > end) return ''; + + return this.substring(start, end); + }, + + concat(...args) { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.concat(...args); + else throw new Error('This function may be used only with primitive or object strings.'); + } + + var res = this; + for (var arg of args) res += arg; + return res; + }, + + trim() { + return this + .replace(/^\s+/g, '') + .replace(/\s+$/g, ''); + } +}); + +setProps(String, { + fromCharCode(val) { + return internals.fromCharCode(val | 0); + }, +}) + +Object.defineProperty(String.prototype, 'length', { + get() { + if (typeof this !== 'string') { + if (this instanceof String) return (this as any).value.length; + else throw new Error('This function may be used only with primitive or object strings.'); + } + + return internals.strlen(this); + }, + configurable: true, + enumerable: false, +}); diff --git a/lib/values/symbol.ts b/lib/values/symbol.ts new file mode 100644 index 0000000..ef0a02c --- /dev/null +++ b/lib/values/symbol.ts @@ -0,0 +1,38 @@ +interface Symbol { + valueOf(): symbol; + constructor: SymbolConstructor; +} +interface SymbolConstructor { + (val?: any): symbol; + prototype: Symbol; + for(key: string): symbol; + keyFor(sym: symbol): string; + readonly typeName: unique symbol; +} + +declare var Symbol: SymbolConstructor; + +gt.Symbol = function(this: any, val?: string) { + if (this !== undefined && this !== null) throw new TypeError("Symbol may not be called with 'new'."); + if (typeof val !== 'string' && val !== undefined) throw new TypeError('val must be a string or undefined.'); + return internals.symbol(val, true); +} as SymbolConstructor; + +Symbol.prototype = internals.symbolProto; +setConstr(Symbol.prototype, Symbol); +(Symbol as any).typeName = Symbol("Symbol.name"); + +setProps(Symbol, { + for(key) { + if (typeof key !== 'string' && key !== undefined) throw new TypeError('key must be a string or undefined.'); + return internals.symbol(key, false); + }, + keyFor(sym) { + if (typeof sym !== 'symbol') throw new TypeError('sym must be a symbol.'); + return internals.symStr(sym); + }, + typeName: Symbol('Symbol.name') as any, +}); + +Object.defineProperty(Object.prototype, Symbol.typeName, { value: 'Object' }); +Object.defineProperty(gt, Symbol.typeName, { value: 'Window' }); diff --git a/src/me/topchetoeu/jscript/Location.java b/src/me/topchetoeu/jscript/Location.java new file mode 100644 index 0000000..bec95d3 --- /dev/null +++ b/src/me/topchetoeu/jscript/Location.java @@ -0,0 +1,63 @@ +package me.topchetoeu.jscript; + +public class Location { + public static final Location INTERNAL = new Location(0, 0, ""); + private int line; + private int start; + private String filename; + + public int line() { return line; } + public int start() { return start; } + public String filename() { return filename; } + + @Override + public String toString() { + return filename + ":" + line + ":" + start; + } + + public Location add(int n, boolean clone) { + if (clone) return new Location(line, start + n, filename); + this.start += n; + return this; + } + public Location add(int n) { + return add(n, false); + } + public Location nextLine() { + line++; + start = 0; + return this; + } + public Location clone() { + return new Location(line, start, filename); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + line; + result = prime * result + start; + result = prime * result + ((filename == null) ? 0 : filename.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; + Location other = (Location) obj; + if (line != other.line) return false; + if (start != other.start) return false; + if (filename == null && other.filename != null) return false; + else if (!filename.equals(other.filename)) return false; + return true; + } + + public Location(int line, int start, String filename) { + this.line = line; + this.start = start; + this.filename = filename; + } +} diff --git a/src/me/topchetoeu/jscript/Main.java b/src/me/topchetoeu/jscript/Main.java new file mode 100644 index 0000000..e702f13 --- /dev/null +++ b/src/me/topchetoeu/jscript/Main.java @@ -0,0 +1,104 @@ +package me.topchetoeu.jscript; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; + +import me.topchetoeu.jscript.engine.Engine; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.events.Observer; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.exceptions.SyntaxException; +import me.topchetoeu.jscript.polyfills.PolyfillEngine; + +public class Main { + static Thread task; + static Engine engine; + + private static Observer valuePrinter = new Observer() { + public void next(Object data) { + try { + Values.printValue(engine.context(), data); + } + catch (InterruptedException e) { } + System.out.println(); + } + + public void error(RuntimeException err) { + try { + if (err instanceof EngineException) { + System.out.println("Uncaught " + ((EngineException)err).toString(engine.context())); + } + else if (err instanceof SyntaxException) { + System.out.println("Syntax error:" + ((SyntaxException)err).msg); + } + else if (err.getCause() instanceof InterruptedException) return; + else { + System.out.println("Internal error ocurred:"); + err.printStackTrace(); + } + } + catch (EngineException ex) { + System.out.println("Uncaught [error while converting to string]"); + } + catch (InterruptedException ex) { + return; + } + } + }; + + public static void main(String args[]) { + var in = new BufferedReader(new InputStreamReader(System.in)); + engine = new PolyfillEngine(new File(".")); + var scope = engine.global().globalChild(); + var exited = new boolean[1]; + + scope.define("exit", ctx -> { + exited[0] = true; + task.interrupt(); + throw new InterruptedException(); + }); + + task = engine.start(); + var reader = new Thread(() -> { + try { + while (true) { + try { + var raw = in.readLine(); + + if (raw == null) break; + engine.pushMsg(false, scope, Map.of(), "", raw, null).toObservable().once(valuePrinter); + } + catch (EngineException e) { + try { + System.out.println("Uncaught " + e.toString(engine.context())); + } + catch (EngineException ex) { + System.out.println("Uncaught [error while converting to string]"); + } + } + } + } + catch (IOException e) { + e.printStackTrace(); + return; + } + catch (SyntaxException ex) { + if (exited[0]) return; + System.out.println("Syntax error:" + ex.msg); + } + catch (RuntimeException ex) { + if (exited[0]) return; + System.out.println("Internal error ocurred:"); + ex.printStackTrace(); + } + catch (InterruptedException e) { return; } + if (exited[0]) return; + }); + reader.setDaemon(true); + reader.setName("STD Reader"); + reader.start(); + } +} diff --git a/src/me/topchetoeu/jscript/MessageReceiver.java b/src/me/topchetoeu/jscript/MessageReceiver.java new file mode 100644 index 0000000..dcf6057 --- /dev/null +++ b/src/me/topchetoeu/jscript/MessageReceiver.java @@ -0,0 +1,6 @@ +package me.topchetoeu.jscript; + +public interface MessageReceiver { + void sendMessage(String msg); + void sendError(String msg); +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/compilation/AssignableStatement.java b/src/me/topchetoeu/jscript/compilation/AssignableStatement.java new file mode 100644 index 0000000..eb88387 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/AssignableStatement.java @@ -0,0 +1,12 @@ +package me.topchetoeu.jscript.compilation; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction.Type; + +public abstract class AssignableStatement extends Statement { + public abstract Statement toAssign(Statement val, Type operation); + + protected AssignableStatement(Location loc) { + super(loc); + } +} diff --git a/src/me/topchetoeu/jscript/compilation/CompileOptions.java b/src/me/topchetoeu/jscript/compilation/CompileOptions.java new file mode 100644 index 0000000..ab59b5d --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/CompileOptions.java @@ -0,0 +1,11 @@ +package me.topchetoeu.jscript.compilation; + +public class CompileOptions { + public final boolean emitBpMap; + public final boolean emitVarNames; + + public CompileOptions(boolean emitBpMap, boolean emitVarNames) { + this.emitBpMap = emitBpMap; + this.emitVarNames = emitVarNames; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/CompoundStatement.java b/src/me/topchetoeu/jscript/compilation/CompoundStatement.java new file mode 100644 index 0000000..d85bbe6 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/CompoundStatement.java @@ -0,0 +1,77 @@ +package me.topchetoeu.jscript.compilation; + +import java.util.ArrayList; +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.control.ContinueStatement; +import me.topchetoeu.jscript.compilation.control.ReturnStatement; +import me.topchetoeu.jscript.compilation.control.ThrowStatement; +import me.topchetoeu.jscript.compilation.values.FunctionStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class CompoundStatement extends Statement { + public final Statement[] statements; + + @Override + public boolean pollutesStack() { + for (var stm : statements) { + if (stm instanceof FunctionStatement) continue; + return true; + } + + return false; + } + + @Override + public void declare(ScopeRecord varsScope) { + for (var stm : statements) { + stm.declare(varsScope); + } + } + + @Override + public void compile(List target, ScopeRecord scope) { + for (var stm : statements) { + if (stm instanceof FunctionStatement) { + int start = target.size(); + ((FunctionStatement)stm).compile(target, scope, null, true); + target.get(start).setDebug(true); + target.add(Instruction.discard()); + } + } + + for (var i = 0; i < statements.length; i++) { + var stm = statements[i]; + + if (stm instanceof FunctionStatement) continue; + if (i != statements.length - 1) stm.compileNoPollution(target, scope, true); + else stm.compileWithPollution(target, scope); + } + } + + @Override + public Statement optimize() { + var res = new ArrayList(); + + for (var i = 0; i < statements.length; i++) { + var stm = statements[i].optimize(); + if (i < statements.length - 1 && stm.pure()) continue; + res.add(stm); + if ( + stm instanceof ContinueStatement || + stm instanceof ReturnStatement || + stm instanceof ThrowStatement || + stm instanceof ContinueStatement + ) break; + } + + if (res.size() == 1) return res.get(0); + else return new CompoundStatement(loc(), res.toArray(Statement[]::new)); + } + + public CompoundStatement(Location loc, Statement... statements) { + super(loc); + this.statements = statements; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/DiscardStatement.java b/src/me/topchetoeu/jscript/compilation/DiscardStatement.java new file mode 100644 index 0000000..3b216eb --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/DiscardStatement.java @@ -0,0 +1,33 @@ +package me.topchetoeu.jscript.compilation; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.values.ConstantStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class DiscardStatement extends Statement { + public final Statement value; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void compile(List target, ScopeRecord scope) { + if (value == null) return; + value.compile(target, scope); + if (value.pollutesStack()) target.add(Instruction.discard()); + } + @Override + public Statement optimize() { + if (value == null) return this; + var val = value.optimize(); + if (val.pure()) return new ConstantStatement(loc(), null); + else return new DiscardStatement(loc(), val); + } + + public DiscardStatement(Location loc, Statement val) { + super(loc); + this.value = val; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/Instruction.java b/src/me/topchetoeu/jscript/compilation/Instruction.java new file mode 100644 index 0000000..07a82c1 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/Instruction.java @@ -0,0 +1,285 @@ +package me.topchetoeu.jscript.compilation; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.exceptions.SyntaxException; + +public class Instruction { + public static enum Type { + RETURN, + SIGNAL, + THROW, + THROW_SYNTAX, + DELETE, + TRY, + NOP, + + CALL, + CALL_NEW, + JMP_IF, + JMP_IFN, + JMP, + + LOAD_VALUE, + + LOAD_VAR, + LOAD_MEMBER, + LOAD_VAL_MEMBER, + LOAD_GLOB, + + LOAD_FUNC, + LOAD_ARR, + LOAD_OBJ, + STORE_SELF_FUNC, + LOAD_REGEX, + + DUP, + + STORE_VAR, + STORE_MEMBER, + DISCARD, + + MAKE_VAR, + DEF_PROP, + KEYS, + + TYPEOF, + INSTANCEOF(true), + IN(true), + + MULTIPLY(true), + DIVIDE(true), + MODULO(true), + ADD(true), + SUBTRACT(true), + + USHIFT_RIGHT(true), + SHIFT_RIGHT(true), + SHIFT_LEFT(true), + + GREATER(true), + LESS(true), + GREATER_EQUALS(true), + LESS_EQUALS(true), + LOOSE_EQUALS(true), + LOOSE_NOT_EQUALS(true), + EQUALS(true), + NOT_EQUALS(true), + + AND(true), + OR(true), + XOR(true), + + NEG(true), + POS(true), + NOT(true), + INVERSE(true); + + final boolean isOperation; + + private Type(boolean isOperation) { + this.isOperation = isOperation; + } + private Type() { + this(false); + } + } + + 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 get(int i) { + if (i >= params.length || i < 0) return null; + return (T)params[i]; + } + public boolean match(Object ...args) { + if (args.length != params.length) return false; + for (int i = 0; i < args.length; i++) { + var a = params[i]; + var b = args[i]; + if (a == null || b == null) { + if (!(a == null && b == null)) return false; + } + if (!a.equals(b)) return false; + } + return true; + } + public boolean is(int i, Object arg) { + if (params.length <= i) return false; + return params[i].equals(arg); + } + + private Instruction(Location location, Type type, Object... params) { + this.location = location; + this.type = type; + this.params = params; + } + + public static Instruction tryInstr(boolean hasCatch, boolean hasFinally) { + return new Instruction(null, Type.TRY, hasCatch, hasFinally); + } + public static Instruction throwInstr() { + return new Instruction(null, Type.THROW); + } + public static Instruction throwSyntax(SyntaxException err) { + return new Instruction(null, Type.THROW_SYNTAX, err.getMessage()); + } + public static Instruction delete() { + return new Instruction(null, Type.DELETE); + } + public static Instruction ret() { + return new Instruction(null, Type.RETURN); + } + public static Instruction debug() { + return new Instruction(null, Type.NOP, "debug"); + } + public static Instruction debugVarNames(String[] names) { + var args = new Object[names.length + 1]; + args[0] = "dbg_vars"; + + System.arraycopy(names, 0, args, 1, names.length); + + return new Instruction(null, Type.NOP, args); + } + + /** + * ATTENTION: Usage outside of try/catch is broken af + */ + public static Instruction signal(String name) { + return new Instruction(null, Type.SIGNAL, name); + } + public static Instruction nop(Object ...params) { + for (var param : params) { + if (param instanceof String) continue; + if (param instanceof Boolean) continue; + if (param instanceof Double) continue; + if (param instanceof Integer) continue; + if (param == null) continue; + + throw new RuntimeException("NOP params may contain only strings, booleans, doubles, integers and nulls."); + } + return new Instruction(null, Type.NOP, params); + } + + public static Instruction call(int argn) { + return new Instruction(null, Type.CALL, argn); + } + public static Instruction callNew(int argn) { + return new Instruction(null, Type.CALL_NEW, argn); + } + public static Instruction jmp(int offset) { + return new Instruction(null, Type.JMP, offset); + } + public static Instruction jmpIf(int offset) { + return new Instruction(null, Type.JMP_IF, offset); + } + public static Instruction jmpIfNot(int offset) { + return new Instruction(null, Type.JMP_IFN, offset); + } + + public static Instruction loadValue(Object val) { + return new Instruction(null, Type.LOAD_VALUE, val); + } + + public static Instruction makeVar(String name) { + return new Instruction(null, Type.MAKE_VAR, name); + } + public static Instruction loadVar(Object i) { + return new Instruction(null, Type.LOAD_VAR, i); + } + public static Instruction loadGlob() { + return new Instruction(null, Type.LOAD_GLOB); + } + public static Instruction loadMember() { + return new Instruction(null, Type.LOAD_MEMBER); + } + public static Instruction loadMember(Object key) { + if (key instanceof Number) key = ((Number)key).doubleValue(); + return new Instruction(null, Type.LOAD_VAL_MEMBER, key); + } + + public static Instruction loadRegex(String pattern, String flags) { + return new Instruction(null, Type.LOAD_REGEX, pattern, flags); + } + public static Instruction loadFunc(int instrN, int varN, int len, int[] captures) { + var args = new Object[3 + captures.length]; + args[0] = instrN; + args[1] = varN; + args[2] = len; + for (var i = 0; i < captures.length; i++) args[i + 3] = captures[i]; + return new Instruction(null, Type.LOAD_FUNC, args); + } + public static Instruction loadObj() { + return new Instruction(null, Type.LOAD_OBJ); + } + public static Instruction loadArr(int count) { + return new Instruction(null, Type.LOAD_ARR, count); + } + public static Instruction dup(int count) { + return new Instruction(null, Type.DUP, count, 0); + } + public static Instruction dup(int count, int offset) { + return new Instruction(null, Type.DUP, count, offset); + } + + public static Instruction storeSelfFunc(int i) { + return new Instruction(null, Type.STORE_SELF_FUNC, i); + } + public static Instruction storeVar(Object i) { + return new Instruction(null, Type.STORE_VAR, i, false); + } + public static Instruction storeVar(Object i, boolean keep) { + return new Instruction(null, Type.STORE_VAR, i, keep); + } + public static Instruction storeMember() { + return new Instruction(null, Type.STORE_MEMBER, false); + } + public static Instruction storeMember(boolean keep) { + return new Instruction(null, Type.STORE_MEMBER, keep); + } + public static Instruction discard() { + return new Instruction(null, Type.DISCARD); + } + + public static Instruction typeof() { + return new Instruction(null, Type.TYPEOF); + } + public static Instruction typeof(String varName) { + return new Instruction(null, Type.TYPEOF, varName); + } + + public static Instruction keys() { + return new Instruction(null, Type.KEYS); + } + + public static Instruction defProp() { + return new Instruction(null, Type.DEF_PROP); + } + + public static Instruction operation(Type op) { + if (!op.isOperation) throw new IllegalArgumentException("The instruction type %s is not an operation.".formatted(op)); + return new Instruction(null, op); + } + + @Override + public String toString() { + var res = type.toString(); + + for (int i = 0; i < params.length; i++) { + res += " " + params[i]; + } + + return res; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/Statement.java b/src/me/topchetoeu/jscript/compilation/Statement.java new file mode 100644 index 0000000..95c5836 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/Statement.java @@ -0,0 +1,42 @@ +package me.topchetoeu.jscript.compilation; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public abstract class Statement { + private Location _loc; + + public abstract boolean pollutesStack(); + public boolean pure() { return false; } + public abstract void compile(List target, ScopeRecord scope); + public void declare(ScopeRecord varsScope) { } + public Statement optimize() { return this; } + + public void compileNoPollution(List target, ScopeRecord scope, boolean debug) { + int start = target.size(); + compile(target, scope); + if (debug && target.size() != start) target.get(start).setDebug(true); + if (pollutesStack()) target.add(Instruction.discard().locate(loc())); + } + public void compileWithPollution(List target, ScopeRecord scope, boolean debug) { + int start = target.size(); + compile(target, scope); + if (debug && target.size() != start) target.get(start).setDebug(true); + if (!pollutesStack()) target.add(Instruction.loadValue(null).locate(loc())); + } + public void compileNoPollution(List target, ScopeRecord scope) { + compileNoPollution(target, scope, false); + } + public void compileWithPollution(List target, ScopeRecord scope) { + compileWithPollution(target, scope, false); + } + + public Location loc() { return _loc; } + public void setLoc(Location loc) { _loc = loc; } + + protected Statement(Location loc) { + this._loc = loc; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/compilation/VariableDeclareStatement.java b/src/me/topchetoeu/jscript/compilation/VariableDeclareStatement.java new file mode 100644 index 0000000..c3b43a8 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/VariableDeclareStatement.java @@ -0,0 +1,44 @@ +package me.topchetoeu.jscript.compilation; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.values.FunctionStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class VariableDeclareStatement extends Statement { + public static record Pair(String name, Statement value) {} + + public final List values; + + @Override + public boolean pollutesStack() { return false; } + @Override + public void declare(ScopeRecord varsScope) { + for (var key : values) { + varsScope.define(key.name()); + } + } + @Override + public void compile(List target, ScopeRecord scope) { + for (var entry : values) { + if (entry.name() == null) continue; + var key = scope.getKey(entry.name()); + if (key instanceof String) target.add(Instruction.makeVar((String)key).locate(loc())); + + if (entry.value() instanceof FunctionStatement) { + ((FunctionStatement)entry.value()).compile(target, scope, entry.name(), false); + target.add(Instruction.storeVar(key).locate(loc())); + } + else if (entry.value() != null) { + entry.value().compileWithPollution(target, scope); + target.add(Instruction.storeVar(key).locate(loc())); + } + } + } + + public VariableDeclareStatement(Location loc, List values) { + super(loc); + this.values = values; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/BreakStatement.java b/src/me/topchetoeu/jscript/compilation/control/BreakStatement.java new file mode 100644 index 0000000..17a530e --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/BreakStatement.java @@ -0,0 +1,25 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class BreakStatement extends Statement { + public final String label; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.nop("break", label).locate(loc())); + } + + public BreakStatement(Location loc, String label) { + super(loc); + this.label = label; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/ContinueStatement.java b/src/me/topchetoeu/jscript/compilation/control/ContinueStatement.java new file mode 100644 index 0000000..3d3f827 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/ContinueStatement.java @@ -0,0 +1,25 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ContinueStatement extends Statement { + public final String label; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.nop("cont", label).locate(loc())); + } + + public ContinueStatement(Location loc, String label) { + super(loc); + this.label = label; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/DebugStatement.java b/src/me/topchetoeu/jscript/compilation/control/DebugStatement.java new file mode 100644 index 0000000..a65f7ae --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/DebugStatement.java @@ -0,0 +1,22 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class DebugStatement extends Statement { + @Override + public boolean pollutesStack() { return false; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.debug().locate(loc())); + } + + public DebugStatement(Location loc) { + super(loc); + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/DeleteStatement.java b/src/me/topchetoeu/jscript/compilation/control/DeleteStatement.java new file mode 100644 index 0000000..01bc3c4 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/DeleteStatement.java @@ -0,0 +1,29 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class DeleteStatement extends Statement { + public final Statement key; + public final Statement value; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + value.compile(target, scope); + key.compile(target, scope); + target.add(Instruction.delete().locate(loc())); + } + + public DeleteStatement(Location loc, Statement key, Statement value) { + super(loc); + this.key = key; + this.value = value; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/DoWhileStatement.java b/src/me/topchetoeu/jscript/compilation/control/DoWhileStatement.java new file mode 100644 index 0000000..e648bf0 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/DoWhileStatement.java @@ -0,0 +1,82 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.CompoundStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.values.ConstantStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class DoWhileStatement extends Statement { + public final Statement condition, body; + public final String label; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void declare(ScopeRecord globScope) { + body.declare(globScope); + } + + @Override + public void compile(List target, ScopeRecord scope) { + if (condition instanceof ConstantStatement) { + int start = target.size(); + body.compileNoPollution(target, scope); + int end = target.size(); + if (Values.toBoolean(((ConstantStatement)condition).value)) { + WhileStatement.replaceBreaks(target, label, start, end, end + 1, end + 1); + } + else { + target.add(Instruction.jmp(start - end).locate(loc())); + WhileStatement.replaceBreaks(target, label, start, end, start, end + 1); + } + return; + } + + int start = target.size(); + body.compileNoPollution(target, scope, true); + int mid = target.size(); + condition.compileWithPollution(target, scope); + int end = target.size(); + + WhileStatement.replaceBreaks(target, label, start, mid - 1, mid, end + 1); + target.add(Instruction.jmpIf(start - end).locate(loc())); + } + + @Override + public Statement optimize() { + var cond = condition.optimize(); + var b = body.optimize(); + + if (b instanceof CompoundStatement) { + var comp = (CompoundStatement)b; + if (comp.statements.length > 0) { + var last = comp.statements[comp.statements.length - 1]; + if (last instanceof ContinueStatement) comp.statements[comp.statements.length - 1] = new CompoundStatement(loc()); + if (last instanceof BreakStatement) { + comp.statements[comp.statements.length - 1] = new CompoundStatement(loc()); + return new CompoundStatement(loc()); + } + } + } + else if (b instanceof ContinueStatement) { + b = new CompoundStatement(loc()); + } + else if (b instanceof BreakStatement) return new CompoundStatement(loc()); + + if (b.pure()) return new DoWhileStatement(loc(), label, cond, new CompoundStatement(loc())); + else return new DoWhileStatement(loc(), label, cond, b); + } + + public DoWhileStatement(Location loc, String label, Statement condition, Statement body) { + super(loc); + this.label = label; + this.condition = condition; + this.body = body; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/ForInStatement.java b/src/me/topchetoeu/jscript/compilation/control/ForInStatement.java new file mode 100644 index 0000000..3851f9d --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/ForInStatement.java @@ -0,0 +1,81 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ForInStatement extends Statement { + public final String varName; + public final boolean isDeclaration; + public final Statement varValue, object, body; + public final String label; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void declare(ScopeRecord globScope) { + body.declare(globScope); + if (isDeclaration) globScope.define(varName); + } + + @Override + public void compile(List target, ScopeRecord scope) { + var key = scope.getKey(varName); + if (key instanceof String) target.add(Instruction.makeVar((String)key)); + + if (varValue != null) { + varValue.compileWithPollution(target, scope); + target.add(Instruction.storeVar(scope.getKey(varName))); + } + + object.compileWithPollution(target, scope); + target.add(Instruction.keys()); + + int start = target.size(); + target.add(Instruction.dup(1)); + target.add(Instruction.loadMember("length")); + target.add(Instruction.loadValue(0)); + target.add(Instruction.operation(Type.LESS_EQUALS)); + int mid = target.size(); + target.add(Instruction.nop()); + + target.add(Instruction.dup(2)); + target.add(Instruction.loadMember("length")); + target.add(Instruction.loadValue(1)); + target.add(Instruction.operation(Type.SUBTRACT)); + target.add(Instruction.dup(1, 2)); + target.add(Instruction.loadValue("length")); + target.add(Instruction.dup(1, 2)); + target.add(Instruction.storeMember()); + target.add(Instruction.loadMember()); + target.add(Instruction.storeVar(key)); + + for (var i = start; i < target.size(); i++) target.get(i).locate(loc()); + + body.compileNoPollution(target, scope, true); + + + int end = target.size(); + + WhileStatement.replaceBreaks(target, label, mid + 1, end, start, end + 1); + + target.add(Instruction.jmp(start - end).locate(loc())); + target.add(Instruction.discard().locate(loc())); + target.set(mid, Instruction.jmpIf(end - mid + 1).locate(loc())); + } + + public ForInStatement(Location loc, String label, boolean isDecl, String varName, Statement varValue, Statement object, Statement body) { + super(loc); + this.label = label; + this.isDeclaration = isDecl; + this.varName = varName; + this.varValue = varValue; + this.object = object; + this.body = body; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/ForStatement.java b/src/me/topchetoeu/jscript/compilation/control/ForStatement.java new file mode 100644 index 0000000..6fce19f --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/ForStatement.java @@ -0,0 +1,97 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.CompoundStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.values.ConstantStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class ForStatement extends Statement { + public final Statement declaration, assignment, condition, body; + public final String label; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void declare(ScopeRecord globScope) { + declaration.declare(globScope); + body.declare(globScope); + } + @Override + public void compile(List target, ScopeRecord scope) { + declaration.compile(target, scope); + + if (condition instanceof ConstantStatement) { + if (Values.toBoolean(((ConstantStatement)condition).value)) { + int start = target.size(); + body.compileNoPollution(target, scope); + int mid = target.size(); + assignment.compileNoPollution(target, scope, true); + int end = target.size(); + WhileStatement.replaceBreaks(target, label, start, mid, mid, end + 1); + target.add(Instruction.jmp(start - target.size()).locate(loc())); + return; + } + } + + int start = target.size(); + condition.compileWithPollution(target, scope); + int mid = target.size(); + target.add(Instruction.nop()); + body.compileNoPollution(target, scope); + int beforeAssign = target.size(); + assignment.compile(target, scope); + int end = target.size(); + + WhileStatement.replaceBreaks(target, label, mid + 1, end, beforeAssign, end + 1); + + target.add(Instruction.jmp(start - end).locate(loc())); + target.set(mid, Instruction.jmpIfNot(end - mid + 1).locate(loc())); + } + @Override + public Statement optimize() { + var decl = declaration.optimize(); + var asgn = assignment.optimize(); + var cond = condition.optimize(); + var b = body.optimize(); + + if (asgn.pure()) { + if (decl.pure()) return new WhileStatement(loc(), label, cond, b).optimize(); + else return new CompoundStatement(loc(), + decl, new WhileStatement(loc(), label, cond, b) + ).optimize(); + } + + else if (b instanceof ContinueStatement) return new CompoundStatement(loc(), + decl, new WhileStatement(loc(), label, cond, new CompoundStatement(loc(), b, asgn)) + ); + else if (b instanceof BreakStatement) return decl; + + if (b.pure()) return new ForStatement(loc(), label, decl, cond, asgn, new CompoundStatement(null)); + else return new ForStatement(loc(), label, decl, cond, asgn, b); + } + + public ForStatement(Location loc, String label, Statement declaration, Statement condition, Statement assignment, Statement body) { + super(loc); + this.label = label; + this.declaration = declaration; + this.condition = condition; + this.assignment = assignment; + this.body = body; + } + + public static CompoundStatement ofFor(Location loc, String label, Statement declaration, Statement condition, Statement increment, Statement body) { + return new CompoundStatement(loc, + declaration, + new WhileStatement(loc, label, condition, new CompoundStatement(loc, + body, + increment + )) + ); + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/IfStatement.java b/src/me/topchetoeu/jscript/compilation/control/IfStatement.java new file mode 100644 index 0000000..b3e1579 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/IfStatement.java @@ -0,0 +1,80 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.CompoundStatement; +import me.topchetoeu.jscript.compilation.DiscardStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.values.ConstantStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class IfStatement extends Statement { + public final Statement condition, body, elseBody; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void declare(ScopeRecord globScope) { + body.declare(globScope); + if (elseBody != null) elseBody.declare(globScope); + } + + @Override + public void compile(List target, ScopeRecord scope) { + if (condition instanceof ConstantStatement) { + if (Values.not(((ConstantStatement)condition).value)) { + if (elseBody != null) elseBody.compileNoPollution(target, scope, true); + } + else { + body.compileNoPollution(target, scope, true); + } + + return; + } + + condition.compileWithPollution(target, scope); + if (elseBody == null) { + int i = target.size(); + target.add(Instruction.nop()); + body.compileNoPollution(target, scope, true); + int endI = target.size(); + target.set(i, Instruction.jmpIfNot(endI - i).locate(loc())); + } + else { + int start = target.size(); + target.add(Instruction.nop()); + body.compileNoPollution(target, scope, true); + target.add(Instruction.nop()); + int mid = target.size(); + elseBody.compileNoPollution(target, scope, true); + int end = target.size(); + + target.set(start, Instruction.jmpIfNot(mid - start).locate(loc())); + target.set(mid - 1, Instruction.jmp(end - mid + 1).locate(loc())); + } + } + + @Override + public Statement optimize() { + var cond = condition.optimize(); + var b = body.optimize(); + var e = elseBody == null ? null : elseBody.optimize(); + + if (b.pure()) b = new CompoundStatement(null); + if (e != null && e.pure()) e = null; + + if (b.pure() && e == null) return new DiscardStatement(loc(), cond).optimize(); + else return new IfStatement(loc(), cond, b, e); + } + + public IfStatement(Location loc, Statement condition, Statement body, Statement elseBody) { + super(loc); + this.condition = condition; + this.body = body; + this.elseBody = elseBody; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/ReturnStatement.java b/src/me/topchetoeu/jscript/compilation/control/ReturnStatement.java new file mode 100644 index 0000000..3139e24 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/ReturnStatement.java @@ -0,0 +1,27 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ReturnStatement extends Statement { + public final Statement value; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void compile(List target, ScopeRecord scope) { + if (value == null) target.add(Instruction.loadValue(null).locate(loc())); + else value.compileWithPollution(target, scope); + target.add(Instruction.ret().locate(loc())); + } + + public ReturnStatement(Location loc, Statement value) { + super(loc); + this.value = value; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/SwitchStatement.java b/src/me/topchetoeu/jscript/compilation/control/SwitchStatement.java new file mode 100644 index 0000000..25d193c --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/SwitchStatement.java @@ -0,0 +1,81 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.HashMap; +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class SwitchStatement extends Statement { + public static record SwitchCase(Statement value, int statementI) {} + + @Override + public boolean pollutesStack() { return false; } + + public final Statement value; + public final SwitchCase[] cases; + public final Statement[] body; + public final int defaultI; + + @Override + public void declare(ScopeRecord varsScope) { + for (var stm : body) stm.declare(varsScope); + } + + @Override + public void compile(List target, ScopeRecord scope) { + var caseMap = new HashMap(); + var stmIndexMap = new HashMap(); + + value.compile(target, scope); + + for (var ccase : cases) { + target.add(Instruction.dup(1).locate(loc())); + ccase.value.compileWithPollution(target, scope); + target.add(Instruction.operation(Type.EQUALS).locate(loc())); + caseMap.put(target.size(), ccase.statementI); + target.add(Instruction.nop()); + } + + int start = target.size(); + + target.add(Instruction.nop()); + + for (var stm : body) { + stmIndexMap.put(stmIndexMap.size(), target.size()); + stm.compileNoPollution(target, scope, true); + } + + if (defaultI < 0 || defaultI >= body.length) target.set(start, Instruction.jmp(target.size() - start).locate(loc())); + else target.set(start, Instruction.jmp(stmIndexMap.get(defaultI) - start)).locate(loc()); + + for (int i = start; i < target.size(); i++) { + var instr = target.get(i); + if (instr.type == Type.NOP && instr.is(0, "break") && instr.get(1) == null) { + target.set(i, Instruction.jmp(target.size() - i).locate(instr.location)); + } + if (instr.type == Type.NOP && instr.is(0, "try_break") && instr.get(1) == null) { + target.set(i, Instruction.signal("jmp_" + (target.size() - (Integer)instr.get(2))).locate(instr.location)); + } + } + for (var el : caseMap.entrySet()) { + 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.add(Instruction.discard().locate(loc())); + } + + public SwitchStatement(Location loc, Statement value, int defaultI, SwitchCase[] cases, Statement[] body) { + super(loc); + this.value = value; + this.defaultI = defaultI; + this.cases = cases; + this.body = body; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/ThrowStatement.java b/src/me/topchetoeu/jscript/compilation/control/ThrowStatement.java new file mode 100644 index 0000000..136446e --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/ThrowStatement.java @@ -0,0 +1,26 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ThrowStatement extends Statement { + public final Statement value; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void compile(List target, ScopeRecord scope) { + value.compileWithPollution(target, scope); + target.add(Instruction.throwInstr().locate(loc())); + } + + public ThrowStatement(Location loc, Statement value) { + super(loc); + this.value = value; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/TryStatement.java b/src/me/topchetoeu/jscript/compilation/control/TryStatement.java new file mode 100644 index 0000000..2af777d --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/TryStatement.java @@ -0,0 +1,87 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class TryStatement extends Statement { + public final Statement tryBody; + public final Statement catchBody; + public final Statement finallyBody; + public final String name; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void declare(ScopeRecord globScope) { + tryBody.declare(globScope); + if (catchBody != null) catchBody.declare(globScope); + if (finallyBody != null) finallyBody.declare(globScope); + } + + private void compileBody(List target, ScopeRecord scope, Statement body, String arg) { + var subscope = scope.child(); + int start = target.size(); + + target.add(Instruction.nop()); + + subscope.define("this"); + var argsVar = subscope.define(""); + + if (arg != null) { + target.add(Instruction.loadVar(argsVar)); + target.add(Instruction.loadMember(0)); + target.add(Instruction.storeVar(subscope.define(arg))); + } + + int bodyStart = target.size(); + body.compile(target, subscope); + target.add(Instruction.signal("no_return")); + + target.get(bodyStart).locate(body.loc()); + + + target.set(start, Instruction.loadFunc(target.size() - start, subscope.localsCount(), 0, subscope.getCaptures())); + } + + @Override + public void compile(List target, ScopeRecord scope) { + int start = target.size(); + + compileBody(target, scope, tryBody, null); + + if (catchBody != null) { + compileBody(target, scope, catchBody, name); + } + if (finallyBody != null) { + compileBody(target, scope, finallyBody, null); + } + + for (int i = start; i < target.size(); i++) { + if (target.get(i).type == Type.NOP) { + var instr = target.get(i); + if (instr.is(0, "break")) { + target.set(i, Instruction.nop("try_break", instr.get(1), target.size()).locate(instr.location)); + } + else if (instr.is(0, "cont")) { + target.set(i, Instruction.nop("try_cont", instr.get(1), target.size()).locate(instr.location)); + } + } + } + + target.add(Instruction.tryInstr(catchBody != null, finallyBody != null).locate(loc())); + } + + public TryStatement(Location loc, Statement tryBody, Statement catchBody, Statement finallyBody, String name) { + super(loc); + this.tryBody = tryBody; + this.catchBody = catchBody; + this.finallyBody = finallyBody; + this.name = name; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/control/WhileStatement.java b/src/me/topchetoeu/jscript/compilation/control/WhileStatement.java new file mode 100644 index 0000000..d7f04a2 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/control/WhileStatement.java @@ -0,0 +1,104 @@ +package me.topchetoeu.jscript.compilation.control; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.CompoundStatement; +import me.topchetoeu.jscript.compilation.DiscardStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.compilation.values.ConstantStatement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class WhileStatement extends Statement { + public final Statement condition, body; + public final String label; + + @Override + public boolean pollutesStack() { return false; } + + @Override + public void declare(ScopeRecord globScope) { + body.declare(globScope); + } + @Override + public void compile(List target, ScopeRecord scope) { + if (condition instanceof ConstantStatement) { + if (Values.toBoolean(((ConstantStatement)condition).value)) { + int start = target.size(); + body.compileNoPollution(target, scope); + int end = target.size(); + replaceBreaks(target, label, start, end, start, end + 1); + target.add(Instruction.jmp(start - target.size()).locate(loc())); + return; + } + } + + int start = target.size(); + condition.compileWithPollution(target, scope); + int mid = target.size(); + target.add(Instruction.nop()); + body.compileNoPollution(target, scope); + + int end = target.size(); + + replaceBreaks(target, label, mid + 1, end, start, end + 1); + + target.add(Instruction.jmp(start - end).locate(loc())); + target.set(mid, Instruction.jmpIfNot(end - mid + 1).locate(loc())); + } + @Override + public Statement optimize() { + var cond = condition.optimize(); + var b = body.optimize(); + + if (b instanceof ContinueStatement) { + b = new CompoundStatement(loc()); + } + else if (b instanceof BreakStatement) return new DiscardStatement(loc(), cond).optimize(); + + if (b.pure()) return new WhileStatement(loc(), label, cond, new CompoundStatement(null)); + else return new WhileStatement(loc(), label, cond, b); + } + + public WhileStatement(Location loc, String label, Statement condition, Statement body) { + super(loc); + this.label = label; + this.condition = condition; + this.body = body; + } + + public static void replaceBreaks(List target, String label, int start, int end, int continuePoint, int breakPoint) { + for (int i = start; i < end; i++) { + var instr = target.get(i); + if (instr.type == Type.NOP && instr.is(0, "cont") && (instr.get(1) == null || instr.is(1, label))) { + target.set(i, Instruction.jmp(continuePoint - i)); + target.get(i).location = instr.location; + } + if (instr.type == Type.NOP && instr.is(0, "break") && (instr.get(1) == null || instr.is(1, label))) { + target.set(i, Instruction.jmp(breakPoint - i)); + target.get(i).location = instr.location; + } + if (instr.type == Type.NOP && instr.is(0, "try_cont") && (instr.get(1) == null || instr.is(1, label))) { + target.set(i, Instruction.signal("jmp_" + (continuePoint - (Integer)instr.get(2)))); + target.get(i).location = instr.location; + } + if (instr.type == Type.NOP && instr.is(0, "try_break") && (instr.get(1) == null || instr.is(1, label))) { + target.set(i, Instruction.signal("jmp_" + (breakPoint - (Integer)instr.get(2)))); + target.get(i).location = instr.location; + } + } + } + + // public static CompoundStatement ofFor(Location loc, String label, Statement declaration, Statement condition, Statement increment, Statement body) { + // return new CompoundStatement(loc, + // declaration, + // new WhileStatement(loc, label, condition, new CompoundStatement(loc, + // body, + // increment + // )) + // ); + // } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/ArrayStatement.java b/src/me/topchetoeu/jscript/compilation/values/ArrayStatement.java new file mode 100644 index 0000000..1c6fdad --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/ArrayStatement.java @@ -0,0 +1,37 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ArrayStatement extends Statement { + public final Statement[] statements; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.loadArr(statements.length).locate(loc())); + var i = 0; + for (var el : statements) { + if (el != null) { + target.add(Instruction.dup(1).locate(loc())); + target.add(Instruction.loadValue(i).locate(loc())); + el.compileWithPollution(target, scope); + target.add(Instruction.storeMember().locate(loc())); + } + i++; + } + } + + public ArrayStatement(Location loc, Statement[] statements) { + super(loc); + this.statements = statements; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/CallStatement.java b/src/me/topchetoeu/jscript/compilation/values/CallStatement.java new file mode 100644 index 0000000..5a9f33c --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/CallStatement.java @@ -0,0 +1,44 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class CallStatement extends Statement { + public final Statement func; + public final Statement[] args; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + if (func instanceof IndexStatement) { + ((IndexStatement)func).compile(target, scope, true); + } + else { + target.add(Instruction.loadValue(null).locate(loc())); + func.compileWithPollution(target, scope); + } + + for (var arg : args) { + arg.compileWithPollution(target, scope); + } + + target.add(Instruction.call(args.length).locate(loc()).setDebug(true)); + } + + public CallStatement(Location loc, Statement func, Statement... args) { + super(loc); + this.func = func; + this.args = args; + } + public CallStatement(Location loc, Statement obj, Object key, Statement... args) { + super(loc); + this.func = new IndexStatement(loc, obj, new ConstantStatement(loc, key)); + this.args = args; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/ChangeStatement.java b/src/me/topchetoeu/jscript/compilation/values/ChangeStatement.java new file mode 100644 index 0000000..bdf4083 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/ChangeStatement.java @@ -0,0 +1,35 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.AssignableStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ChangeStatement extends Statement { + public final AssignableStatement value; + public final double addAmount; + public final boolean postfix; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + value.toAssign(new ConstantStatement(loc(), -addAmount), Type.SUBTRACT).compileWithPollution(target, scope); + if (postfix) { + target.add(Instruction.loadValue(addAmount).locate(loc())); + target.add(Instruction.operation(Type.SUBTRACT).locate(loc())); + } + } + + public ChangeStatement(Location loc, AssignableStatement value, double addAmount, boolean postfix) { + super(loc); + this.value = value; + this.addAmount = addAmount; + this.postfix = postfix; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/CommaStatement.java b/src/me/topchetoeu/jscript/compilation/values/CommaStatement.java new file mode 100644 index 0000000..541b5c2 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/CommaStatement.java @@ -0,0 +1,38 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class CommaStatement extends Statement { + public final Statement first; + public final Statement second; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return first.pure() && second.pure(); } + + @Override + public void compile(List target, ScopeRecord scope) { + first.compileNoPollution(target, scope); + second.compileWithPollution(target, scope); + } + + @Override + public Statement optimize() { + var f = first.optimize(); + var s = second.optimize(); + if (f.pure()) return s; + else return new CommaStatement(loc(), f, s); + } + + public CommaStatement(Location loc, Statement first, Statement second) { + super(loc); + this.first = first; + this.second = second; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/ConstantStatement.java b/src/me/topchetoeu/jscript/compilation/values/ConstantStatement.java new file mode 100644 index 0000000..e254eee --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/ConstantStatement.java @@ -0,0 +1,27 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ConstantStatement extends Statement { + public final Object value; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.loadValue(value).locate(loc())); + } + + public ConstantStatement(Location loc, Object val) { + super(loc); + this.value = val; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/FunctionStatement.java b/src/me/topchetoeu/jscript/compilation/values/FunctionStatement.java new file mode 100644 index 0000000..2afe17c --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/FunctionStatement.java @@ -0,0 +1,110 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.CompoundStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.exceptions.SyntaxException; + +public class FunctionStatement extends Statement { + public final CompoundStatement body; + public final String name; + public final String[] args; + + @Override + public boolean pure() { return name == null; } + @Override + public boolean pollutesStack() { return true; } + + @Override + public void declare(ScopeRecord scope) { + if (name != null) scope.define(name); + } + + public static void checkBreakAndCont(List target, int start) { + for (int i = start; i < target.size(); i++) { + if (target.get(i).type == Type.NOP) { + if (target.get(i).is(0, "break") || target.get(i).is(0, "try_break")) { + throw new SyntaxException(target.get(i).location, "Break was placed outside a loop."); + } + if (target.get(i).is(0, "cont") || target.get(i).is(0, "try_cont")) { + throw new SyntaxException(target.get(i).location, "Continue was placed outside a loop."); + } + } + } + } + + public void compile(List target, ScopeRecord scope, String name, boolean isStatement) { + for (var i = 0; i < args.length; i++) { + for (var j = 0; j < i; j++) { + if (args[i].equals(args[j])){ + target.add(Instruction.throwSyntax(new SyntaxException(loc(), "Duplicate parameter '" + args[i] + "'."))); + return; + } + } + } + var subscope = scope.child(); + + int start = target.size(); + + target.add(Instruction.nop()); + subscope.define("this"); + + var argsVar = subscope.define("arguments"); + if (args.length > 0) { + target.add(Instruction.loadVar(argsVar).locate(loc())); + if (args.length != 1) target.add(Instruction.dup(args.length - 1).locate(loc())); + + for (var i = 0; i < args.length; i++) { + target.add(Instruction.loadMember(i).locate(loc())); + target.add(Instruction.storeVar(subscope.define(args[i])).locate(loc())); + } + } + + if (!isStatement && this.name != null) { + target.add(Instruction.storeSelfFunc((int)subscope.define(this.name))); + } + + body.declare(subscope); + target.add(Instruction.debugVarNames(subscope.locals())); + body.compile(target, subscope); + + checkBreakAndCont(target, start); + + if (!(body instanceof CompoundStatement)) target.add(Instruction.ret().locate(loc())); + + target.set(start, Instruction.loadFunc(target.size() - start, subscope.localsCount(), args.length, subscope.getCaptures()).locate(loc())); + + if (name == null) name = this.name; + + if (name != null) { + target.add(Instruction.dup(1).locate(loc())); + target.add(Instruction.loadValue("name").locate(loc())); + target.add(Instruction.loadValue(name).locate(loc())); + target.add(Instruction.storeMember().locate(loc())); + } + + if (this.name != null && isStatement) { + var key = scope.getKey(this.name); + + if (key instanceof String) target.add(Instruction.makeVar((String)key).locate(loc())); + target.add(Instruction.storeVar(scope.getKey(this.name), true).locate(loc())); + } + } + @Override + public void compile(List target, ScopeRecord scope) { + compile(target, scope, null, false); + } + + public FunctionStatement(Location loc, String name, String[] args, CompoundStatement body) { + super(loc); + this.name = name; + + this.args = args; + this.body = body; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/GlobalThisStatement.java b/src/me/topchetoeu/jscript/compilation/values/GlobalThisStatement.java new file mode 100644 index 0000000..15e0515 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/GlobalThisStatement.java @@ -0,0 +1,24 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class GlobalThisStatement extends Statement { + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.loadGlob().locate(loc())); + } + + public GlobalThisStatement(Location loc) { + super(loc); + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/IndexAssignStatement.java b/src/me/topchetoeu/jscript/compilation/values/IndexAssignStatement.java new file mode 100644 index 0000000..1a55646 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/IndexAssignStatement.java @@ -0,0 +1,51 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class IndexAssignStatement extends Statement { + public final Statement object; + public final Statement index; + public final Statement value; + public final Type operation; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + int start = 0; + if (operation != null) { + object.compileWithPollution(target, scope); + index.compileWithPollution(target, scope); + + target.add(Instruction.dup(1, 1).locate(loc())); + target.add(Instruction.dup(1, 1).locate(loc())); + target.add(Instruction.loadMember().locate(loc())); + value.compileWithPollution(target, scope); + target.add(Instruction.operation(operation).locate(loc())); + + target.add(Instruction.storeMember(true).locate(loc())); + } + else { + object.compileWithPollution(target, scope); + index.compileWithPollution(target, scope); + value.compileWithPollution(target, scope); + target.add(Instruction.storeMember(true).locate(loc())); + } + target.get(start).setDebug(true); + } + + public IndexAssignStatement(Location loc, Statement object, Statement index, Statement value, Type operation) { + super(loc); + this.object = object; + this.index = index; + this.value = value; + this.operation = operation; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/IndexStatement.java b/src/me/topchetoeu/jscript/compilation/values/IndexStatement.java new file mode 100644 index 0000000..c1fd5fe --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/IndexStatement.java @@ -0,0 +1,53 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.AssignableStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class IndexStatement extends AssignableStatement { + public final Statement object; + public final Statement index; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public Statement toAssign(Statement val, Type operation) { + return new IndexAssignStatement(loc(), object, index, val, operation); + } + public void compile(List target, ScopeRecord scope, boolean dupObj) { + int start = 0; + object.compileWithPollution(target, scope); + if (dupObj) target.add(Instruction.dup(1).locate(loc())); + if (index instanceof ConstantStatement) { + target.add(Instruction.loadMember(((ConstantStatement)index).value).locate(loc())); + return; + } + + index.compileWithPollution(target, scope); + target.add(Instruction.loadMember().locate(loc())); + target.get(start).setDebug(true); + } + @Override + public void compile(List target, ScopeRecord scope) { + compile(target, scope, false); + } + + public IndexStatement(Location loc, Statement object, Statement index) { + super(loc); + this.object = object; + this.index = index; + } + public IndexStatement(Location loc, Statement object, Object index) { + super(loc); + this.object = object; + this.index = new ConstantStatement(loc, index); + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/LazyAndStatement.java b/src/me/topchetoeu/jscript/compilation/values/LazyAndStatement.java new file mode 100644 index 0000000..00f55e7 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/LazyAndStatement.java @@ -0,0 +1,45 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class LazyAndStatement extends Statement { + public final Statement first, second; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { + return first.pure() && second.pure(); + } + + @Override + public void compile(List target, ScopeRecord scope) { + if (first instanceof ConstantStatement) { + if (Values.not(((ConstantStatement)first).value)) { + first.compileWithPollution(target, scope); + } + else second.compileWithPollution(target, scope); + return; + } + + first.compileWithPollution(target, scope); + target.add(Instruction.dup(1).locate(loc())); + int start = target.size(); + target.add(Instruction.nop()); + target.add(Instruction.discard().locate(loc())); + second.compileWithPollution(target, scope); + target.set(start, Instruction.jmpIfNot(target.size() - start).locate(loc())); + } + + public LazyAndStatement(Location loc, Statement first, Statement second) { + super(loc); + this.first = first; + this.second = second; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/LazyOrStatement.java b/src/me/topchetoeu/jscript/compilation/values/LazyOrStatement.java new file mode 100644 index 0000000..4cc5dd0 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/LazyOrStatement.java @@ -0,0 +1,45 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class LazyOrStatement extends Statement { + public final Statement first, second; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { + return first.pure() && second.pure(); + } + + @Override + public void compile(List target, ScopeRecord scope) { + if (first instanceof ConstantStatement) { + if (Values.not(((ConstantStatement)first).value)) { + second.compileWithPollution(target, scope); + } + else first.compileWithPollution(target, scope); + return; + } + + first.compileWithPollution(target, scope); + target.add(Instruction.dup(1).locate(loc())); + int start = target.size(); + target.add(Instruction.nop()); + target.add(Instruction.discard().locate(loc())); + second.compileWithPollution(target, scope); + target.set(start, Instruction.jmpIf(target.size() - start).locate(loc())); + } + + public LazyOrStatement(Location loc, Statement first, Statement second) { + super(loc); + this.first = first; + this.second = second; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/NewStatement.java b/src/me/topchetoeu/jscript/compilation/values/NewStatement.java new file mode 100644 index 0000000..c325429 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/NewStatement.java @@ -0,0 +1,32 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class NewStatement extends Statement { + public final Statement func; + public final Statement[] args; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + func.compileWithPollution(target, scope); + for (var arg : args) { + arg.compileWithPollution(target, scope); + } + + target.add(Instruction.callNew(args.length).locate(loc()).setDebug(true)); + } + + public NewStatement(Location loc, Statement func, Statement... args) { + super(loc); + this.func = func; + this.args = args; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/ObjectStatement.java b/src/me/topchetoeu/jscript/compilation/values/ObjectStatement.java new file mode 100644 index 0000000..ecfc97a --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/ObjectStatement.java @@ -0,0 +1,57 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class ObjectStatement extends Statement { + public final Map map; + public final Map getters; + public final Map setters; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.loadObj().locate(loc())); + if (!map.isEmpty()) target.add(Instruction.dup(map.size()).locate(loc())); + + for (var el : map.entrySet()) { + target.add(Instruction.loadValue(el.getKey()).locate(loc())); + var val = el.getValue(); + if (val instanceof FunctionStatement) ((FunctionStatement)val).compile(target, scope, el.getKey().toString(), false); + else val.compileWithPollution(target, scope); + target.add(Instruction.storeMember().locate(loc())); + } + + var keys = new ArrayList(); + keys.addAll(getters.keySet()); + keys.addAll(setters.keySet()); + + for (var key : keys) { + if (key instanceof String) target.add(Instruction.loadValue((String)key).locate(loc())); + else target.add(Instruction.loadValue((Double)key).locate(loc())); + + if (getters.containsKey(key)) getters.get(key).compileWithPollution(target, scope); + else target.add(Instruction.loadValue(null).locate(loc())); + + if (setters.containsKey(key)) setters.get(key).compileWithPollution(target, scope); + else target.add(Instruction.loadValue(null).locate(loc())); + + target.add(Instruction.defProp().locate(loc())); + } + } + + public ObjectStatement(Location loc, Map map, Map getters, Map setters) { + super(loc); + this.map = Map.copyOf(map); + this.getters = Map.copyOf(getters); + this.setters = Map.copyOf(setters); + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/OperationStatement.java b/src/me/topchetoeu/jscript/compilation/values/OperationStatement.java new file mode 100644 index 0000000..642e865 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/OperationStatement.java @@ -0,0 +1,104 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.control.ThrowStatement; +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class OperationStatement extends Statement { + public final Statement[] args; + public final Instruction.Type operation; + + @Override + public void compile(List target, ScopeRecord scope) { + for (var arg : args) { + arg.compileWithPollution(target, scope); + } + target.add(Instruction.operation(operation).locate(loc())); + } + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { + for (var arg : args) { + if (!arg.pure()) return false; + } + return true; + } + + @Override + public Statement optimize() { + var args = new Statement[this.args.length]; + var allConst = true; + + for (var i = 0; i < this.args.length; i++) { + args[i] = this.args[i].optimize(); + if (!(args[i] instanceof ConstantStatement)) allConst = false; + } + + if (allConst) { + var vals = new Object[this.args.length]; + + for (var i = 0; i < args.length; i++) { + vals[i] = ((ConstantStatement)args[i]).value; + } + + try { + var ctx = new CallContext(null); + + switch (operation) { + case ADD: return new ConstantStatement(loc(), Values.add(ctx, vals[0], vals[1])); + case SUBTRACT: return new ConstantStatement(loc(), Values.subtract(ctx, vals[0], vals[1])); + case DIVIDE: return new ConstantStatement(loc(), Values.divide(ctx, vals[0], vals[1])); + case MULTIPLY: return new ConstantStatement(loc(), Values.multiply(ctx, vals[0], vals[1])); + case MODULO: return new ConstantStatement(loc(), Values.modulo(ctx, vals[0], vals[1])); + + case AND: return new ConstantStatement(loc(), Values.and(ctx, vals[0], vals[1])); + case OR: return new ConstantStatement(loc(), Values.or(ctx, vals[0], vals[1])); + case XOR: return new ConstantStatement(loc(), Values.xor(ctx, vals[0], vals[1])); + + case EQUALS: return new ConstantStatement(loc(), Values.strictEquals(vals[0], vals[1])); + case NOT_EQUALS: return new ConstantStatement(loc(), !Values.strictEquals(vals[0], vals[1])); + case LOOSE_EQUALS: return new ConstantStatement(loc(), Values.looseEqual(ctx, vals[0], vals[1])); + case LOOSE_NOT_EQUALS: return new ConstantStatement(loc(), !Values.looseEqual(ctx, vals[0], vals[1])); + + case GREATER: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) < 0); + case GREATER_EQUALS: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) <= 0); + case LESS: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) > 0); + case LESS_EQUALS: return new ConstantStatement(loc(), Values.compare(ctx, vals[0], vals[1]) >= 0); + + case INVERSE: return new ConstantStatement(loc(), Values.bitwiseNot(ctx, vals[0])); + case NOT: return new ConstantStatement(loc(), Values.not(vals[0])); + case POS: return new ConstantStatement(loc(), Values.toNumber(ctx, vals[0])); + case NEG: return new ConstantStatement(loc(), Values.negative(ctx, vals[0])); + + case SHIFT_LEFT: return new ConstantStatement(loc(), Values.shiftLeft(ctx, vals[0], vals[1])); + case SHIFT_RIGHT: return new ConstantStatement(loc(), Values.shiftRight(ctx, vals[0], vals[1])); + case USHIFT_RIGHT: return new ConstantStatement(loc(), Values.unsignedShiftRight(ctx, vals[0], vals[1])); + + default: break; + } + } + catch (EngineException e) { + return new ThrowStatement(loc(), new ConstantStatement(loc(), e.value)); + } + catch (InterruptedException e) { return null; } + } + + return new OperationStatement(loc(), operation, args); + + } + + public OperationStatement(Location loc, Instruction.Type operation, Statement... args) { + super(loc); + this.operation = operation; + this.args = args; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/RegexStatement.java b/src/me/topchetoeu/jscript/compilation/values/RegexStatement.java new file mode 100644 index 0000000..ad92abd --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/RegexStatement.java @@ -0,0 +1,28 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class RegexStatement extends Statement { + public final String pattern, flags; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.loadRegex(pattern, flags).locate(loc())); + } + + public RegexStatement(Location loc, String pattern, String flags) { + super(loc); + this.pattern = pattern; + this.flags = flags; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/TernaryStatement.java b/src/me/topchetoeu/jscript/compilation/values/TernaryStatement.java new file mode 100644 index 0000000..d2c5a6d --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/TernaryStatement.java @@ -0,0 +1,58 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.Values; + +public class TernaryStatement extends Statement { + public final Statement condition; + public final Statement first; + public final Statement second; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + if (condition instanceof ConstantStatement) { + if (!Values.toBoolean(((ConstantStatement)condition).value)) { + second.compileWithPollution(target, scope); + } + else first.compileWithPollution(target, scope); + return; + } + + condition.compileWithPollution(target, scope); + int start = target.size(); + target.add(Instruction.nop()); + first.compileWithPollution(target, scope); + int mid = target.size(); + target.add(Instruction.nop()); + second.compileWithPollution(target, scope); + int end = target.size(); + + target.set(start, Instruction.jmpIfNot(mid - start + 1).locate(loc())); + target.set(mid, Instruction.jmp(end - mid).locate(loc())); + } + + @Override + public Statement optimize() { + var cond = condition.optimize(); + var f = first.optimize(); + var s = second.optimize(); + return new TernaryStatement(loc(), cond, f, s); + } + + public TernaryStatement(Location loc, Statement condition, Statement first, Statement second) { + super(loc); + this.condition = condition; + this.first = first; + this.second = second; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/TypeofStatement.java b/src/me/topchetoeu/jscript/compilation/values/TypeofStatement.java new file mode 100644 index 0000000..6825066 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/TypeofStatement.java @@ -0,0 +1,55 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.Symbol; + +public class TypeofStatement extends Statement { + public final Statement value; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + if (value instanceof VariableStatement) { + var i = scope.getKey(((VariableStatement)value).name); + if (i instanceof String) { + target.add(Instruction.typeof((String)i)); + return; + } + } + value.compileWithPollution(target, scope); + target.add(Instruction.typeof().locate(loc())); + } + + @Override + public Statement optimize() { + var val = value.optimize(); + + if (val instanceof ConstantStatement) { + var cnst = (ConstantStatement)val; + if (cnst.value == null) return new ConstantStatement(loc(), "undefined"); + if (cnst.value instanceof Number) return new ConstantStatement(loc(), "number"); + if (cnst.value instanceof Boolean) return new ConstantStatement(loc(), "boolean"); + if (cnst.value instanceof String) return new ConstantStatement(loc(), "string"); + if (cnst.value instanceof Symbol) return new ConstantStatement(loc(), "symbol"); + if (cnst.value instanceof FunctionValue) return new ConstantStatement(loc(), "function"); + return new ConstantStatement(loc(), "object"); + } + + return new TypeofStatement(loc(), val); + } + + public TypeofStatement(Location loc, Statement value) { + super(loc); + this.value = value; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/VariableAssignStatement.java b/src/me/topchetoeu/jscript/compilation/values/VariableAssignStatement.java new file mode 100644 index 0000000..13be56b --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/VariableAssignStatement.java @@ -0,0 +1,42 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class VariableAssignStatement extends Statement { + public final String name; + public final Statement value; + public final Type operation; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + var i = scope.getKey(name); + if (operation != null) { + target.add(Instruction.loadVar(i).locate(loc())); + if (value instanceof FunctionStatement) ((FunctionStatement)value).compile(target, scope, name, false); + else value.compileWithPollution(target, scope); + target.add(Instruction.operation(operation).locate(loc())); + } + else { + if (value instanceof FunctionStatement) ((FunctionStatement)value).compile(target, scope, name, false); + else value.compileWithPollution(target, scope); + } + + target.add(Instruction.storeVar(i, true).locate(loc())); + } + + public VariableAssignStatement(Location loc, String name, Statement val, Type operation) { + super(loc); + this.name = name; + this.value = val; + this.operation = operation; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/VariableIndexStatement.java b/src/me/topchetoeu/jscript/compilation/values/VariableIndexStatement.java new file mode 100644 index 0000000..2556af8 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/VariableIndexStatement.java @@ -0,0 +1,27 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class VariableIndexStatement extends Statement { + public final int index; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + target.add(Instruction.loadVar(index).locate(loc())); + } + + public VariableIndexStatement(Location loc, int i) { + super(loc); + this.index = i; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/VariableStatement.java b/src/me/topchetoeu/jscript/compilation/values/VariableStatement.java new file mode 100644 index 0000000..1ccff35 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/VariableStatement.java @@ -0,0 +1,35 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.AssignableStatement; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; + +public class VariableStatement extends AssignableStatement { + public final String name; + + @Override + public boolean pollutesStack() { return true; } + @Override + public boolean pure() { return true; } + + @Override + public Statement toAssign(Statement val, Type operation) { + return new VariableAssignStatement(loc(), name, val, operation); + } + + @Override + public void compile(List target, ScopeRecord scope) { + var i = scope.getKey(name); + target.add(Instruction.loadVar(i).locate(loc())); + } + + public VariableStatement(Location loc, String name) { + super(loc); + this.name = name; + } +} diff --git a/src/me/topchetoeu/jscript/compilation/values/VoidStatement.java b/src/me/topchetoeu/jscript/compilation/values/VoidStatement.java new file mode 100644 index 0000000..dbcbe38 --- /dev/null +++ b/src/me/topchetoeu/jscript/compilation/values/VoidStatement.java @@ -0,0 +1,34 @@ +package me.topchetoeu.jscript.compilation.values; + +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Statement; +import me.topchetoeu.jscript.engine.scope.ScopeRecord; +import me.topchetoeu.jscript.compilation.Instruction; + +public class VoidStatement extends Statement { + public final Statement value; + + @Override + public boolean pollutesStack() { return true; } + + @Override + public void compile(List target, ScopeRecord scope) { + if (value != null) value.compileNoPollution(target, scope); + target.add(Instruction.loadValue(null).locate(loc())); + } + + @Override + public Statement optimize() { + if (value == null) return this; + var val = value.optimize(); + if (val.pure()) return new ConstantStatement(loc(), null); + else return new VoidStatement(loc(), val); + } + + public VoidStatement(Location loc, Statement value) { + super(loc); + this.value = value; + } +} diff --git a/src/me/topchetoeu/jscript/engine/BreakpointData.java b/src/me/topchetoeu/jscript/engine/BreakpointData.java new file mode 100644 index 0000000..e126e72 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/BreakpointData.java @@ -0,0 +1,5 @@ +package me.topchetoeu.jscript.engine; + +import me.topchetoeu.jscript.Location; + +public record BreakpointData(Location loc, CallContext ctx) { } \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/engine/CallContext.java b/src/me/topchetoeu/jscript/engine/CallContext.java new file mode 100644 index 0000000..41c0fd7 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/CallContext.java @@ -0,0 +1,58 @@ +package me.topchetoeu.jscript.engine; + +import java.util.Collections; +import java.util.Hashtable; +import java.util.Map; + +public class CallContext { + public static final class DataKey {} + + public final Engine engine; + private final Map, Object> data = new Hashtable<>(); + + public Engine engine() { return engine; } + public Map, Object> data() { return Collections.unmodifiableMap(data); } + + public CallContext copy() { + return new CallContext(engine).mergeData(data); + } + public CallContext mergeData(Map, Object> objs) { + data.putAll(objs); + return this; + } + public CallContext setData(DataKey key, T val) { + data.put(key, val); + return this; + } + @SuppressWarnings("unchecked") + public T addData(DataKey key, T val) { + if (data.containsKey(key)) return (T)data.get(key); + else { + data.put(key, val); + return val; + } + } + public boolean hasData(DataKey key) { return data.containsKey(key); } + public T getData(DataKey key) { + return getData(key, null); + } + @SuppressWarnings("unchecked") + public T getData(DataKey key, T defaultVal) { + if (!hasData(key)) return defaultVal; + else return (T)data.get(key); + } + + public CallContext changeData(DataKey key, int n, int start) { + return setData(key, getData(key, start) + n); + } + public CallContext changeData(DataKey key, int n) { + return changeData(key, n, 0); + } + public CallContext changeData(DataKey key) { + return changeData(key, 1, 0); + } + + public CallContext(Engine engine) { + this.engine = engine; + } +} diff --git a/src/me/topchetoeu/jscript/engine/DebugCommand.java b/src/me/topchetoeu/jscript/engine/DebugCommand.java new file mode 100644 index 0000000..1afb346 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/DebugCommand.java @@ -0,0 +1,8 @@ +package me.topchetoeu.jscript.engine; + +public enum DebugCommand { + NORMAL, + STEP_OVER, + STEP_OUT, + STEP_INTO, +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/engine/Engine.java b/src/me/topchetoeu/jscript/engine/Engine.java new file mode 100644 index 0000000..b19cdc8 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/Engine.java @@ -0,0 +1,195 @@ +package me.topchetoeu.jscript.engine; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +import me.topchetoeu.jscript.engine.CallContext.DataKey; +import me.topchetoeu.jscript.engine.debug.DebugState; +import me.topchetoeu.jscript.engine.modules.ModuleManager; +import me.topchetoeu.jscript.engine.scope.GlobalScope; +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.ObjectValue.PlaceholderProto; +import me.topchetoeu.jscript.events.Awaitable; +import me.topchetoeu.jscript.events.DataNotifier; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.NativeTypeRegister; +import me.topchetoeu.jscript.parsing.Parsing; + +public class Engine { + private static record RawFunction(GlobalScope scope, String filename, String raw) { } + private static class Task { + public final Object func; + public final Object thisArg; + public final Object[] args; + public final Map, Object> data; + public final DataNotifier notifier = new DataNotifier<>(); + + public Task(Object func, Map, Object> data, Object thisArg, Object[] args) { + this.func = func; + this.data = data; + this.thisArg = thisArg; + this.args = args; + } + } + + public static final DataKey DEBUG_STATE_KEY = new DataKey<>(); + private static int nextId = 0; + + private Map, Object> callCtxVals = new HashMap<>(); + private GlobalScope global = new GlobalScope(); + private ObjectValue arrayProto = new ObjectValue(); + private ObjectValue boolProto = new ObjectValue(); + private ObjectValue funcProto = new ObjectValue(); + private ObjectValue numProto = new ObjectValue(); + private ObjectValue objProto = new ObjectValue(PlaceholderProto.NONE); + private ObjectValue strProto = new ObjectValue(); + private ObjectValue symProto = new ObjectValue(); + private ObjectValue errProto = new ObjectValue(); + private ObjectValue syntaxErrProto = new ObjectValue(PlaceholderProto.ERROR); + private ObjectValue typeErrProto = new ObjectValue(PlaceholderProto.ERROR); + private ObjectValue rangeErrProto = new ObjectValue(PlaceholderProto.ERROR); + private NativeTypeRegister typeRegister; + private Thread thread; + + private LinkedBlockingDeque macroTasks = new LinkedBlockingDeque<>(); + private LinkedBlockingDeque microTasks = new LinkedBlockingDeque<>(); + + public final int id = ++nextId; + public final DebugState debugState = new DebugState(); + + public ObjectValue arrayProto() { return arrayProto; } + public ObjectValue booleanProto() { return boolProto; } + public ObjectValue functionProto() { return funcProto; } + public ObjectValue numberProto() { return numProto; } + public ObjectValue objectProto() { return objProto; } + public ObjectValue stringProto() { return strProto; } + public ObjectValue symbolProto() { return symProto; } + public ObjectValue errorProto() { return errProto; } + public ObjectValue syntaxErrorProto() { return syntaxErrProto; } + public ObjectValue typeErrorProto() { return typeErrProto; } + public ObjectValue rangeErrorProto() { return rangeErrProto; } + + public GlobalScope global() { return global; } + public NativeTypeRegister typeRegister() { return typeRegister; } + + public void copyFrom(Engine other) { + global = other.global; + typeRegister = other.typeRegister; + arrayProto = other.arrayProto; + boolProto = other.boolProto; + funcProto = other.funcProto; + numProto = other.numProto; + objProto = other.objProto; + strProto = other.strProto; + symProto = other.symProto; + errProto = other.errProto; + syntaxErrProto = other.syntaxErrProto; + typeErrProto = other.typeErrProto; + rangeErrProto = other.rangeErrProto; + } + + private void runTask(Task task) throws InterruptedException { + try { + FunctionValue func; + if (task.func instanceof FunctionValue) func = (FunctionValue)task.func; + else { + var raw = (RawFunction)task.func; + func = compile(raw.scope, raw.filename, raw.raw); + } + + task.notifier.next(func.call(context().mergeData(task.data), task.thisArg, task.args)); + } + catch (InterruptedException e) { + task.notifier.error(new RuntimeException(e)); + throw e; + } + catch (EngineException e) { + task.notifier.error(e); + } + catch (RuntimeException e) { + task.notifier.error(e); + e.printStackTrace(); + } + } + private void run() { + while (true) { + try { + runTask(macroTasks.take()); + + while (!microTasks.isEmpty()) { + runTask(microTasks.take()); + } + } + catch (InterruptedException e) { + for (var msg : macroTasks) { + msg.notifier.error(new RuntimeException(e)); + } + break; + } + } + } + + public void exposeClass(String name, Class clazz) { + global.define(name, true, typeRegister.getConstr(clazz)); + } + public void exposeNamespace(String name, Class clazz) { + global.define(name, true, NativeTypeRegister.makeNamespace(clazz)); + } + + public Thread start() { + if (this.thread == null) { + this.thread = new Thread(this::run, "JavaScript Runner #" + id); + this.thread.start(); + } + return this.thread; + } + public void stop() { + thread.interrupt(); + thread = null; + } + public boolean inExecThread() { + return Thread.currentThread() == thread; + } + public boolean isRunning() { + return this.thread != null; + } + + public Object makeRegex(String pattern, String flags) { + throw EngineException.ofError("Regular expressions not supported."); + } + public ModuleManager modules() { + return null; + } + public ObjectValue getPrototype(Class clazz) { + return typeRegister.getProto(clazz); + } + public CallContext context() { return new CallContext(this).mergeData(callCtxVals); } + + public Awaitable pushMsg(boolean micro, FunctionValue func, Map, Object> data, Object thisArg, Object... args) { + var msg = new Task(func, data, thisArg, args); + if (micro) microTasks.addLast(msg); + else macroTasks.addLast(msg); + return msg.notifier; + } + public Awaitable pushMsg(boolean micro, GlobalScope scope, Map, Object> data, String filename, String raw, Object thisArg, Object... args) { + var msg = new Task(new RawFunction(scope, filename, raw), data, thisArg, args); + if (micro) microTasks.addLast(msg); + else macroTasks.addLast(msg); + return msg.notifier; + } + + public CodeFunction compile(GlobalScope scope, String filename, String raw) throws InterruptedException { + return Parsing.compile(scope, filename, raw); + } + + public Engine(NativeTypeRegister register) { + this.typeRegister = register; + this.callCtxVals.put(DEBUG_STATE_KEY, debugState); + } + public Engine() { + this(new NativeTypeRegister()); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/DebugServer.java b/src/me/topchetoeu/jscript/engine/debug/DebugServer.java new file mode 100644 index 0000000..225a73f --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/DebugServer.java @@ -0,0 +1,154 @@ +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 me.topchetoeu.jscript.engine.Engine; +import me.topchetoeu.jscript.engine.debug.WebSocketMessage.Type; +import me.topchetoeu.jscript.engine.debug.handlers.DebuggerHandles; +import me.topchetoeu.jscript.exceptions.SyntaxException; + +public class DebugServer { + public static String browserDisplayName = "jscript"; + public static String targetName = "target"; + + public final Engine engine; + + private static void send(Socket socket, String val) throws IOException { + Http.writeResponse(socket.getOutputStream(), 200, "OK", "application/json", val.getBytes()); + } + + // SILENCE JAVA + private MessageDigest getDigestInstance() { + try { + return MessageDigest.getInstance("sha1"); + } + catch (Throwable a) { return null; } + } + + 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) throws InterruptedException, IOException { + WebSocketMessage raw; + + while ((raw = ws.receive()) != null) { + if (raw.type != Type.Text) { + ws.send(new V8Error("Expected a text message.")); + continue; + } + + V8Message msg; + + try { + msg = new V8Message(raw.textData()); + } + catch (SyntaxException e) { + ws.send(new V8Error(e.getMessage())); + return; + } + + switch (msg.name) { + case "Debugger.enable": DebuggerHandles.enable(msg, engine, ws); continue; + case "Debugger.disable": DebuggerHandles.disable(msg, engine, ws); continue; + case "Debugger.stepInto": DebuggerHandles.stepInto(msg, engine, ws); continue; + } + } + } + private void onWsConnect(HttpRequest req, Socket socket) throws IOException { + var key = req.headers().get("sec-websocket-key"); + + if (key == null) { + Http.writeResponse( + socket.getOutputStream(), 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() + )); + + Http.writeCode(socket.getOutputStream(), 101, "Switching Protocols"); + Http.writeHeader(socket.getOutputStream(), "Connection", "Upgrade"); + Http.writeHeader(socket.getOutputStream(), "Sec-WebSocket-Accept", resKey); + Http.writeLastHeader(socket.getOutputStream(), "Upgrade", "WebSocket"); + + var ws = new WebSocket(socket); + + runAsync(() -> { + try { + handle(ws); + } + catch (InterruptedException e) { return; } + catch (IOException e) { e.printStackTrace(); } + finally { ws.close(); } + }, "Debug Server Message Reader"); + runAsync(() -> { + try { + handle(ws); + } + catch (InterruptedException e) { return; } + catch (IOException e) { e.printStackTrace(); } + finally { ws.close(); } + }, "Debug Server Event Writer"); + } + + public void open(InetSocketAddress address) throws IOException { + ServerSocket server = new ServerSocket(); + server.bind(address); + + try { + while (true) { + var socket = server.accept(); + var req = Http.readRequest(socket.getInputStream()); + + switch (req.path()) { + case "/json/version": + send(socket, "{\"Browser\":\"" + browserDisplayName + "\",\"Protocol-Version\":\"1.2\"}"); + break; + case "/json/list": + case "/json": + var addr = "ws://" + address.getHostString() + ":" + address.getPort() + "/devtools/page/" + targetName; + send(socket, "{\"id\":\"" + browserDisplayName + "\",\"webSocketDebuggerUrl\":\"" + addr + "\"}"); + break; + case "/json/new": + case "/json/activate": + case "/json/protocol": + case "/json/close": + case "/devtools/inspector.html": + Http.writeResponse( + socket.getOutputStream(), + 501, "Not Implemented", "text/txt", + "This feature isn't (and won't be) implemented.".getBytes() + ); + break; + default: + if (req.path().equals("/devtools/page/" + targetName)) onWsConnect(req, socket); + else { + Http.writeResponse( + socket.getOutputStream(), + 404, "Not Found", "text/txt", + "Not found :/".getBytes() + ); + } + break; + } + } + } + finally { server.close(); } + } + + public DebugServer(Engine engine) { + this.engine = engine; + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/DebugState.java b/src/me/topchetoeu/jscript/engine/debug/DebugState.java new file mode 100644 index 0000000..3cf2209 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/DebugState.java @@ -0,0 +1,52 @@ +package me.topchetoeu.jscript.engine.debug; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.engine.BreakpointData; +import me.topchetoeu.jscript.engine.DebugCommand; +import me.topchetoeu.jscript.engine.frame.CodeFrame; +import me.topchetoeu.jscript.events.Event; + +public class DebugState { + private boolean paused = false; + + public final HashSet breakpoints = new HashSet<>(); + public final List frames = new ArrayList<>(); + public final Map sources = new HashMap<>(); + + public final Event breakpointNotifier = new Event<>(); + public final Event commandNotifier = new Event<>(); + public final Event sourceAdded = new Event<>(); + + public DebugState pushFrame(CodeFrame frame) { + frames.add(frame); + return this; + } + public DebugState popFrame() { + if (frames.size() > 0) frames.remove(frames.size() - 1); + return this; + } + + public DebugCommand pause(BreakpointData data) throws InterruptedException { + paused = true; + breakpointNotifier.next(data); + return commandNotifier.toAwaitable().await(); + } + public void resume(DebugCommand command) { + paused = false; + commandNotifier.next(command); + } + + // public void addSource()? + + public boolean paused() { return paused; } + + public boolean isBreakpoint(Location loc) { + return breakpoints.contains(loc); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/Http.java b/src/me/topchetoeu/jscript/engine/debug/Http.java new file mode 100644 index 0000000..2760dfa --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/Http.java @@ -0,0 +1,65 @@ +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.util.HashMap; +import java.util.IllegalFormatException; + +// We dont need no http library +public class Http { + public static void writeCode(OutputStream str, int code, String name) throws IOException { + str.write(("HTTP/1.1 " + code + " " + name + "\r\n").getBytes()); + } + public static void writeHeader(OutputStream str, String name, String value) throws IOException { + str.write((name + ": " + value + "\r\n").getBytes()); + } + public static void writeLastHeader(OutputStream str, String name, String value) throws IOException { + str.write((name + ": " + value + "\r\n").getBytes()); + writeHeadersEnd(str); + } + public static void writeHeadersEnd(OutputStream str) throws IOException { + str.write("\n".getBytes()); + } + + public static void writeResponse(OutputStream str, int code, String name, String type, byte[] data) throws IOException { + writeCode(str, code, name); + writeHeader(str, "Content-Type", type); + writeLastHeader(str, "Content-Length", data.length + ""); + str.write(data); + str.close(); + } + + public static HttpRequest readRequest(InputStream str) throws IOException { + var lines = new BufferedReader(new InputStreamReader(str)); + var line = lines.readLine(); + var i1 = line.indexOf(" "); + var i2 = line.lastIndexOf(" "); + + var method = line.substring(0, i1).trim().toUpperCase(); + var path = line.substring(i1 + 1, i2).trim(); + var headers = new HashMap(); + + while (!(line = lines.readLine()).isEmpty()) { + var i = line.indexOf(":"); + if (i < 0) continue; + var name = line.substring(0, i).trim().toLowerCase(); + var value = line.substring(i + 1).trim(); + + if (name.length() == 0) continue; + headers.put(name, value); + } + + if (headers.containsKey("content-length")) { + try { + var i = Integer.parseInt(headers.get("content-length")); + str.skip(i); + } + catch (IllegalFormatException e) { /* ¯\_(ツ)_/¯ */ } + } + + return new HttpRequest(method, path, headers); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/HttpRequest.java b/src/me/topchetoeu/jscript/engine/debug/HttpRequest.java new file mode 100644 index 0000000..a45fe0e --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/HttpRequest.java @@ -0,0 +1,6 @@ +package me.topchetoeu.jscript.engine.debug; + +import java.util.Map; + +public record HttpRequest(String method, String path, Map headers) {} + diff --git a/src/me/topchetoeu/jscript/engine/debug/V8Error.java b/src/me/topchetoeu/jscript/engine/debug/V8Error.java new file mode 100644 index 0000000..778160f --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/V8Error.java @@ -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) + )); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/V8Event.java b/src/me/topchetoeu/jscript/engine/debug/V8Event.java new file mode 100644 index 0000000..437c337 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/V8Event.java @@ -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) + ); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/V8Message.java b/src/me/topchetoeu/jscript/engine/debug/V8Message.java new file mode 100644 index 0000000..1c5dfb2 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/V8Message.java @@ -0,0 +1,50 @@ +package me.topchetoeu.jscript.engine.debug; + +import java.util.Map; + +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 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("json", raw).map()); + } + + public JSONMap toMap() { + var res = new JSONMap(); + return res; + } + @Override + public String toString() { + return JSON.stringify(new JSONMap() + .set("method", name) + .set("params", params) + .set("id", id) + ); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/V8Result.java b/src/me/topchetoeu/jscript/engine/debug/V8Result.java new file mode 100644 index 0000000..e2b8bd3 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/V8Result.java @@ -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) + ); + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/WebSocket.java b/src/me/topchetoeu/jscript/engine/debug/WebSocket.java new file mode 100644 index 0000000..b282d67 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/WebSocket.java @@ -0,0 +1,185 @@ +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; + +public class WebSocket implements AutoCloseable { + public long maxLength = 2000000; + + private Socket socket; + private boolean closed = false; + + private OutputStream out() throws IOException { + return socket.getOutputStream(); + } + private InputStream in() throws IOException { + return socket.getInputStream(); + } + + private long readLen(int byteLen) throws IOException { + long res = 0; + + if (byteLen == 126) { + res |= in().read() << 8; + res |= in().read(); + return res; + } + else if (byteLen == 127) { + res |= in().read() << 56; + res |= in().read() << 48; + res |= in().read() << 40; + res |= in().read() << 32; + res |= in().read() << 24; + res |= in().read() << 16; + res |= in().read() << 8; + res |= in().read(); + return res; + } + else return byteLen; + } + private byte[] readMask(boolean has) throws IOException { + if (has) { + return new byte[] { + (byte)in().read(), + (byte)in().read(), + (byte)in().read(), + (byte)in().read() + }; + } + else return new byte[4]; + } + + private void writeLength(long len) throws IOException { + if (len < 126) { + out().write((int)len); + } + else if (len < 0xFFFF) { + out().write(126); + out().write((int)(len >> 8) & 0xFF); + out().write((int)len & 0xFF); + } + else { + out().write(127); + out().write((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); + } + } + private synchronized void write(int type, byte[] data) throws IOException { + out().write(type | 0x80); + writeLength(data.length); + for (int i = 0; i < data.length; i++) { + out().write(data[i]); + } + } + + public void send(String data) throws IOException { + if (closed) throw new IllegalStateException("Object is closed."); + write(1, data.getBytes()); + } + public void send(byte[] data) throws IOException { + if (closed) throw new IllegalStateException("Object is closed."); + write(2, data); + } + public void send(WebSocketMessage msg) throws IOException { + if (msg.type == Type.Binary) send(msg.binaryData()); + else send(msg.textData()); + } + public void send(Object data) throws IOException { + if (closed) throw new IllegalStateException("Object is closed."); + write(1, data.toString().getBytes()); + } + + public void close(String reason) { + if (socket != null) { + try { write(8, reason.getBytes()); } catch (IOException e) { /* ¯\_(ツ)_/¯ */ } + try { socket.close(); } catch (IOException e) { e.printStackTrace(); } + } + + socket = null; + closed = true; + } + public void close() { + close(""); + } + + private WebSocketMessage fail(String reason) { + System.out.println("WebSocket Error: " + reason); + close(reason); + return null; + } + + private byte[] readData() throws IOException { + var maskLen = in().read(); + var hasMask = (maskLen & 0x80) != 0; + var len = (int)readLen(maskLen & 0x7F); + var mask = readMask(hasMask); + + if (len > maxLength) fail("WebSocket Error: client exceeded configured max message size"); + else { + var buff = new byte[len]; + + if (in().read(buff) < len) fail("WebSocket Error: payload too short"); + else { + for (int i = 0; i < len; i++) { + buff[i] ^= mask[(int)(i % 4)]; + } + return buff; + } + } + + return null; + } + + public WebSocketMessage receive() throws InterruptedException { + try { + var data = new ByteArrayOutputStream(); + var type = 0; + + while (socket != null && !closed) { + var finId = in().read(); + 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; + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/WebSocketMessage.java b/src/me/topchetoeu/jscript/engine/debug/WebSocketMessage.java new file mode 100644 index 0000000..beb06de --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/WebSocketMessage.java @@ -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; + } +} diff --git a/src/me/topchetoeu/jscript/engine/debug/handlers/DebuggerHandles.java b/src/me/topchetoeu/jscript/engine/debug/handlers/DebuggerHandles.java new file mode 100644 index 0000000..fce42c3 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/debug/handlers/DebuggerHandles.java @@ -0,0 +1,29 @@ +package me.topchetoeu.jscript.engine.debug.handlers; + +import java.io.IOException; + +import me.topchetoeu.jscript.engine.DebugCommand; +import me.topchetoeu.jscript.engine.Engine; +import me.topchetoeu.jscript.engine.debug.V8Error; +import me.topchetoeu.jscript.engine.debug.V8Message; +import me.topchetoeu.jscript.engine.debug.WebSocket; +import me.topchetoeu.jscript.json.JSONMap; + +public class DebuggerHandles { + public static void enable(V8Message msg, Engine engine, WebSocket ws) throws IOException { + if (engine.debugState == null) ws.send(new V8Error("Debugging is disabled for this engine.")); + else ws.send(msg.respond(new JSONMap().set("debuggerId", 1))); + } + public static void disable(V8Message msg, Engine engine, WebSocket ws) throws IOException { + if (engine.debugState == null) ws.send(msg.respond()); + else ws.send(new V8Error("Debugger may not be disabled.")); + } + public static void stepInto(V8Message msg, Engine engine, WebSocket ws) throws IOException { + if (engine.debugState == null) ws.send(new V8Error("Debugging is disabled for this engine.")); + else if (!engine.debugState.paused()) ws.send(new V8Error("Debugger is not paused.")); + else { + engine.debugState.resume(DebugCommand.STEP_INTO); + ws.send(msg.respond()); + } + } +} diff --git a/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java b/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java new file mode 100644 index 0000000..5264f8d --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/frame/CodeFrame.java @@ -0,0 +1,177 @@ +package me.topchetoeu.jscript.engine.frame; + +import java.util.ArrayList; +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.BreakpointData; +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.DebugCommand; +import me.topchetoeu.jscript.engine.Engine; +import me.topchetoeu.jscript.engine.CallContext.DataKey; +import me.topchetoeu.jscript.engine.scope.LocalScope; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class CodeFrame { + public static final DataKey STACK_N_KEY = new DataKey<>(); + public static final DataKey MAX_STACK_KEY = new DataKey<>(); + public static final DataKey STOP_AT_START_KEY = new DataKey<>(); + public static final DataKey STEPPING_TROUGH_KEY = new DataKey<>(); + + public final LocalScope scope; + public final Object thisArg; + public final Object[] args; + public final List stack = new ArrayList<>(); + public final CodeFunction function; + + public int codePtr = 0; + private DebugCommand debugCmd = null; + private Location prevLoc = null; + + public Object peek() { + return peek(0); + } + public Object peek(int offset) { + if (stack.size() <= offset) return null; + else return stack.get(stack.size() - 1 - offset); + } + public Object pop() { + if (stack.size() == 0) return null; + else return stack.remove(stack.size() - 1); + } + public void push(Object val) { + stack.add(stack.size(), Values.normalize(val)); + } + + private void cleanup(CallContext ctx) { + stack.clear(); + codePtr = 0; + debugCmd = null; + var debugState = ctx.getData(Engine.DEBUG_STATE_KEY); + + if (debugState != null) debugState.popFrame(); + ctx.changeData(STACK_N_KEY, -1); + } + + public Object next(CallContext ctx) throws InterruptedException { + var debugState = ctx.getData(Engine.DEBUG_STATE_KEY); + + if (debugCmd == null) { + if (ctx.getData(STACK_N_KEY, 0) >= ctx.addData(MAX_STACK_KEY, 1000)) + throw EngineException.ofRange("Stack overflow!"); + ctx.changeData(STACK_N_KEY); + + if (ctx.getData(STOP_AT_START_KEY, false)) debugCmd = DebugCommand.STEP_OVER; + else debugCmd = DebugCommand.NORMAL; + + if (debugState != null) debugState.pushFrame(this); + } + + if (Thread.currentThread().isInterrupted()) throw new InterruptedException(); + var instr = function.body[codePtr]; + var loc = instr.location; + if (loc != null) prevLoc = loc; + + if (debugState != null && loc != null) { + if ( + instr.type == Type.NOP && instr.match("debug") || debugState.breakpoints.contains(loc) || ( + ctx.getData(STEPPING_TROUGH_KEY, false) && + (debugCmd == DebugCommand.STEP_INTO || debugCmd == DebugCommand.STEP_OVER) + ) + ) { + ctx.setData(STEPPING_TROUGH_KEY, true); + + debugState.breakpointNotifier.next(new BreakpointData(loc, ctx)); + debugCmd = debugState.commandNotifier.toAwaitable().await(); + if (debugCmd == DebugCommand.NORMAL) ctx.setData(STEPPING_TROUGH_KEY, false); + } + } + + try { + var res = Runners.exec(debugCmd, instr, this, ctx); + if (res != Runners.NO_RETURN) cleanup(ctx); + return res; + } + catch (EngineException e) { + cleanup(ctx); + throw e.add(function.name, prevLoc); + } + catch (RuntimeException e) { + cleanup(ctx); + throw e; + } + } + + public Object run(CallContext ctx) throws InterruptedException { + var debugState = ctx.getData(Engine.DEBUG_STATE_KEY); + DebugCommand command = ctx.getData(STOP_AT_START_KEY, false) ? DebugCommand.STEP_OVER : DebugCommand.NORMAL; + + if (ctx.getData(STACK_N_KEY, 0) >= ctx.addData(MAX_STACK_KEY, 200)) throw EngineException.ofRange("Stack overflow!"); + ctx.changeData(STACK_N_KEY); + + if (debugState != null) debugState.pushFrame(this); + + Location loc = null; + + try { + while (codePtr >= 0 && codePtr < function.body.length) { + var _loc = function.body[codePtr].location; + if (_loc != null) loc = _loc; + + if (Thread.currentThread().isInterrupted()) throw new InterruptedException(); + var instr = function.body[codePtr]; + + if (debugState != null && loc != null) { + if ( + instr.type == Type.NOP && instr.match("debug") || + ( + (command == DebugCommand.STEP_INTO || command == DebugCommand.STEP_OVER) && + ctx.getData(STEPPING_TROUGH_KEY, false) + ) || + debugState.breakpoints.contains(loc) + ) { + ctx.setData(STEPPING_TROUGH_KEY, true); + + debugState.breakpointNotifier.next(new BreakpointData(loc, ctx)); + command = debugState.commandNotifier.toAwaitable().await(); + if (command == DebugCommand.NORMAL) ctx.setData(STEPPING_TROUGH_KEY, false); + } + } + + try { + var res = Runners.exec(command, instr, this, ctx); + if (res != Runners.NO_RETURN) return res; + } + catch (EngineException e) { + throw e.add(function.name, instr.location); + } + } + return null; + } + // catch (StackOverflowError e) { + // e.printStackTrace(); + // throw EngineException.ofRange("Stack overflow!").add(function.name, loc); + // } + finally { + ctx.changeData(STACK_N_KEY, -1); + } + } + + public CodeFrame(Object thisArg, Object[] args, CodeFunction func) { + this.args = args.clone(); + this.scope = new LocalScope(func.localsN, func.captures); + this.scope.get(0).set(null, thisArg); + var argsObj = new ArrayValue(); + for (var i = 0; i < args.length; i++) { + argsObj.set(i, args[i]); + } + this.scope.get(1).value = argsObj; + + this.thisArg = thisArg; + this.function = func; + } +} diff --git a/src/me/topchetoeu/jscript/engine/frame/ConvertHint.java b/src/me/topchetoeu/jscript/engine/frame/ConvertHint.java new file mode 100644 index 0000000..dcea46c --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/frame/ConvertHint.java @@ -0,0 +1,6 @@ +package me.topchetoeu.jscript.engine.frame; + +public enum ConvertHint { + TOSTRING, + VALUEOF, +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/engine/frame/InstructionResult.java b/src/me/topchetoeu/jscript/engine/frame/InstructionResult.java new file mode 100644 index 0000000..32e5caf --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/frame/InstructionResult.java @@ -0,0 +1,9 @@ +package me.topchetoeu.jscript.engine.frame; + +public class InstructionResult { + public final Object value; + + public InstructionResult(Object value) { + this.value = value; + } +} diff --git a/src/me/topchetoeu/jscript/engine/frame/Runners.java b/src/me/topchetoeu/jscript/engine/frame/Runners.java new file mode 100644 index 0000000..ce78b8a --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/frame/Runners.java @@ -0,0 +1,605 @@ +package me.topchetoeu.jscript.engine.frame; + +import java.util.Collections; + +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.DebugCommand; +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.SignalValue; +import me.topchetoeu.jscript.engine.values.Symbol; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class Runners { + public static final Object NO_RETURN = new Object(); + + public static Object execReturn(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.codePtr++; + return frame.pop(); + } + public static Object execSignal(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.codePtr++; + return new SignalValue(instr.get(0)); + } + public static Object execThrow(Instruction instr, CodeFrame frame, CallContext ctx) { + throw new EngineException(frame.pop()); + } + public static Object execThrowSyntax(Instruction instr, CodeFrame frame, CallContext ctx) { + throw EngineException.ofSyntax((String)instr.get(0)); + } + + private static Object call(DebugCommand state, CallContext ctx, Object func, Object thisArg, Object... args) throws InterruptedException { + ctx.setData(CodeFrame.STOP_AT_START_KEY, state == DebugCommand.STEP_INTO); + return Values.call(ctx, func, thisArg, args); + } + + public static Object execCall(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + int n = instr.get(0); + + var callArgs = new Object[n]; + for (var i = n - 1; i >= 0; i--) callArgs[i] = frame.pop(); + var func = frame.pop(); + var thisArg = frame.pop(); + + frame.push(call(state, ctx, func, thisArg, callArgs)); + + frame.codePtr++; + return NO_RETURN; + } + public static Object execCallNew(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + int n = instr.get(0); + + var callArgs = new Object[n]; + for (var i = n - 1; i >= 0; i--) callArgs[i] = frame.pop(); + var funcObj = frame.pop(); + + if (Values.isFunction(funcObj) && Values.function(funcObj).special) { + frame.push(call(state, ctx, funcObj, null, callArgs)); + } + else { + var proto = Values.getMember(ctx, funcObj, "prototype"); + var obj = new ObjectValue(); + obj.setPrototype(ctx, proto); + call(state, ctx, funcObj, obj, callArgs); + frame.push(obj); + } + + frame.codePtr++; + return NO_RETURN; + } + + public static Object execMakeVar(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var name = (String)instr.get(0); + frame.function.globals.define(name); + frame.codePtr++; + return NO_RETURN; + } + public static Object execDefProp(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var setter = frame.pop(); + var getter = frame.pop(); + var name = frame.pop(); + var obj = frame.pop(); + + if (getter != null && !Values.isFunction(getter)) throw EngineException.ofType("Getter must be a function or undefined."); + if (setter != null && !Values.isFunction(setter)) throw EngineException.ofType("Setter must be a function or undefined."); + if (!Values.isObject(obj)) throw EngineException.ofType("Property apply target must be an object."); + Values.object(obj).defineProperty(name, Values.function(getter), Values.function(setter), false, false); + + frame.push(obj); + frame.codePtr++; + return NO_RETURN; + } + public static Object execInstanceof(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var type = frame.pop(); + var obj = frame.pop(); + + if (!Values.isPrimitive(type)) { + var proto = Values.getMember(ctx, type, "prototype"); + frame.push(Values.isInstanceOf(ctx, obj, proto)); + } + else { + frame.push(false); + } + + frame.codePtr++; + return NO_RETURN; + } + public static Object execKeys(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var val = frame.pop(); + + var arr = new ObjectValue(); + var i = 0; + + var members = Values.getMembers(ctx, val, false, false); + Collections.reverse(members); + for (var el : members) { + if (el instanceof Symbol) continue; + arr.defineProperty(i++, el); + } + + arr.defineProperty("length", i); + + frame.push(arr); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execTry(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var finallyFunc = (boolean)instr.get(1) ? frame.pop() : null; + var catchFunc = (boolean)instr.get(0) ? frame.pop() : null; + var func = frame.pop(); + + if ( + !Values.isFunction(func) || + catchFunc != null && !Values.isFunction(catchFunc) || + finallyFunc != null && !Values.isFunction(finallyFunc) + ) throw EngineException.ofType("TRY instruction can be applied only upon functions."); + + Object res = new SignalValue("no_return"); + EngineException exception = null; + + Values.function(func).name = frame.function.name + "::try"; + if (catchFunc != null) Values.function(catchFunc).name = frame.function.name + "::catch"; + if (finallyFunc != null) Values.function(finallyFunc).name = frame.function.name + "::finally"; + + try { + ctx.setData(CodeFrame.STOP_AT_START_KEY, state != DebugCommand.NORMAL); + res = Values.call(ctx, func, frame.thisArg); + } + catch (EngineException e) { + exception = e.setCause(exception); + } + + if (exception != null && catchFunc != null) { + var exc = exception; + exception = null; + try { + ctx.setData(CodeFrame.STOP_AT_START_KEY, state != DebugCommand.NORMAL); + var _res = Values.call(ctx, catchFunc, frame.thisArg, exc); + if (!SignalValue.isSignal(_res, "no_return")) res = _res; + } + catch (EngineException e) { + exception = e.setCause(exc); + } + } + + if (finallyFunc != null) { + try { + ctx.setData(CodeFrame.STOP_AT_START_KEY, state != DebugCommand.NORMAL); + var _res = Values.call(ctx, finallyFunc, frame.thisArg); + if (!SignalValue.isSignal(_res, "no_return")) { + res = _res; + exception = null; + } + } + catch (EngineException e) { + exception = e.setCause(exception); + } + } + + if (exception != null) throw exception; + if (SignalValue.isSignal(res, "no_return")) { + frame.codePtr++; + return NO_RETURN; + } + else if (SignalValue.isSignal(res, "jmp_*")) { + frame.codePtr += Integer.parseInt(((SignalValue)res).data.substring(4)); + return NO_RETURN; + } + else return res; + } + + public static Object execDup(Instruction instr, CodeFrame frame, CallContext ctx) { + var val = frame.peek(instr.get(1)); + for (int i = 0; i < (int)instr.get(0); i++) { + frame.push(val); + } + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadUndefined(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.push(null); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadValue(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.push(instr.get(0)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadVar(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var i = instr.get(0); + + if (i instanceof String) frame.push(frame.function.globals.get(ctx, (String)i)); + else frame.push(frame.scope.get((int)i).get(ctx)); + + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadObj(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.push(new ObjectValue()); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadGlob(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.push(frame.function.globals.obj); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadArr(Instruction instr, CodeFrame frame, CallContext ctx) { + var res = new ArrayValue(); + res.setSize(instr.get(0)); + frame.push(res); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadFunc(Instruction instr, CodeFrame frame, CallContext ctx) { + int n = (Integer)instr.get(0); + int localsN = (Integer)instr.get(1); + int len = (Integer)instr.get(2); + var captures = new ValueVariable[instr.params.length - 3]; + + for (var i = 3; i < instr.params.length; i++) { + captures[i - 3] = frame.scope.get(instr.get(i)); + } + + var start = frame.codePtr + 1; + var end = start + n - 1; + var body = new Instruction[end - start]; + System.arraycopy(frame.function.body, start, body, 0, end - start); + + var func = new CodeFunction("", localsN, len, frame.function.globals, captures, body); + frame.push(func); + + frame.codePtr += n; + return NO_RETURN; + } + public static Object execLoadMember(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var key = frame.pop(); + var obj = frame.pop(); + + try { + ctx.setData(CodeFrame.STOP_AT_START_KEY, state == DebugCommand.STEP_INTO); + frame.push(Values.getMember(ctx, obj, key)); + } + catch (IllegalArgumentException e) { + throw EngineException.ofType(e.getMessage()); + } + frame.codePtr++; + return NO_RETURN; + } + public static Object execLoadKeyMember(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.push(instr.get(0)); + return execLoadMember(state, instr, frame, ctx); + } + public static Object execLoadRegEx(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.push(ctx.engine().makeRegex(instr.get(0), instr.get(1))); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execDiscard(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.pop(); + frame.codePtr++; + return NO_RETURN; + } + public static Object execStoreMember(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var val = frame.pop(); + var key = frame.pop(); + var obj = frame.pop(); + + ctx.setData(CodeFrame.STOP_AT_START_KEY, state == DebugCommand.STEP_INTO); + if (!Values.setMember(ctx, obj, key, val)) throw EngineException.ofSyntax("Can't set member '" + key + "'."); + if ((boolean)instr.get(0)) frame.push(val); + frame.codePtr++; + return NO_RETURN; + } + public static Object execStoreVar(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var val = (boolean)instr.get(1) ? frame.peek() : frame.pop(); + var i = instr.get(0); + + if (i instanceof String) frame.function.globals.set(ctx, (String)i, val); + else frame.scope.get((int)i).set(ctx, val); + + frame.codePtr++; + return NO_RETURN; + } + public static Object execStoreSelfFunc(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.scope.locals[(int)instr.get(0)].set(ctx, frame.function); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execJmp(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.codePtr += (int)instr.get(0); + return NO_RETURN; + } + public static Object execJmpIf(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.codePtr += Values.toBoolean(frame.pop()) ? (int)instr.get(0) : 1; + return NO_RETURN; + } + public static Object execJmpIfNot(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.codePtr += Values.not(frame.pop()) ? (int)instr.get(0) : 1; + return NO_RETURN; + } + + public static Object execIn(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var obj = frame.pop(); + var index = frame.pop(); + + frame.push(Values.hasMember(ctx, obj, index, false)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execTypeof(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + String name = instr.get(0); + Object obj; + + if (name != null) { + if (frame.function.globals.has(ctx, name)) { + obj = frame.function.globals.get(ctx, name); + } + else obj = null; + } + else obj = frame.pop(); + + frame.push(Values.type(obj)); + + frame.codePtr++; + return NO_RETURN; + } + public static Object execNop(Instruction instr, CodeFrame frame, CallContext ctx) { + if (instr.is(0, "dbg_names")) { + var names = new String[instr.params.length - 1]; + for (var i = 0; i < instr.params.length - 1; i++) { + if (!(instr.params[i + 1] instanceof String)) throw EngineException.ofSyntax("NOP dbg_names instruction must specify only string parameters."); + names[i] = (String)instr.params[i + 1]; + } + frame.scope.setNames(names); + } + + frame.codePtr++; + return NO_RETURN; + } + + public static Object execDelete(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + var key = frame.pop(); + var val = frame.pop(); + + if (!Values.deleteMember(ctx, val, key)) throw EngineException.ofSyntax("Can't delete member '" + key + "'."); + frame.push(true); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execAdd(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + frame.push(Values.add(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execSubtract(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + frame.push(Values.subtract(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execMultiply(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + frame.push(Values.multiply(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execDivide(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + frame.push(Values.divide(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execModulo(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + frame.push(Values.modulo(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execAnd(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.and(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execOr(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.or(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execXor(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.xor(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execLeftShift(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.shiftLeft(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execRightShift(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.shiftRight(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execUnsignedRightShift(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.unsignedShiftRight(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execNot(Instruction instr, CodeFrame frame, CallContext ctx) { + frame.push(Values.not(frame.pop())); + frame.codePtr++; + return NO_RETURN; + } + public static Object execNeg(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.push(Values.negative(ctx, frame.pop())); + frame.codePtr++; + return NO_RETURN; + } + public static Object execPos(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.push(Values.toNumber(ctx, frame.pop())); + frame.codePtr++; + return NO_RETURN; + } + public static Object execInverse(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + frame.push(Values.bitwiseNot(ctx, frame.pop())); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execGreaterThan(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.compare(ctx, a, b) > 0); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLessThan(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.compare(ctx, a, b) < 0); + frame.codePtr++; + return NO_RETURN; + } + public static Object execGreaterThanEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.compare(ctx, a, b) >= 0); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLessThanEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.compare(ctx, a, b) <= 0); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execLooseEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.looseEqual(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execLooseNotEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(!Values.looseEqual(ctx, a, b)); + frame.codePtr++; + return NO_RETURN; + } + + public static Object execEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(Values.strictEquals(a, b)); + frame.codePtr++; + return NO_RETURN; + } + public static Object execNotEquals(Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + Object b = frame.pop(), a = frame.pop(); + + frame.push(!Values.strictEquals(a, b)); + frame.codePtr++; + return NO_RETURN; + } + + public static Object exec(DebugCommand state, Instruction instr, CodeFrame frame, CallContext ctx) throws InterruptedException { + switch (instr.type) { + case NOP: return execNop(instr, frame, ctx); + case RETURN: return execReturn(instr, frame, ctx); + case SIGNAL: return execSignal(instr, frame, ctx); + case THROW: return execThrow(instr, frame, ctx); + case THROW_SYNTAX: return execThrowSyntax(instr, frame, ctx); + case CALL: return execCall(state, instr, frame, ctx); + case CALL_NEW: return execCallNew(state, instr, frame, ctx); + case TRY: return execTry(state, instr, frame, ctx); + + case DUP: return execDup(instr, frame, ctx); + case LOAD_VALUE: return execLoadValue(instr, frame, ctx); + case LOAD_VAR: return execLoadVar(instr, frame, ctx); + case LOAD_OBJ: return execLoadObj(instr, frame, ctx); + case LOAD_ARR: return execLoadArr(instr, frame, ctx); + case LOAD_FUNC: return execLoadFunc(instr, frame, ctx); + case LOAD_MEMBER: return execLoadMember(state, instr, frame, ctx); + case LOAD_VAL_MEMBER: return execLoadKeyMember(state, instr, frame, ctx); + case LOAD_REGEX: return execLoadRegEx(instr, frame, ctx); + case LOAD_GLOB: return execLoadGlob(instr, frame, ctx); + + case DISCARD: return execDiscard(instr, frame, ctx); + case STORE_MEMBER: return execStoreMember(state, instr, frame, ctx); + case STORE_VAR: return execStoreVar(instr, frame, ctx); + case STORE_SELF_FUNC: return execStoreSelfFunc(instr, frame, ctx); + case MAKE_VAR: return execMakeVar(instr, frame, ctx); + + case IN: return execIn(instr, frame, ctx); + case KEYS: return execKeys(instr, frame, ctx); + case DEF_PROP: return execDefProp(instr, frame, ctx); + case TYPEOF: return execTypeof(instr, frame, ctx); + case DELETE: return execDelete(instr, frame, ctx); + case INSTANCEOF: return execInstanceof(instr, frame, ctx); + + case JMP: return execJmp(instr, frame, ctx); + case JMP_IF: return execJmpIf(instr, frame, ctx); + case JMP_IFN: return execJmpIfNot(instr, frame, ctx); + + case ADD: return execAdd(instr, frame, ctx); + case SUBTRACT: return execSubtract(instr, frame, ctx); + case MULTIPLY: return execMultiply(instr, frame, ctx); + case DIVIDE: return execDivide(instr, frame, ctx); + case MODULO: return execModulo(instr, frame, ctx); + + case AND: return execAnd(instr, frame, ctx); + case OR: return execOr(instr, frame, ctx); + case XOR: return execXor(instr, frame, ctx); + + case SHIFT_LEFT: return execLeftShift(instr, frame, ctx); + case SHIFT_RIGHT: return execRightShift(instr, frame, ctx); + case USHIFT_RIGHT: return execUnsignedRightShift(instr, frame, ctx); + + case NOT: return execNot(instr, frame, ctx); + case NEG: return execNeg(instr, frame, ctx); + case POS: return execPos(instr, frame, ctx); + case INVERSE: return execInverse(instr, frame, ctx); + + case GREATER: return execGreaterThan(instr, frame, ctx); + case GREATER_EQUALS: return execGreaterThanEquals(instr, frame, ctx); + case LESS: return execLessThan(instr, frame, ctx); + case LESS_EQUALS: return execLessThanEquals(instr, frame, ctx); + + case LOOSE_EQUALS: return execLooseEquals(instr, frame, ctx); + case LOOSE_NOT_EQUALS: return execLooseNotEquals(instr, frame, ctx); + case EQUALS: return execEquals(instr, frame, ctx); + case NOT_EQUALS: return execNotEquals(instr, frame, ctx); + + default: throw EngineException.ofSyntax("Invalid instruction " + instr.type.name() + "."); + } + } +} diff --git a/src/me/topchetoeu/jscript/engine/modules/FileModuleProvider.java b/src/me/topchetoeu/jscript/engine/modules/FileModuleProvider.java new file mode 100644 index 0000000..59c1f41 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/modules/FileModuleProvider.java @@ -0,0 +1,50 @@ +package me.topchetoeu.jscript.engine.modules; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; + +import me.topchetoeu.jscript.polyfills.PolyfillEngine; + +public class FileModuleProvider implements ModuleProvider { + public File root; + public final boolean allowOutside; + + private boolean checkInside(Path modFile) { + return modFile.toAbsolutePath().startsWith(root.toPath().toAbsolutePath()); + } + + @Override + public Module getModule(File cwd, String name) { + var realName = getRealName(cwd, name); + if (realName == null) return null; + var path = Path.of(realName + ".js").normalize(); + + try { + var res = PolyfillEngine.streamToString(new FileInputStream(path.toFile())); + return new Module(realName, path.toString(), res); + } + catch (IOException e) { + return null; + } + } + @Override + public String getRealName(File cwd, String name) { + var path = Path.of(".", Path.of(cwd.toString(), name).normalize().toString()); + var fileName = path.getFileName().toString(); + if (fileName == null) return null; + if (!fileName.equals("index") && path.toFile().isDirectory()) return getRealName(cwd, name + "/index"); + path = Path.of(path.toString() + ".js"); + if (!allowOutside && !checkInside(path)) return null; + if (!path.toFile().isFile() || !path.toFile().canRead()) return null; + var res = path.toString().replace('\\', '/'); + var i = res.lastIndexOf('.'); + return res.substring(0, i); + } + + public FileModuleProvider(File root, boolean allowOutside) { + this.root = root.toPath().normalize().toFile(); + this.allowOutside = allowOutside; + } +} diff --git a/src/me/topchetoeu/jscript/engine/modules/Module.java b/src/me/topchetoeu/jscript/engine/modules/Module.java new file mode 100644 index 0000000..84458b1 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/modules/Module.java @@ -0,0 +1,57 @@ +package me.topchetoeu.jscript.engine.modules; + +import java.io.File; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.CallContext.DataKey; +import me.topchetoeu.jscript.engine.scope.Variable; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.interop.NativeGetter; +import me.topchetoeu.jscript.interop.NativeSetter; + +public class Module { + public class ExportsVariable implements Variable { + @Override + public boolean readonly() { return false; } + @Override + public Object get(CallContext ctx) { return exports; } + @Override + public void set(CallContext ctx, Object val) { exports = val; } + } + + public static DataKey KEY = new DataKey<>(); + + public final String filename; + public final String source; + public final String name; + private Object exports = new ObjectValue(); + private boolean executing = false; + + @NativeGetter("name") + public String name() { return name; } + @NativeGetter("exports") + public Object exports() { return exports; } + @NativeSetter("exports") + public void setExports(Object val) { exports = val; } + + public void execute(CallContext ctx) throws InterruptedException { + if (executing) return; + + executing = true; + var scope = ctx.engine().scope().globalChild(); + scope.define("module", true, this); + scope.define("exports", new ExportsVariable()); + + var parent = new File(filename).getParentFile(); + if (parent == null) parent = new File("."); + + ctx.engine().compile(scope, filename, source).call(ctx.copy().setData(KEY, this), null); + executing = false; + } + + public Module(String name, String filename, String source) { + this.name = name; + this.filename = filename; + this.source = source; + } +} diff --git a/src/me/topchetoeu/jscript/engine/modules/ModuleManager.java b/src/me/topchetoeu/jscript/engine/modules/ModuleManager.java new file mode 100644 index 0000000..e6e8645 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/modules/ModuleManager.java @@ -0,0 +1,80 @@ +package me.topchetoeu.jscript.engine.modules; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import me.topchetoeu.jscript.engine.CallContext; + +public class ModuleManager { + private final List providers = new ArrayList<>(); + private final HashMap cache = new HashMap<>(); + public final FileModuleProvider files; + + public void addProvider(ModuleProvider provider) { + this.providers.add(provider); + } + + public boolean isCached(File cwd, String name) { + name = name.replace("\\", "/"); + + // Absolute paths are forbidden + if (name.startsWith("/")) return false; + // Look for files if we have a relative path + if (name.startsWith("../") || name.startsWith("./")) { + var realName = files.getRealName(cwd, name); + if (cache.containsKey(realName)) return true; + else return false; + } + + for (var provider : providers) { + var realName = provider.getRealName(cwd, name); + if (realName == null) continue; + if (cache.containsKey(realName)) return true; + } + + return false; + } + public Module tryLoad(CallContext ctx, String name) throws InterruptedException { + name = name.replace('\\', '/'); + + var pcwd = Path.of("."); + + if (ctx.hasData(Module.KEY)) { + pcwd = Path.of(((Module)ctx.getData(Module.KEY)).filename).getParent(); + if (pcwd == null) pcwd = Path.of("."); + } + + + var cwd = pcwd.toFile(); + + if (name.startsWith("/")) return null; + if (name.startsWith("../") || name.startsWith("./")) { + var realName = files.getRealName(cwd, name); + if (realName == null) return null; + if (cache.containsKey(realName)) return cache.get(realName); + var mod = files.getModule(cwd, name); + // cache.put(mod.name(), mod); + mod.execute(ctx); + return mod; + } + + for (var provider : providers) { + var realName = provider.getRealName(cwd, name); + if (realName == null) continue; + if (cache.containsKey(realName)) return cache.get(realName); + var mod = provider.getModule(cwd, name); + // cache.put(mod.name(), mod); + mod.execute(ctx); + return mod; + } + + return null; + } + + public ModuleManager(File root) { + files = new FileModuleProvider(root, false); + } +} diff --git a/src/me/topchetoeu/jscript/engine/modules/ModuleProvider.java b/src/me/topchetoeu/jscript/engine/modules/ModuleProvider.java new file mode 100644 index 0000000..ad122c1 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/modules/ModuleProvider.java @@ -0,0 +1,9 @@ +package me.topchetoeu.jscript.engine.modules; + +import java.io.File; + +public interface ModuleProvider { + Module getModule(File cwd, String name); + String getRealName(File cwd, String name); + default boolean hasModule(File cwd, String name) { return getRealName(cwd, name) != null; } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java b/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java new file mode 100644 index 0000000..a14d881 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java @@ -0,0 +1,83 @@ +package me.topchetoeu.jscript.engine.scope; + +import java.util.HashSet; +import java.util.Set; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class GlobalScope implements ScopeRecord { + public final ObjectValue obj; + + public boolean has(CallContext ctx, String name) throws InterruptedException { + return obj.hasMember(ctx, name, false); + } + public Object getKey(String name) { + return name; + } + + public GlobalScope globalChild() { + return new GlobalScope(this); + } + public LocalScopeRecord child() { + return new LocalScopeRecord(this); + } + + public Object define(String name) { + try { + if (obj.hasMember(null, name, true)) return name; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return name; + } + obj.defineProperty(name, null); + return name; + } + public void define(String name, Variable val) { + obj.defineProperty(name, + new NativeFunction("get " + name, (ctx, th, a) -> val.get(ctx)), + new NativeFunction("set " + name, (ctx, th, args) -> { val.set(ctx, args.length > 0 ? args[0] : null); return null; }), + true, true + ); + } + public void define(String name, boolean readonly, Object val) { + obj.defineProperty(name, val, readonly, true, true); + } + public void define(String... names) { + for (var n : names) define(n); + } + public void define(boolean readonly, FunctionValue val) { + define(val.name, readonly, val); + } + + public Object get(CallContext ctx, String name) throws InterruptedException { + if (!obj.hasMember(ctx, name, false)) throw EngineException.ofSyntax("The variable '" + name + "' doesn't exist."); + else return obj.getMember(ctx, name); + } + public void set(CallContext ctx, String name, Object val) throws InterruptedException { + if (!obj.hasMember(ctx, name, false)) throw EngineException.ofSyntax("The variable '" + name + "' doesn't exist."); + if (!obj.setMember(ctx, name, val, false)) throw EngineException.ofSyntax("The global '" + name + "' is readonly."); + } + + public Set keys() { + var res = new HashSet(); + + for (var key : keys()) { + if (key instanceof String) res.add((String)key); + } + + return res; + } + + public GlobalScope() { + this.obj = new ObjectValue(); + } + public GlobalScope(GlobalScope parent) { + this.obj = new ObjectValue(); + this.obj.setPrototype(null, parent.obj); + } +} diff --git a/src/me/topchetoeu/jscript/engine/scope/LocalScope.java b/src/me/topchetoeu/jscript/engine/scope/LocalScope.java new file mode 100644 index 0000000..39457a8 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/scope/LocalScope.java @@ -0,0 +1,58 @@ +package me.topchetoeu.jscript.engine.scope; + +public class LocalScope { + private String[] names; + private LocalScope parent; + public final ValueVariable[] captures; + public final ValueVariable[] locals; + + public ValueVariable get(int i) { + if (i >= 0) return locals[i]; + else return captures[~i]; + } + + public String[] getNames() { + var res = new String[locals.length]; + + for (var i = 0; i < locals.length; i++) { + if (names == null || i >= names.length) res[i] = "local_" + i; + else res[i] = names[i]; + } + + return res; + } + public void setNames(String[] names) { + this.names = names; + } + + public int size() { + return captures.length + locals.length; + } + + public GlobalScope toGlobal(GlobalScope global) { + GlobalScope res; + + if (parent == null) res = new GlobalScope(global); + else res = new GlobalScope(parent.toGlobal(global)); + + var names = getNames(); + for (var i = 0; i < names.length; i++) { + res.define(names[i], locals[i]); + } + + return res; + } + + public LocalScope(int n, ValueVariable[] captures) { + locals = new ValueVariable[n]; + this.captures = captures; + + for (int i = 0; i < n; i++) { + locals[i] = new ValueVariable(false, null); + } + } + public LocalScope(int n, ValueVariable[] captures, LocalScope parent) { + this(n, captures); + this.parent = parent; + } +} diff --git a/src/me/topchetoeu/jscript/engine/scope/LocalScopeRecord.java b/src/me/topchetoeu/jscript/engine/scope/LocalScopeRecord.java new file mode 100644 index 0000000..0cb5ae1 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/scope/LocalScopeRecord.java @@ -0,0 +1,79 @@ +package me.topchetoeu.jscript.engine.scope; + +import java.util.ArrayList; + +import me.topchetoeu.jscript.engine.CallContext; + +public class LocalScopeRecord implements ScopeRecord { + public final LocalScopeRecord parent; + public final GlobalScope global; + + private final ArrayList captures = new ArrayList<>(); + private final ArrayList locals = new ArrayList<>(); + + public String[] locals() { + return locals.toArray(String[]::new); + } + + public LocalScopeRecord child() { + return new LocalScopeRecord(this, global); + } + + public int localsCount() { + return locals.size(); + } + public int capturesCount() { + return captures.size(); + } + + public int[] getCaptures() { + var buff = new int[captures.size()]; + var i = 0; + + for (var name : captures) { + var index = parent.getKey(name); + if (index instanceof Integer) buff[i++] = (int)index; + } + + var res = new int[i]; + System.arraycopy(buff, 0, res, 0, i); + + return res; + } + + public Object getKey(String name) { + var capI = captures.indexOf(name); + var locI = locals.indexOf(name); + if (locI >= 0) return locI; + if (capI >= 0) return ~capI; + if (parent != null) { + var res = parent.getKey(name); + if (res != null && res instanceof Integer) { + captures.add(name); + return -captures.size(); + } + } + + return name; + } + public boolean has(CallContext ctx, String name) throws InterruptedException { + return + global.has(ctx, name) || + locals.contains(name) || + parent != null && parent.has(ctx, name); + } + public Object define(String name) { + if (locals.contains(name)) return locals.indexOf(name); + locals.add(name); + return locals.size() - 1; + } + + public LocalScopeRecord(GlobalScope global) { + this.parent = null; + this.global = global; + } + public LocalScopeRecord(LocalScopeRecord parent, GlobalScope global) { + this.parent = parent; + this.global = global; + } +} diff --git a/src/me/topchetoeu/jscript/engine/scope/ScopeRecord.java b/src/me/topchetoeu/jscript/engine/scope/ScopeRecord.java new file mode 100644 index 0000000..fe4d5d3 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/scope/ScopeRecord.java @@ -0,0 +1,7 @@ +package me.topchetoeu.jscript.engine.scope; + +public interface ScopeRecord { + public Object getKey(String name); + public Object define(String name); + public LocalScopeRecord child(); +} diff --git a/src/me/topchetoeu/jscript/engine/scope/ValueVariable.java b/src/me/topchetoeu/jscript/engine/scope/ValueVariable.java new file mode 100644 index 0000000..ce34b9f --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/scope/ValueVariable.java @@ -0,0 +1,28 @@ +package me.topchetoeu.jscript.engine.scope; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.Values; + +public class ValueVariable implements Variable { + public boolean readonly; + public Object value; + + @Override + public boolean readonly() { return readonly; } + + @Override + public Object get(CallContext ctx) { + return value; + } + + @Override + public void set(CallContext ctx, Object val) { + if (readonly) return; + this.value = Values.normalize(val); + } + + public ValueVariable(boolean readonly, Object val) { + this.readonly = readonly; + this.value = val; + } +} diff --git a/src/me/topchetoeu/jscript/engine/scope/Variable.java b/src/me/topchetoeu/jscript/engine/scope/Variable.java new file mode 100644 index 0000000..83b724c --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/scope/Variable.java @@ -0,0 +1,9 @@ +package me.topchetoeu.jscript.engine.scope; + +import me.topchetoeu.jscript.engine.CallContext; + +public interface Variable { + Object get(CallContext ctx) throws InterruptedException; + default boolean readonly() { return true; } + default void set(CallContext ctx, Object val) throws InterruptedException { } +} diff --git a/src/me/topchetoeu/jscript/engine/values/ArrayValue.java b/src/me/topchetoeu/jscript/engine/values/ArrayValue.java new file mode 100644 index 0000000..b0e8e02 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/ArrayValue.java @@ -0,0 +1,160 @@ +package me.topchetoeu.jscript.engine.values; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import me.topchetoeu.jscript.engine.CallContext; + +public class ArrayValue extends ObjectValue { + private static final Object EMPTY = new Object(); + private final ArrayList values = new ArrayList<>(); + + public int size() { return values.size(); } + public boolean setSize(int val) { + if (val < 0) return false; + while (size() > val) { + values.remove(values.size() - 1); + } + while (size() < val) { + values.add(EMPTY); + } + return true; + } + + public Object get(int i) { + if (i < 0 || i >= values.size()) return null; + var res = values.get(i); + if (res == EMPTY) return null; + else return res; + } + public void set(int i, Object val) { + if (i < 0) return; + + while (values.size() <= i) { + values.add(EMPTY); + } + + values.set(i, Values.normalize(val)); + } + public boolean has(int i) { + return i >= 0 && i < values.size() && values.get(i) != EMPTY; + } + public void remove(int i) { + if (i < 0 || i >= values.size()) return; + values.set(i, EMPTY); + } + public void shrink(int n) { + if (n > values.size()) values.clear(); + else { + for (int i = 0; i < n && values.size() > 0; i++) { + values.remove(values.size() - 1); + } + } + } + + public void sort(Comparator comparator) { + values.sort((a, b) -> { + var _a = 0; + var _b = 0; + + if (a == null) _a = 1; + if (a == EMPTY) _a = 2; + + if (b == null) _b = 1; + if (b == EMPTY) _b = 2; + + if (Integer.compare(_a, _b) != 0) return Integer.compare(_a, _b); + + return comparator.compare(a, b); + }); + } + + public Object[] toArray() { + Object[] res = new Object[values.size()]; + + for (var i = 0; i < values.size(); i++) { + if (values.get(i) == EMPTY) res[i] = null; + else res[i] = values.get(i); + } + + return res; + } + + @Override + protected Object getField(CallContext ctx, Object key) throws InterruptedException { + if (key.equals("length")) return values.size(); + if (key instanceof Number) { + var i = ((Number)key).doubleValue(); + if (i >= 0 && i - Math.floor(i) == 0) { + return get((int)i); + } + } + + return super.getField(ctx, key); + } + @Override + protected boolean setField(CallContext ctx, Object key, Object val) throws InterruptedException { + if (key.equals("length")) { + return setSize((int)Values.toNumber(ctx, val)); + } + if (key instanceof Number) { + var i = Values.number(key); + if (i >= 0 && i - Math.floor(i) == 0) { + set((int)i, val); + return true; + } + } + + return super.setField(ctx, key, val); + } + @Override + protected boolean hasField(CallContext ctx, Object key) throws InterruptedException { + if (key.equals("length")) return true; + if (key instanceof Number) { + var i = Values.number(key); + if (i >= 0 && i - Math.floor(i) == 0) { + return has((int)i); + } + } + + return super.hasField(ctx, key); + } + @Override + protected void deleteField(CallContext ctx, Object key) throws InterruptedException { + if (key instanceof Number) { + var i = Values.number(key); + if (i >= 0 && i - Math.floor(i) == 0) { + remove((int)i); + return; + } + } + + super.deleteField(ctx, key); + } + + @Override + public List keys(boolean includeNonEnumerable) { + var res = super.keys(includeNonEnumerable); + for (var i = 0; i < size(); i++) { + if (has(i)) res.add(i); + } + if (includeNonEnumerable) res.add("length"); + return res; + } + + public ArrayValue() { + super(PlaceholderProto.ARRAY); + nonEnumerableSet.add("length"); + nonConfigurableSet.add("length"); + } + public ArrayValue(Object ...values) { + this(); + for (var i = 0; i < values.length; i++) this.values.add(values[i]); + } + + public static ArrayValue of(Collection values) { + return new ArrayValue(values.toArray(Object[]::new)); + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/CodeFunction.java b/src/me/topchetoeu/jscript/engine/values/CodeFunction.java new file mode 100644 index 0000000..9b93d96 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/CodeFunction.java @@ -0,0 +1,50 @@ +package me.topchetoeu.jscript.engine.values; + +import java.util.LinkedHashMap; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.frame.CodeFrame; +import me.topchetoeu.jscript.engine.scope.GlobalScope; +import me.topchetoeu.jscript.engine.scope.ValueVariable; + +public class CodeFunction extends FunctionValue { + public final int localsN; + public final int length; + public final Instruction[] body; + public final LinkedHashMap breakableLocToIndex = new LinkedHashMap<>(); + public final LinkedHashMap breakableIndexToLoc = new LinkedHashMap<>(); + public final ValueVariable[] captures; + public final GlobalScope globals; + + public Location loc() { + for (var instr : body) { + if (instr.location != null) return instr.location; + } + return null; + } + + @Override + public Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException { + return new CodeFrame(thisArg, args, this).run(ctx); + } + + public CodeFunction(String name, int localsN, int length, GlobalScope globals, ValueVariable[] captures, Instruction[] body) { + super(name, length); + this.captures = captures; + this.globals = globals; + this.localsN = localsN; + this.length = length; + this.body = body; + + for (var i = 0; i < body.length; i++) { + if (body[i].type == Type.LOAD_FUNC) i += (int)body[i].get(0); + if (body[i].debugged && body[i].location != null) { + breakableLocToIndex.put(body[i].location, i); + breakableIndexToLoc.put(i, body[i].location); + } + } + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/FunctionValue.java b/src/me/topchetoeu/jscript/engine/values/FunctionValue.java new file mode 100644 index 0000000..8a20164 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/FunctionValue.java @@ -0,0 +1,70 @@ +package me.topchetoeu.jscript.engine.values; + +import java.util.List; + +import me.topchetoeu.jscript.engine.CallContext; + +public abstract class FunctionValue extends ObjectValue { + public String name = ""; + public boolean special = false; + public int length; + + @Override + public String toString() { + return "function(...) { ... }"; + } + + public abstract Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException; + public Object call(CallContext ctx) throws InterruptedException { + return call(ctx, null); + } + + @Override + protected Object getField(CallContext ctx, Object key) throws InterruptedException { + if (key.equals("name")) return name; + if (key.equals("length")) return length; + return super.getField(ctx, key); + } + @Override + protected boolean setField(CallContext ctx, Object key, Object val) throws InterruptedException { + if (key.equals("name")) name = Values.toString(ctx, val); + else if (key.equals("length")) length = (int)Values.toNumber(ctx, val); + else return super.setField(ctx, key, val); + return true; + } + @Override + protected boolean hasField(CallContext ctx, Object key) throws InterruptedException { + if (key.equals("name")) return true; + if (key.equals("length")) return true; + return super.hasField(ctx, key); + } + + @Override + public List keys(boolean includeNonEnumerable) { + var res = super.keys(includeNonEnumerable); + if (includeNonEnumerable) { + res.add("name"); + res.add("length"); + } + return res; + } + + public FunctionValue(String name, int length) { + super(PlaceholderProto.FUNCTION); + + if (name == null) name = ""; + this.length = length; + this.name = name; + + nonConfigurableSet.add("name"); + nonEnumerableSet.add("name"); + nonWritableSet.add("length"); + nonConfigurableSet.add("length"); + nonEnumerableSet.add("length"); + + var proto = new ObjectValue(); + proto.defineProperty("constructor", this, true, false, false); + this.defineProperty("prototype", proto, true, false, false); + } +} + diff --git a/src/me/topchetoeu/jscript/engine/values/NativeFunction.java b/src/me/topchetoeu/jscript/engine/values/NativeFunction.java new file mode 100644 index 0000000..60c8691 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/NativeFunction.java @@ -0,0 +1,21 @@ +package me.topchetoeu.jscript.engine.values; + +import me.topchetoeu.jscript.engine.CallContext; + +public class NativeFunction extends FunctionValue { + public static interface NativeFunctionRunner { + Object run(CallContext ctx, Object thisArg, Object[] values) throws InterruptedException; + } + + public final NativeFunctionRunner action; + + @Override + public Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException { + return action.run(ctx, thisArg, args); + } + + public NativeFunction(String name, NativeFunctionRunner action) { + super(name, 0); + this.action = action; + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java b/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java new file mode 100644 index 0000000..7a5e1d0 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/NativeWrapper.java @@ -0,0 +1,19 @@ +package me.topchetoeu.jscript.engine.values; + +import me.topchetoeu.jscript.engine.CallContext; + +public class NativeWrapper extends ObjectValue { + private static final Object NATIVE_PROTO = new Object(); + public final Object wrapped; + + @Override + public ObjectValue getPrototype(CallContext ctx) throws InterruptedException { + if (prototype == NATIVE_PROTO) return ctx.engine.getPrototype(wrapped.getClass()); + else return super.getPrototype(ctx); + } + + public NativeWrapper(Object wrapped) { + this.wrapped = wrapped; + prototype = NATIVE_PROTO; + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/ObjectValue.java b/src/me/topchetoeu/jscript/engine/values/ObjectValue.java new file mode 100644 index 0000000..64d13f6 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/ObjectValue.java @@ -0,0 +1,331 @@ +package me.topchetoeu.jscript.engine.values; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.engine.CallContext; + +public class ObjectValue { + public static enum PlaceholderProto { + NONE, + OBJECT, + ARRAY, + FUNCTION, + ERROR, + SYNTAX_ERROR, + TYPE_ERROR, + RANGE_ERROR, + } + public static enum State { + NORMAL, + NO_EXTENSIONS, + SEALED, + FROZEN, + } + + public static record Property(FunctionValue getter, FunctionValue setter) {} + + private static final Object OBJ_PROTO = new Object(); + private static final Object ARR_PROTO = new Object(); + private static final Object FUNC_PROTO = new Object(); + private static final Object ERR_PROTO = new Object(); + private static final Object SYNTAX_ERR_PROTO = new Object(); + private static final Object TYPE_ERR_PROTO = new Object(); + private static final Object RANGE_ERR_PROTO = new Object(); + + protected Object prototype; + + public State state = State.NORMAL; + public HashMap values = new HashMap<>(); + public HashMap properties = new HashMap<>(); + public HashSet nonWritableSet = new HashSet<>(); + public HashSet nonConfigurableSet = new HashSet<>(); + public HashSet nonEnumerableSet = new HashSet<>(); + + public final boolean memberWritable(Object key) { + if (state == State.FROZEN) return false; + return !nonWritableSet.contains(key); + } + public final boolean memberConfigurable(Object key) { + if (state == State.SEALED || state == State.FROZEN) return false; + return !nonConfigurableSet.contains(key); + } + public final boolean memberEnumerable(Object key) { + return !nonEnumerableSet.contains(key); + } + public final boolean extensible() { + return state == State.NORMAL; + } + + public final void preventExtensions() { + if (state == State.NORMAL) state = State.NO_EXTENSIONS; + } + public final void seal() { + if (state == State.NORMAL || state == State.NO_EXTENSIONS) state = State.SEALED; + } + public final void freeze() { + state = State.FROZEN; + } + + public final boolean defineProperty(Object key, Object val, boolean writable, boolean configurable, boolean enumerable) { + key = Values.normalize(key); val = Values.normalize(val); + boolean reconfigured = + writable != memberWritable(key) || + configurable != memberConfigurable(key) || + enumerable != memberEnumerable(key); + + if (!reconfigured) { + if (!memberWritable(key)) { + var a = values.get(key); + var b = val; + if (a == null || b == null) return a == null && b == null; + return a == b || a.equals(b); + } + values.put(key, val); + return true; + } + + if ( + properties.containsKey(key) && + values.get(key) == val && + !reconfigured + ) return true; + + if (!extensible() && !values.containsKey(key) && !properties.containsKey(key)) return false; + if (!memberConfigurable(key)) + return false; + + nonWritableSet.remove(key); + nonEnumerableSet.remove(key); + properties.remove(key); + values.remove(key); + + if (!writable) nonWritableSet.add(key); + if (!configurable) nonConfigurableSet.add(key); + if (!enumerable) nonEnumerableSet.add(key); + + values.put(key, val); + return true; + } + public final boolean defineProperty(Object key, Object val) { + return defineProperty(Values.normalize(key), Values.normalize(val), true, true, true); + } + public final boolean defineProperty(Object key, FunctionValue getter, FunctionValue setter, boolean configurable, boolean enumerable) { + key = Values.normalize(key); + if ( + properties.containsKey(key) && + properties.get(key).getter == getter && + properties.get(key).setter == setter && + !configurable == nonConfigurableSet.contains(key) && + !enumerable == nonEnumerableSet.contains(key) + ) return true; + if (!extensible() && !values.containsKey(key) && !properties.containsKey(key)) return false; + if (!memberConfigurable(key)) return false; + + nonWritableSet.remove(key); + nonEnumerableSet.remove(key); + properties.remove(key); + values.remove(key); + + if (!configurable) nonConfigurableSet.add(key); + if (!enumerable) nonEnumerableSet.add(key); + + properties.put(key, new Property(getter, setter)); + return true; + } + + public ObjectValue getPrototype(CallContext ctx) throws InterruptedException { + try { + if (prototype == OBJ_PROTO) return ctx.engine().objectProto(); + if (prototype == ARR_PROTO) return ctx.engine().arrayProto(); + if (prototype == FUNC_PROTO) return ctx.engine().functionProto(); + if (prototype == ERR_PROTO) return ctx.engine().errorProto(); + if (prototype == RANGE_ERR_PROTO) return ctx.engine().rangeErrorProto(); + if (prototype == SYNTAX_ERR_PROTO) return ctx.engine().syntaxErrorProto(); + if (prototype == TYPE_ERR_PROTO) return ctx.engine().typeErrorProto(); + } + catch (NullPointerException e) { + return null; + } + + return (ObjectValue)prototype; + } + public final boolean setPrototype(CallContext ctx, Object val) { + val = Values.normalize(val); + + if (!extensible()) return false; + if (val == null || val == Values.NULL) prototype = null; + else if (Values.isObject(val)) { + var obj = Values.object(val); + + if (ctx != null && ctx.engine() != null) { + if (obj == ctx.engine().objectProto()) prototype = OBJ_PROTO; + else if (obj == ctx.engine().arrayProto()) prototype = ARR_PROTO; + else if (obj == ctx.engine().functionProto()) prototype = FUNC_PROTO; + else if (obj == ctx.engine().errorProto()) prototype = ERR_PROTO; + else if (obj == ctx.engine().syntaxErrorProto()) prototype = SYNTAX_ERR_PROTO; + else if (obj == ctx.engine().typeErrorProto()) prototype = TYPE_ERR_PROTO; + else if (obj == ctx.engine().rangeErrorProto()) prototype = RANGE_ERR_PROTO; + else prototype = obj; + } + else prototype = obj; + + return true; + } + return false; + } + public final boolean setPrototype(PlaceholderProto val) { + if (!extensible()) return false; + switch (val) { + case OBJECT: prototype = OBJ_PROTO; break; + case FUNCTION: prototype = FUNC_PROTO; break; + case ARRAY: prototype = ARR_PROTO; break; + case ERROR: prototype = ERR_PROTO; break; + case SYNTAX_ERROR: prototype = SYNTAX_ERR_PROTO; break; + case TYPE_ERROR: prototype = TYPE_ERR_PROTO; break; + case RANGE_ERROR: prototype = RANGE_ERR_PROTO; break; + case NONE: prototype = null; break; + } + return true; + } + + protected Property getProperty(CallContext ctx, Object key) throws InterruptedException { + if (properties.containsKey(key)) return properties.get(key); + var proto = getPrototype(ctx); + if (proto != null) return proto.getProperty(ctx, key); + else return null; + } + protected Object getField(CallContext ctx, Object key) throws InterruptedException { + if (values.containsKey(key)) return values.get(key); + var proto = getPrototype(ctx); + if (proto != null) return proto.getField(ctx, key); + else return null; + } + protected boolean setField(CallContext ctx, Object key, Object val) throws InterruptedException { + values.put(key, val); + return true; + } + protected void deleteField(CallContext ctx, Object key) throws InterruptedException { + values.remove(key); + } + protected boolean hasField(CallContext ctx, Object key) throws InterruptedException { + return values.containsKey(key); + } + + public final Object getMember(CallContext ctx, Object key, Object thisArg) throws InterruptedException { + key = Values.normalize(key); + + if (key.equals("__proto__")) { + var res = getPrototype(ctx); + return res == null ? Values.NULL : res; + } + + var prop = getProperty(ctx, key); + + if (prop != null) { + if (prop.getter == null) return null; + else return prop.getter.call(ctx, Values.normalize(thisArg)); + } + else return getField(ctx, key); + } + public final Object getMember(CallContext ctx, Object key) throws InterruptedException { + return getMember(ctx, key, this); + } + + public final boolean setMember(CallContext ctx, Object key, Object val, Object thisArg, boolean onlyProps) throws InterruptedException { + key = Values.normalize(key); val = Values.normalize(val); + + var prop = getProperty(ctx, key); + if (prop != null) { + if (prop.setter == null) return false; + prop.setter.call(ctx, Values.normalize(thisArg), val); + return true; + } + else if (onlyProps) return false; + else if (!extensible() && !values.containsKey(key)) return false; + else if (key == null) { + values.put(key, val); + return true; + } + else if (key.equals("__proto__")) return setPrototype(ctx, val); + else if (nonWritableSet.contains(key)) return false; + else return setField(ctx, key, val); + } + public final boolean setMember(CallContext ctx, Object key, Object val, boolean onlyProps) throws InterruptedException { + return setMember(ctx, Values.normalize(key), Values.normalize(val), this, onlyProps); + } + + public final boolean hasMember(CallContext ctx, Object key, boolean own) throws InterruptedException { + key = Values.normalize(key); + + if (key != null && key.equals("__proto__")) return true; + if (hasField(ctx, key)) return true; + if (properties.containsKey(key)) return true; + if (own) return false; + return prototype != null && getPrototype(ctx).hasMember(ctx, key, own); + } + public final boolean deleteMember(CallContext ctx, Object key) throws InterruptedException { + key = Values.normalize(key); + + if (!memberConfigurable(key)) return false; + properties.remove(key); + nonWritableSet.remove(key); + nonEnumerableSet.remove(key); + deleteField(ctx, key); + return true; + } + + public final ObjectValue getMemberDescriptor(CallContext ctx, Object key) throws InterruptedException { + key = Values.normalize(key); + + var prop = properties.get(key); + var res = new ObjectValue(); + + res.defineProperty("configurable", memberConfigurable(key)); + res.defineProperty("enumerable", memberEnumerable(key)); + + if (prop != null) { + res.defineProperty("get", prop.getter); + res.defineProperty("set", prop.setter); + } + else if (hasField(ctx, key)) { + res.defineProperty("value", values.get(key)); + res.defineProperty("writable", memberWritable(key)); + } + else return null; + return res; + } + + public List keys(boolean includeNonEnumerable) { + var res = new ArrayList(); + + for (var key : values.keySet()) { + if (nonEnumerableSet.contains(key) && !includeNonEnumerable) continue; + res.add(key); + } + for (var key : properties.keySet()) { + if (nonEnumerableSet.contains(key) && !includeNonEnumerable) continue; + res.add(key); + } + + return res; + } + + public ObjectValue(Map values) { + this(PlaceholderProto.OBJECT); + for (var el : values.entrySet()) { + defineProperty(el.getKey(), el.getValue()); + } + } + public ObjectValue(PlaceholderProto proto) { + nonConfigurableSet.add("__proto__"); + nonEnumerableSet.add("__proto__"); + setPrototype(proto); + } + public ObjectValue() { + this(PlaceholderProto.OBJECT); + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/SignalValue.java b/src/me/topchetoeu/jscript/engine/values/SignalValue.java new file mode 100644 index 0000000..2c2ad2a --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/SignalValue.java @@ -0,0 +1,17 @@ +package me.topchetoeu.jscript.engine.values; + +public final class SignalValue { + public final String data; + + public SignalValue(String data) { + this.data = data; + } + + public static boolean isSignal(Object signal, String value) { + if (!(signal instanceof SignalValue)) return false; + var val = ((SignalValue)signal).data; + + if (value.endsWith("*")) return val.startsWith(value.substring(0, value.length() - 1)); + else return val.equals(value); + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/Symbol.java b/src/me/topchetoeu/jscript/engine/values/Symbol.java new file mode 100644 index 0000000..5715430 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/Symbol.java @@ -0,0 +1,15 @@ +package me.topchetoeu.jscript.engine.values; + +public final class Symbol { + public final String value; + + public Symbol(String value) { + this.value = value; + } + + @Override + public String toString() { + if (value == null) return "Symbol"; + else return "Symbol(" + value + ")"; + } +} diff --git a/src/me/topchetoeu/jscript/engine/values/Values.java b/src/me/topchetoeu/jscript/engine/values/Values.java new file mode 100644 index 0000000..e5e8399 --- /dev/null +++ b/src/me/topchetoeu/jscript/engine/values/Values.java @@ -0,0 +1,608 @@ +package me.topchetoeu.jscript.engine.values; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.frame.ConvertHint; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class Values { + public static final Object NULL = new Object(); + + public static boolean isObject(Object val) { return val instanceof ObjectValue; } + public static boolean isFunction(Object val) { return val instanceof FunctionValue; } + public static boolean isArray(Object val) { return val instanceof ArrayValue; } + public static boolean isWrapper(Object val) { return val instanceof NativeWrapper; } + public static boolean isWrapper(Object val, Class clazz) { + if (!isWrapper(val)) return false; + var res = (NativeWrapper)val; + return res != null && clazz.isInstance(res.wrapped); + } + public static boolean isNan(Object val) { return val instanceof Number && Double.isNaN(number(val)); } + + public static ObjectValue object(Object val) { + if (val instanceof ObjectValue) return (ObjectValue)val; + else return null; + } + public static ArrayValue array(Object val) { + if (val instanceof ArrayValue) return (ArrayValue)val; + else return null; + } + public static FunctionValue function(Object val) { + if (val instanceof FunctionValue) return (FunctionValue)val; + else return null; + } + public static double number(Object val) { + if (val instanceof Number) return ((Number)val).doubleValue(); + else return Double.NaN; + } + + @SuppressWarnings("unchecked") + public static T wrapper(Object val, Class clazz) { + if (!isWrapper(val)) return null; + + var res = (NativeWrapper)val; + if (res != null && clazz.isInstance(res.wrapped)) return (T)res.wrapped; + else return null; + } + + public static String type(Object val) { + if (val == null) return "undefined"; + if (val instanceof String) return "string"; + if (val instanceof Number) return "number"; + if (val instanceof Boolean) return "boolean"; + if (val instanceof Symbol) return "symbol"; + if (val instanceof FunctionValue) return "function"; + return "object"; + } + + private static Object tryCallConvertFunc(CallContext ctx, Object obj, String name) throws InterruptedException { + var func = getMember(ctx, obj, name); + + if (func != null) { + var res = ((FunctionValue)func).call(ctx, obj); + if (isPrimitive(res)) return res; + } + + throw EngineException.ofType("Value couldn't be converted to a primitive."); + } + + public static boolean isPrimitive(Object obj) { + return + obj instanceof Number || + obj instanceof String || + obj instanceof Boolean || + obj instanceof Symbol || + obj instanceof SignalValue || + obj == null || + obj == NULL; + } + + public static Object toPrimitive(CallContext ctx, Object obj, ConvertHint hint) throws InterruptedException { + obj = normalize(obj); + if (isPrimitive(obj)) return obj; + + var first = hint == ConvertHint.VALUEOF ? "valueOf" : "toString"; + var second = hint == ConvertHint.VALUEOF ? "toString" : "valueOf"; + + if (ctx != null) { + try { + return tryCallConvertFunc(ctx, obj, first); + } + catch (EngineException unused) { + return tryCallConvertFunc(ctx, obj, second); + } + } + + throw EngineException.ofType("Value couldn't be converted to a primitive."); + } + public static boolean toBoolean(Object obj) { + if (obj == NULL || obj == null) return false; + if (obj instanceof Number && number(obj) == 0) return false; + if (obj instanceof String && ((String)obj).equals("")) return false; + if (obj instanceof Boolean) return (Boolean)obj; + return true; + } + public static double toNumber(CallContext ctx, Object obj) throws InterruptedException { + var val = toPrimitive(ctx, obj, ConvertHint.VALUEOF); + + if (val instanceof Number) return number(obj); + if (val instanceof Boolean) return ((Boolean)obj) ? 1 : 0; + if (val instanceof String) { + try { + return Double.parseDouble((String)val); + } + catch (NumberFormatException e) { } + } + return Double.NaN; + } + public static String toString(CallContext ctx, Object obj) throws InterruptedException { + var val = toPrimitive(ctx, obj, ConvertHint.VALUEOF); + + if (val == null) return "undefined"; + if (val == NULL) return "null"; + + if (val instanceof Number) { + var d = number(obj); + if (d == Double.NEGATIVE_INFINITY) return "-Infinity"; + if (d == Double.POSITIVE_INFINITY) return "Infinity"; + if (Double.isNaN(d)) return "NaN"; + return BigDecimal.valueOf(d).stripTrailingZeros().toPlainString(); + } + if (val instanceof Boolean) return (Boolean)val ? "true" : "false"; + if (val instanceof String) return (String)val; + if (val instanceof Symbol) return ((Symbol)val).toString(); + if (val instanceof SignalValue) return "[signal '" + ((SignalValue)val).data + "']"; + + return "Unknown value"; + } + + public static Object add(CallContext ctx, Object a, Object b) throws InterruptedException { + if (a instanceof String || b instanceof String) return toString(ctx, a) + toString(ctx, b); + else return toNumber(ctx, a) + toNumber(ctx, b); + } + public static double subtract(CallContext ctx, Object a, Object b) throws InterruptedException { + return toNumber(ctx, a) - toNumber(ctx, b); + } + public static double multiply(CallContext ctx, Object a, Object b) throws InterruptedException { + return toNumber(ctx, a) * toNumber(ctx, b); + } + public static double divide(CallContext ctx, Object a, Object b) throws InterruptedException { + return toNumber(ctx, a) / toNumber(ctx, b); + } + public static double modulo(CallContext ctx, Object a, Object b) throws InterruptedException { + return toNumber(ctx, a) % toNumber(ctx, b); + } + + public static double negative(CallContext ctx, Object obj) throws InterruptedException { + return -toNumber(ctx, obj); + } + + public static int and(CallContext ctx, Object a, Object b) throws InterruptedException { + return (int)toNumber(ctx, a) & (int)toNumber(ctx, b); + } + public static int or(CallContext ctx, Object a, Object b) throws InterruptedException { + return (int)toNumber(ctx, a) | (int)toNumber(ctx, b); + } + public static int xor(CallContext ctx, Object a, Object b) throws InterruptedException { + return (int)toNumber(ctx, a) ^ (int)toNumber(ctx, b); + } + public static int bitwiseNot(CallContext ctx, Object obj) throws InterruptedException { + return ~(int)toNumber(ctx, obj); + } + + public static int shiftLeft(CallContext ctx, Object a, Object b) throws InterruptedException { + return (int)toNumber(ctx, a) << (int)toNumber(ctx, b); + } + public static int shiftRight(CallContext ctx, Object a, Object b) throws InterruptedException { + return (int)toNumber(ctx, a) >> (int)toNumber(ctx, b); + } + public static long unsignedShiftRight(CallContext ctx, Object a, Object b) throws InterruptedException { + long _a = (long)toNumber(ctx, a); + long _b = (long)toNumber(ctx, b); + + if (_a < 0) _a += 0x100000000l; + if (_b < 0) _b += 0x100000000l; + return _a >>> _b; + } + + public static int compare(CallContext ctx, Object a, Object b) throws InterruptedException { + a = toPrimitive(ctx, a, ConvertHint.VALUEOF); + b = toPrimitive(ctx, b, ConvertHint.VALUEOF); + + if (a instanceof String && b instanceof String) return ((String)a).compareTo((String)b); + else return Double.compare(toNumber(ctx, a), toNumber(ctx, b)); + } + + public static boolean not(Object obj) { + return !toBoolean(obj); + } + + public static boolean isInstanceOf(CallContext ctx, Object obj, Object proto) throws InterruptedException { + if (obj == null || obj == NULL || proto == null || proto == NULL) return false; + var val = getPrototype(ctx, obj); + + while (val != null) { + if (val.equals(proto)) return true; + val = val.getPrototype(ctx); + } + + return false; + } + + public static Object getMember(CallContext ctx, Object obj, Object key) throws InterruptedException { + obj = normalize(obj); key = normalize(key); + if (obj == null) throw new IllegalArgumentException("Tried to access member of undefined."); + if (obj == NULL) throw new IllegalArgumentException("Tried to access member of null."); + if (isObject(obj)) return object(obj).getMember(ctx, key); + + if (obj instanceof String && key instanceof Number) { + var i = number(key); + var s = (String)obj; + if (i >= 0 && i < s.length() && i - Math.floor(i) == 0) { + return s.charAt((int)i) + ""; + } + } + + var proto = getPrototype(ctx, obj); + + if (proto == null) return key.equals("__proto__") ? NULL : null; + else if (key != null && key.equals("__proto__")) return proto; + else return proto.getMember(ctx, key, obj); + } + public static boolean setMember(CallContext ctx, Object obj, Object key, Object val) throws InterruptedException { + obj = normalize(obj); key = normalize(key); val = normalize(val); + if (obj == null) throw EngineException.ofType("Tried to access member of undefined."); + if (obj == NULL) throw EngineException.ofType("Tried to access member of null."); + if (key.equals("__proto__")) return setPrototype(ctx, obj, val); + if (isObject(obj)) return object(obj).setMember(ctx, key, val, false); + + var proto = getPrototype(ctx, obj); + return proto.setMember(ctx, key, val, obj, true); + } + public static boolean hasMember(CallContext ctx, Object obj, Object key, boolean own) throws InterruptedException { + if (obj == null || obj == NULL) return false; + obj = normalize(obj); key = normalize(key); + + if (key.equals("__proto__")) return true; + if (isObject(obj)) return object(obj).hasMember(ctx, key, own); + + if (obj instanceof String && key instanceof Number) { + var i = number(key); + var s = (String)obj; + if (i >= 0 && i < s.length() && i - Math.floor(i) == 0) return true; + } + + if (own) return false; + + var proto = getPrototype(ctx, obj); + return proto != null && proto.hasMember(ctx, key, own); + } + public static boolean deleteMember(CallContext ctx, Object obj, Object key) throws InterruptedException { + if (obj == null || obj == NULL) return false; + obj = normalize(obj); key = normalize(key); + + if (isObject(obj)) return object(obj).deleteMember(ctx, key); + else return false; + } + public static ObjectValue getPrototype(CallContext ctx, Object obj) throws InterruptedException { + if (obj == null || obj == NULL) return null; + obj = normalize(obj); + if (isObject(obj)) return object(obj).getPrototype(ctx); + if (ctx == null) return null; + + if (obj instanceof String) return ctx.engine().stringProto(); + else if (obj instanceof Number) return ctx.engine().numberProto(); + else if (obj instanceof Boolean) return ctx.engine().booleanProto(); + else if (obj instanceof Symbol) return ctx.engine().symbolProto(); + + return null; + } + public static boolean setPrototype(CallContext ctx, Object obj, Object proto) throws InterruptedException { + obj = normalize(obj); proto = normalize(proto); + return isObject(obj) && object(obj).setPrototype(ctx, proto); + } + public static List getMembers(CallContext ctx, Object obj, boolean own, boolean includeNonEnumerable) throws InterruptedException { + List res = new ArrayList<>(); + + if (isObject(obj)) res = object(obj).keys(includeNonEnumerable); + if (obj instanceof String) { + for (var i = 0; i < ((String)obj).length(); i++) res.add((double)i); + } + + if (!own) { + var proto = getPrototype(ctx, obj); + + while (proto != null) { + res.addAll(proto.keys(includeNonEnumerable)); + proto = proto.getPrototype(ctx); + } + } + + + return res; + } + + public static Object call(CallContext ctx, Object func, Object thisArg, Object ...args) throws InterruptedException { + if (!isFunction(func)) throw EngineException.ofType("Attempted to call a non-function value."); + return function(func).call(ctx, thisArg, args); + } + + public static boolean strictEquals(Object a, Object b) { + a = normalize(a); b = normalize(b); + + if (a == null || b == null) return a == null && b == null; + if (isNan(a) || isNan(b)) return false; + if (a instanceof Number && number(a) == -0.) a = 0.; + if (b instanceof Number && number(b) == -0.) b = 0.; + + return a == b || a.equals(b); + } + public static boolean looseEqual(CallContext ctx, Object a, Object b) throws InterruptedException { + a = normalize(a); b = normalize(b); + + // In loose equality, null is equivalent to undefined + if (a == NULL) a = null; + if (b == NULL) b = null; + + if (a == null || b == null) return a == null && b == null; + // If both are objects, just compare their references + if (!isPrimitive(a) && !isPrimitive(b)) return a == b; + + // Convert values to primitives + a = toPrimitive(ctx, a, ConvertHint.VALUEOF); + b = toPrimitive(ctx, b, ConvertHint.VALUEOF); + + // Compare symbols by reference + if (a instanceof Symbol || b instanceof Symbol) return a == b; + if (a instanceof Boolean || b instanceof Boolean) return toBoolean(a) == toBoolean(b); + if (a instanceof Number || b instanceof Number) return strictEquals(toNumber(ctx, a), toNumber(ctx, b)); + + // Default to strings + return toString(ctx, a).equals(toString(ctx, b)); + } + + public static Object normalize(Object val) { + if (val instanceof Number) return number(val); + if (isPrimitive(val) || val instanceof ObjectValue) return val; + if (val instanceof Character) return val + ""; + + if (val instanceof Map) { + var res = new ObjectValue(); + + for (var entry : ((Map)val).entrySet()) { + res.defineProperty(entry.getKey(), entry.getValue()); + } + + return res; + } + + if (val instanceof Iterable) { + var res = new ArrayValue(); + + for (var entry : ((Iterable)val)) { + res.set(res.size(), entry); + } + + return res; + } + + return new NativeWrapper(val); + } + + @SuppressWarnings("unchecked") + public static T convert(CallContext ctx, Object obj, Class clazz) throws InterruptedException { + if (clazz == Void.class) return null; + if (clazz == null || clazz == Object.class) return (T)obj; + + var err = new IllegalArgumentException("Cannot convert '%s' to '%s'.".formatted(type(obj), clazz.getName())); + + if (obj instanceof NativeWrapper) { + var res = ((NativeWrapper)obj).wrapped; + if (clazz.isInstance(res)) return (T)res; + } + + if (obj instanceof ArrayValue) { + var raw = array(obj).toArray(); + + if (clazz.isAssignableFrom(ArrayList.class)) { + var res = new ArrayList<>(); + for (var i = 0; i < raw.length; i++) res.add(convert(ctx, raw[i], Object.class)); + return (T)new ArrayList<>(res); + } + if (clazz.isAssignableFrom(HashSet.class)) { + var res = new HashSet<>(); + for (var i = 0; i < raw.length; i++) res.add(convert(ctx, raw[i], Object.class)); + return (T)new HashSet<>(res); + } + if (clazz.isArray()) { + Object res = Array.newInstance(clazz.arrayType(), raw.length); + for (var i = 0; i < raw.length; i++) Array.set(res, i, convert(ctx, raw[i], Object.class)); + return (T)res; + } + } + + if (obj instanceof ObjectValue && clazz.isAssignableFrom(HashMap.class)) { + var res = new HashMap<>(); + for (var el : object(obj).values.entrySet()) res.put( + convert(ctx, el.getKey(), null), + convert(ctx, el.getValue(), null) + ); + return (T)res; + } + + if (clazz == String.class) return (T)toString(ctx, obj); + if (clazz == Boolean.class || clazz == Boolean.TYPE) return (T)(Boolean)toBoolean(obj); + if (clazz == Byte.class || clazz == byte.class) return (T)(Byte)(byte)toNumber(ctx, obj); + if (clazz == Integer.class || clazz == int.class) return (T)(Integer)(int)toNumber(ctx, obj); + if (clazz == Long.class || clazz == long.class) return (T)(Long)(long)toNumber(ctx, obj); + if (clazz == Short.class || clazz == short.class) return (T)(Short)(short)toNumber(ctx, obj); + if (clazz == Float.class || clazz == float.class) return (T)(Float)(float)toNumber(ctx, obj); + if (clazz == Double.class || clazz == double.class) return (T)(Double)toNumber(ctx, obj); + + if (clazz == Character.class || clazz == char.class) { + if (obj instanceof Number) return (T)(Character)(char)number(obj); + else if (obj == NULL) throw new IllegalArgumentException("Cannot convert null to character."); + else if (obj == null) throw new IllegalArgumentException("Cannot convert undefined to character."); + else { + var res = toString(ctx, obj); + if (res.length() == 0) throw new IllegalArgumentException("Cannot convert empty string to character."); + else return (T)(Character)res.charAt(0); + } + } + + if (obj == null) return null; + if (clazz.isInstance(obj)) return (T)obj; + + throw err; + } + + public static Iterable toJavaIterable(CallContext ctx, Object obj) throws InterruptedException { + return () -> { + try { + var constr = getMember(ctx, ctx.engine().symbolProto(), "constructor"); + var symbol = getMember(ctx, constr, "iterator"); + + var iteratorFunc = getMember(ctx, obj, symbol); + if (!isFunction(iteratorFunc)) return Collections.emptyIterator(); + var iterator = getMember(ctx, call(ctx, iteratorFunc, obj), "next"); + if (!isFunction(iterator)) return Collections.emptyIterator(); + var iterable = obj; + + return new Iterator() { + private Object value = null; + public boolean consumed = true; + private FunctionValue next = function(iterator); + + private void loadNext() throws InterruptedException { + if (next == null) value = null; + else if (consumed) { + var curr = object(next.call(ctx, iterable)); + if (curr == null) { next = null; value = null; } + if (toBoolean(curr.getMember(ctx, "done"))) { next = null; value = null; } + else { + this.value = curr.getMember(ctx, "value"); + consumed = false; + } + } + } + + @Override + public boolean hasNext() { + try { + loadNext(); + return next != null; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + @Override + public Object next() { + try { + loadNext(); + var res = value; + value = null; + consumed = true; + return res; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } + }; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + catch (IllegalArgumentException | NullPointerException e) { + return Collections.emptyIterator(); + } + }; + } + + public static ObjectValue fromJavaIterable(CallContext ctx, Iterable iterable) throws InterruptedException { + var res = new ObjectValue(); + var it = iterable.iterator(); + + try { + var key = getMember(ctx, getMember(ctx, ctx.engine().symbolProto(), "constructor"), "iterable"); + res.defineProperty(key, new NativeFunction("", (_ctx, thisArg, args) -> fromJavaIterable(ctx, iterable))); + } + catch (IllegalArgumentException | NullPointerException e) { } + + res.defineProperty("next", new NativeFunction("", (_ctx, _th, _args) -> { + if (!it.hasNext()) return new ObjectValue(Map.of("done", true)); + else return new ObjectValue(Map.of("value", it.next())); + })); + + return res; + } + + private static void printValue(CallContext ctx, Object val, HashSet passed, int tab) throws InterruptedException { + if (passed.contains(val)) { + System.out.print("[circular]"); + return; + } + + var printed = true; + + if (val instanceof FunctionValue) { + System.out.print("function "); + var name = Values.getMember(ctx, val, "name"); + if (name != null) System.out.print(Values.toString(ctx, name)); + System.out.print("(...)"); + var loc = val instanceof CodeFunction ? ((CodeFunction)val).loc() : null; + + if (loc != null) System.out.print(" @ " + loc); + } + else if (val instanceof ArrayValue) { + System.out.print("["); + var obj = ((ArrayValue)val); + for (int i = 0; i < obj.size(); i++) { + if (i != 0) System.out.print(", "); + else System.out.print(" "); + if (obj.has(i)) printValue(ctx, obj.get(i), passed, tab); + else System.out.print(", "); + } + System.out.print(" ] "); + } + else if (val instanceof NativeWrapper) { + var obj = ((NativeWrapper)val).wrapped; + System.out.print("Native " + obj.toString() + " "); + } + else printed = false; + + if (val instanceof ObjectValue) { + if (tab > 3) { + System.out.print("{...}"); + return; + } + passed.add(val); + + var obj = (ObjectValue)val; + if (obj.values.size() + obj.properties.size() == 0) { + if (!printed) System.out.println("{}"); + } + else { + System.out.println("{"); + + for (var el : obj.values.entrySet()) { + for (int i = 0; i < tab + 1; i++) System.out.print(" "); + printValue(ctx, el.getKey(), passed, tab + 1); + System.out.print(": "); + printValue(ctx, el.getValue(), passed, tab + 1); + System.out.println(","); + } + for (var el : obj.properties.entrySet()) { + for (int i = 0; i < tab + 1; i++) System.out.print(" "); + printValue(ctx, el.getKey(), passed, tab + 1); + System.out.println(": [prop],"); + } + + for (int i = 0; i < tab; i++) System.out.print(" "); + System.out.print("}"); + + passed.remove(val); + } + } + else if (val == null) System.out.print("undefined"); + else if (val == Values.NULL) System.out.print("null"); + else if (val instanceof String) System.out.print("'" + val + "'"); + else System.out.print(Values.toString(ctx, val)); + } + public static void printValue(CallContext ctx, Object val) throws InterruptedException { + printValue(ctx, val, new HashSet<>(), 0); + } +} diff --git a/src/me/topchetoeu/jscript/events/Awaitable.java b/src/me/topchetoeu/jscript/events/Awaitable.java new file mode 100644 index 0000000..7583892 --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Awaitable.java @@ -0,0 +1,25 @@ +package me.topchetoeu.jscript.events; + +public interface Awaitable { + T await() throws FinishedException, InterruptedException; + + default Observable toObservable() { + return sub -> { + var thread = new Thread(() -> { + try { + sub.next(await()); + sub.finish(); + } + catch (InterruptedException | FinishedException e) { + sub.finish(); + } + catch (RuntimeException e) { + sub.error(e); + } + }, "Awaiter"); + thread.start(); + + return () -> thread.interrupt(); + }; + } +} diff --git a/src/me/topchetoeu/jscript/events/DataNotifier.java b/src/me/topchetoeu/jscript/events/DataNotifier.java new file mode 100644 index 0000000..e52d16e --- /dev/null +++ b/src/me/topchetoeu/jscript/events/DataNotifier.java @@ -0,0 +1,34 @@ +package me.topchetoeu.jscript.events; + +public class DataNotifier implements Awaitable { + private Notifier notifier = new Notifier(); + private boolean isErr; + private T val; + private RuntimeException err; + + public void error(RuntimeException t) { + err = t; + isErr = true; + notifier.next(); + } + public void error(Throwable t) { + error(new RuntimeException(t)); + } + public void next(T val) { + this.val = val; + isErr = false; + notifier.next(); + } + public T await() throws InterruptedException { + notifier.await(); + + try { + if (isErr) throw err; + else return val; + } + finally { + this.err = null; + this.val = null; + } + } +} diff --git a/src/me/topchetoeu/jscript/events/Event.java b/src/me/topchetoeu/jscript/events/Event.java new file mode 100644 index 0000000..512083a --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Event.java @@ -0,0 +1,49 @@ +package me.topchetoeu.jscript.events; + +import java.util.HashSet; + +public class Event implements Observer, Observable { + private HashSet> handlers = new HashSet<>(); + + public Handle on(Observer handler) { + if (handlers == null) { + handler.finish(); + return () -> {}; + } + + handlers.add(handler); + return () -> { + if (handlers == null) return; + handlers.remove(handler); + }; + } + + public boolean isFinished() { + return handlers == null; + } + + public void next(T value) { + if (handlers == null) throw new IllegalStateException("Cannot use a finished event."); + for (var handler : handlers) { + handler.next(value); + } + } + public void error(RuntimeException value) { + if (handlers == null) throw new IllegalStateException("Cannot use a finished event."); + for (var handler : handlers) { + handler.error(value); + } + + handlers.clear(); + handlers = null; + } + public void finish() { + if (handlers == null) throw new IllegalStateException("Cannot use a finished event."); + for (var handler : handlers) { + handler.finish(); + } + + handlers.clear(); + handlers = null; + } +} diff --git a/src/me/topchetoeu/jscript/events/FinishedException.java b/src/me/topchetoeu/jscript/events/FinishedException.java new file mode 100644 index 0000000..75d8d34 --- /dev/null +++ b/src/me/topchetoeu/jscript/events/FinishedException.java @@ -0,0 +1,7 @@ +package me.topchetoeu.jscript.events; + +public class FinishedException extends RuntimeException { + public FinishedException() { + super("The observable has ended."); + } +} diff --git a/src/me/topchetoeu/jscript/events/Handle.java b/src/me/topchetoeu/jscript/events/Handle.java new file mode 100644 index 0000000..b098f6a --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Handle.java @@ -0,0 +1,5 @@ +package me.topchetoeu.jscript.events; + +public interface Handle { + void free(); +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/events/Notifier.java b/src/me/topchetoeu/jscript/events/Notifier.java new file mode 100644 index 0000000..36e45b9 --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Notifier.java @@ -0,0 +1,14 @@ +package me.topchetoeu.jscript.events; + +public class Notifier { + private boolean ok = false; + + public synchronized void next() { + ok = true; + notifyAll(); + } + public synchronized void await() throws InterruptedException { + while (!ok) wait(); + ok = false; + } +} diff --git a/src/me/topchetoeu/jscript/events/Observable.java b/src/me/topchetoeu/jscript/events/Observable.java new file mode 100644 index 0000000..ea926fe --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Observable.java @@ -0,0 +1,75 @@ +package me.topchetoeu.jscript.events; + +public interface Observable { + Handle on(Observer val); + + default Handle once(Observer observer) { + // Java is fucking retarded + var unhandler = new Handle[1]; + var shouldUnsub = new boolean[1]; + + unhandler[0] = on(new Observer<>() { + public void next(T data) { + observer.next(data); + if (unhandler[0] == null) shouldUnsub[0] = true; + else unhandler[0].free(); + } + public void error(RuntimeException err) { + observer.error(err); + if (unhandler[0] == null) shouldUnsub[0] = true; + else unhandler[0].free(); + } + public void finish() { + observer.finish(); + if (unhandler[0] == null) shouldUnsub[0] = true; + else unhandler[0].free(); + } + }); + + if (shouldUnsub[0]) { + unhandler[0].free(); + return () -> {}; + } + else return unhandler[0]; + } + @SuppressWarnings("unchecked") + default Awaitable toAwaitable() { + return () -> { + var notifier = new Notifier(); + var valRef = new Object[1]; + var isErrRef = new boolean[1]; + + once(new Observer<>() { + public void next(T data) { + valRef[0] = data; + notifier.next(); + } + public void error(RuntimeException err) { + isErrRef[0] = true; + valRef[0] = err; + notifier.next(); + } + public void finish() { + isErrRef[0] = true; + valRef[0] = new FinishedException(); + notifier.next(); + } + }); + + notifier.await(); + + if (isErrRef[0]) throw (RuntimeException)valRef[0]; + else return (T)valRef[0]; + }; + } + default Observable encapsulate() { + return val -> on(val); + } + + default Observable pipe(Pipe pipe) { + return sub -> on(pipe.apply(sub)); + } + default WarmObservable warmUp() { + return new WarmObservable<>(this); + } +} diff --git a/src/me/topchetoeu/jscript/events/Observer.java b/src/me/topchetoeu/jscript/events/Observer.java new file mode 100644 index 0000000..9c49e1e --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Observer.java @@ -0,0 +1,7 @@ +package me.topchetoeu.jscript.events; + +public interface Observer { + public void next(T data); + public default void error(RuntimeException err) {} + public default void finish() { } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/events/Pipe.java b/src/me/topchetoeu/jscript/events/Pipe.java new file mode 100644 index 0000000..4916ec5 --- /dev/null +++ b/src/me/topchetoeu/jscript/events/Pipe.java @@ -0,0 +1,59 @@ +package me.topchetoeu.jscript.events; + +public interface Pipe { + Observer apply(Observer obs); + // void next(T val, Observer target); + // default void error(RuntimeException err, Observer target) { + // target.error(err); + // } + // default void finish(Observer target) { + // target.finish(); + // } + + public static interface MapFunc { + T2 map(T1 val); + } + + public static Pipe map(MapFunc func) { + return o -> val -> o.next(func.map(val)); + } + public static Pipe filter(MapFunc func) { + return o -> val -> { + if (func.map(val)) o.next(val); + }; + } + public static Pipe skip(int n) { + var i = new int[1]; + + return target -> val -> { + if (i[0] >= n) target.next(val); + else i[0]++; + }; + } + public static Pipe limit(int n) { + return target -> new Observer() { + private int i; + + public void next(T val) { + if (i >= n) target.finish(); + else { + target.next(val); + i++; + } + } + public void error(RuntimeException err) { + if (i < n) target.error(err); + } + public void finish() { + if (i < n) target.finish(); + } + }; + } + public static Pipe first() { + return limit(1); + } + + public static Pipe, T> merge() { + return target -> val -> val.on(target); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/events/WarmObservable.java b/src/me/topchetoeu/jscript/events/WarmObservable.java new file mode 100644 index 0000000..133d772 --- /dev/null +++ b/src/me/topchetoeu/jscript/events/WarmObservable.java @@ -0,0 +1,46 @@ +package me.topchetoeu.jscript.events; + +import java.util.HashSet; + +public class WarmObservable implements Observable, Handle { + private HashSet> observers = new HashSet<>(); + private Handle handle; + + @Override + public Handle on(Observer val) { + if (observers == null) return () -> {}; + observers.add(val); + return () -> observers.remove(val); + } + + @Override + public void free() { + if (observers == null) return; + handle.free(); + handle = null; + observers = null; + } + + public WarmObservable(Observable observable) { + observable.on(new Observer<>() { + public void next(T data) { + for (var obs : observers) obs.next(data); + } + public void error(RuntimeException err) { + for (var obs : observers) obs.error(err); + handle = null; + observers = null; + } + public void finish() { + for (var obs : observers) obs.finish(); + handle = null; + observers = null; + } + }); + } + + @Override + public WarmObservable warmUp() { + return this; + } +} diff --git a/src/me/topchetoeu/jscript/exceptions/EngineException.java b/src/me/topchetoeu/jscript/exceptions/EngineException.java new file mode 100644 index 0000000..fcc7cfc --- /dev/null +++ b/src/me/topchetoeu/jscript/exceptions/EngineException.java @@ -0,0 +1,70 @@ +package me.topchetoeu.jscript.exceptions; + +import java.util.ArrayList; +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.engine.values.ObjectValue.PlaceholderProto; + +public class EngineException extends RuntimeException { + public final Object value; + public EngineException cause; + public final List stackTrace = new ArrayList<>(); + + public EngineException add(String name, Location location) { + var res = ""; + + if (location != null) res += "at " + location.toString() + " "; + if (name != null && !name.equals("")) res += "in " + name + " "; + + this.stackTrace.add(res.trim()); + return this; + } + public EngineException setCause(EngineException cause) { + this.cause = cause; + return this; + } + + public String toString(CallContext ctx) throws InterruptedException { + var ss = new StringBuilder(); + ss.append(Values.toString(ctx, value)).append('\n'); + for (var line : stackTrace) { + ss.append(" ").append(line).append('\n'); + } + if (cause != null) ss.append("Caused by ").append(cause.toString(ctx)).append('\n'); + ss.deleteCharAt(ss.length() - 1); + return ss.toString(); + } + + private static Object err(String msg, PlaceholderProto proto) { + var res = new ObjectValue(proto); + res.defineProperty("message", msg); + return res; + } + + public EngineException(Object error) { + super(error == null ? "null" : error.toString()); + + this.value = error; + this.cause = null; + } + + public static EngineException ofError(String msg) { + return new EngineException(err(msg, PlaceholderProto.ERROR)); + } + public static EngineException ofSyntax(SyntaxException e) { + return new EngineException(err(e.msg, PlaceholderProto.SYNTAX_ERROR)).add(null, e.loc); + } + public static EngineException ofSyntax(String msg) { + return new EngineException(err(msg, PlaceholderProto.SYNTAX_ERROR)); + } + public static EngineException ofType(String msg) { + return new EngineException(err(msg, PlaceholderProto.TYPE_ERROR)); + } + public static EngineException ofRange(String msg) { + return new EngineException(err(msg, PlaceholderProto.RANGE_ERROR)); + } +} diff --git a/src/me/topchetoeu/jscript/exceptions/SyntaxException.java b/src/me/topchetoeu/jscript/exceptions/SyntaxException.java new file mode 100644 index 0000000..0fef28b --- /dev/null +++ b/src/me/topchetoeu/jscript/exceptions/SyntaxException.java @@ -0,0 +1,14 @@ +package me.topchetoeu.jscript.exceptions; + +import me.topchetoeu.jscript.Location; + +public class SyntaxException extends RuntimeException { + public final Location loc; + public final String msg; + + public SyntaxException(Location loc, String msg) { + super("Syntax error (at %s): %s".formatted(loc, msg)); + this.loc = loc; + this.msg = msg; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/filesystem/File.java b/src/me/topchetoeu/jscript/filesystem/File.java new file mode 100644 index 0000000..1f57cee --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/File.java @@ -0,0 +1,22 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public interface File { + int read(byte[] buff) throws IOException; + void write(byte[] buff) throws IOException; + long tell() throws IOException; + void seek(long offset, int pos) throws IOException; + void close() throws IOException; + Permissions perms(); + + default String readToString() throws IOException { + seek(0, 2); + long len = tell(); + if (len < 0) return null; + seek(0, 0); + byte[] res = new byte[(int)len]; + if (read(res) < 0) return null; + return new String(res); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/filesystem/Filesystem.java b/src/me/topchetoeu/jscript/filesystem/Filesystem.java new file mode 100644 index 0000000..eee941f --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/Filesystem.java @@ -0,0 +1,17 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; +import java.nio.file.Path; + +public interface Filesystem extends PermissionsProvider { + public static enum EntryType { + NONE, + FILE, + FOLDER, + } + + File open(Path path) throws IOException; + EntryType type(Path path); + boolean mkdir(Path path); + boolean rm(Path path) throws IOException; +} diff --git a/src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java b/src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java new file mode 100644 index 0000000..e2e9b74 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/InaccessibleFile.java @@ -0,0 +1,27 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public class InaccessibleFile implements File { + @Override + public int read(byte[] buff) throws IOException { + return -1; + } + @Override + public void write(byte[] buff) throws IOException { + } + @Override + public long tell() throws IOException { + return -1; + } + @Override + public void seek(long offset, int pos) throws IOException { + } + @Override + public void close() throws IOException { + } + @Override + public Permissions perms() { + return Permissions.NONE; + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/MemoryFile.java b/src/me/topchetoeu/jscript/filesystem/MemoryFile.java new file mode 100644 index 0000000..0f3a7da --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/MemoryFile.java @@ -0,0 +1,51 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; + +public class MemoryFile implements File { + public byte[] data; + private int ptr = 0; + + @Override + public void close() { + data = null; + ptr = -1; + } + + @Override + public int read(byte[] buff) throws IOException { + if (data == null) return -1; + if (ptr == data.length) return -1; + int n = Math.min(buff.length, data.length - ptr); + System.arraycopy(data, ptr, buff, 0, n); + return n; + } + + @Override + public void seek(long offset, int pos) throws IOException { + if (data == null) return; + if (pos == 0) ptr = (int)offset; + else if (pos == 1) ptr += (int)offset; + else ptr = data.length - (int)offset; + + ptr = (int)Math.max(Math.min(ptr, data.length), 0); + } + + @Override + public long tell() throws IOException { + if (data == null) return -1; + return ptr; + } + + @Override + public void write(byte[] buff) throws IOException { } + @Override + public Permissions perms() { + if (data == null) return Permissions.NONE; + else return Permissions.READ; + } + + public MemoryFile(byte[] data) { + this.data = data; + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/Permissions.java b/src/me/topchetoeu/jscript/filesystem/Permissions.java new file mode 100644 index 0000000..c9112ad --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/Permissions.java @@ -0,0 +1,17 @@ +package me.topchetoeu.jscript.filesystem; + +public enum Permissions { + NONE("", false, false), + READ("r", true, false), + READ_WRITE("rw", true, true); + + public final String readMode; + public final boolean readable; + public final boolean writable; + + private Permissions(String mode, boolean r, boolean w) { + this.readMode = mode; + this.readable = r; + this.writable = w; + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/PermissionsProvider.java b/src/me/topchetoeu/jscript/filesystem/PermissionsProvider.java new file mode 100644 index 0000000..06998c0 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/PermissionsProvider.java @@ -0,0 +1,7 @@ +package me.topchetoeu.jscript.filesystem; + +import java.nio.file.Path; + +public interface PermissionsProvider { + Permissions perms(Path file); +} diff --git a/src/me/topchetoeu/jscript/filesystem/PhysicalFile.java b/src/me/topchetoeu/jscript/filesystem/PhysicalFile.java new file mode 100644 index 0000000..9b88a18 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/PhysicalFile.java @@ -0,0 +1,53 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; + +public class PhysicalFile implements File { + private RandomAccessFile file; + private Permissions perms; + + @Override + public int read(byte[] buff) throws IOException { + if (!perms.readable) return -1; + return file.read(buff); + } + @Override + public void write(byte[] buff) throws IOException { + if (!perms.writable) return; + file.write(buff); + } + @Override + public long tell() throws IOException { + if (!perms.readable) return -1; + return file.getFilePointer(); + } + @Override + public void seek(long offset, int pos) throws IOException { + if (!perms.readable) return; + if (pos == 0) file.seek(offset); + else if (pos == 1) file.seek(tell() + offset); + else file.seek(file.length() + offset); + } + @Override + public void close() throws IOException { + if (!perms.readable) return; + file.close(); + file = null; + perms = Permissions.NONE; + } + @Override + public Permissions perms() { + return perms; + } + + public PhysicalFile(Path path, Permissions perms) throws IOException { + if (!path.toFile().canWrite() && perms.writable) perms = Permissions.READ; + if (!path.toFile().canRead() && perms.readable) perms = Permissions.NONE; + + this.perms = perms; + if (perms == Permissions.NONE) this.file = null; + else this.file = new RandomAccessFile(path.toString(), perms.readMode); + } +} diff --git a/src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java b/src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java new file mode 100644 index 0000000..b985938 --- /dev/null +++ b/src/me/topchetoeu/jscript/filesystem/PhysicalFilesystem.java @@ -0,0 +1,74 @@ +package me.topchetoeu.jscript.filesystem; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class PhysicalFilesystem implements Filesystem { + public final Path root; + public final PermissionsProvider perms; + + private static Path joinPaths(Path root, Path file) { + if (file.isAbsolute()) file = file.getRoot().relativize(file); + file = file.normalize(); + + while (true) { + if (file.startsWith("..")) file = file.subpath(1, file.getNameCount()); + else if (file.startsWith(".")) file = file.subpath(1, file.getNameCount()); + else break; + } + + return Path.of(root.toString(), file.toString()); + } + + @Override + public boolean mkdir(Path path) { + if (!perms(path).writable) return false; + path = joinPaths(root, path); + return path.toFile().mkdirs(); + } + @Override + public File open(Path path) throws IOException { + var perms = perms(path); + if (perms == Permissions.NONE) return new InaccessibleFile(); + path = joinPaths(root, path); + + if (path.toFile().isDirectory()) { + return new MemoryFile(String.join("\n", Files.list(path).map(Path::toString).toList()).getBytes()); + } + else if (path.toFile().isFile()) { + return new PhysicalFile(path, perms); + } + else return new InaccessibleFile(); + } + @Override + public boolean rm(Path path) { + if (!perms(path).writable) return false; + return joinPaths(root, path).toFile().delete(); + } + @Override + public EntryType type(Path path) { + if (!perms(path).readable) return EntryType.NONE; + path = joinPaths(root, path); + + if (!path.toFile().exists()) return EntryType.NONE; + if (path.toFile().isFile()) return EntryType.FILE; + else return EntryType.FOLDER; + + } + @Override + public Permissions perms(Path path) { + path = joinPaths(root, path); + var res = perms.perms(path); + + if (!path.toFile().canWrite() && res.writable) res = Permissions.READ; + if (!path.toFile().canRead() && res.readable) res = Permissions.NONE; + + return res; + } + + public PhysicalFilesystem(Path root, PermissionsProvider perms) { + this.root = root; + this.perms = perms; + } +} diff --git a/src/me/topchetoeu/jscript/interop/Native.java b/src/me/topchetoeu/jscript/interop/Native.java new file mode 100644 index 0000000..15742ba --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/Native.java @@ -0,0 +1,12 @@ +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, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Native { + public String value() default ""; +} diff --git a/src/me/topchetoeu/jscript/interop/NativeGetter.java b/src/me/topchetoeu/jscript/interop/NativeGetter.java new file mode 100644 index 0000000..ea5651b --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/NativeGetter.java @@ -0,0 +1,12 @@ +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 }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NativeGetter { + public String value(); +} diff --git a/src/me/topchetoeu/jscript/interop/NativeSetter.java b/src/me/topchetoeu/jscript/interop/NativeSetter.java new file mode 100644 index 0000000..bcb5cd3 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/NativeSetter.java @@ -0,0 +1,12 @@ +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 }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NativeSetter { + public String value(); +} diff --git a/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java b/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java new file mode 100644 index 0000000..429639b --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/NativeTypeRegister.java @@ -0,0 +1,162 @@ +package me.topchetoeu.jscript.interop; + +import java.lang.reflect.Modifier; +import java.util.HashMap; + +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class NativeTypeRegister { + private final HashMap, FunctionValue> constructors = new HashMap<>(); + private final HashMap, ObjectValue> prototypes = new HashMap<>(); + + private static void applyMethods(boolean member, ObjectValue target, Class clazz) { + for (var method : clazz.getDeclaredMethods()) { + if (!Modifier.isStatic(method.getModifiers()) != member) continue; + + var nat = method.getAnnotation(Native.class); + var get = method.getAnnotation(NativeGetter.class); + var set = method.getAnnotation(NativeSetter.class); + + if (nat != null) { + var name = nat.value(); + var val = target.values.get(name); + + if (name.equals("")) name = method.getName(); + if (!(val instanceof OverloadFunction)) target.defineProperty(name, val = new OverloadFunction(name)); + + ((OverloadFunction)val).overloads.add(Overload.fromMethod(method)); + } + else { + if (get != null) { + var name = get.value(); + var prop = target.properties.get(name); + OverloadFunction getter = null; + var setter = prop == null ? null : prop.setter(); + + if (prop != null && prop.getter() instanceof OverloadFunction) getter = (OverloadFunction)prop.getter(); + else getter = new OverloadFunction("get " + name); + + getter.overloads.add(Overload.fromMethod(method)); + target.defineProperty(name, getter, setter, true, true); + } + if (set != null) { + var name = set.value(); + var prop = target.properties.get(name); + var getter = prop == null ? null : prop.getter(); + OverloadFunction setter = null; + + if (prop != null && prop.setter() instanceof OverloadFunction) setter = (OverloadFunction)prop.setter(); + else setter = new OverloadFunction("set " + name); + + setter.overloads.add(Overload.fromMethod(method)); + target.defineProperty(name, getter, setter, true, true); + } + } + } + } + private static void applyFields(boolean member, ObjectValue target, Class clazz) { + for (var field : clazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) != member) continue; + var nat = field.getAnnotation(Native.class); + + if (nat != null) { + var name = nat.value(); + if (name.equals("")) name = field.getName(); + var getter = new OverloadFunction("get " + name).add(Overload.getterFromField(field)); + var setter = new OverloadFunction("set " + name).add(Overload.setterFromField(field)); + target.defineProperty(name, getter, setter, true, false); + } + } + } + + public static ObjectValue makeProto(Class clazz) { + var res = new ObjectValue(); + + applyMethods(true, res, clazz); + applyFields(true, res, clazz); + + return res; + } + public static FunctionValue makeConstructor(Class clazz) { + FunctionValue func = new OverloadFunction(clazz.getName()); + + for (var overload : clazz.getConstructors()) { + if (overload.getAnnotation(Native.class) == null) continue; + ((OverloadFunction)func).add(Overload.fromConstructor(overload)); + } + + if (((OverloadFunction)func).overloads.size() == 0) { + func = new NativeFunction(clazz.getName(), (a, b, c) -> { throw EngineException.ofError("This constructor is not invokable."); }); + } + + applyMethods(false, func, clazz); + applyFields(false, func, clazz); + + func.special = true; + + return func; + } + public static ObjectValue makeNamespace(Class clazz) { + ObjectValue res = new ObjectValue(); + + applyMethods(false, res, clazz); + applyFields(false, res, clazz); + + return res; + } + + private void initType(Class clazz, FunctionValue constr, ObjectValue proto) { + if (constr != null && proto != null) return; + // i vomit + if ( + clazz == Object.class || + clazz == Void.class || + clazz == Number.class || clazz == Double.class || clazz == Float.class || + clazz == Long.class || clazz == Integer.class || clazz == Short.class || + clazz == Character.class || clazz == Byte.class || clazz == Boolean.class || + clazz.isPrimitive() || + clazz.isArray() || + clazz.isAnonymousClass() || + clazz.isEnum() || + clazz.isInterface() || + clazz.isSynthetic() + ) return; + + if (constr == null) constr = makeConstructor(clazz); + if (proto == null) proto = makeProto(clazz); + + proto.values.put("constructor", constr); + constr.values.put("prototype", proto); + + prototypes.put(clazz, proto); + constructors.put(clazz, constr); + + var parent = clazz.getSuperclass(); + if (parent == null) return; + + var parentProto = getProto(parent); + var parentConstr = getConstr(parent); + + if (parentProto != null) proto.setPrototype(null, parentProto); + if (parentConstr != null) constr.setPrototype(null, parentConstr); + } + + public ObjectValue getProto(Class clazz) { + initType(clazz, constructors.get(clazz), prototypes.get(clazz)); + return prototypes.get(clazz); + } + public FunctionValue getConstr(Class clazz) { + initType(clazz, constructors.get(clazz), prototypes.get(clazz)); + return constructors.get(clazz); + } + + public void setProto(Class clazz, ObjectValue value) { + prototypes.put(clazz, value); + } + public void setConstr(Class clazz, FunctionValue value) { + constructors.put(clazz, value); + } +} diff --git a/src/me/topchetoeu/jscript/interop/Overload.java b/src/me/topchetoeu/jscript/interop/Overload.java new file mode 100644 index 0000000..3131790 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/Overload.java @@ -0,0 +1,62 @@ +package me.topchetoeu.jscript.interop; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import me.topchetoeu.jscript.engine.CallContext; + +public class Overload { + public static interface OverloadRunner { + Object run(CallContext ctx, Object thisArg, Object[] args) throws + InterruptedException, + ReflectiveOperationException, + IllegalArgumentException; + } + + public final Overload.OverloadRunner runner; + public final boolean variadic; + public final Class thisArg; + public final Class[] params; + + public static Overload fromMethod(Method method) { + return new Overload( + (ctx, th, args) -> method.invoke(th, args), + method.isVarArgs(), + Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), + method.getParameterTypes() + ); + } + public static Overload fromConstructor(Constructor method) { + return new Overload( + (ctx, th, args) -> method.newInstance(args), + method.isVarArgs(), + Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), + method.getParameterTypes() + ); + } + public static Overload getterFromField(Field field) { + return new Overload( + (ctx, th, args) -> field.get(th), false, + Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), + new Class[0] + ); + } + public static Overload setterFromField(Field field) { + if (Modifier.isFinal(field.getModifiers())) return null; + return new Overload( + (ctx, th, args) -> { field.set(th, args[0]); return null; }, false, + Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), + new Class[0] + ); + } + + + public Overload(Overload.OverloadRunner runner, boolean variadic, Class thisArg, Class args[]) { + this.runner = runner; + this.variadic = variadic; + this.thisArg = thisArg; + this.params = args; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/OverloadFunction.java b/src/me/topchetoeu/jscript/interop/OverloadFunction.java new file mode 100644 index 0000000..00c3e75 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/OverloadFunction.java @@ -0,0 +1,85 @@ +package me.topchetoeu.jscript.interop; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; + +public class OverloadFunction extends FunctionValue { + public final List overloads = new ArrayList<>(); + + public Object call(CallContext ctx, Object thisArg, Object... args) throws InterruptedException { + for (var overload : overloads) { + boolean consumesEngine = overload.params.length > 0 && overload.params[0] == CallContext.class; + int start = consumesEngine ? 1 : 0; + int end = overload.params.length - (overload.variadic ? 1 : 0); + + Object[] newArgs = new Object[overload.params.length]; + + for (var i = start; i < end; i++) { + Object val; + + if (i - start >= args.length) val = null; + else val = args[i - start]; + + newArgs[i] = Values.convert(ctx, val, overload.params[i]); + } + + if (overload.variadic) { + var type = overload.params[overload.params.length - 1].componentType(); + var n = Math.max(args.length - end + start, 0); + Object varArg = Array.newInstance(type, n); + + for (var i = 0; i < n; i++) { + Array.set(varArg, i, Values.convert(ctx, args[i + end - start], type)); + } + + newArgs[newArgs.length - 1] = varArg; + } + + if (consumesEngine) newArgs[0] = ctx; + + Object _this = overload.thisArg == null ? null : Values.convert(ctx, thisArg, overload.thisArg); + + try { + return Values.normalize(overload.runner.run(ctx, _this, newArgs)); + } + catch (InstantiationException e) { + throw EngineException.ofError("The class may not be instantiated."); + } + catch (IllegalAccessException | IllegalArgumentException e) { + continue; + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof EngineException) { + throw ((EngineException)e.getTargetException()); + } + else { + throw EngineException.ofError(e.getTargetException().getMessage()); + } + } + catch (ReflectiveOperationException e) { + throw EngineException.ofError(e.getMessage()); + } + catch (Exception e) { + throw e; + } + } + + throw EngineException.ofType("No overload found for native method."); + } + + public OverloadFunction add(Overload overload) { + this.overloads.add(overload); + return this; + } + + public OverloadFunction(String name) { + super(name, 0); + } +} diff --git a/src/me/topchetoeu/jscript/js/bootstrap.js b/src/me/topchetoeu/jscript/js/bootstrap.js new file mode 100644 index 0000000..204a85a --- /dev/null +++ b/src/me/topchetoeu/jscript/js/bootstrap.js @@ -0,0 +1,108 @@ +// TODO: load this in java +var ts = require('./ts__'); + +var src = '', lib = libs.join(''), decls = [], version = 0; +var libSnapshot = ts.ScriptSnapshot.fromString(lib); + +var settings = { + outDir: "/out", + declarationDir: "/out", + target: ts.ScriptTarget.ES5, + lib: [ ], + module: ts.ModuleKind.CommonJS, + declaration: true, + stripInternal: true, + downlevelIteration: true, + forceConsistentCasingInFileNames: true, + experimentalDecorators: true, + strict: true, +}; + +var reg = ts.createDocumentRegistry(); +var service = ts.createLanguageService({ + getCanonicalFileName: function (fileName) { return fileName; }, + useCaseSensitiveFileNames: function () { return true; }, + getNewLine: function () { return "\n"; }, + getEnvironmentVariable: function () { return ""; }, + + log: function() { + log.apply(undefined, arguments); + }, + fileExists: function (fileName) { + return ( + fileName === "/src.ts" || + fileName === "/lib.d.ts" || + fileName === "/glob.d.ts" + ); + }, + readFile: function (fileName) { + if (fileName === "/src.ts") return src; + if (fileName === "/lib.d.ts") return lib; + if (fileName === "/glob.d.ts") return decls.join('\n'); + throw new Error("File '" + fileName + "' doesn't exist."); + }, + writeFile: function (fileName, data) { + if (fileName.endsWith(".js")) res = data; + else if (fileName.endsWith(".d.ts")) decls.push(data); + else throw new Error("File '" + fileName + "' isn't writable."); + }, + getCompilationSettings: function () { + return settings; + }, + getCurrentDirectory: function() { return "/"; }, + getDefaultLibFileName: function() { return "/lib_.d.ts"; }, + getScriptFileNames: function() { return [ "/src.ts", "/lib.d.ts", "/glob.d.ts" ]; }, + getScriptSnapshot: function(filename) { + if (filename === "/lib.d.ts") return libSnapshot; + else return ts.ScriptSnapshot.fromString(this.readFile(filename)); + }, + getScriptVersion: function (filename) { + if (filename === "/lib.d.ts") return 0; + else return version; + }, +}, reg); + +service.getEmitOutput('/lib.d.ts'); +log('Loaded libraries!'); + + +function compile(code) { + src = code; + version++; + + var emit = service.getEmitOutput("/src.ts"); + + var res = emit.outputFiles[0].text; + var decl = emit.outputFiles[1].text; + + var diagnostics = [] + .concat(service.getCompilerOptionsDiagnostics()) + .concat(service.getSyntacticDiagnostics("/src.ts")) + .concat(service.getSemanticDiagnostics("/src.ts")) + .map(function (diagnostic) { + var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); + if (diagnostic.file) { + var pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + return diagnostic.file.fileName.substring(1) + ":" + (pos.line + 1) + ":" + (pos.character + 1) + ": " + message; + } + else return "Error: " + message; + }); + + if (diagnostics.length > 0) { + throw new SyntaxError(diagnostics.join('\n')); + } + + decls.push(decl); + + return { + result: res, + diagnostics: diagnostics + }; +} + +log("Loaded typescript!"); +init(function (code) { + var res = compile(code); + return res.result; +}); + diff --git a/src/me/topchetoeu/jscript/json/JSON.java b/src/me/topchetoeu/jscript/json/JSON.java new file mode 100644 index 0000000..3edfe7c --- /dev/null +++ b/src/me/topchetoeu/jscript/json/JSON.java @@ -0,0 +1,167 @@ +package me.topchetoeu.jscript.json; + +import java.util.List; + +import me.topchetoeu.jscript.exceptions.SyntaxException; +import me.topchetoeu.jscript.parsing.Operator; +import me.topchetoeu.jscript.parsing.ParseRes; +import me.topchetoeu.jscript.parsing.Parsing; +import me.topchetoeu.jscript.parsing.Token; + +public class JSON { + public static ParseRes parseIdentifier(List tokens, int i) { + try { + if (tokens.get(i).isIdentifier()) return ParseRes.res(tokens.get(i).identifier(), 1); + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + public static ParseRes parseString(String filename, List tokens, int i) { + try { + if (tokens.get(i).isString()) return ParseRes.res(tokens.get(i).string(), 1); + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + public static ParseRes parseNumber(String filename, List tokens, int i) { + try { + if (tokens.get(i).isNumber()) return ParseRes.res(tokens.get(i).number(), 1); + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + public static ParseRes parseBool(String filename, List tokens, int i) { + var id = parseIdentifier(tokens, i); + + if (!id.isSuccess()) return ParseRes.failed(); + else if (id.result.equals("true")) return ParseRes.res(true, 1); + else if (id.result.equals("false")) return ParseRes.res(false, 1); + else return ParseRes.failed(); + } + + public static ParseRes parseValue(String filename, List tokens, int i) { + return ParseRes.any( + parseString(filename, tokens, i), + parseNumber(filename, tokens, i), + parseBool(filename, tokens, i), + parseMap(filename, tokens, i), + parseList(filename, tokens, i) + ); + } + + public static ParseRes parseMap(String filename, List tokens, int i) { + int n = 0; + if (!Parsing.isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.failed(); + + var values = new JSONMap(); + + while (true) { + if (Parsing.isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + break; + } + + var name = ParseRes.any( + parseIdentifier(tokens, i + n), + parseString(filename, tokens, i + n), + parseNumber(filename, tokens, i + n) + ); + if (!name.isSuccess()) return ParseRes.error(Parsing.getLoc(filename, tokens, i + n), "Expected an index.", name); + else n += name.n; + + if (!Parsing.isOperator(tokens, i + n, Operator.COLON)) { + return ParseRes.error(Parsing.getLoc(filename, tokens, i + n), "Expected a colon.", name); + } + n++; + + var res = parseValue(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(Parsing.getLoc(filename, tokens, i + n), "Expected a list element.", res); + else n += res.n; + + values.put(name.result.toString(), JSONElement.of(res.result)); + + if (Parsing.isOperator(tokens, i + n, Operator.COMMA)) n++; + else if (Parsing.isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + break; + } + } + + return ParseRes.res(values, n); + } + public static ParseRes parseList(String filename, List tokens, int i) { + int n = 0; + if (!Parsing.isOperator(tokens, i + n++, Operator.BRACKET_OPEN)) return ParseRes.failed(); + + var values = new JSONList(); + + while (true) { + if (Parsing.isOperator(tokens, i + n, Operator.BRACKET_CLOSE)) { + n++; + break; + } + + var res = parseValue(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(Parsing.getLoc(filename, tokens, i + n), "Expected a list element.", res); + else n += res.n; + + values.add(JSONElement.of(res.result)); + + if (Parsing.isOperator(tokens, i + n, Operator.COMMA)) n++; + else if (Parsing.isOperator(tokens, i + n, Operator.BRACKET_CLOSE)) { + n++; + break; + } + } + + return ParseRes.res(values, n); + } + public static JSONElement parse(String 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); + else return JSONElement.of(res.result); + } + + public static String stringify(JSONElement el) { + 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.isList()) { + var res = new StringBuilder().append("["); + for (int i = 0; i < el.list().size(); i++) { + if (i != 0) res.append(","); + res.append(stringify(el.list().get(i))); + } + res.append("]"); + return res.toString(); + } + if (el.isMap()) { + var res = new StringBuilder().append("{"); + var entries = el.map().entrySet().stream().toList(); + + for (int i = 0; i < entries.size(); i++) { + if (i != 0) res.append(","); + res.append(stringify(JSONElement.string(entries.get(i).getKey()))); + res.append(":"); + res.append(stringify(entries.get(i).getValue())); + } + res.append("}"); + return res.toString(); + } + return null; + } + public static String stringify(JSONMap map) { + return stringify(JSONElement.of(map)); + } + public static String stringify(JSONList list) { + return stringify(JSONElement.of(list)); + } +} diff --git a/src/me/topchetoeu/jscript/json/JSONElement.java b/src/me/topchetoeu/jscript/json/JSONElement.java new file mode 100644 index 0000000..16aef1b --- /dev/null +++ b/src/me/topchetoeu/jscript/json/JSONElement.java @@ -0,0 +1,76 @@ +package me.topchetoeu.jscript.json; + +public class JSONElement { + public static enum Type { + STRING, + NUMBER, + BOOLEAN, + NULL, + LIST, + MAP, + } + + public static JSONElement NULL = new JSONElement(Type.NULL, null); + + public static JSONElement map(JSONMap val) { + return new JSONElement(Type.MAP, val); + } + public static JSONElement list(JSONList val) { + return new JSONElement(Type.LIST, val); + } + public static JSONElement string(String val) { + return new JSONElement(Type.STRING, val); + } + public static JSONElement number(double val) { + return new JSONElement(Type.NUMBER, val); + } + public static JSONElement bool(boolean val) { + return new JSONElement(Type.BOOLEAN, val); + } + + public static JSONElement of(Object val) { + if (val instanceof JSONMap) return map((JSONMap)val); + else if (val instanceof JSONList) return list((JSONList)val); + else if (val instanceof String) return string((String)val); + else if (val instanceof Boolean) return bool((Boolean)val); + else if (val instanceof Number) return number(((Number)val).doubleValue()); + else if (val == null) return NULL; + else throw new IllegalArgumentException("val must be: String, Boolean, Number, JSONList or JSONMap."); + } + + public final Type type; + private final Object value; + + public boolean isMap() { return type == Type.MAP; } + public boolean isList() { return type == Type.LIST; } + public boolean isString() { return type == Type.STRING; } + public boolean isNumber() { return type == Type.NUMBER; } + public boolean isBoolean() { return type == Type.BOOLEAN; } + public boolean isNull() { return type == Type.NULL; } + + public JSONMap map() { + if (!isMap()) throw new IllegalStateException("Element is not a map."); + return (JSONMap)value; + } + public JSONList list() { + if (!isList()) throw new IllegalStateException("Element is not a map."); + return (JSONList)value; + } + public String string() { + if (!isString()) throw new IllegalStateException("Element is not a string."); + return (String)value; + } + public double number() { + if (!isNumber()) throw new IllegalStateException("Element is not a number."); + return (double)value; + } + public boolean bool() { + if (!isNumber()) throw new IllegalStateException("Element is not a boolean."); + return (boolean)value; + } + + private JSONElement(Type type, Object val) { + this.type = type; + this.value = val; + } +} diff --git a/src/me/topchetoeu/jscript/json/JSONList.java b/src/me/topchetoeu/jscript/json/JSONList.java new file mode 100644 index 0000000..eb343ac --- /dev/null +++ b/src/me/topchetoeu/jscript/json/JSONList.java @@ -0,0 +1,21 @@ +package me.topchetoeu.jscript.json; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class JSONList extends ArrayList { + public JSONList() {} + public JSONList(JSONElement ...els) { + super(List.of(els)); + } + + public JSONList addNull() { this.add(JSONElement.NULL); return this; } + public JSONList add(String val) { this.add(JSONElement.of(val)); return this; } + public JSONList add(double val) { this.add(JSONElement.of(val)); return this; } + public JSONList add(boolean val) { this.add(JSONElement.of(val)); return this; } + public JSONList add(Map val) { this.add(JSONElement.of(val)); return this; } + public JSONList add(Collection val) { this.add(JSONElement.of(val)); return this; } + +} diff --git a/src/me/topchetoeu/jscript/json/JSONMap.java b/src/me/topchetoeu/jscript/json/JSONMap.java new file mode 100644 index 0000000..9f805c5 --- /dev/null +++ b/src/me/topchetoeu/jscript/json/JSONMap.java @@ -0,0 +1,150 @@ +package me.topchetoeu.jscript.json; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class JSONMap implements Map { + private Map elements = new HashMap<>(); + + public JSONElement get(String path) { + var curr = this; + var segs = path.split("\\."); + var i = 0; + + while (true) { + var tmp = curr.elements.get(segs[i++]); + if (i == segs.length) return tmp; + if (!tmp.isMap()) return null; + curr = tmp.map(); + } + } + + public boolean isMap(String path) { + var el = get(path); + return el != null && el.isMap(); + } + public boolean isList(String path) { + var el = get(path); + return el != null && el.isList(); + } + public boolean isString(String path) { + var el = get(path); + return el != null && el.isString(); + } + public boolean isNumber(String path) { + var el = get(path); + return el != null && el.isNumber(); + } + public boolean isBoolean(String path) { + var el = get(path); + return el != null && el.isBoolean(); + } + public boolean isNull(String path) { + var el = get(path); + return el != null && el.isNull(); + } + public boolean contains(String path) { + return get(path) != null; + } + + public JSONMap map(String path) { + var el = get(path); + if (el == null) throw new IllegalStateException("'%s' doesn't exist.".formatted(path)); + return el.map(); + } + public JSONMap map(String path, JSONMap defaultVal) { + var el = get(path); + if (el == null) return defaultVal; + if (el.isMap()) return el.map(); + return defaultVal; + } + + public JSONList list(String path) { + var el = get(path); + if (el == null) throw new IllegalStateException("'%s' doesn't exist.".formatted(path)); + return el.list(); + } + public JSONList list(String path, JSONList defaultVal) { + var el = get(path); + if (el == null) return defaultVal; + if (el.isList()) return el.list(); + return defaultVal; + } + + public String string(String path) { + var el = get(path); + if (el == null) throw new IllegalStateException("'%s' doesn't exist.".formatted(path)); + return el.string(); + } + public String string(String path, String defaultVal) { + var el = get(path); + if (el == null) return defaultVal; + if (el.isString()) return el.string(); + return defaultVal; + } + + public double number(String path) { + var el = get(path); + if (el == null) throw new IllegalStateException("'%s' doesn't exist.".formatted(path)); + return el.number(); + } + public double number(String path, double defaultVal) { + var el = get(path); + if (el == null) return defaultVal; + if (el.isNumber()) return el.number(); + return defaultVal; + } + + public boolean bool(String path) { + var el = get(path); + if (el == null) throw new IllegalStateException("'%s' doesn't exist.".formatted(path)); + return el.bool(); + } + public boolean bool(String path, boolean defaultVal) { + var el = get(path); + if (el == null) return defaultVal; + if (el.isBoolean()) return el.bool(); + return defaultVal; + } + + public JSONMap setNull(String key) { elements.put(key, JSONElement.NULL); return this; } + public JSONMap set(String key, String val) { elements.put(key, JSONElement.of(val)); return this; } + public JSONMap set(String key, double val) { elements.put(key, JSONElement.of(val)); return this; } + public JSONMap set(String key, boolean val) { elements.put(key, JSONElement.of(val)); return this; } + public JSONMap set(String key, Map val) { elements.put(key, JSONElement.of(val)); return this; } + public JSONMap set(String key, Collection val) { elements.put(key, JSONElement.of(val)); return this; } + + @Override + public int size() { return elements.size(); } + @Override + public boolean isEmpty() { return elements.isEmpty(); } + @Override + public boolean containsKey(Object key) { return elements.containsKey(key); } + @Override + public boolean containsValue(Object value) { return elements.containsValue(value); } + @Override + public JSONElement get(Object key) { return elements.get(key); } + @Override + public JSONElement put(String key, JSONElement value) { return elements.put(key, value); } + @Override + public JSONElement remove(Object key) { return elements.remove(key); } + @Override + public void putAll(Map m) { elements.putAll(m); } + + @Override + public void clear() { elements.clear(); } + + @Override + public Set keySet() { return elements.keySet(); } + @Override + public Collection values() { return elements.values(); } + @Override + public Set> entrySet() { return elements.entrySet(); } + + public JSONMap() { } + public JSONMap(Map els) { + this.elements = new HashMap<>(els); + } +} diff --git a/src/me/topchetoeu/jscript/parsing/Operator.java b/src/me/topchetoeu/jscript/parsing/Operator.java new file mode 100644 index 0000000..5c2b939 --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/Operator.java @@ -0,0 +1,114 @@ +package me.topchetoeu.jscript.parsing; + +import java.util.HashMap; +import java.util.Map; + +import me.topchetoeu.jscript.compilation.Instruction; +import me.topchetoeu.jscript.compilation.Instruction.Type; + +public enum Operator { + MULTIPLY("*", Type.MULTIPLY, 13), + DIVIDE("/", Type.DIVIDE, 12), + MODULO("%", Type.MODULO, 12), + SUBTRACT("-", Type.SUBTRACT, 11), + ADD("+", Type.ADD, 11), + SHIFT_RIGHT(">>", Type.SHIFT_RIGHT, 10), + SHIFT_LEFT("<<", Type.SHIFT_LEFT, 10), + USHIFT_RIGHT(">>>", Type.USHIFT_RIGHT, 10), + GREATER(">", Type.GREATER, 9), + LESS("<", Type.LESS, 9), + GREATER_EQUALS(">=", Type.GREATER_EQUALS, 9), + LESS_EQUALS("<=", Type.LESS_EQUALS, 9), + NOT_EQUALS("!=", Type.LOOSE_NOT_EQUALS, 8), + LOOSE_NOT_EQUALS("!==", Type.NOT_EQUALS, 8), + EQUALS("==", Type.LOOSE_EQUALS, 8), + LOOSE_EQUALS("===", Type.EQUALS, 8), + AND("&", Type.AND, 7), + XOR("^", Type.XOR, 6), + OR("|", Type.OR, 5), + LAZY_AND("&&", 4), + LAZY_OR("||", 3), + ASSIGN_SHIFT_LEFT("<<=", 2, true), + ASSIGN_SHIFT_RIGHT(">>=", 2, true), + ASSIGN_USHIFT_RIGHT(">>>=", 2, true), + ASSIGN_AND("&=", 2, true), + ASSIGN_OR("|=", 2, true), + ASSIGN_XOR("^=", 2, true), + ASSIGN_MODULO("%=", 2, true), + ASSIGN_DIVIDE("/=", 2, true), + ASSIGN_MULTIPLY("*=", 2, true), + ASSIGN_SUBTRACT("-=", 2, true), + ASSIGN_ADD("+=", 2, true), + ASSIGN("=", 2, true), + SEMICOLON(";"), + COLON(":"), + PAREN_OPEN("("), + PAREN_CLOSE(")"), + BRACKET_OPEN("["), + BRACKET_CLOSE("]"), + BRACE_OPEN("{"), + BRACE_CLOSE("}"), + DOT("."), + COMMA(","), + NOT("!"), + QUESTION("?"), + INVERSE("~"), + INCREASE("++"), + DECREASE("--"); + + public final String value; + public final Instruction.Type operation; + public final int precedence; + public final boolean reverse; + private static final Map ops = new HashMap<>(); + + static { + for (var el : Operator.values()) { + ops.put(el.value, el); + } + } + + public boolean isAssign() { return precedence == 2; } + + public static Operator parse(String val) { + return ops.get(val); + } + + private Operator() { + this.value = null; + this.operation = null; + this.precedence = -1; + this.reverse = false; + } + private Operator(String value) { + this. value = value; + this.operation = null; + this.precedence = -1; + this.reverse = false; + } + private Operator(String value, int precedence) { + this. value = value; + this.operation = null; + this.precedence = precedence; + this.reverse = false; + } + private Operator(String value, int precedence, boolean reverse) { + this. value = value; + this.operation = null; + this.precedence = precedence; + this.reverse = reverse; + } + + private Operator(String value, Instruction.Type funcName, int precedence) { + this. value = value; + this.operation = funcName; + this.precedence = precedence; + this.reverse = false; + } + private Operator(String value, Instruction.Type funcName, int precedence, boolean reverse) { + this.value = value; + this.operation = funcName; + this.precedence = precedence; + this.reverse = reverse; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/parsing/ParseRes.java b/src/me/topchetoeu/jscript/parsing/ParseRes.java new file mode 100644 index 0000000..307b405 --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/ParseRes.java @@ -0,0 +1,97 @@ +package me.topchetoeu.jscript.parsing; + +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.parsing.Parsing.Parser; + +public class ParseRes { + public static enum State { + SUCCESS, + FAILED, + ERROR; + + public boolean isSuccess() { return this == SUCCESS; } + public boolean isFailed() { return this == FAILED; } + public boolean isError() { return this == ERROR; } + } + + public final ParseRes.State state; + public final String error; + public final T result; + public final int n; + + private ParseRes(ParseRes.State state, String error, T result, int readN) { + this.result = result; + this.n = readN; + this.state = state; + this.error = error; + } + + public ParseRes setN(int i) { + if (!state.isSuccess()) return this; + return new ParseRes<>(state, null, result, i); + } + public ParseRes addN(int i) { + if (!state.isSuccess()) return this; + return new ParseRes<>(state, null, result, this.n + i); + } + public ParseRes transform() { + if (isSuccess()) throw new RuntimeException("Can't transform a ParseRes that hasn't failed."); + return new ParseRes<>(state, error, null, 0); + } + public TestRes toTest() { + if (isSuccess()) return TestRes.res(n); + else if (isError()) return TestRes.error(null, error); + else return TestRes.failed(); + } + + public boolean isSuccess() { return state.isSuccess(); } + public boolean isFailed() { return state.isFailed(); } + public boolean isError() { return state.isError(); } + + public static ParseRes failed() { + return new ParseRes(State.FAILED, null, null, 0); + } + public static ParseRes error(Location loc, String error) { + if (loc != null) error = loc + ": " + error; + return new ParseRes<>(State.ERROR, error, null, 0); + } + public static ParseRes error(Location loc, String error, ParseRes other) { + if (loc != null) error = loc + ": " + error; + if (!other.isError()) return new ParseRes<>(State.ERROR, error, null, 0); + return new ParseRes<>(State.ERROR, other.error, null, 0); + } + public static ParseRes res(T val, int i) { + return new ParseRes<>(State.SUCCESS, null, val, i); + } + + @SafeVarargs + public static ParseRes any(ParseRes... parsers) { + ParseRes best = null; + ParseRes error = ParseRes.failed(); + + for (var parser : parsers) { + if (parser.isSuccess()) { + if (best == null || best.n < parser.n) best = parser; + } + else if (parser.isError() && error.isFailed()) error = parser.transform(); + } + + if (best != null) return best; + else return error; + } + @SafeVarargs + public static ParseRes first(String filename, List tokens, Map> named, Parser... parsers) { + ParseRes error = ParseRes.failed(); + + for (var parser : parsers) { + var res = parser.parse(null, tokens, 0); + if (res.isSuccess()) return res; + else if (res.isError() && error.isFailed()) error = res.transform(); + } + + return error; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/parsing/Parsing.java b/src/me/topchetoeu/jscript/parsing/Parsing.java new file mode 100644 index 0000000..dd576b8 --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/Parsing.java @@ -0,0 +1,1877 @@ +package me.topchetoeu.jscript.parsing; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.compilation.*; +import me.topchetoeu.jscript.compilation.Instruction.Type; +import me.topchetoeu.jscript.compilation.VariableDeclareStatement.Pair; +import me.topchetoeu.jscript.compilation.control.*; +import me.topchetoeu.jscript.compilation.control.SwitchStatement.SwitchCase; +import me.topchetoeu.jscript.compilation.values.*; +import me.topchetoeu.jscript.engine.scope.GlobalScope; +import me.topchetoeu.jscript.engine.scope.ValueVariable; +import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.SyntaxException; +import me.topchetoeu.jscript.parsing.ParseRes.State; + +// TODO: this has to be rewritten +public class Parsing { + public static interface Parser { + ParseRes parse(String filename, List tokens, int i); + } + + private static record ObjProp(Object name, String access, FunctionStatement func) {} + private static final HashSet reserved = new HashSet(); + static { + reserved.add("true"); + reserved.add("false"); + reserved.add("void"); + reserved.add("null"); + reserved.add("this"); + reserved.add("NaN"); + reserved.add("Infinity"); + reserved.add("if"); + reserved.add("else"); + reserved.add("try"); + reserved.add("catch"); + reserved.add("finally"); + reserved.add("for"); + reserved.add("do"); + reserved.add("while"); + reserved.add("switch"); + reserved.add("case"); + reserved.add("default"); + reserved.add("new"); + reserved.add("function"); + reserved.add("var"); + reserved.add("return"); + reserved.add("throw"); + reserved.add("typeof"); + reserved.add("delete"); + reserved.add("break"); + reserved.add("continue"); + reserved.add("debug"); + reserved.add("let"); + reserved.add("implements"); + reserved.add("interface"); + reserved.add("package"); + reserved.add("private"); + reserved.add("protected"); + reserved.add("public"); + reserved.add("static"); + reserved.add("yield"); + // Although the standards allow it, these are keywords in newer ES, so we won't allow them + reserved.add("const"); + reserved.add("await"); + reserved.add("async"); + // These are allowed too, however our parser considers them keywords + reserved.add("undefined"); + reserved.add("arguments"); + reserved.add("globalThis"); + reserved.add("window"); + reserved.add("self"); + } + + + public static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + public static boolean isWhitespace(char c) { + return isAny(c, " \t\r\n"); + } + public static boolean isLetter(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + public static boolean isAlphanumeric(char c) { + return isLetter(c) || isDigit(c); + } + public static boolean isAny(char c, String alphabet) { + return alphabet.contains(Character.toString(c)); + } + + private static final int CURR_NONE = 0; + private static final int CURR_NUMBER = 1; + private static final int CURR_FLOAT = 11; + private static final int CURR_SCIENTIFIC_NOT = 12; + private static final int CURR_NEG_SCIENTIFIC_NOT = 13; + private static final int CURR_HEX = 14; + private static final int CURR_STRING = 2; + private static final int CURR_LITERAL = 3; + private static final int CURR_OPERATOR = 4; + private static final int CURR_REGEX = 6; + private static final int CURR_REGEX_FLAGS = 7; + 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 tokens) { + var res = currToken.toString(); + + switch (currStage) { + case CURR_STRING: tokens.add(new RawToken(res, TokenType.STRING, line, lastStart)); break; + case CURR_REGEX_FLAGS: tokens.add(new RawToken(res, TokenType.REGEX, line, lastStart)); break; + case CURR_NUMBER: + case CURR_HEX: + case CURR_NEG_SCIENTIFIC_NOT: + case CURR_SCIENTIFIC_NOT: + case CURR_FLOAT: + tokens.add(new RawToken(res, TokenType.NUMBER, line, lastStart)); break; + case CURR_LITERAL: tokens.add(new RawToken(res, TokenType.LITERAL, line, lastStart)); break; + case CURR_OPERATOR: tokens.add(new RawToken(res, TokenType.OPERATOR, line, lastStart)); break; + } + + currToken.delete(0, currToken.length()); + } + + // 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 splitTokens(String filename, String raw) { + var tokens = new ArrayList(); + var currToken = new StringBuilder(64); + + // Those are state variables, and will be reset every time a token has ended parsing + boolean lastEscape = false, inBrackets = false; + + int line = 1, start = 1, lastStart = 1, parenI = 0; + var loc = new Location(line, lastStart, filename); + int currStage = CURR_NONE; + + // when we want to continue parsing a token, we will execute continue;, which will skip + // the token end logic + loop: for (int i = 0; i < raw.length(); i++) { + char c = raw.charAt(i); + + start++; + + switch (currStage) { + case CURR_STRING: + currToken.append(c); + + if (!lastEscape) { + if (c == '\n') throw new SyntaxException(loc, "Can't have a multiline string."); + else if (c == '\\') { + lastEscape = true; + continue; + } + else if (c != currToken.charAt(0)) continue; + } + else { + lastEscape = false; + continue; + } + break; + case CURR_REGEX: + currToken.append(c); + if (!lastEscape) { + if (c == '\\') lastEscape = true; + if (c == '/' & parenI == 0 & !inBrackets) { + currStage = CURR_REGEX_FLAGS; + continue; + } + if (c == '[') inBrackets = true; + if (c == ']') inBrackets = false; + if (c == '(' && !inBrackets) parenI++; + if (c == ')' && !inBrackets) parenI--; + } + else lastEscape = false; + continue; + case CURR_REGEX_FLAGS: + if (isAny(c, "dgimsuy")) { + currToken.append(c); + continue; + } + i--; start--; + break; + case CURR_NUMBER: + if (c == '.') currStage = CURR_FLOAT; + else if (c == 'e' || c == 'E') currStage = CURR_SCIENTIFIC_NOT; + else if ((c == 'x' || c == 'X') && currToken.toString().equals("0")) currStage = CURR_HEX; + else if (!isDigit(c)) { + i--; start--; + break; + } + currToken.append(c); + continue; + case CURR_FLOAT: + if (c == 'e' || c == 'E') currStage = CURR_SCIENTIFIC_NOT; + else if (!isDigit(c)) { + i--; start--; + break; + } + currToken.append(c); + continue; + case CURR_SCIENTIFIC_NOT: + if (c == '-') currStage = CURR_NEG_SCIENTIFIC_NOT; + else if (!isDigit(c)) { + i--; start--; + break; + } + currToken.append(c); + continue; + case CURR_NEG_SCIENTIFIC_NOT: + if (isDigit(c)) currToken.append(c); + else { + i--; start--; + break; + } + continue; + case CURR_HEX: + if (isDigit(c) || isAny(c, "ABCDEFabcdef")) currToken.append(c); + else { + i--; start--; + break; + } + continue; + case CURR_SINGLE_COMMENT: + currToken.delete(0, currToken.length()); + if (c != '\n') continue; + else { + line++; + start = 1; + } + break; + case CURR_MULTI_COMMENT: + if (c == '\n') line++; + if (!(currToken.charAt(0) == '*' && c == '/')) { + currToken.delete(0, currToken.length()); + currToken.append(c); + continue; + } + break; + case CURR_LITERAL: + if (isAlphanumeric(c) || c == '_') { + currToken.append(c); + continue; + } + else { i--; start--; } + break; + case CURR_OPERATOR: { + // here we do several things: + // - detect a comment + // - detect a regular expression + // - detect a float number (.xxxx) + // - read an operator greedily + + // this variable keeps track of whether we're still reading an operator + boolean ok = false; + if (currToken.length() == 1) { + // double operators + if (currToken.charAt(0) == c && isAny(c, "&|=+-<>")) ok = true; + // assignments + else if (c == '=' && isAny(currToken.charAt(0), "&|^+-/*%!<>")) ok = true; + // detect float numbers + else if (isDigit(c) && currToken.charAt(0) == '.') { + currStage = CURR_FLOAT; + currToken.append(c); + continue; + } + else if (currToken.charAt(0) == '/') { + // single line comments + if (c == '/') { + currStage = CURR_SINGLE_COMMENT; + continue; + } + // multiline comments + else if (c == '*') { + currStage = CURR_MULTI_COMMENT; + continue; + } + // regular expressions + else { + // regular expressions must be in the start of a file, or be followed by a + // newline, or an operator + // this is because of expressions like 1 / 2 / 3 (/ 2 /) will get recognized as regex + // still, the closing paren must be ignored, because in an expression, we can't have a value, following a paren + var prevToken = tokens.size() == 0 ? null : tokens.get(tokens.size() - 1); + if (tokens.size() == 0 || ( + prevToken.line < line || + prevToken.type == TokenType.OPERATOR && !prevToken.value.equals(")") || + prevToken.value.equals("return") || + prevToken.value.equals("throe") + )) { + // we look for a second / on the same line + // if we don't find one, we determine the current operator + // to be a division + for (int j = i; j < raw.length(); j++) { + if (raw.charAt(j) == '/') { + i--; start--; + currStage = CURR_REGEX; + continue loop; + } + if (raw.charAt(j) == '\n') break; + } + } + } + } + } + if (currToken.length() == 2) { + var a = currToken.charAt(0); + var b = currToken.charAt(1); + if (( + a == '=' && b == '=' || + a == '!' && b == '=' || + a == '<' && b == '<' || + a == '>' && b == '>' || + a == '>' && b == '>' + ) && c == '=') ok = true; + if (a == '>' && b == '>' && c == '>') ok = true; + } + if ( + currToken.length() == 3 && + currToken.charAt(0) == '>' && + currToken.charAt(1) == '>' && + currToken.charAt(2) == '>' && + c == '=' + ) ok = true; + + if (ok) { + currToken.append(c); + continue; + } + else { i--; start--; } + break; + } + default: + // here we detect what type of token we're reading + if (isAny(c, " \t\n\r")) { + if (c == '\n') { + line++; + start = 1; + } + } + else if (isDigit(c)) { + currToken.append(c); + currStage = CURR_NUMBER; + continue; + } + else if (isAlphanumeric(c) || c == '_' || c == '$') { + currToken.append(c); + currStage = CURR_LITERAL; + continue; + } + else if (isAny(c, "+-/*%=!&|^(){}[];.,<>!:~?")) { + currToken.append(c); + currStage = CURR_OPERATOR; + continue; + } + else if (c == '"' || c == '\'') { + currToken.append(c); + currStage = CURR_STRING; + continue; + } + else throw new SyntaxException(new Location(line, start, filename), "Unrecognized character %s.".formatted(c)); + } + + // if we got here, we know that we have encountered the end of a token + addToken(currToken, currStage, line, lastStart, filename, tokens); + lastEscape = inBrackets = false; + currStage = CURR_NONE; + lastStart = start; + } + + // here, we save a leftover token (if any) + switch (currStage) { + case CURR_STRING: throw new SyntaxException(new Location(line, start, filename), "Unterminated string literal."); + case CURR_REGEX: throw new SyntaxException(new Location(line, start, filename), "Incomplete regex."); + } + addToken(currToken, currStage, line, lastStart, filename, tokens); + + return tokens; + } + + private static int fromHex(char c) { + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= '0' && c <= '9') return c - '0'; + return -1; + } + + private static String parseString(Location loc, String literal) { + var res = new StringBuilder(); + + for (var i = 1; i < literal.length() - 1; i++) { + if (literal.charAt(i) == '\\') { + char c = literal.charAt(++i); + if (c == 'b') res.append('\b'); + else if (c == 't') res.append('\t'); + else if (c == 'n') res.append('\n'); + else if (c == 'f') res.append('\f'); + else if (c == 'r') res.append('\r'); + else if (c == '0') { + if (i + 1 >= literal.length()) res.append((char)0); + c = literal.charAt(i + 1); + if (c >= '0' && c <= '9') throw new SyntaxException(loc.add(i), "Octal escape sequences not allowed."); + res.append((char)0); + } + else if (c >= '1' && c <= '9') { + throw new SyntaxException(loc.add(i), "Octal escape sequences not allowed."); + } + else if (c == 'x') { + var newC = 0; + i++; + for (var j = 0; j < 2; j++) { + if (i >= literal.length()) throw new SyntaxException(loc.add(i), "Incomplete unicode escape sequence."); + int val = fromHex(literal.charAt(i++)); + if (val == -1) throw new SyntaxException(loc.add(i + 1), "Invalid character in unicode escape sequence."); + newC = (newC << 4) | val; + } + i--; + + res.append((char)newC); + } + else if (c == 'u') { + var newC = 0; + i++; + for (var j = 0; j < 4; j++) { + if (i >= literal.length()) throw new SyntaxException(loc.add(i), "Incomplete unicode escape sequence."); + int val = fromHex(literal.charAt(i++)); + if (val == -1) throw new SyntaxException(loc.add(i + 1), "Invalid character in unicode escape sequence."); + newC = (newC << 4) | val; + } + i--; + + res.append((char)newC); + } + else res.append(c); + } + else res.append(literal.charAt(i)); + } + + return res.toString(); + } + private static String parseRegex(Location loc, String literal) { + var res = new StringBuilder(); + + int end = literal.lastIndexOf('/'); + + for (var i = 1; i < end; i++) { + if (literal.charAt(i) == '\\') { + char c = literal.charAt(++i); + if (c == 'b') res.append('\b'); + else if (c == 't') res.append('\t'); + else if (c == 'n') res.append('\n'); + else if (c == 'f') res.append('\f'); + else if (c == 'r') res.append('\r'); + else if (c == '0') { + if (i + 1 >= literal.length()) res.append((char)0); + c = literal.charAt(i + 1); + if (c >= '0' && c <= '9') throw new SyntaxException(loc.add(i), "Octal escape sequences not allowed."); + res.append((char)0); + } + else if (c >= '1' && c <= '9') { + res.append((char)(c - '0')); + i++; + } + else if (c == 'x') { + var newC = 0; + i++; + for (var j = 0; j < 2; j++) { + if (i >= literal.length()) throw new SyntaxException(loc.add(i), "Incomplete unicode escape sequence."); + int val = fromHex(literal.charAt(i++)); + if (val == -1) throw new SyntaxException(loc.add(i + 1), "Invalid character in unicode escape sequence."); + newC = (newC << 4) | val; + } + i--; + + res.append((char)newC); + } + else if (c == 'u') { + var newC = 0; + i++; + for (var j = 0; j < 4; j++) { + if (i >= literal.length()) throw new SyntaxException(loc.add(i), "Incomplete unicode escape sequence."); + int val = fromHex(literal.charAt(i++)); + if (val == -1) throw new SyntaxException(loc.add(i + 1), "Invalid character in unicode escape sequence."); + newC = (newC << 4) | val; + } + i--; + + res.append((char)newC); + } + else res.append("\\" + c); + } + else res.append(literal.charAt(i)); + } + + return '/' + res.toString() + literal.substring(end); + } + + private static double parseHex(String literal) { + double res = 0; + + for (int i = 2; i < literal.length(); i++) { + res *= 16; + res += fromHex(literal.charAt(i)); + } + + return res; + } + + private static List parseTokens(String filename, Collection tokens) { + var res = new ArrayList(); + + for (var el : tokens) { + var loc = new Location(el.line, el.start, filename); + switch (el.type) { + case LITERAL: res.add(Token.identifier(el.line, el.start, el.value)); break; + case NUMBER: + if (el.value.startsWith("0x") || el.value.startsWith("0X")) { + if (el.value.endsWith("x") || el.value.endsWith("X")) { + throw new SyntaxException(loc, "Invalid number format."); + } + res.add(Token.number(el.line, el.start, parseHex(el.value))); break; + } + if ( + el.value.endsWith("e") || el.value.endsWith("E") || el.value.endsWith("-") + ) throw new SyntaxException(loc, "Invalid number format."); + else res.add(Token.number(el.line, el.start, Double.parseDouble(el.value))); break; + case OPERATOR: + Operator op = Operator.parse(el.value); + if (op == null) throw new SyntaxException(loc, "Unrecognized operator '%s'.".formatted(el.value)); + res.add(Token.operator(el.line, el.start, op)); + break; + case STRING: + res.add(Token.string(el.line, el.start, parseString(loc, el.value))); + break; + case REGEX: + res.add(Token.regex(el.line, el.start, parseRegex(loc, el.value))); + break; + } + } + + return res; + } + + public static List tokenize(String filename, String raw) { + return parseTokens(filename, splitTokens(filename, raw)); + } + + public static Location getLoc(String filename, List 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); + } + public static int getLines(List tokens) { + if (tokens.size() == 0) return 1; + return tokens.get(tokens.size() - 1).line; + } + + public static ParseRes parseIdentifier(List tokens, int i) { + try { + if (tokens.get(i).isIdentifier()) { + return ParseRes.res(tokens.get(i).identifier(), 1); + } + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + public static ParseRes parseOperator(List tokens, int i) { + try { + if (tokens.get(i).isOperator()) { + return ParseRes.res(tokens.get(i).operator(), 1); + } + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + + public static boolean isIdentifier(List tokens, int i, String lit) { + try { + if (tokens.get(i).isIdentifier(lit)) { + return true; + } + else return false; + } + catch (IndexOutOfBoundsException e) { + return false; + } + } + public static boolean isOperator(List tokens, int i, Operator op) { + try { + if (tokens.get(i).isOperator(op)) { + return true; + } + else return false; + } + catch (IndexOutOfBoundsException e) { + return false; + } + } + public static boolean isStatementEnd(List tokens, int i) { + if (isOperator(tokens, i, Operator.SEMICOLON)) return true; + if (isOperator(tokens, i, Operator.BRACE_CLOSE)) return true; + if (i < 0) return false; + if (i >= tokens.size()) return true; + return getLoc(null, tokens, i).line() > getLoc(null, tokens, i - 1).line(); + } + public static boolean checkVarName(String name) { + return !reserved.contains(name); + } + + public static ParseRes parseString(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + try { + if (tokens.get(i).isString()) { + return ParseRes.res(new ConstantStatement(loc, tokens.get(i).string()), 1); + } + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + public static ParseRes parseNumber(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + try { + if (tokens.get(i).isNumber()) { + return ParseRes.res(new ConstantStatement(loc, tokens.get(i).number()), 1); + } + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + + } + public static ParseRes parseRegex(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + try { + if (tokens.get(i).isRegex()) { + var val = tokens.get(i).regex(); + var index = val.lastIndexOf('/'); + var first = val.substring(1, index); + var second = val.substring(index + 1); + return ParseRes.res(new NewStatement(loc, + new VariableStatement(null, "RegExp"), + new ConstantStatement(loc, first), + new ConstantStatement(loc, second) + ), 1); + } + else return ParseRes.failed(); + } + catch (IndexOutOfBoundsException e) { + return ParseRes.failed(); + } + } + + public static ParseRes parseArray(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isOperator(tokens, i + n++, Operator.BRACKET_OPEN)) return ParseRes.failed(); + + var values = new ArrayList(); + + // Java allows labels, so me uses labels + loop: while (true) { + if (isOperator(tokens, i + n, Operator.BRACKET_CLOSE)) { + n++; + break; + } + + while (isOperator(tokens, i + n, Operator.COMMA)) { + n++; + values.add(null); + if (isOperator(tokens, i + n, Operator.BRACKET_CLOSE)) { + n++; + break loop; + } + } + + var res = parseValue(filename, tokens, i + n, 2); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected an array element.", res); + else n += res.n; + + values.add(res.result); + + if (isOperator(tokens, i + n, Operator.COMMA)) n++; + else if (isOperator(tokens, i + n, Operator.BRACKET_CLOSE)) { + n++; + break; + } + } + + return ParseRes.res(new ArrayStatement(loc, values.toArray(Statement[]::new)), n); + } + + public static ParseRes> parseParamList(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a parameter list."); + + var args = new ArrayList(); + + if (isOperator(tokens, i + n, Operator.PAREN_CLOSE)) { + n++; + } + else { + while (true) { + var argRes = parseIdentifier(tokens, i + n); + if (argRes.isSuccess()) { + args.add(argRes.result); + n++; + if (isOperator(tokens, i + n, Operator.COMMA)) { + n++; + } + if (isOperator(tokens, i + n, Operator.PAREN_CLOSE)) { + n++; + break; + } + } + else return ParseRes.error(loc, "Expected an argument, comma or a closing brace."); + } + } + + return ParseRes.res(args, n); + } + + public static ParseRes parsePropName(String filename, List tokens, int i) { + var idRes = parseIdentifier(tokens, i); + if (idRes.isSuccess()) return ParseRes.res(idRes.result, 1); + var strRes = parseString(null, tokens, i); + if (strRes.isSuccess()) return ParseRes.res(strRes.result.value, 1); + var numRes = parseNumber(null, tokens, i); + if (numRes.isSuccess()) return ParseRes.res(numRes.result.value, 1); + + return ParseRes.failed(); + } + public static ParseRes parseObjectProp(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var accessRes = parseIdentifier(tokens, i + n++); + if (!accessRes.isSuccess()) return ParseRes.failed(); + var access = accessRes.result; + if (!access.equals("get") && !access.equals("set")) return ParseRes.failed(); + + var nameRes = parsePropName(filename, tokens, i + n); + if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a property name after '" + access + "'."); + var name = nameRes.result; + n += nameRes.n; + + var argsRes = parseParamList(filename, tokens, i + n); + if (!argsRes.isSuccess()) return ParseRes.error(loc, "Expected an argument list.", argsRes); + n += argsRes.n; + + var res = parseCompound(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected a compound statement for property accessor.", res); + n += res.n; + + return ParseRes.res(new ObjProp( + name, access, + new FunctionStatement(loc, access + " " + name.toString(), argsRes.result.toArray(String[]::new), res.result) + ), n); + } + public static ParseRes parseObject(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.failed(); + + var values = new LinkedHashMap(); + var getters = new LinkedHashMap(); + var setters = new LinkedHashMap(); + + if (isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + return ParseRes.res(new ObjectStatement(loc, values, getters, setters), n); + } + + while (true) { + var propRes = parseObjectProp(filename, tokens, i + n); + + if (propRes.isSuccess()) { + n += propRes.n; + if (propRes.result.access.equals("set")) { + setters.put(propRes.result.name, propRes.result.func); + } + else { + getters.put(propRes.result.name, propRes.result.func); + } + } + else { + var nameRes = parsePropName(filename, tokens, i + n); + if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a field name.", propRes); + n += nameRes.n; + + if (!isOperator(tokens, i + n++, Operator.COLON)) return ParseRes.error(loc, "Expected a colon."); + + var valRes = parseValue(filename, tokens, i + n, 2); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value in array list.", valRes); + n += valRes.n; + + values.put(nameRes.result, valRes.result); + } + + if (isOperator(tokens, i + n, Operator.COMMA)) { + n++; + if (isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + break; + } + continue; + } + else if (isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + break; + } + else ParseRes.error(loc, "Expected a comma or a closing brace."); + } + + return ParseRes.res(new ObjectStatement(loc, values, getters, setters), n); + } + public static ParseRes parseNew(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + var n = 0; + if (!isIdentifier(tokens, i + n++, "new")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 18); + n += valRes.n; + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'new' keyword.", valRes); + var callRes = parseCall(filename, tokens, i + n, valRes.result, 0); + n += callRes.n; + if (callRes.isError()) return callRes.transform(); + else if (callRes.isFailed()) return ParseRes.res(new NewStatement(loc, valRes.result), n); + var call = (CallStatement)callRes.result; + + return ParseRes.res(new NewStatement(loc, call.func, call.args), n); + } + public static ParseRes parseTypeof(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + var n = 0; + if (!isIdentifier(tokens, i + n++, "typeof")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 15); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'typeof' keyword.", valRes); + n += valRes.n; + + return ParseRes.res(new TypeofStatement(loc, valRes.result), n); + } + public static ParseRes parseVoid(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + var n = 0; + if (!isIdentifier(tokens, i + n++, "void")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 14); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'void' keyword.", valRes); + n += valRes.n; + + return ParseRes.res(new VoidStatement(loc, valRes.result), n); + } + public static ParseRes parseDelete(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isIdentifier(tokens, i + n++, "delete")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 15); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'delete'.", valRes); + n += valRes.n; + + if (valRes.result instanceof IndexStatement) { + var index = (IndexStatement)valRes.result; + return ParseRes.res(new DeleteStatement(loc, index.index, index.object), n); + } + else if (valRes.result instanceof VariableStatement) { + return ParseRes.error(loc, "A variable may not be deleted."); + } + else { + return ParseRes.res(new ConstantStatement(loc, true), n); + } + } + + public static ParseRes parseFunction(String filename, List tokens, int i, boolean statement) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (!isIdentifier(tokens, i + n++, "function")) return ParseRes.failed(); + + var nameRes = parseIdentifier(tokens, i + n); + if (!nameRes.isSuccess() && statement) return ParseRes.error(loc, "A statement function requires a name, one is not present."); + var name = nameRes.result; + n += nameRes.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a parameter list."); + + var args = new ArrayList(); + + if (isOperator(tokens, i + n, Operator.PAREN_CLOSE)) { + n++; + } + else { + while (true) { + var argRes = parseIdentifier(tokens, i + n); + if (argRes.isSuccess()) { + args.add(argRes.result); + n++; + if (isOperator(tokens, i + n, Operator.COMMA)) { + n++; + } + if (isOperator(tokens, i + n, Operator.PAREN_CLOSE)) { + n++; + break; + } + } + else return ParseRes.error(loc, "Expected an argument, comma or a closing brace."); + } + } + + var res = parseCompound(filename, tokens, i + n); + n += res.n; + + if (res.isSuccess()) return ParseRes.res(new FunctionStatement(loc, name, args.toArray(String[]::new), res.result), n); + else return ParseRes.error(loc, "Expected a compound statement for function.", res); + } + + public static ParseRes parseUnary(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var opState = parseOperator(tokens, i + n++); + if (!opState.isSuccess()) return ParseRes.failed(); + var op = opState.result; + + Type operation = null; + + if (op == Operator.ADD) operation = Type.POS; + else if (op == Operator.SUBTRACT) operation = Type.NEG; + else if (op == Operator.INVERSE) operation = Type.INVERSE; + else if (op == Operator.NOT) operation = Type.NOT; + else return ParseRes.failed(); + + var res = parseValue(filename, tokens, n + i, 14); + + if (res.isSuccess()) return ParseRes.res(new OperationStatement(loc, operation, res.result), n + res.n); + else return ParseRes.error(loc, "Expected a value after the unary operator '%s'.".formatted(op.value), res); + } + public static ParseRes parsePrefixChange(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var opState = parseOperator(tokens, i + n++); + if (!opState.isSuccess()) return ParseRes.failed(); + + int change = 0; + + if (opState.result == Operator.INCREASE) change = 1; + else if (opState.result == Operator.DECREASE) change = -1; + else return ParseRes.failed(); + + var res = parseValue(filename, tokens, i + n, 15); + 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 parseParens(String filename, List tokens, int i) { + int n = 0; + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.failed(); + + var res = parseValue(filename, tokens, i + n, 0); + if (!res.isSuccess()) return res; + n += res.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.failed(); + + return ParseRes.res(res.result, n); + } + @SuppressWarnings("all") + public static ParseRes parseSimple(String filename, List tokens, int i, boolean statement) { + var res = new ArrayList<>(List.of( + parseVariable(filename, tokens, i), + parseLiteral(filename, tokens, i), + parseString(filename, tokens, i), + parseRegex(filename, tokens, i), + parseNumber(filename, tokens, i), + parseUnary(filename, tokens, i), + parseArray(filename, tokens, i), + parsePrefixChange(filename, tokens, i), + parseParens(filename, tokens, i), + parseNew(filename, tokens, i), + parseTypeof(filename, tokens, i), + parseVoid(filename, tokens, i), + parseDelete(filename, tokens, i) + )); + + if (!statement) { + res.add(parseObject(filename, tokens, i)); + res.add(parseFunction(filename, tokens, i, false)); + } + + return ParseRes.any(res.toArray(ParseRes[]::new)); + } + + public static ParseRes parseVariable(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + var literal = parseIdentifier(tokens, i); + + if (!literal.isSuccess()) return ParseRes.failed(); + + if (!checkVarName(literal.result)) { + if (literal.result.equals("await")) return ParseRes.error(loc, "'await' expressions are not supported."); + if (literal.result.equals("async")) return ParseRes.error(loc, "'async' is not supported."); + if (literal.result.equals("const")) return ParseRes.error(loc, "'const' declarations are not supported."); + if (literal.result.equals("let")) return ParseRes.error(loc, "'let' declarations are not supported."); + return ParseRes.error(loc, "Unexpected identifier '%s'.".formatted(literal.result)); + } + + return ParseRes.res(new VariableStatement(loc, literal.result), 1); + } + public static ParseRes parseLiteral(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + var id = parseIdentifier(tokens, i); + if (!id.isSuccess()) return id.transform(); + + if (id.result.equals("true")) { + return ParseRes.res(new ConstantStatement(loc, true), 1); + } + if (id.result.equals("false")) { + return ParseRes.res(new ConstantStatement(loc, false), 1); + } + if (id.result.equals("undefined")) { + return ParseRes.res(new ConstantStatement(loc, null), 1); + } + if (id.result.equals("null")) { + return ParseRes.res(new ConstantStatement(loc, Values.NULL), 1); + } + if (id.result.equals("NaN")) { + return ParseRes.res(new ConstantStatement(loc, Double.NaN), 1); + } + if (id.result.equals("Infinity")) { + return ParseRes.res(new ConstantStatement(loc, Double.POSITIVE_INFINITY), 1); + } + if (id.result.equals("this")) { + return ParseRes.res(new VariableIndexStatement(loc, 0), 1); + } + if (id.result.equals("arguments")) { + return ParseRes.res(new VariableIndexStatement(loc, 1), 1); + } + if (id.result.equals("globalThis") || id.result.equals("window") || id.result.equals("self")) { + return ParseRes.res(new GlobalThisStatement(loc), 1); + } + return ParseRes.failed(); + } + public static ParseRes parseMember(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + var n = 0; + + if (precedence > 18) return ParseRes.failed(); + + if (!isOperator(tokens, i + n++, Operator.DOT)) return ParseRes.failed(); + + var literal = parseIdentifier(tokens, i + n++); + if (!literal.isSuccess()) return ParseRes.error(loc, "Expected an identifier after member access."); + + return ParseRes.res(new IndexStatement(loc, prev, new ConstantStatement(loc, literal.result)), n); + } + public static ParseRes parseIndex(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + var n = 0; + + if (precedence > 18) return ParseRes.failed(); + + if (!isOperator(tokens, i + n++, Operator.BRACKET_OPEN)) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 0); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value in index expression.", valRes); + n += valRes.n; + + if (!isOperator(tokens, i + n++, Operator.BRACKET_CLOSE)) return ParseRes.error(loc, "Expected a closing bracket."); + + return ParseRes.res(new IndexStatement(loc, prev, valRes.result), n); + } + public static ParseRes parseAssign(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + int n = 0 ; + + if (precedence > 2) return ParseRes.failed(); + + var opRes = parseOperator(tokens, i + n++); + if (opRes.state != State.SUCCESS) return ParseRes.failed(); + + var op = opRes.result; + if (!op.isAssign()) return ParseRes.failed(); + + if (!(prev instanceof AssignableStatement)) return ParseRes.error(loc, "Invalid expression on left hand side of assign operator."); + + var res = parseValue(filename, tokens, i + n, 2); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected value after assignment operator '%s'.".formatted(op.value), res); + n += res.n; + + Type operation = null; + + if (op == Operator.ASSIGN_ADD) operation = Type.ADD; + if (op == Operator.ASSIGN_SUBTRACT) operation = Type.SUBTRACT; + if (op == Operator.ASSIGN_MULTIPLY) operation = Type.MULTIPLY; + if (op == Operator.ASSIGN_DIVIDE) operation = Type.DIVIDE; + if (op == Operator.ASSIGN_MODULO) operation = Type.MODULO; + if (op == Operator.ASSIGN_OR) operation = Type.OR; + if (op == Operator.ASSIGN_XOR) operation = Type.XOR; + if (op == Operator.ASSIGN_AND) operation = Type.AND; + if (op == Operator.ASSIGN_SHIFT_LEFT) operation = Type.SHIFT_LEFT; + if (op == Operator.ASSIGN_SHIFT_RIGHT) operation = Type.SHIFT_RIGHT; + if (op == Operator.ASSIGN_USHIFT_RIGHT) operation = Type.USHIFT_RIGHT; + + return ParseRes.res(((AssignableStatement)prev).toAssign(res.result, operation), n); + } + public static ParseRes parseCall(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + var n = 0; + + if (precedence > 17) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.failed(); + + var args = new ArrayList(); + boolean prevArg = false; + + while (true) { + var argRes = parseValue(filename, tokens, i + n, 2); + if (argRes.isSuccess()) { + args.add(argRes.result); + n += argRes.n; + prevArg = true; + } + else if (argRes.isError()) return argRes.transform(); + else if (isOperator(tokens, i + n, Operator.COMMA)) { + if (!prevArg) args.add(null); + prevArg = false; + n++; + } + else if (isOperator(tokens, i + n, Operator.PAREN_CLOSE)) { + n++; + break; + } + else return ParseRes.failed(); + } + + return ParseRes.res(new CallStatement(loc, prev, args.toArray(Statement[]::new)), n); + } + public static ParseRes parsePostfixChange(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (precedence > 15) return ParseRes.failed(); + + var opState = parseOperator(tokens, i + n++); + if (!opState.isSuccess()) return ParseRes.failed(); + + int change = 0; + + if (opState.result == Operator.INCREASE) change = 1; + else if (opState.result == Operator.DECREASE) change = -1; + else return ParseRes.failed(); + + 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 parseInstanceof(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (precedence > 9) return ParseRes.failed(); + if (!isIdentifier(tokens, i + n++, "instanceof")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 10); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'instanceof'.", valRes); + n += valRes.n; + + return ParseRes.res(new OperationStatement(loc, Type.INSTANCEOF, prev, valRes.result), n); + } + public static ParseRes parseIn(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (precedence > 9) return ParseRes.failed(); + if (!isIdentifier(tokens, i + n++, "in")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 10); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'in'.", valRes); + n += valRes.n; + + return ParseRes.res(new OperationStatement(loc, Type.IN, prev, valRes.result), n); + } + public static ParseRes parseComma(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + var n = 0; + + if (precedence > 1) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.COMMA)) return ParseRes.failed(); + + var res = parseValue(filename, tokens, i + n, 2); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected a value after the comma.", res); + n += res.n; + + return ParseRes.res(new CommaStatement(loc, prev, res.result), n); + } + public static ParseRes parseTernary(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + var n = 0; + + if (precedence > 2) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.QUESTION)) return ParseRes.failed(); + + var a = parseValue(filename, tokens, i + n, 2); + if (!a.isSuccess()) return ParseRes.error(loc, "Expected a value after the ternary operator.", a); + n += a.n; + + if (!isOperator(tokens, i + n++, Operator.COLON)) return ParseRes.failed(); + + var b = parseValue(filename, tokens, i + n, 2); + if (!b.isSuccess()) return ParseRes.error(loc, "Expected a second value after the ternary operator.", b); + n += b.n; + + return ParseRes.res(new TernaryStatement(loc, prev, a.result, b.result), n); + } + public static ParseRes parseOperator(String filename, List tokens, int i, Statement prev, int precedence) { + var loc = getLoc(filename, tokens, i); + var n = 0; + + var opRes = parseOperator(tokens, i + n++); + if (!opRes.isSuccess()) return ParseRes.failed(); + var op = opRes.result; + + if (op.precedence < precedence) return ParseRes.failed(); + if (op.isAssign()) return parseAssign(filename, tokens, i + n - 1, prev, precedence); + + var res = parseValue(filename, tokens, i + n, op.precedence + (op.reverse ? 0 : 1)); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected a value after the '%s' operator.".formatted(op.value), res); + n += res.n; + + if (op == Operator.LAZY_AND) { + return ParseRes.res(new LazyAndStatement(loc, prev, res.result), n); + } + if (op == Operator.LAZY_OR) { + return ParseRes.res(new LazyOrStatement(loc, prev, res.result), n); + } + + return ParseRes.res(new OperationStatement(loc, op.operation, prev, res.result), n); + } + + public static ParseRes parseValue(String filename, List tokens, int i, int precedence, boolean statement) { + Statement prev = null; + int n = 0; + + while (true) { + if (prev == null) { + var res = parseSimple(filename, tokens, i + n, statement); + if (res.isSuccess()) { + n += res.n; + prev = res.result; + } + else if (res.isError()) return res.transform(); + else break; + } + else { + var res = ParseRes.any( + parseOperator(filename, tokens, i + n, prev, precedence), + parseMember(filename, tokens, i + n, prev, precedence), + parseIndex(filename, tokens, i + n, prev, precedence), + parseCall(filename, tokens, i + n, prev, precedence), + parsePostfixChange(filename, tokens, i + n, prev, precedence), + parseInstanceof(filename, tokens, i + n, prev, precedence), + parseIn(filename, tokens, i + n, prev, precedence), + parseComma(filename, tokens, i + n, prev, precedence), + parseTernary(filename, tokens, i + n, prev, precedence) + ); + + if (res.isSuccess()) { + n += res.n; + prev = res.result; + continue; + } + else if (res.isError()) return res.transform(); + + break; + } + } + + if (prev == null) return ParseRes.failed(); + else return ParseRes.res(prev, n); + } + public static ParseRes parseValue(String filename, List tokens, int i, int precedence) { + return parseValue(filename, tokens, i, precedence, false); + } + + public static ParseRes parseValueStatement(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + var valRes = parseValue(filename, tokens, i, 0, true); + if (!valRes.isSuccess()) return valRes.transform(); + + valRes.result.setLoc(loc); + var res = ParseRes.res(valRes.result, valRes.n); + + if (isStatementEnd(tokens, i + res.n)) { + if (isOperator(tokens, i + res.n, Operator.SEMICOLON)) return res.addN(1); + else return res; + } + else if (isIdentifier(tokens, i, "const") || isIdentifier(tokens, i, "let")) { + return ParseRes.error(getLoc(filename, tokens, i), "Detected the usage of 'const'/'let'. Please, use 'var' instead."); + } + else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.", res); + } + public static ParseRes parseVariableDeclare(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isIdentifier(tokens, i + n++, "var")) return ParseRes.failed(); + + var res = new ArrayList(); + + if (isStatementEnd(tokens, i + n)) { + if (isOperator(tokens, i + n, Operator.SEMICOLON)) return ParseRes.res(new VariableDeclareStatement(loc, res), 2); + else return ParseRes.res(new VariableDeclareStatement(loc, res), 1); + } + + while (true) { + var nameRes = parseIdentifier(tokens, i + n++); + if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a variable name."); + + if (!checkVarName(nameRes.result)) { + return ParseRes.error(loc, "Unexpected identifier '%s'.".formatted(nameRes.result)); + } + + Statement val = null; + + if (isOperator(tokens, i + n, Operator.ASSIGN)) { + n++; + var valRes = parseValue(filename, tokens, i + n, 2); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after '='.", valRes); + n += valRes.n; + val = valRes.result; + } + + res.add(new Pair(nameRes.result, val)); + + if (isOperator(tokens, i + n, Operator.COMMA)) { + n++; + continue; + } + else if (isStatementEnd(tokens, i + n)) { + if (isOperator(tokens, i + n, Operator.SEMICOLON)) return ParseRes.res(new VariableDeclareStatement(loc, res), n + 1); + else return ParseRes.res(new VariableDeclareStatement(loc, res), n); + } + else return ParseRes.error(loc, "Expected a comma or end of statement."); + } + } + + public static ParseRes parseReturn(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isIdentifier(tokens, i + n++, "return")) return ParseRes.failed(); + + if (isStatementEnd(tokens, i + n)) { + if (isOperator(tokens, i + n, Operator.SEMICOLON)) return ParseRes.res(new ReturnStatement(loc, null), 2); + else return ParseRes.res(new ReturnStatement(loc, null), 1); + } + + var valRes = parseValue(filename, tokens, i + n, 0); + n += valRes.n; + if (valRes.isError()) + return ParseRes.error(loc, "Expected a return value.", valRes); + + var res = ParseRes.res(new ReturnStatement(loc, valRes.result), n); + + if (isStatementEnd(tokens, i + n)) { + if (isOperator(tokens, i + n, Operator.SEMICOLON)) return res.addN(1); + else return res; + } + else + return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.", valRes); + } + public static ParseRes parseThrow(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isIdentifier(tokens, i + n++, "throw")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 0); + n += valRes.n; + if (valRes.isError()) return ParseRes.error(loc, "Expected a throw value.", valRes); + + var res = ParseRes.res(new ThrowStatement(loc, valRes.result), n); + + if (isStatementEnd(tokens, i + n)) { + if (isOperator(tokens, i + n, Operator.SEMICOLON)) return res.addN(1); + else return res; + } + else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement.", valRes); + } + + public static ParseRes parseBreak(String filename, List tokens, int i) { + if (!isIdentifier(tokens, i, "break")) return ParseRes.failed(); + + if (isStatementEnd(tokens, i + 1)) { + if (isOperator(tokens, i + 1, Operator.SEMICOLON)) return ParseRes.res(new BreakStatement(getLoc(filename, tokens, i), null), 2); + else return ParseRes.res(new BreakStatement(getLoc(filename, tokens, i), null), 1); + } + + var labelRes = parseIdentifier(tokens, i + 1); + if (labelRes.isFailed()) return ParseRes.error(getLoc(filename, tokens, i), "Expected a label name or an end of statement."); + var label = labelRes.result; + + if (isStatementEnd(tokens, i + 2)) { + if (isOperator(tokens, i + 2, Operator.SEMICOLON)) return ParseRes.res(new BreakStatement(getLoc(filename, tokens, i), label), 3); + else return ParseRes.res(new BreakStatement(getLoc(filename, tokens, i), label), 2); + } + else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement."); + } + public static ParseRes parseContinue(String filename, List tokens, int i) { + if (!isIdentifier(tokens, i, "continue")) return ParseRes.failed(); + + if (isStatementEnd(tokens, i + 1)) { + if (isOperator(tokens, i + 1, Operator.SEMICOLON)) return ParseRes.res(new ContinueStatement(getLoc(filename, tokens, i), null), 2); + else return ParseRes.res(new ContinueStatement(getLoc(filename, tokens, i), null), 1); + } + + var labelRes = parseIdentifier(tokens, i + 1); + if (labelRes.isFailed()) return ParseRes.error(getLoc(filename, tokens, i), "Expected a label name or an end of statement."); + var label = labelRes.result; + + if (isStatementEnd(tokens, i + 2)) { + if (isOperator(tokens, i + 2, Operator.SEMICOLON)) return ParseRes.res(new ContinueStatement(getLoc(filename, tokens, i), label), 3); + else return ParseRes.res(new ContinueStatement(getLoc(filename, tokens, i), label), 2); + } + else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement."); + } + public static ParseRes parseDebug(String filename, List tokens, int i) { + if (!isIdentifier(tokens, i, "debug")) 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); + else return ParseRes.res(new DebugStatement(getLoc(filename, tokens, i)), 1); + } + else return ParseRes.error(getLoc(filename, tokens, i), "Expected an end of statement."); + } + + public static ParseRes parseCompound(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + if (!isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.failed(); + + var statements = new ArrayList(); + + while (true) { + if (isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + break; + } + if (isOperator(tokens, i + n, Operator.SEMICOLON)) { + n++; + continue; + } + + var res = parseStatement(filename, tokens, i + n); + if (!res.isSuccess()) { + return ParseRes.error(getLoc(filename, tokens, i), "Expected a statement.", res); + } + n += res.n; + + statements.add(res.result); + } + + return ParseRes.res(new CompoundStatement(loc, statements.toArray(Statement[]::new)), n); + } + public static ParseRes parseLabel(List tokens, int i) { + int n = 0; + + var nameRes = parseIdentifier(tokens, i + n++); + if (!isOperator(tokens, i + n++, Operator.COLON)) return ParseRes.failed(); + + return ParseRes.res(nameRes.result, n); + } + public static ParseRes parseIf(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (!isIdentifier(tokens, i + n++, "if")) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a open paren after 'if'."); + + var condRes = parseValue(filename, tokens, i + n, 0); + if (!condRes.isSuccess()) return ParseRes.error(loc, "Expected an if condition.", condRes); + n += condRes.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) + return ParseRes.error(loc, "Expected a closing paren after if condition."); + + var res = parseStatement(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected an if body.", res); + n += res.n; + + if (!isIdentifier(tokens, i + n, "else")) return ParseRes.res(new IfStatement(loc, condRes.result, res.result, null), n); + n++; + + var elseRes = parseStatement(filename, tokens, i + n); + if (!elseRes.isSuccess()) return ParseRes.error(loc, "Expected an else body.", elseRes); + n += elseRes.n; + + return ParseRes.res(new IfStatement(loc, condRes.result, res.result, elseRes.result), n); + } + public static ParseRes parseWhile(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var labelRes = parseLabel(tokens, i + n); + n += labelRes.n; + + if (!isIdentifier(tokens, i + n++, "while")) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a open paren after 'while'."); + + var condRes = parseValue(filename, tokens, i + n, 0); + if (!condRes.isSuccess()) return ParseRes.error(loc, "Expected a while condition.", condRes); + n += condRes.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.error(loc, "Expected a closing paren after while condition."); + + var res = parseStatement(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected a while body.", res); + n += res.n; + + return ParseRes.res(new WhileStatement(loc, labelRes.result, condRes.result, res.result), n); + } + public static ParseRes parseSwitchCase(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (!isIdentifier(tokens, i + n++, "case")) return ParseRes.failed(); + + var valRes = parseValue(filename, tokens, i + n, 0); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after 'case'.", valRes); + n += valRes.n; + + if (!isOperator(tokens, i + n++, Operator.COLON)) return ParseRes.error(loc, "Expected colons after 'case' value."); + + return ParseRes.res(valRes.result, n); + } + public static ParseRes parseDefaultCase(List tokens, int i) { + if (!isIdentifier(tokens, i, "default")) return ParseRes.failed(); + if (!isOperator(tokens, i + 1, Operator.COLON)) return ParseRes.error(getLoc(null, tokens, i), "Expected colons after 'default'."); + + return ParseRes.res(null, 2); + } + public static ParseRes parseSwitch(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (!isIdentifier(tokens, i + n++, "switch")) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a open paren after 'switch'."); + + var valRes = parseValue(filename, tokens, i + n, 0); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a switch value.", valRes); + n += valRes.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.error(loc, "Expected a closing paren after switch value."); + if (!isOperator(tokens, i + n++, Operator.BRACE_OPEN)) return ParseRes.error(loc, "Expected an opening brace after switch value."); + + var statements = new ArrayList(); + var cases = new ArrayList(); + var defaultI = -1; + + while (true) { + if (isOperator(tokens, i + n, Operator.BRACE_CLOSE)) { + n++; + break; + } + if (isOperator(tokens, i + n, Operator.SEMICOLON)) { + n++; + continue; + } + + var defaultRes = parseDefaultCase(tokens, i + n); + var caseRes = parseSwitchCase(filename, tokens, i + n); + + if (defaultRes.isSuccess()) { + defaultI = statements.size(); + n += defaultRes.n; + } + else if (caseRes.isSuccess()) { + cases.add(new SwitchCase(caseRes.result, statements.size())); + n += caseRes.n; + } + else if (defaultRes.isError()) return defaultRes.transform(); + else if (caseRes.isError()) return defaultRes.transform(); + else { + var res = ParseRes.any( + parseStatement(filename, tokens, i + n), + parseCompound(filename, tokens, i + n) + ); + if (!res.isSuccess()) { + return ParseRes.error(getLoc(filename, tokens, i), "Expected a statement.", res); + } + n += res.n; + statements.add(res.result); + } + } + + return ParseRes.res(new SwitchStatement( + loc, valRes.result, defaultI, + cases.toArray(SwitchCase[]::new), + statements.toArray(Statement[]::new) + ), n); + } + public static ParseRes parseDoWhile(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var labelRes = parseLabel(tokens, i + n); + n += labelRes.n; + + if (!isIdentifier(tokens, i + n++, "do")) return ParseRes.failed(); + var bodyRes = parseStatement(filename, tokens, i + n); + if (!bodyRes.isSuccess()) return ParseRes.error(loc, "Expected a do-while body.", bodyRes); + n += bodyRes.n; + + if (!isIdentifier(tokens, i + n++, "while")) return ParseRes.error(loc, "Expected 'while' keyword."); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a open paren after 'while'."); + + var condRes = parseValue(filename, tokens, i + n, 0); + if (!condRes.isSuccess()) return ParseRes.error(loc, "Expected a while condition.", condRes); + n += condRes.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.error(loc, "Expected a closing paren after while condition."); + + var res = ParseRes.res(new DoWhileStatement(loc, labelRes.result, condRes.result, bodyRes.result), n); + + if (isStatementEnd(tokens, i + n)) { + if (isOperator(tokens, i + n, Operator.SEMICOLON)) return res.addN(1); + else return res; + } + else return ParseRes.error(getLoc(filename, tokens, i), "Expected a semicolon."); + } + public static ParseRes parseFor(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var labelRes = parseLabel(tokens, i + n); + n += labelRes.n; + + if (!isIdentifier(tokens, i + n++, "for")) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a open paren after 'for'."); + + Statement decl, cond, inc; + + if (isOperator(tokens, i + n, Operator.SEMICOLON)) { + n++; + decl = new CompoundStatement(loc); + } + else { + var declRes = ParseRes.any( + parseVariableDeclare(filename, tokens, i + n), + parseValueStatement(filename, tokens, i + n) + ); + if (!declRes.isSuccess()) + return ParseRes.error(loc, "Expected a declaration or an expression.", declRes); + n += declRes.n; + decl = declRes.result; + } + + if (isOperator(tokens, i + n, Operator.SEMICOLON)) { + n++; + cond = new ConstantStatement(loc, 1); + } + else { + var condRes = parseValue(filename, tokens, i + n, 0); + if (!condRes.isSuccess()) return ParseRes.error(loc, "Expected a condition.", condRes); + n += condRes.n; + if (!isOperator(tokens, i + n++, Operator.SEMICOLON)) return ParseRes.error(loc, "Expected a semicolon.", condRes); + cond = condRes.result; + } + + if (isOperator(tokens, i + n, Operator.PAREN_CLOSE)) { + n++; + inc = new CompoundStatement(loc); + } + else { + var incRes = parseValue(filename, tokens, i + n, 0); + if (!incRes.isSuccess()) return ParseRes.error(loc, "Expected a condition.", incRes); + n += incRes.n; + inc = incRes.result; + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.error(loc, "Expected a closing paren after for."); + } + + + var res = parseStatement(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected a for body.", res); + n += res.n; + + return ParseRes.res(new ForStatement(loc, labelRes.result, decl, cond, inc, res.result), n); + } + public static ParseRes parseForIn(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + var labelRes = parseLabel(tokens, i + n); + var isDecl = false; + n += labelRes.n; + + if (!isIdentifier(tokens, i + n++, "for")) return ParseRes.failed(); + if (!isOperator(tokens, i + n++, Operator.PAREN_OPEN)) return ParseRes.error(loc, "Expected a open paren after 'for'."); + + if (isIdentifier(tokens, i + n, "var")) { + isDecl = true; + n++; + } + + var nameRes = parseIdentifier(tokens, i + n); + if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a variable name for 'for' loop."); + n += nameRes.n; + + Statement varVal = null; + + if (isOperator(tokens, i + n, Operator.ASSIGN)) { + n++; + + var valRes = parseValue(filename, tokens, i + n, 2); + if (!valRes.isSuccess()) return ParseRes.error(loc, "Expected a value after '='.", valRes); + n += nameRes.n; + + varVal = valRes.result; + } + + if (!isIdentifier(tokens, i + n++, "in")) { + if (varVal == null) { + if (nameRes.result.equals("const")) return ParseRes.error(loc, "'const' declarations are not supported."); + else if (nameRes.result.equals("let")) return ParseRes.error(loc, "'let' declarations are not supported."); + } + return ParseRes.error(loc, "Expected 'in' keyword after variable declaration."); + } + + var objRes = parseValue(filename, tokens, i + n, 0); + if (!objRes.isSuccess()) return ParseRes.error(loc, "Expected a value.", objRes); + n += objRes.n; + + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.error(loc, "Expected a closing paren after for."); + + + var bodyRes = parseStatement(filename, tokens, i + n); + if (!bodyRes.isSuccess()) return ParseRes.error(loc, "Expected a for body.", bodyRes); + n += bodyRes.n; + + return ParseRes.res(new ForInStatement(loc, labelRes.result, isDecl, nameRes.result, varVal, objRes.result, bodyRes.result), n); + } + public static ParseRes parseCatch(String filename, List tokens, int i) { + var loc = getLoc(filename, tokens, i); + int n = 0; + + if (!isIdentifier(tokens, i + n++, "try")) return ParseRes.failed(); + + var res = parseStatement(filename, tokens, i + n); + if (!res.isSuccess()) return ParseRes.error(loc, "Expected an if body.", res); + n += res.n; + + String name = null; + Statement catchBody = null, finallyBody = null; + + + if (isIdentifier(tokens, i + n, "catch")) { + n++; + if (isOperator(tokens, i + n, Operator.PAREN_OPEN)) { + n++; + var nameRes = parseIdentifier(tokens, i + n++); + if (!nameRes.isSuccess()) return ParseRes.error(loc, "Expected a catch variable name."); + name = nameRes.result; + if (!isOperator(tokens, i + n++, Operator.PAREN_CLOSE)) return ParseRes.error(loc, "Expected a closing paren after catch variable name."); + } + + var catchRes = parseStatement(filename, tokens, i + n); + if (!catchRes.isSuccess()) return ParseRes.error(loc, "Expected a catch body.", catchRes); + n += catchRes.n; + catchBody = catchRes.result; + } + + if (isIdentifier(tokens, i + n, "finally")) { + n++; + var finallyRes = parseStatement(filename, tokens, i + n); + if (!finallyRes.isSuccess()) return ParseRes.error(loc, "Expected a finally body.", finallyRes); + n += finallyRes.n; + finallyBody = finallyRes.result; + } + + return ParseRes.res(new TryStatement(loc, res.result, catchBody, finallyBody, name), n); + } + + public static ParseRes parseStatement(String filename, List 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( + parseVariableDeclare(filename, tokens, i), + parseReturn(filename, tokens, i), + parseThrow(filename, tokens, i), + parseContinue(filename, tokens, i), + parseBreak(filename, tokens, i), + parseDebug(filename, tokens, i), + parseValueStatement(filename, tokens, i), + parseIf(filename, tokens, i), + parseWhile(filename, tokens, i), + parseSwitch(filename, tokens, i), + parseFor(filename, tokens, i), + parseForIn(filename, tokens, i), + parseDoWhile(filename, tokens, i), + parseCatch(filename, tokens, i), + parseCompound(filename, tokens, i), + parseFunction(filename, tokens, i, true) + ); + } + + public static Statement[] parse(String filename, String raw) { + var tokens = tokenize(filename, raw); + var list = new ArrayList(); + int i = 0; + + while (true) { + if (i >= tokens.size()) break; + + var res = Parsing.parseStatement(filename, tokens, i); + + if (res.isError()) throw new SyntaxException(getLoc(filename, tokens, i), res.error); + else if (res.isFailed()) throw new SyntaxException(getLoc(filename, tokens, i), "Unexpected syntax."); + + i += res.n; + + list.add(res.result); + } + + return list.toArray(Statement[]::new); + } + + public static CodeFunction compile(GlobalScope scope, Statement... statements) { + var target = scope.globalChild(); + var subscope = target.child(); + var res = new ArrayList(); + var body = new CompoundStatement(null, statements); + // var optimized = body.optimize(); + if (body instanceof CompoundStatement) body = (CompoundStatement)body; + else body = new CompoundStatement(null, new Statement[] { body }); + + subscope.define("this"); + subscope.define("arguments"); + + body.declare(target); + + try { + body.compile(res, subscope); + FunctionStatement.checkBreakAndCont(res, 0); + } + catch (SyntaxException e) { + res.clear(); + res.add(Instruction.throwSyntax(e)); + } + + if (res.size() != 0 && res.get(res.size() - 1).type == Type.DISCARD) { + res.set(res.size() - 1, Instruction.ret()); + } + else res.add(Instruction.ret()); + + return new CodeFunction("", subscope.localsCount(), 0, scope, new ValueVariable[0], res.toArray(Instruction[]::new)); + } + public static CodeFunction compile(GlobalScope scope, String filename, String raw) { + try { + return compile(scope, parse(filename, raw)); + } + catch (SyntaxException e) { + return new CodeFunction(null, 2, 0, scope, new ValueVariable[0], new Instruction[] { Instruction.throwSyntax(e) }); + } + } +} diff --git a/src/me/topchetoeu/jscript/parsing/RawToken.java b/src/me/topchetoeu/jscript/parsing/RawToken.java new file mode 100644 index 0000000..f105b7d --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/RawToken.java @@ -0,0 +1,15 @@ +package me.topchetoeu.jscript.parsing; + +public class RawToken { + public final String value; + public final TokenType type; + public final int line; + public final int start; + + public RawToken(String value, TokenType type, int line, int start) { + this.value = value; + this.type = type; + this.line = line; + this.start = start; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/parsing/TestRes.java b/src/me/topchetoeu/jscript/parsing/TestRes.java new file mode 100644 index 0000000..a72fe97 --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/TestRes.java @@ -0,0 +1,45 @@ +package me.topchetoeu.jscript.parsing; + +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.parsing.ParseRes.State; + +public class TestRes { + public final State state; + public final String error; + public final int i; + + private TestRes(ParseRes.State state, String error, int i) { + this.i = i; + this.state = state; + this.error = error; + } + + public TestRes add(int n) { + return new TestRes(state, null, this.i + n); + } + public ParseRes transform() { + if (isSuccess()) throw new RuntimeException("Can't transform a TestRes that hasn't failed."); + else if (isError()) return ParseRes.error(null, error); + else return ParseRes.failed(); + } + + public boolean isSuccess() { return state.isSuccess(); } + public boolean isFailed() { return state.isFailed(); } + public boolean isError() { return state.isError(); } + + public static TestRes failed() { + return new TestRes(State.FAILED, null, 0); + } + public static TestRes error(Location loc, String error) { + if (loc != null) error = loc + ": " + error; + return new TestRes(State.ERROR, error, 0); + } + public static TestRes error(Location loc, String error, TestRes other) { + if (loc != null) error = loc + ": " + error; + if (!other.isError()) return new TestRes(State.ERROR, error, 0); + return new TestRes(State.ERROR, other.error, 0); + } + public static TestRes res(int i) { + return new TestRes(State.SUCCESS, null, i); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/parsing/Token.java b/src/me/topchetoeu/jscript/parsing/Token.java new file mode 100644 index 0000000..c614632 --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/Token.java @@ -0,0 +1,55 @@ +package me.topchetoeu.jscript.parsing; + +public class Token { + public final Object value; + public final boolean isString; + public final boolean isRegex; + public final int line; + public final int start; + + private Token(int line, int start, Object value, boolean isString, boolean isRegex) { + this.value = value; + this.line = line; + this.start = start; + this.isString = isString; + this.isRegex = isRegex; + } + private Token(int line, int start, Object value) { + this.value = value; + this.line = line; + this.start = start; + this.isString = false; + this.isRegex = false; + } + + public boolean isString() { return isString; } + public boolean isRegex() { return isRegex; } + public boolean isNumber() { return value instanceof Number; } + public boolean isIdentifier() { return !isString && !isRegex && value instanceof String; } + public boolean isOperator() { return value instanceof Operator; } + + public boolean isIdentifier(String lit) { return !isString && !isRegex && value.equals(lit); } + public boolean isOperator(Operator op) { return value.equals(op); } + + public String string() { return (String)value; } + public String regex() { return (String)value; } + public double number() { return (double)value; } + public String identifier() { return (String)value; } + public Operator operator() { return (Operator)value; } + + public static Token regex(int line, int start, String val) { + return new Token(line, start, val, false, true); + } + public static Token string(int line, int start, String val) { + return new Token(line, start, val, true, false); + } + public static Token number(int line, int start, double val) { + return new Token(line, start, val); + } + public static Token identifier(int line, int start, String val) { + return new Token(line, start, val); + } + public static Token operator(int line, int start, Operator val) { + return new Token(line, start, val); + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/parsing/TokenType.java b/src/me/topchetoeu/jscript/parsing/TokenType.java new file mode 100644 index 0000000..6cdfdfa --- /dev/null +++ b/src/me/topchetoeu/jscript/parsing/TokenType.java @@ -0,0 +1,9 @@ +package me.topchetoeu.jscript.parsing; + +enum TokenType { + REGEX, + STRING, + NUMBER, + LITERAL, + OPERATOR, +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/polyfills/Date.java b/src/me/topchetoeu/jscript/polyfills/Date.java new file mode 100644 index 0000000..8582d11 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/Date.java @@ -0,0 +1,302 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.Calendar; +import java.util.TimeZone; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.interop.Native; + +public class Date { + private Calendar normal; + private Calendar utc; + + public Date(long timestamp) { + normal = Calendar.getInstance(); + utc = Calendar.getInstance(); + normal.setTimeInMillis(timestamp); + utc.setTimeZone(TimeZone.getTimeZone("UTC")); + utc.setTimeInMillis(timestamp); + } + + @Native + public Date() { + this(new java.util.Date().getTime()); + } + + private void updateUTC() { + if (utc == null || normal == null) return; + utc.setTimeInMillis(normal.getTimeInMillis()); + } + private void updateNormal() { + if (utc == null || normal == null) return; + normal.setTimeInMillis(utc.getTimeInMillis()); + } + private void invalidate() { + normal = utc = null; + } + + @Native + public static double now() { + return new Date().getTime(); + } + + @Native + public double getYear() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.YEAR) - 1900; + } + @Native + public double setYear(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (real >= 0 && real <= 99) real = real + 1900; + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.YEAR, (int)real); + updateUTC(); + return getTime(); + } + + @Native + public double getFullYear() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.YEAR); + } + @Native + public double getMonth() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.MONTH); + } + @Native + public double getDate() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.DAY_OF_MONTH); + } + @Native + public double getDay() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.DAY_OF_WEEK); + } + @Native + public double getHours() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.HOUR_OF_DAY); + } + @Native + public double getMinutes() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.MINUTE); + } + @Native + public double getSeconds() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.SECOND); + } + @Native + public double getMilliseconds() { + if (normal == null) return Double.NaN; + return normal.get(Calendar.MILLISECOND); + } + + @Native + public double getUTCFullYear() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.YEAR); + } + @Native + public double getUTCMonth() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.MONTH); + } + @Native + public double getUTCDate() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.DAY_OF_MONTH); + } + @Native + public double getUTCDay() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.DAY_OF_WEEK); + } + @Native + public double getUTCHours() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.HOUR_OF_DAY); + } + @Native + public double getUTCMinutes() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.MINUTE); + } + @Native + public double getUTCSeconds() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.SECOND); + } + @Native + public double getUTCMilliseconds() { + if (utc == null) return Double.NaN; + return utc.get(Calendar.MILLISECOND); + } + + @Native + public double setFullYear(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.YEAR, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setMonth(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.MONTH, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setDate(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.DAY_OF_MONTH, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setDay(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.DAY_OF_WEEK, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setHours(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.HOUR_OF_DAY, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setMinutes(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.MINUTE, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setSeconds(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.SECOND, (int)real); + updateUTC(); + return getTime(); + } + @Native + public double setMilliseconds(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else normal.set(Calendar.MILLISECOND, (int)real); + updateUTC(); + return getTime(); + } + + @Native + public double setUTCFullYear(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.YEAR, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCMonth(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.MONTH, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCDate(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.DAY_OF_MONTH, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCDay(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.DAY_OF_WEEK, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCHours(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.HOUR_OF_DAY, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCMinutes(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.MINUTE, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCSeconds(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.SECOND, (int)real); + updateNormal(); + return getTime(); + } + @Native + public double setUTCMilliseconds(CallContext ctx, Object val) throws InterruptedException { + var real = Values.toNumber(ctx, val); + if (Double.isNaN(real)) invalidate(); + else utc.set(Calendar.MILLISECOND, (int)real); + updateNormal(); + return getTime(); + } + + @Native + public double getTime() { + if (utc == null) return Double.NaN; + return utc.getTimeInMillis(); + } + @Native + public double getTimezoneOffset() { + if (normal == null) return Double.NaN; + return normal.getTimeZone().getRawOffset() / 60000; + } + + @Native + public double valueOf() { + if (normal == null) return Double.NaN; + else return normal.getTimeInMillis(); + } + + // I'm not dealing with locales rn + + // @Native + // public String toTimeString() { + // if (normal == null || utc == null) return "Invalid date"; + + // var res = ""; + + // } + // @Native @Override + // public String toString() { + // if (normal == null || utc == null) return "Invalid date"; + // else return DateFormat..format(normal.getTime()); + // } +} diff --git a/src/me/topchetoeu/jscript/polyfills/Internals.java b/src/me/topchetoeu/jscript/polyfills/Internals.java new file mode 100644 index 0000000..110f9c0 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/Internals.java @@ -0,0 +1,254 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.HashMap; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +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.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; + +public class Internals { + private HashMap intervals = new HashMap<>(); + private HashMap timeouts = new HashMap<>(); + private HashMap symbols = new HashMap<>(); + private int nextId = 0; + + @Native + public double parseFloat(CallContext ctx, Object val) throws InterruptedException { + return Values.toNumber(ctx, val); + } + + @NativeGetter("symbolProto") + public ObjectValue symbolProto(CallContext ctx) { + return ctx.engine().symbolProto(); + } + @Native + public final Object apply(CallContext ctx, FunctionValue func, Object th, ArrayValue args) throws InterruptedException { + return func.call(ctx, th, args.toArray()); + } + @Native + public boolean defineProp(ObjectValue obj, Object key, FunctionValue getter, FunctionValue setter, boolean enumerable, boolean configurable) { + return obj.defineProperty(key, getter, setter, configurable, enumerable); + } + @Native + public boolean defineField(ObjectValue obj, Object key, Object val, boolean writable, boolean enumerable, boolean configurable) { + return obj.defineProperty(key, val, writable, configurable, enumerable); + } + + @Native + public int strlen(String str) { + return str.length(); + } + @Native + public String substring(String str, int start, int end) { + if (start > end) return substring(str, end, start); + + if (start < 0) start = 0; + if (start >= str.length()) return ""; + + if (end < 0) end = 0; + if (end > str.length()) end = str.length(); + + return str.substring(start, end); + } + @Native + public String toLower(String str) { + return str.toLowerCase(); + } + @Native + public String toUpper(String str) { + return str.toUpperCase(); + } + @Native + public int toCharCode(String str) { + return str.codePointAt(0); + } + @Native + public String fromCharCode(int code) { + return Character.toString((char)code); + } + @Native + public boolean startsWith(String str, String term, int offset) { + return str.startsWith(term, offset); + } + @Native + public boolean endsWith(String str, String term, int offset) { + try { + return str.substring(0, offset).endsWith(term); + } + catch (IndexOutOfBoundsException e) { return false; } + + } + + @Native + public int setInterval(CallContext ctx, FunctionValue func, double delay) { + var thread = new Thread((Runnable)() -> { + var ms = (long)delay; + var ns = (int)((delay - ms) * 10000000); + + while (true) { + try { + Thread.sleep(ms, ns); + } + catch (InterruptedException e) { return; } + + ctx.engine().pushMsg(false, func, ctx.data(), null); + } + }); + thread.start(); + + intervals.put(++nextId, thread); + + return nextId; + } + @Native + public int setTimeout(CallContext ctx, FunctionValue func, double delay) { + var thread = new Thread((Runnable)() -> { + var ms = (long)delay; + var ns = (int)((delay - ms) * 1000000); + + try { + Thread.sleep(ms, ns); + } + catch (InterruptedException e) { return; } + + ctx.engine().pushMsg(false, func, ctx.data(), null); + }); + thread.start(); + + timeouts.put(++nextId, thread); + + return nextId; + } + + @Native + public void clearInterval(int id) { + var thread = intervals.remove(id); + if (thread != null) thread.interrupt(); + } + @Native + public void clearTimeout(int id) { + var thread = timeouts.remove(id); + if (thread != null) thread.interrupt(); + } + + @Native + public void sort(CallContext ctx, ArrayValue arr, FunctionValue cmp) { + arr.sort((a, b) -> { + try { + var res = Values.toNumber(ctx, cmp.call(ctx, null, a, b)); + if (res < 0) return -1; + if (res > 0) return 1; + return 0; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return 0; + } + }); + } + + @Native + public void special(FunctionValue... funcs) { + for (var func : funcs) { + func.special = true; + } + } + + @Native + public Symbol symbol(String name, boolean unique) { + if (!unique && symbols.containsKey(name)) { + return symbols.get(name); + } + else { + var val = new Symbol(name); + if (!unique) symbols.put(name, val); + return val; + } + } + @Native + public String symStr(Symbol symbol) { + return symbol.value; + } + + @Native + public void freeze(ObjectValue val) { + val.freeze(); + } + @Native + public void seal(ObjectValue val) { + val.seal(); + } + @Native + public void preventExtensions(ObjectValue val) { + val.preventExtensions(); + } + + @Native + public boolean extensible(Object val) { + return Values.isObject(val) && Values.object(val).extensible(); + } + + @Native + public ArrayValue keys(CallContext ctx, Object obj, boolean onlyString) throws InterruptedException { + var res = new ArrayValue(); + + var i = 0; + var list = Values.getMembers(ctx, obj, true, false); + + for (var el : list) { + if (el instanceof Symbol && onlyString) continue; + res.set(i++, el); + } + + return res; + } + @Native + public ArrayValue ownPropKeys(CallContext ctx, Object obj, boolean symbols) throws InterruptedException { + var res = new ArrayValue(); + + if (Values.isObject(obj)) { + var i = 0; + var list = Values.object(obj).keys(true); + + for (var el : list) { + if (el instanceof Symbol == symbols) res.set(i++, el); + } + } + + return res; + } + @Native + public ObjectValue ownProp(CallContext ctx, ObjectValue val, Object key) throws InterruptedException { + return val.getMemberDescriptor(ctx, key); + } + + @Native + public Object require(CallContext ctx, Object name) throws InterruptedException { + var res = ctx.engine().modules().tryLoad(ctx, Values.toString(ctx, name)); + if (res == null) throw EngineException.ofError("The module '" + name + "' doesn\'t exist."); + return res.exports(); + } + + @NativeGetter("err") + public ObjectValue errProto(CallContext ctx) { + return ctx.engine.errorProto(); + } + @NativeGetter("syntax") + public ObjectValue syntaxProto(CallContext ctx) { + return ctx.engine.syntaxErrorProto(); + } + @NativeGetter("range") + public ObjectValue rangeProto(CallContext ctx) { + return ctx.engine.rangeErrorProto(); + } + @NativeGetter("type") + public ObjectValue typeProto(CallContext ctx) { + return ctx.engine.typeErrorProto(); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/JSON.java b/src/me/topchetoeu/jscript/polyfills/JSON.java new file mode 100644 index 0000000..953e17c --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/JSON.java @@ -0,0 +1,38 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.exceptions.SyntaxException; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.json.JSONElement; + +public class JSON { + private static Object convert(JSONElement val) { + if (val.isBoolean()) return val.bool(); + if (val.isString()) return val.string(); + if (val.isNumber()) return val.number(); + if (val.isList()) return ArrayValue.of(val.list().stream().map(JSON::convert).toList()); + if (val.isMap()) { + var res = new ObjectValue(); + for (var el : val.map().entrySet()) { + res.defineProperty(el.getKey(), convert(el.getValue())); + } + return res; + } + if (val.isNull()) return Values.NULL; + return null; + } + + @Native + public static Object parse(CallContext ctx, String val) throws InterruptedException { + try { + return convert(me.topchetoeu.jscript.json.JSON.parse("", val)); + } + catch (SyntaxException e) { + throw EngineException.ofSyntax(e.msg); + } + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/Map.java b/src/me/topchetoeu/jscript/polyfills/Map.java new file mode 100644 index 0000000..9d92e1d --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/Map.java @@ -0,0 +1,85 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.LinkedHashMap; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; + +public class Map { + private LinkedHashMap objs = new LinkedHashMap<>(); + + @Native + public Object get(Object key) { + return objs.get(key); + } + @Native + public Map set(Object key, Object val) { + objs.put(key, val); + return this; + } + @Native + public boolean delete(Object key) { + if (objs.containsKey(key)) { + objs.remove(key); + return true; + } + else return false; + } + @Native + public boolean has(Object key) { + return objs.containsKey(key); + } + + @Native + public void clear() { + objs.clear(); + } + + @Native + public void forEach(CallContext ctx, FunctionValue func, Object thisArg) throws InterruptedException { + + for (var el : objs.entrySet().stream().toList()) { + func.call(ctx, thisArg, el.getValue(), el.getKey(), this); + } + } + + @Native + public Object entries(CallContext ctx) throws InterruptedException { + return Values.fromJavaIterable(ctx, objs + .entrySet() + .stream() + .map(v -> new ArrayValue(v.getKey(), v.getValue())) + .toList() + ); + } + @Native + public Object keys(CallContext ctx) throws InterruptedException { + return Values.fromJavaIterable(ctx, objs.keySet().stream().toList()); + } + @Native + public Object values(CallContext ctx) throws InterruptedException { + return Values.fromJavaIterable(ctx, objs.values().stream().toList()); + } + + @NativeGetter("size") + public int size() { + return objs.size(); + } + + @Native + public Map(CallContext ctx, Object iterable) throws InterruptedException { + if (Values.isPrimitive(iterable)) return; + + for (var val : Values.toJavaIterable(ctx, iterable)) { + var first = Values.getMember(ctx, val, 0); + var second = Values.getMember(ctx, val, 1); + + set(first, second); + } + } + public Map() { } +} diff --git a/src/me/topchetoeu/jscript/polyfills/Math.java b/src/me/topchetoeu/jscript/polyfills/Math.java new file mode 100644 index 0000000..8a4380f --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/Math.java @@ -0,0 +1,211 @@ +package me.topchetoeu.jscript.polyfills; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.interop.Native; + +public class Math { + @Native + public static final double E = java.lang.Math.E; + @Native + public static final double PI = java.lang.Math.PI; + @Native + public static final double SQRT2 = java.lang.Math.sqrt(2); + @Native + public static final double SQRT1_2 = java.lang.Math.sqrt(.5); + @Native + public static final double LN2 = java.lang.Math.log(2); + @Native + public static final double LN10 = java.lang.Math.log(10); + @Native + public static final double LOG2E = java.lang.Math.log(java.lang.Math.E) / LN2; + @Native + public static final double LOG10E = java.lang.Math.log10(java.lang.Math.E); + + @Native + public static double asin(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.asin(x); + } + @Native + public static double acos(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.acos(x); + } + @Native + public static double atan(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.atan(x); + } + @Native + public static double atan2(CallContext ctx, double y, double x) throws InterruptedException { + double _y = y; + double _x = x; + if (_x == 0) { + if (_y == 0) return Double.NaN; + return java.lang.Math.signum(_y) * java.lang.Math.PI / 2; + } + else { + var val = java.lang.Math.atan(_y / _x); + if (_x > 0) return val; + else if (_y < 0) return val - java.lang.Math.PI; + else return val + java.lang.Math.PI; + } + + } + + @Native + public static double asinh(CallContext ctx, double x) throws InterruptedException { + double _x = x; + return java.lang.Math.log(_x + java.lang.Math.sqrt(_x * _x + 1)); + } + @Native + public static double acosh(CallContext ctx, double x) throws InterruptedException { + double _x = x; + return java.lang.Math.log(_x + java.lang.Math.sqrt(_x * _x - 1)); + } + @Native + public static double atanh(CallContext ctx, double x) throws InterruptedException { + double _x = x; + if (_x <= -1 || _x >= 1) return Double.NaN; + return .5 * java.lang.Math.log((1 + _x) / (1 - _x)); + } + + @Native + public static double sin(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.sin(x); + } + @Native + public static double cos(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.cos(x); + } + @Native + public static double tan(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.tan(x); + } + + @Native + public static double sinh(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.sinh(x); + } + @Native + public static double cosh(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.cosh(x); + } + @Native + public static double tanh(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.tanh(x); + } + + @Native + public static double sqrt(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.sqrt(x); + } + @Native + public static double cbrt(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.cbrt(x); + } + + @Native + public static double hypot(CallContext ctx, double... vals) throws InterruptedException { + var res = 0.; + for (var el : vals) { + var val = el; + res += val * val; + } + return java.lang.Math.sqrt(res); + } + @Native + public static int imul(CallContext ctx, double a, double b) throws InterruptedException { + return (int)a * (int)b; + } + + @Native + public static double exp(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.exp(x); + } + @Native + public static double expm1(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.expm1(x); + } + @Native + public static double pow(CallContext ctx, double x, double y) throws InterruptedException { + return java.lang.Math.pow(x, y); + } + + @Native + public static double log(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.log(x); + } + @Native + public static double log10(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.log10(x); + } + @Native + public static double log1p(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.log1p(x); + } + @Native + public static double log2(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.log(x) / LN2; + } + + @Native + public static double ceil(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.ceil(x); + } + @Native + public static double floor(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.floor(x); + } + @Native + public static double round(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.round(x); + } + @Native + public static float fround(CallContext ctx, double x) throws InterruptedException { + return (float)x; + } + @Native + public static double trunc(CallContext ctx, double x) throws InterruptedException { + var _x = x; + return java.lang.Math.floor(java.lang.Math.abs(_x)) * java.lang.Math.signum(_x); + } + @Native + public static double abs(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.abs(x); + } + + @Native + public static double max(CallContext ctx, double... vals) throws InterruptedException { + var res = Double.NEGATIVE_INFINITY; + + for (var el : vals) { + var val = el; + if (val > res) res = val; + } + + return res; + } + @Native + public static double min(CallContext ctx, double... vals) throws InterruptedException { + var res = Double.POSITIVE_INFINITY; + + for (var el : vals) { + var val = el; + if (val < res) res = val; + } + + return res; + } + + @Native + public static double sign(CallContext ctx, double x) throws InterruptedException { + return java.lang.Math.signum(x); + } + + @Native + public static double random() { + return java.lang.Math.random(); + } + @Native + public static int clz32(CallContext ctx, double x) throws InterruptedException { + return Integer.numberOfLeadingZeros((int)x); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/PolyfillEngine.java b/src/me/topchetoeu/jscript/polyfills/PolyfillEngine.java new file mode 100644 index 0000000..19cde7e --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/PolyfillEngine.java @@ -0,0 +1,110 @@ +package me.topchetoeu.jscript.polyfills; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import me.topchetoeu.jscript.engine.Engine; +import me.topchetoeu.jscript.engine.modules.ModuleManager; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.parsing.Parsing; + +public class PolyfillEngine extends Engine { + 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 (IOException e) { + return null; + } + } + public static String resourceToString(String name) { + var str = PolyfillEngine.class.getResourceAsStream("/me/topchetoeu/jscript/" + name); + if (str == null) return null; + return streamToString(str); + } + + public final ModuleManager modules; + + @Override + public Object makeRegex(String pattern, String flags) { + return new RegExp(pattern, flags); + } + + @Override + public ModuleManager modules() { + return modules; + } + public PolyfillEngine(File root) { + super(); + + this.modules = new ModuleManager(root); + + exposeNamespace("Math", Math.class); + exposeNamespace("JSON", JSON.class); + exposeClass("Promise", Promise.class); + exposeClass("RegExp", RegExp.class); + exposeClass("Date", Date.class); + exposeClass("Map", Map.class); + exposeClass("Set", Set.class); + + global().define("Object", "Function", "String", "Number", "Boolean", "Symbol"); + global().define("Array", "require"); + global().define("Error", "SyntaxError", "TypeError", "RangeError"); + global().define("setTimeout", "setInterval", "clearTimeout", "clearInterval"); + // global().define("process", true, "trololo"); + global().define("debugger"); + + global().define(true, new NativeFunction("measure", (ctx, thisArg, values) -> { + var start = System.nanoTime(); + try { + return Values.call(ctx, values[0], ctx); + } + finally { + System.out.println("Function took %s ms".formatted((System.nanoTime() - start) / 1000000.)); + } + })); + global().define(true, new NativeFunction("isNaN", (ctx, thisArg, args) -> { + if (args.length == 0) return true; + else return Double.isNaN(Values.toNumber(ctx, args[0])); + })); + global().define(true, new NativeFunction("log", (el, t, args) -> { + for (var obj : args) Values.printValue(el, obj); + System.out.println(); + return null; + })); + + var scope = global().globalChild(); + scope.define("gt", true, global().obj); + scope.define("lgt", true, scope.obj); + scope.define("setProps", "setConstr"); + scope.define("internals", true, new Internals()); + scope.define(true, new NativeFunction("run", (ctx, thisArg, args) -> { + var filename = (String)args[0]; + boolean pollute = args.length > 1 && args[1].equals(true); + FunctionValue func; + var src = resourceToString("js/" + filename); + if (src == null) throw new RuntimeException("The source '" + filename + "' doesn't exist."); + + if (pollute) func = Parsing.compile(global(), filename, src); + else func = Parsing.compile(scope.globalChild(), filename, src); + + func.call(ctx); + return null; + })); + + pushMsg(false, scope.globalChild(), java.util.Map.of(), "core.js", resourceToString("js/core.js"), null); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/Promise.java b/src/me/topchetoeu/jscript/polyfills/Promise.java new file mode 100644 index 0000000..b973184 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/Promise.java @@ -0,0 +1,350 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; + +public class Promise { + private static record Handle(CallContext ctx, FunctionValue fulfilled, FunctionValue rejected) {} + + @Native("resolve") + public static Promise ofResolved(CallContext engine, Object val) { + if (Values.isWrapper(val, Promise.class)) return Values.wrapper(val, Promise.class); + var res = new Promise(); + res.fulfill(engine, val); + return res; + } + public static Promise ofResolved(Object val) { + if (Values.isWrapper(val, Promise.class)) return Values.wrapper(val, Promise.class); + var res = new Promise(); + res.fulfill(val); + return res; + } + + @Native("reject") + public static Promise ofRejected(CallContext engine, Object val) { + var res = new Promise(); + res.reject(engine, val); + return res; + } + public static Promise ofRejected(Object val) { + var res = new Promise(); + res.fulfill(val); + return res; + } + + @Native + public static Promise any(CallContext engine, Object _promises) { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(new ArrayValue()); + var n = new int[] { promises.size() }; + var res = new Promise(); + + var errors = new ArrayValue(); + + for (var i = 0; i < promises.size(); i++) { + var index = i; + var val = promises.get(i); + if (Values.isWrapper(val, Promise.class)) Values.wrapper(val, Promise.class).then( + engine, + new NativeFunction(null, (e, th, args) -> { res.fulfill(e, args[0]); return null; }), + new NativeFunction(null, (e, th, args) -> { + errors.set(index, args[0]); + n[0]--; + if (n[0] <= 0) res.reject(e, errors); + return null; + }) + ); + else { + res.fulfill(engine, val); + break; + } + } + + return res; + } + @Native + public static Promise race(CallContext engine, Object _promises) { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(new ArrayValue()); + var res = new Promise(); + + for (var i = 0; i < promises.size(); i++) { + var val = promises.get(i); + if (Values.isWrapper(val, Promise.class)) Values.wrapper(val, Promise.class).then( + engine, + new NativeFunction(null, (e, th, args) -> { res.fulfill(e, args[0]); return null; }), + new NativeFunction(null, (e, th, args) -> { res.reject(e, args[0]); return null; }) + ); + else { + res.fulfill(val); + break; + } + } + + return res; + } + @Native + public static Promise all(CallContext engine, Object _promises) { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(new ArrayValue()); + var n = new int[] { promises.size() }; + var res = new Promise(); + + var result = new ArrayValue(); + + for (var i = 0; i < promises.size(); i++) { + var index = i; + var val = promises.get(i); + if (Values.isWrapper(val, Promise.class)) Values.wrapper(val, Promise.class).then( + engine, + new NativeFunction(null, (e, th, args) -> { + result.set(index, args[0]); + n[0]--; + if (n[0] <= 0) res.fulfill(e, result); + return null; + }), + new NativeFunction(null, (e, th, args) -> { res.reject(e, args[0]); return null; }) + ); + else { + result.set(i, val); + break; + } + } + + if (n[0] <= 0) res.fulfill(engine, result); + + return res; + } + @Native + public static Promise allSettled(CallContext engine, Object _promises) { + if (!Values.isArray(_promises)) throw EngineException.ofType("Expected argument for any to be an array."); + var promises = Values.array(_promises); + if (promises.size() == 0) return ofResolved(new ArrayValue()); + var n = new int[] { promises.size() }; + var res = new Promise(); + + var result = new ArrayValue(); + + for (var i = 0; i < promises.size(); i++) { + var index = i; + var val = promises.get(i); + if (Values.isWrapper(val, Promise.class)) Values.wrapper(val, Promise.class).then( + engine, + new NativeFunction(null, (e, th, args) -> { + result.set(index, new ObjectValue(Map.of( + "status", "fulfilled", + "value", args[0] + ))); + n[0]--; + if (n[0] <= 0) res.fulfill(e, result); + return null; + }), + new NativeFunction(null, (e, th, args) -> { + result.set(index, new ObjectValue(Map.of( + "status", "rejected", + "reason", args[0] + ))); + n[0]--; + if (n[0] <= 0) res.fulfill(e, result); + return null; + }) + ); + else { + result.set(i, new ObjectValue(Map.of( + "status", "fulfilled", + "value", val + ))); + n[0]--; + } + } + + if (n[0] <= 0) res.fulfill(engine, result); + + return res; + } + + private List handles = new ArrayList<>(); + + private static final int STATE_PENDING = 0; + private static final int STATE_FULFILLED = 1; + private static final int STATE_REJECTED = 2; + + private int state = STATE_PENDING; + private Object val; + + /** + * Thread safe - call from any thread + */ + public void fulfill(Object val) { + if (Values.isWrapper(val, Promise.class)) throw new IllegalArgumentException("A promise may not be a fulfil value."); + if (state != STATE_PENDING) return; + + this.state = STATE_FULFILLED; + this.val = val; + for (var el : handles) el.ctx().engine().pushMsg(true, el.fulfilled, el.ctx().data(), null, val); + handles = null; + } + /** + * Thread safe - call from any thread + */ + public void fulfill(CallContext ctx, Object val) { + if (Values.isWrapper(val, Promise.class)) Values.wrapper(val, Promise.class).then(ctx, + new NativeFunction(null, (e, th, args) -> { + this.fulfill(args[0]); + return null; + }), + new NativeFunction(null, (e, th, args) -> { + this.reject(args[0]); + return null; + }) + ); + else this.fulfill(val); + } + /** + * Thread safe - call from any thread + */ + public void reject(Object val) { + if (Values.isWrapper(val, Promise.class)) throw new IllegalArgumentException("A promise may not be a reject value."); + if (state != STATE_PENDING) return; + + this.state = STATE_REJECTED; + this.val = val; + for (var el : handles) el.ctx().engine().pushMsg(true, el.rejected, el.ctx().data(), null, val); + handles = null; + } + /** + * Thread safe - call from any thread + */ + public void reject(CallContext ctx, Object val) { + if (Values.isWrapper(val, Promise.class)) Values.wrapper(val, Promise.class).then(ctx, + new NativeFunction(null, (e, th, args) -> { + this.reject(args[0]); + return null; + }), + new NativeFunction(null, (e, th, args) -> { + this.reject(args[0]); + return null; + }) + ); + else this.reject(val); + } + + private void handle(CallContext ctx, FunctionValue fulfill, FunctionValue reject) { + if (state == STATE_FULFILLED) ctx.engine().pushMsg(true, fulfill, ctx.data(), null, val); + else if (state == STATE_REJECTED) ctx.engine().pushMsg(true, reject, ctx.data(), null, val); + else handles.add(new Handle(ctx, fulfill, reject)); + } + + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native + public Promise then(CallContext ctx, Object onFulfill, Object onReject) { + if (!(onFulfill instanceof FunctionValue)) onFulfill = null; + if (!(onReject instanceof FunctionValue)) onReject = null; + + var res = new Promise(); + + var fulfill = (FunctionValue)onFulfill; + var reject = (FunctionValue)onReject; + + handle(ctx, + new NativeFunction(null, (e, th, args) -> { + if (fulfill == null) res.fulfill(e, args[0]); + else { + try { res.fulfill(e, fulfill.call(e, null, args[0])); } + catch (EngineException err) { res.reject(e, err.value); } + } + return null; + }), + new NativeFunction(null, (e, th, args) -> { + if (reject == null) res.reject(e, args[0]); + else { + try { res.fulfill(e, reject.call(e, null, args[0])); } + catch (EngineException err) { res.reject(e, err.value); } + } + return null; + }) + ); + + return res; + } + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native("catch") + public Promise _catch(CallContext ctx, Object onReject) { + return then(ctx, null, onReject); + } + /** + * Thread safe - you can call this from anywhere + * HOWEVER, it's strongly recommended to use this only in javascript + */ + @Native("finally") + public Promise _finally(CallContext ctx, Object handle) { + return then(ctx, + new NativeFunction(null, (e, th, args) -> { + if (handle instanceof FunctionValue) ((FunctionValue)handle).call(ctx); + return args[0]; + }), + new NativeFunction(null, (e, th, args) -> { + if (handle instanceof FunctionValue) ((FunctionValue)handle).call(ctx); + throw new EngineException(args[0]); + }) + ); + } + + /** + * NOT THREAD SAFE - must be called from the engine executor thread + */ + @Native + public Promise(CallContext ctx, FunctionValue func) throws InterruptedException { + if (!(func instanceof FunctionValue)) throw EngineException.ofType("A function must be passed to the promise constructor."); + try { + func.call( + ctx, null, + new NativeFunction(null, (e, th, args) -> { + fulfill(e, args.length > 0 ? args[0] : null); + return null; + }), + new NativeFunction(null, (e, th, args) -> { + reject(e, args.length > 0 ? args[0] : null); + return null; + }) + ); + } + catch (EngineException e) { + reject(ctx, e.value); + } + } + + @Override @Native + public String toString() { + if (state == STATE_PENDING) return "Promise (pending)"; + else if (state == STATE_FULFILLED) return "Promise (fulfilled)"; + else return "Promise (rejected)"; + } + + private Promise(int state, Object val) { + this.state = state; + this.val = val; + } + public Promise() { + this(STATE_PENDING, null); + } +} diff --git a/src/me/topchetoeu/jscript/polyfills/RegExp.java b/src/me/topchetoeu/jscript/polyfills/RegExp.java new file mode 100644 index 0000000..752f24a --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/RegExp.java @@ -0,0 +1,187 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.NativeWrapper; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; +import me.topchetoeu.jscript.interop.NativeSetter; + +public class RegExp { + // I used Regex to analyze Regex + private static final Pattern NAMED_PATTERN = Pattern.compile("\\(\\?<([^=!].*?)>", Pattern.DOTALL); + private static final Pattern ESCAPE_PATTERN = Pattern.compile("[/\\-\\\\^$*+?.()|\\[\\]{}]"); + + private static String cleanupPattern(CallContext ctx, Object val) throws InterruptedException { + if (val == null) return "(?:)"; + if (val instanceof RegExp) return ((RegExp)val).source; + if (val instanceof NativeWrapper && ((NativeWrapper)val).wrapped instanceof RegExp) { + return ((RegExp)((NativeWrapper)val).wrapped).source; + } + var res = Values.toString(ctx, val); + if (res.equals("")) return "(?:)"; + return res; + } + private static String cleanupFlags(CallContext ctx, Object val) throws InterruptedException { + if (val == null) return ""; + return Values.toString(ctx, val); + } + + private static boolean checkEscaped(String s, int pos) { + int n = 0; + + while (true) { + if (pos <= 0) break; + if (s.charAt(pos) != '\\') break; + n++; + pos--; + } + + return (n % 2) != 0; + } + + @Native + public static RegExp escape(CallContext ctx, Object raw, Object flags) throws InterruptedException { + return escape(Values.toString(ctx, raw), cleanupFlags(ctx, flags)); + } + public static RegExp escape(String raw, String flags) { + return new RegExp(ESCAPE_PATTERN.matcher(raw).replaceAll("\\\\$0"), flags); + } + + private Pattern pattern; + private String[] namedGroups; + private int flags; + private int lastI = 0; + + @Native + public final String source; + @Native + public final boolean hasIndices; + @Native + public final boolean global; + @Native + public final boolean sticky; + + @NativeGetter("ignoreCase") + public boolean ignoreCase() { return (flags & Pattern.CASE_INSENSITIVE) != 0; } + @NativeGetter("multiline") + public boolean multiline() { return (flags & Pattern.MULTILINE) != 0; } + @NativeGetter("unicode") + public boolean unicode() { return (flags & Pattern.UNICODE_CHARACTER_CLASS) != 0; } + @NativeGetter("unicode") + public boolean dotAll() { return (flags & Pattern.DOTALL) != 0; } + + @NativeGetter("lastIndex") + public int lastIndex() { return lastI; } + @NativeSetter("lastIndex") + public void setLastIndex(CallContext ctx, Object i) throws InterruptedException { + lastI = (int)Values.toNumber(ctx, i); + } + public void setLastIndex(int i) { + lastI = i; + } + + @NativeGetter("flags") + public final String flags() { + String res = ""; + if (hasIndices) res += 'd'; + if (global) res += 'g'; + if (ignoreCase()) res += 'i'; + if (multiline()) res += 'm'; + if (dotAll()) res += 's'; + if (unicode()) res += 'u'; + if (sticky) res += 'y'; + return res; + } + + @Native + public Object exec(CallContext ctx, Object str) throws InterruptedException { + return exec(Values.toString(ctx, str)); + } + public Object exec(String str) { + var matcher = pattern.matcher(str); + if (lastI > str.length() || !matcher.find(lastI) || sticky && matcher.start() != lastI) { + lastI = 0; + return Values.NULL; + } + if (sticky || global) { + lastI = matcher.end(); + if (matcher.end() == matcher.start()) lastI++; + } + + var obj = new ArrayValue(); + var groups = new ObjectValue(); + + for (var el : namedGroups) { + try { + groups.defineProperty(el, matcher.group(el)); + } + catch (IllegalArgumentException e) { } + } + if (groups.values.size() == 0) groups = null; + + + for (int i = 0; i < matcher.groupCount() + 1; i++) { + obj.set(i, matcher.group(i)); + } + obj.defineProperty("groups", groups); + obj.defineProperty("index", matcher.start()); + obj.defineProperty("input", str); + + if (hasIndices) { + var indices = new ArrayValue(); + for (int i = 0; i < matcher.groupCount() + 1; i++) { + indices.set(i, new ArrayValue(matcher.start(i), matcher.end(i))); + } + var groupIndices = new ObjectValue(); + for (var el : namedGroups) { + groupIndices.defineProperty(el, new ArrayValue(matcher.start(el), matcher.end(el))); + } + indices.defineProperty("groups", groupIndices); + obj.defineProperty("indices", indices); + } + + return obj; + } + + @Native + public RegExp(CallContext ctx, Object pattern, Object flags) throws InterruptedException { + this(cleanupPattern(ctx, pattern), cleanupFlags(ctx, flags)); + } + public RegExp(String pattern, String flags) { + if (pattern == null || pattern.equals("")) pattern = "(?:)"; + if (flags == null || flags.equals("")) flags = ""; + + this.flags = 0; + this.hasIndices = flags.contains("d"); + this.global = flags.contains("g"); + this.sticky = flags.contains("y"); + this.source = pattern; + + if (flags.contains("i")) this.flags |= Pattern.CASE_INSENSITIVE; + if (flags.contains("m")) this.flags |= Pattern.MULTILINE; + if (flags.contains("s")) this.flags |= Pattern.DOTALL; + if (flags.contains("u")) this.flags |= Pattern.UNICODE_CHARACTER_CLASS; + + this.pattern = Pattern.compile(pattern.replace("\\d", "[0-9]"), this.flags); + + var matcher = NAMED_PATTERN.matcher(source); + var groups = new ArrayList(); + + while (matcher.find()) { + if (!checkEscaped(source, matcher.start() - 1)) { + groups.add(matcher.group(1)); + } + } + + namedGroups = groups.toArray(String[]::new); + } + + public RegExp(String pattern) { this(pattern, null); } + public RegExp() { this(null, null); } +} diff --git a/src/me/topchetoeu/jscript/polyfills/Set.java b/src/me/topchetoeu/jscript/polyfills/Set.java new file mode 100644 index 0000000..51a4265 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/Set.java @@ -0,0 +1,111 @@ +package me.topchetoeu.jscript.polyfills; + +import java.util.LinkedHashSet; + +import me.topchetoeu.jscript.engine.CallContext; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; +import me.topchetoeu.jscript.engine.values.ObjectValue; +import me.topchetoeu.jscript.engine.values.Values; +import me.topchetoeu.jscript.exceptions.EngineException; +import me.topchetoeu.jscript.interop.Native; +import me.topchetoeu.jscript.interop.NativeGetter; + +public class Set { + private LinkedHashSet objs = new LinkedHashSet<>(); + + @Native + public Set add(Object key) { + objs.add(key); + return this; + } + @Native + public boolean delete(Object key) { + return objs.remove(key); + } + @Native + public boolean has(Object key) { + return objs.contains(key); + } + @Native + public void clear() { + objs.clear(); + } + + @Native + public void forEach(CallContext ctx, Object func, Object thisArg) throws InterruptedException { + if (!(func instanceof FunctionValue)) throw EngineException.ofType("func must be a function."); + + for (var el : objs.stream().toList()) { + ((FunctionValue)func).call(ctx, thisArg, el, el, this); + } + } + + @Native + public ObjectValue entries() { + var it = objs.stream().toList().iterator(); + + var next = new NativeFunction("next", (ctx, thisArg, args) -> { + if (it.hasNext()) { + var val = it.next(); + return new ObjectValue(java.util.Map.of( + "value", new ArrayValue(val, val), + "done", false + )); + } + else return new ObjectValue(java.util.Map.of("done", true)); + }); + + return new ObjectValue(java.util.Map.of("next", next)); + } + @Native + public ObjectValue keys() { + var it = objs.stream().toList().iterator(); + + var next = new NativeFunction("next", (ctx, thisArg, args) -> { + if (it.hasNext()) { + var val = it.next(); + return new ObjectValue(java.util.Map.of( + "value", val, + "done", false + )); + } + else return new ObjectValue(java.util.Map.of("done", true)); + }); + + return new ObjectValue(java.util.Map.of("next", next)); + } + @Native + public ObjectValue values() { + var it = objs.stream().toList().iterator(); + + var next = new NativeFunction("next", (ctx, thisArg, args) -> { + if (it.hasNext()) { + var val = it.next(); + return new ObjectValue(java.util.Map.of( + "value", val, + "done", false + )); + } + else return new ObjectValue(java.util.Map.of("done", true)); + }); + + return new ObjectValue(java.util.Map.of("next", next)); + } + + @NativeGetter("size") + public int size() { + return objs.size(); + } + + @Native + public Set(CallContext ctx, Object iterable) throws InterruptedException { + if (Values.isPrimitive(iterable)) return; + + for (var val : Values.toJavaIterable(ctx, iterable)) { + add(val); + } + } + public Set() { } +} diff --git a/src/me/topchetoeu/jscript/polyfills/TypescriptEngine.java b/src/me/topchetoeu/jscript/polyfills/TypescriptEngine.java new file mode 100644 index 0000000..31ec4c5 --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/TypescriptEngine.java @@ -0,0 +1,51 @@ +package me.topchetoeu.jscript.polyfills; + +import java.io.File; +import java.util.ArrayList; +import java.util.Map; + +import me.topchetoeu.jscript.engine.scope.GlobalScope; +import me.topchetoeu.jscript.engine.values.ArrayValue; +import me.topchetoeu.jscript.engine.values.CodeFunction; +import me.topchetoeu.jscript.engine.values.FunctionValue; +import me.topchetoeu.jscript.engine.values.NativeFunction; + +public class TypescriptEngine extends PolyfillEngine { + private FunctionValue ts; + + @Override + public CodeFunction compile(GlobalScope scope, String filename, String raw) throws InterruptedException { + if (ts != null) raw = (String)ts.call(context(), null, raw); + return super.compile(scope, filename, raw); + } + + public TypescriptEngine(File root) { + super(root); + var scope = global().globalChild(); + + var decls = new ArrayList(); + decls.add(resourceToString("dts/core.d.ts")); + decls.add(resourceToString("dts/iterators.d.ts")); + decls.add(resourceToString("dts/map.d.ts")); + decls.add(resourceToString("dts/promise.d.ts")); + decls.add(resourceToString("dts/regex.d.ts")); + decls.add(resourceToString("dts/require.d.ts")); + decls.add(resourceToString("dts/set.d.ts")); + decls.add(resourceToString("dts/values/array.d.ts")); + decls.add(resourceToString("dts/values/boolean.d.ts")); + decls.add(resourceToString("dts/values/number.d.ts")); + decls.add(resourceToString("dts/values/errors.d.ts")); + decls.add(resourceToString("dts/values/function.d.ts")); + decls.add(resourceToString("dts/values/object.d.ts")); + decls.add(resourceToString("dts/values/string.d.ts")); + decls.add(resourceToString("dts/values/symbol.d.ts")); + + scope.define("libs", true, ArrayValue.of(decls)); + scope.define(true, new NativeFunction("init", (el, t, args) -> { + ts = (FunctionValue)args[0]; + return null; + })); + + pushMsg(false, scope, Map.of(), "bootstrap.js", resourceToString("js/bootstrap.js"), null); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..199cb22 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": [ "lib/**/*.ts" ], + "compilerOptions": { + "outDir": "bin/me/topchetoeu/jscript/js", + "declarationDir": "bin/me/topchetoeu/jscript/dts", + "target": "ES5", + "lib": [], + "module": "CommonJS", + "declaration": true, + "stripInternal": true, + "downlevelIteration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + } +}