├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── docs └── jsPython-interpreter.md ├── index.html ├── jest.config.js ├── package.json ├── rollup.config.dev.js ├── rollup.config.js ├── src ├── assets │ └── mode-jspython.js ├── common │ ├── ast-types.ts │ ├── index.ts │ ├── operators.ts │ ├── parser-types.ts │ ├── token-types.ts │ └── utils.ts ├── evaluator │ ├── evaluator.ts │ ├── evaluatorAsync.ts │ ├── index.ts │ └── scope.ts ├── initialScope.ts ├── interpreter.spec.ts ├── interpreter.ts ├── interpreter.v1.spec.ts ├── parser │ ├── index.ts │ ├── parser.spec.ts │ └── parser.ts └── tokenizer │ ├── index.ts │ ├── tokenizer.spec.ts │ └── tokenizer.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | **/*/*.d.ts 4 | ./*js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-explicit-any": "warn", 14 | "@typescript-eslint/explicit-module-boundary-types": "warn", 15 | "@typescript-eslint/explicit-function-return-type": "warn", 16 | "@typescript-eslint/no-unused-vars": "warn", 17 | "@typescript-eslint/no-control-regex": "off", 18 | "@typescript-eslint/no-constant-condition": "off" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /release 8 | /lib 9 | 10 | # dependencies 11 | /node_modules 12 | /website/node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | package-lock.json 45 | *.tgz 46 | /*.log 47 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /node_modules 3 | .gitignore 4 | index.html 5 | rollup.config.js 6 | rollup.config.dev.js 7 | jest.config.js 8 | *.tgz 9 | *.ts 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "parser": "typescript", 6 | "singleQuote": true, 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:10001", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, FalconSoft Ltd 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSPython 2 | 3 | JSPython is a python-like syntax interpreter implemented with javascript that runs entirely in the web browser and/or in the NodeJS environment. 4 | 5 | It does not transpile/compile your code into JavaScript, instead, it provides an interactive interpreter that reads Python code and carries out their instructions. With JSPython you should be able to safely use or interact with any JavaScript libraries or APIs in a nice Python language. 6 | 7 | ```py 8 | arr = [4, 9, 16] 9 | 10 | def sqrt(a): 11 | return Math.sqrt(a) 12 | 13 | # use Array.map() with Python function 14 | roots = arr.map(sqrt).join(",") 15 | 16 | # use Array.map() or use arrow function 17 | roots = arr.map(i => Math.sqrt(i)).join(",") 18 | 19 | ``` 20 | ## Try out JSPython in the wild 21 | Interactive [Worksheet Systems JSPython editor](https://run.worksheet.systems/data-studio/app/guest/jspy-playground?file=main.jspy) with an ability to query REST APIs and display results in Object Explorer, a configurable Excel-like data grid or just as a JSON or text. 22 | 23 | ## Why would you use it? 24 | You can easily embed `JSPython` into your web app and your end users will benefit from a Python-like scripting facility to: 25 | * to build data transformation and data analysis tasks 26 | * allow users to configure JS objects at run-time 27 | * run a comprehensive testing scenarios 28 | * experiment with your JS Libraries or features. 29 | * bring a SAFE run-time script evaluation functions to your web app 30 | * bring Python language to NodeJS environment 31 | 32 | ## Features 33 | Our aim here is to provide a SAFE Python experience to Javascript or NodeJS users at run-time. So far, we implement a core set of Python feature which should be OK to start coding. 34 | 35 | * **Syntax and code flow** Same as Python, In `JSPython` we use indentation to indicate a block of code. All flow features like `if - else`, `for`, `while` loops - along with `break` and `continue` 36 | 37 | * **Objects, Arrays** `JSPython` allows you to work with JavaScript objects and arrays and you should be able to invoke their methods and properties as normal. So, all methods including prototype functions `push()`, `pop()`, `splice()` and [many more](https://www.w3schools.com/js/js_array_methods.asp) will work out of box. 38 | 39 | * **JSON** JSPython allows you to work with JSON same way as you would in JavaScript or Python dictionaries 40 | 41 | * **Functions** Functions def `def` `async def`, arrow functions `=>` - (including multiline arrow functions) 42 | 43 | * **Strings** Syntax and code flow `s = "Strings are double quoated only! For now."` represent a multiline string. A single or triple quotes are not supported yet. 44 | 45 | * **Date and Time** We have `dateTime()` function what returns JavaScript's Date object. So, you can use all Date [get](https://www.w3schools.com/js/js_date_methods.asp) and [set](https://www.w3schools.com/js/js_date_methods_set.asp) methods 46 | 47 | * **None, null** `null` and `None` are synonyms and can be used interchangeably 48 | 49 | ## JSPython distinctive features 50 | We haven't implemented all the features available in Python yet. However, we do already have several useful and distinctive features that are popular in other modern languages, but aren't in Python: 51 | - A single line arrow functions `=>` (no lambda keyword required) 52 | - A multiline arrow function `=>`. Particularly useful when building data transformation pipelines 53 | - Null conditioning (null-coalescing) `myObj?.property?.subProperty or "N/A"`. 54 | 55 | ## Quick start 56 | 57 | Zero install ! 58 | The simplest way to get started, without anything to install, is to use the distribution available online through jsDelivr. You can choose the latest stable release : 59 | ``` 60 | 62 | ``` 63 | 64 | Or local install 65 | ``` 66 | npm install jspython-interpreter 67 | ``` 68 | Run JS Python from your Javascript App or web page. 69 | ### Basic 70 | ```js 71 | jsPython() 72 | .evaluate('print("Hello World!")') 73 | .then( 74 | r => console.log("Result => ", r), 75 | e => console.log("Error => ", error) 76 | ) 77 | ``` 78 | ### Or with your own data context and custom function: 79 | ```js 80 | const script = ` 81 | x = [1, 2, 3] 82 | x.map(r => add(r, y)).join(",") 83 | `; 84 | const context = {y: 10} 85 | 86 | const result = await jsPython() 87 | .addFunction("add", (a, b) => a + b) 88 | .evaluate(script, context); 89 | // result will be a string "11,12,13" 90 | ``` 91 | Also, you can provide an entire JS Object or even a library. 92 | 93 | ## Run JSPython in a NodeJS with JSPython-CLI 94 | 95 | [JSPython-cli](https://github.com/jspython-dev/jspython-cli) is a command line tool what allows you to run JSPython in NodeJS environment 96 | 97 | 98 | ## License 99 | A permissive [BSD 3-Clause License](https://github.com/jspython-dev/jspython/blob/master/LICENSE) (c) FalconSoft Ltd. -------------------------------------------------------------------------------- /docs/jsPython-interpreter.md: -------------------------------------------------------------------------------- 1 | # jsPython interpreter 2 | v2 is a complete rewrite of jsPython interpreter. It uses a tree-walk interpreter approach. What consists of three major steps 3 | 4 | > Tokenize -> Parse -> Evaluate 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSPython dev 5 | 6 | 7 | 8 | 26 | 27 | 28 | 29 |
30 |

JSPython development console

31 |
32 | a = 33 33 | b = 33 34 | if b > a: 35 | return print("b is greater than a") 36 | elif a == b: 37 | return print("a and b are equal") 38 |
39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 |
47 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | export default { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: true, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/tmp/jest_rs", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | globals: { 62 | 'ts-jest': { 63 | diagnostics: false 64 | } 65 | }, 66 | 67 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 68 | // maxWorkers: "50%", 69 | 70 | // An array of directory names to be searched recursively up from the requiring module's location 71 | // moduleDirectories: [ 72 | // "node_modules" 73 | // ], 74 | 75 | // An array of file extensions your modules use 76 | // moduleFileExtensions: [ 77 | // "js", 78 | // "json", 79 | // "jsx", 80 | // "ts", 81 | // "tsx", 82 | // "node" 83 | // ], 84 | 85 | // A map from regular expressions to module names that allow to stub out resources with a single module 86 | // moduleNameMapper: {}, 87 | 88 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 89 | // modulePathIgnorePatterns: [], 90 | 91 | // Activates notifications for test results 92 | // notify: false, 93 | 94 | // An enum that specifies notification mode. Requires { notify: true } 95 | // notifyMode: "failure-change", 96 | 97 | // A preset that is used as a base for Jest's configuration 98 | preset: 'ts-jest', 99 | 100 | // Run tests from one or more projects 101 | // projects: null, 102 | 103 | // Use this configuration option to add custom reporters to Jest 104 | // reporters: undefined, 105 | 106 | // Automatically reset mock state between every test 107 | // resetMocks: false, 108 | 109 | // Reset the module registry before running each individual test 110 | // resetModules: false, 111 | 112 | // A path to a custom resolver 113 | // resolver: null, 114 | 115 | // Automatically restore mock state between every test 116 | // restoreMocks: false, 117 | 118 | // The root directory that Jest should scan for tests and modules within 119 | // rootDir: null, 120 | 121 | // A list of paths to directories that Jest should use to search for files in 122 | // roots: [ 123 | // "" 124 | // ], 125 | 126 | // Allows you to use a custom runner instead of Jest's default test runner 127 | // runner: "jest-runner", 128 | 129 | // The paths to modules that run some code to configure or set up the testing environment before each test 130 | // setupFiles: [], 131 | 132 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 133 | // setupFilesAfterEnv: [], 134 | 135 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 136 | // snapshotSerializers: [], 137 | 138 | // The test environment that will be used for testing 139 | // testEnvironment: "jest-environment-jsdom", 140 | 141 | // Options that will be passed to the testEnvironment 142 | // testEnvironmentOptions: {}, 143 | 144 | // Adds a location field to test results 145 | // testLocationInResults: false, 146 | 147 | // The glob patterns Jest uses to detect test files 148 | // testMatch: [ 149 | // "**/__tests__/**/*.[jt]s?(x)", 150 | // "**/?(*.)+(spec|test).[tj]s?(x)" 151 | // ], 152 | 153 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 154 | // testPathIgnorePatterns: [ 155 | // "/node_modules/" 156 | // ], 157 | 158 | // The regexp pattern or array of patterns that Jest uses to detect test files 159 | testRegex: ['\\.spec\\.ts$'], 160 | 161 | // This option allows the use of a custom results processor 162 | // testResultsProcessor: null, 163 | 164 | // This option allows use of a custom test runner 165 | // testRunner: "jasmine2", 166 | 167 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 168 | // testURL: "http://localhost", 169 | 170 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 171 | // timers: "real", 172 | 173 | // A map from regular expressions to paths to transformers 174 | // transform: null, 175 | 176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 177 | // transformIgnorePatterns: [ 178 | // "/node_modules/" 179 | // ], 180 | 181 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 182 | // unmockedModulePathPatterns: undefined, 183 | 184 | // Indicates whether each individual test should be reported during the run 185 | // verbose: null, 186 | 187 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 188 | // watchPathIgnorePatterns: [], 189 | 190 | // Whether to use watchman for file crawling 191 | // watchman: true, 192 | }; 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jspython-interpreter", 3 | "version": "2.1.16", 4 | "description": "JSPython is a javascript implementation of Python language that runs within web browser or NodeJS environment", 5 | "keywords": [ 6 | "python", 7 | "interpreter", 8 | "evaluator", 9 | "parser" 10 | ], 11 | "type": "module", 12 | "main": "dist/jspython-interpreter.min.js", 13 | "module": "dist/jspython-interpreter.esm.js", 14 | "typings": "dist/interpreter.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "test": "jest", 20 | "test:dev": "jest --watch", 21 | "test:dev:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch", 22 | "build": "npx rollup -c", 23 | "build:publish": "npx rollup -c && npm publish", 24 | "dev": "npx rollup --config rollup.config.dev.js --watch", 25 | "lint": "npx eslint . --ext .ts", 26 | "lint-fix": "eslint . --ext .ts --fix" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/jspython-dev/jspython.git" 31 | }, 32 | "author": { 33 | "name": "Pavlo Paska - ppaska@falconsoft-ltd.com" 34 | }, 35 | "license": "BSD 3-Clause", 36 | "bugs": { 37 | "url": "https://github.com/jspython-dev/jspython/issues" 38 | }, 39 | "homepage": "https://jspython.dev", 40 | "dependencies": {}, 41 | "devDependencies": { 42 | "@rollup/plugin-terser": "^0.4.0", 43 | "@types/jest": "^29.0.4", 44 | "@typescript-eslint/eslint-plugin": "^5.49.0", 45 | "@typescript-eslint/parser": "^5.49.0", 46 | "ace-builds": "^1.15.0", 47 | "eslint": "^8.32.0", 48 | "husky": "^8.0.3", 49 | "jest": "^29.4.1", 50 | "rollup": "^3.11.0", 51 | "rollup-plugin-copy": "^3.4.0", 52 | "rollup-plugin-livereload": "^2.0.5", 53 | "rollup-plugin-serve": "^2.0.2", 54 | "rollup-plugin-typescript2": "^0.34.1", 55 | "ts-jest": "^29.0.5", 56 | "tslib": "^2.5.0", 57 | "typescript": "^4.9.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import serve from 'rollup-plugin-serve' 3 | import livereload from 'rollup-plugin-livereload' 4 | 5 | export default { 6 | 7 | input: 'src/interpreter.ts', 8 | output: { 9 | name: 'jspython', 10 | file: 'dist/jspython-interpreter.js', 11 | format: 'umd', 12 | sourcemap: true, 13 | globals: {} 14 | }, 15 | external: [], 16 | plugins: [ 17 | typescript({ 18 | abortOnError: false 19 | }), 20 | serve({contentBase: '', open: true}), 21 | livereload('dist') 22 | ], 23 | watch: { 24 | exclude: ['node_modules/**'], 25 | include: 'src/**' 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import copy from 'rollup-plugin-copy' 3 | import terser from "@rollup/plugin-terser"; 4 | 5 | const input = 'src/interpreter.ts'; 6 | 7 | const pkgMain = 'dist/jspython-interpreter.min.js'; 8 | const pkgModule = 'dist/jspython-interpreter.esm.js'; 9 | 10 | export default [{ 11 | input, 12 | output: { file: pkgMain, name: 'jspython', format: 'umd', sourcemap: true, compact: true }, 13 | external: [], 14 | treeshake: true, 15 | plugins: [ 16 | typescript({ 17 | clean: true 18 | }), 19 | copy({ 20 | targets: [ 21 | { src: 'src/assets', dest: 'dist' } 22 | ] 23 | }), 24 | terser() 25 | ] 26 | }, { 27 | input, 28 | output: { file: pkgModule, format: 'esm', sourcemap: true, compact: true }, 29 | external: [], 30 | plugins: [ 31 | typescript({ 32 | clean: true 33 | }) 34 | ] 35 | }]; 36 | -------------------------------------------------------------------------------- /src/assets/mode-jspython.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/mode/python_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) { 2 | "use strict"; 3 | 4 | var oop = require("../lib/oop"); 5 | var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; 6 | 7 | var PythonHighlightRules = function() { 8 | 9 | var keywords = ( 10 | "async|def|if|return|and|or|while|for|in|break|continue|else|from|import" 11 | ); 12 | 13 | var builtinConstants = ( 14 | "true|false|True|False" 15 | ); 16 | 17 | var builtinFunctions = ( 18 | "print|format|assert|isNull|httpGet|httpPost|httpPut|httpDelete" 19 | ); 20 | var keywordMapper = this.createKeywordMapper({ 21 | "invalid.deprecated": "debugger", 22 | "support.function": builtinFunctions, 23 | "variable.language": "self|cls", 24 | "constant.language": builtinConstants, 25 | "keyword": keywords 26 | }, "identifier"); 27 | 28 | var strPre = "[uU]?"; 29 | var strRawPre = "[rR]"; 30 | var strFormatPre = "[fF]"; 31 | var strRawFormatPre = "(?:[rR][fF]|[fF][rR])"; 32 | var decimalInteger = "(?:(?:[1-9]\\d*)|(?:0))"; 33 | var octInteger = "(?:0[oO]?[0-7]+)"; 34 | var hexInteger = "(?:0[xX][\\dA-Fa-f]+)"; 35 | var binInteger = "(?:0[bB][01]+)"; 36 | var integer = "(?:" + decimalInteger + "|" + octInteger + "|" + hexInteger + "|" + binInteger + ")"; 37 | 38 | var exponent = "(?:[eE][+-]?\\d+)"; 39 | var fraction = "(?:\\.\\d+)"; 40 | var intPart = "(?:\\d+)"; 41 | var pointFloat = "(?:(?:" + intPart + "?" + fraction + ")|(?:" + intPart + "\\.))"; 42 | var exponentFloat = "(?:(?:" + pointFloat + "|" + intPart + ")" + exponent + ")"; 43 | var floatNumber = "(?:" + exponentFloat + "|" + pointFloat + ")"; 44 | 45 | var stringEscape = "\\\\(x[0-9A-Fa-f]{2}|[0-7]{3}|[\\\\abfnrtv'\"]|U[0-9A-Fa-f]{8}|u[0-9A-Fa-f]{4})"; 46 | 47 | this.$rules = { 48 | "start" : [ { 49 | token : "comment", 50 | regex : "#.*$" 51 | }, { 52 | token : "string", // multi line """ string start 53 | regex : strPre + '"{3}', 54 | next : "qqstring3" 55 | }, { 56 | token : "string", // " string 57 | regex : strPre + '"(?=.)', 58 | next : "qqstring" 59 | }, { 60 | token : "string", // multi line ''' string start 61 | regex : strPre + "'{3}", 62 | next : "qstring3" 63 | }, { 64 | token : "string", // ' string 65 | regex : strPre + "'(?=.)", 66 | next : "qstring" 67 | }, { 68 | token: "string", 69 | regex: strRawPre + '"{3}', 70 | next: "rawqqstring3" 71 | }, { 72 | token: "string", 73 | regex: strRawPre + '"(?=.)', 74 | next: "rawqqstring" 75 | }, { 76 | token: "string", 77 | regex: strRawPre + "'{3}", 78 | next: "rawqstring3" 79 | }, { 80 | token: "string", 81 | regex: strRawPre + "'(?=.)", 82 | next: "rawqstring" 83 | }, { 84 | token: "string", 85 | regex: strFormatPre + '"{3}', 86 | next: "fqqstring3" 87 | }, { 88 | token: "string", 89 | regex: strFormatPre + '"(?=.)', 90 | next: "fqqstring" 91 | }, { 92 | token: "string", 93 | regex: strFormatPre + "'{3}", 94 | next: "fqstring3" 95 | }, { 96 | token: "string", 97 | regex: strFormatPre + "'(?=.)", 98 | next: "fqstring" 99 | },{ 100 | token: "string", 101 | regex: strRawFormatPre + '"{3}', 102 | next: "rfqqstring3" 103 | }, { 104 | token: "string", 105 | regex: strRawFormatPre + '"(?=.)', 106 | next: "rfqqstring" 107 | }, { 108 | token: "string", 109 | regex: strRawFormatPre + "'{3}", 110 | next: "rfqstring3" 111 | }, { 112 | token: "string", 113 | regex: strRawFormatPre + "'(?=.)", 114 | next: "rfqstring" 115 | }, { 116 | token: "keyword.operator", 117 | regex: "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|@|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|=" 118 | }, { 119 | token: "punctuation", 120 | regex: ",|:|;|\\->|\\+=|\\-=|\\*=|\\/=|\\/\\/=|%=|@=|&=|\\|=|^=|>>=|<<=|\\*\\*=" 121 | }, { 122 | token: "paren.lparen", 123 | regex: "[\\[\\(\\{]" 124 | }, { 125 | token: "paren.rparen", 126 | regex: "[\\]\\)\\}]" 127 | }, { 128 | token: "text", 129 | regex: "\\s+" 130 | }, { 131 | include: "constants" 132 | }], 133 | "qqstring3": [{ 134 | token: "constant.language.escape", 135 | regex: stringEscape 136 | }, { 137 | token: "string", // multi line """ string end 138 | regex: '"{3}', 139 | next: "start" 140 | }, { 141 | defaultToken: "string" 142 | }], 143 | "qstring3": [{ 144 | token: "constant.language.escape", 145 | regex: stringEscape 146 | }, { 147 | token: "string", // multi line ''' string end 148 | regex: "'{3}", 149 | next: "start" 150 | }, { 151 | defaultToken: "string" 152 | }], 153 | "qqstring": [{ 154 | token: "constant.language.escape", 155 | regex: stringEscape 156 | }, { 157 | token: "string", 158 | regex: "\\\\$", 159 | next: "qqstring" 160 | }, { 161 | token: "string", 162 | regex: '"|$', 163 | next: "start" 164 | }, { 165 | defaultToken: "string" 166 | }], 167 | "qstring": [{ 168 | token: "constant.language.escape", 169 | regex: stringEscape 170 | }, { 171 | token: "string", 172 | regex: "\\\\$", 173 | next: "qstring" 174 | }, { 175 | token: "string", 176 | regex: "'|$", 177 | next: "start" 178 | }, { 179 | defaultToken: "string" 180 | }], 181 | "rawqqstring3": [{ 182 | token: "string", // multi line """ string end 183 | regex: '"{3}', 184 | next: "start" 185 | }, { 186 | defaultToken: "string" 187 | }], 188 | "rawqstring3": [{ 189 | token: "string", // multi line ''' string end 190 | regex: "'{3}", 191 | next: "start" 192 | }, { 193 | defaultToken: "string" 194 | }], 195 | "rawqqstring": [{ 196 | token: "string", 197 | regex: "\\\\$", 198 | next: "rawqqstring" 199 | }, { 200 | token: "string", 201 | regex: '"|$', 202 | next: "start" 203 | }, { 204 | defaultToken: "string" 205 | }], 206 | "rawqstring": [{ 207 | token: "string", 208 | regex: "\\\\$", 209 | next: "rawqstring" 210 | }, { 211 | token: "string", 212 | regex: "'|$", 213 | next: "start" 214 | }, { 215 | defaultToken: "string" 216 | }], 217 | "fqqstring3": [{ 218 | token: "constant.language.escape", 219 | regex: stringEscape 220 | }, { 221 | token: "string", // multi line """ string end 222 | regex: '"{3}', 223 | next: "start" 224 | }, { 225 | token: "paren.lparen", 226 | regex: "{", 227 | push: "fqstringParRules" 228 | }, { 229 | defaultToken: "string" 230 | }], 231 | "fqstring3": [{ 232 | token: "constant.language.escape", 233 | regex: stringEscape 234 | }, { 235 | token: "string", // multi line ''' string end 236 | regex: "'{3}", 237 | next: "start" 238 | }, { 239 | token: "paren.lparen", 240 | regex: "{", 241 | push: "fqstringParRules" 242 | }, { 243 | defaultToken: "string" 244 | }], 245 | "fqqstring": [{ 246 | token: "constant.language.escape", 247 | regex: stringEscape 248 | }, { 249 | token: "string", 250 | regex: "\\\\$", 251 | next: "fqqstring" 252 | }, { 253 | token: "string", 254 | regex: '"|$', 255 | next: "start" 256 | }, { 257 | token: "paren.lparen", 258 | regex: "{", 259 | push: "fqstringParRules" 260 | }, { 261 | defaultToken: "string" 262 | }], 263 | "fqstring": [{ 264 | token: "constant.language.escape", 265 | regex: stringEscape 266 | }, { 267 | token: "string", 268 | regex: "'|$", 269 | next: "start" 270 | }, { 271 | token: "paren.lparen", 272 | regex: "{", 273 | push: "fqstringParRules" 274 | }, { 275 | defaultToken: "string" 276 | }], 277 | "rfqqstring3": [{ 278 | token: "string", // multi line """ string end 279 | regex: '"{3}', 280 | next: "start" 281 | }, { 282 | token: "paren.lparen", 283 | regex: "{", 284 | push: "fqstringParRules" 285 | }, { 286 | defaultToken: "string" 287 | }], 288 | "rfqstring3": [{ 289 | token: "string", // multi line ''' string end 290 | regex: "'{3}", 291 | next: "start" 292 | }, { 293 | token: "paren.lparen", 294 | regex: "{", 295 | push: "fqstringParRules" 296 | }, { 297 | defaultToken: "string" 298 | }], 299 | "rfqqstring": [{ 300 | token: "string", 301 | regex: "\\\\$", 302 | next: "rfqqstring" 303 | }, { 304 | token: "string", 305 | regex: '"|$', 306 | next: "start" 307 | }, { 308 | token: "paren.lparen", 309 | regex: "{", 310 | push: "fqstringParRules" 311 | }, { 312 | defaultToken: "string" 313 | }], 314 | "rfqstring": [{ 315 | token: "string", 316 | regex: "'|$", 317 | next: "start" 318 | }, { 319 | token: "paren.lparen", 320 | regex: "{", 321 | push: "fqstringParRules" 322 | }, { 323 | defaultToken: "string" 324 | }], 325 | "fqstringParRules": [{//TODO: nested {} 326 | token: "paren.lparen", 327 | regex: "[\\[\\(]" 328 | }, { 329 | token: "paren.rparen", 330 | regex: "[\\]\\)]" 331 | }, { 332 | token: "string", 333 | regex: "\\s+" 334 | }, { 335 | token: "string", 336 | regex: "'(.)*'" 337 | }, { 338 | token: "string", 339 | regex: '"(.)*"' 340 | }, { 341 | token: "function.support", 342 | regex: "(!s|!r|!a)" 343 | }, { 344 | include: "constants" 345 | },{ 346 | token: 'paren.rparen', 347 | regex: "}", 348 | next: 'pop' 349 | },{ 350 | token: 'paren.lparen', 351 | regex: "{", 352 | push: "fqstringParRules" 353 | }], 354 | "constants": [{ 355 | token: "constant.numeric", // imaginary 356 | regex: "(?:" + floatNumber + "|\\d+)[jJ]\\b" 357 | }, { 358 | token: "constant.numeric", // float 359 | regex: floatNumber 360 | }, { 361 | token: "constant.numeric", // long integer 362 | regex: integer + "[lL]\\b" 363 | }, { 364 | token: "constant.numeric", // integer 365 | regex: integer + "\\b" 366 | }, { 367 | token: ["punctuation", "function.support"],// method 368 | regex: "(\\.)([a-zA-Z_]+)\\b" 369 | }, { 370 | token: keywordMapper, 371 | regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" 372 | }] 373 | }; 374 | this.normalizeRules(); 375 | }; 376 | 377 | oop.inherits(PythonHighlightRules, TextHighlightRules); 378 | 379 | exports.PythonHighlightRules = PythonHighlightRules; 380 | }); 381 | 382 | ace.define("ace/mode/folding/pythonic",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode"], function(require, exports, module) { 383 | "use strict"; 384 | 385 | var oop = require("../../lib/oop"); 386 | var BaseFoldMode = require("./fold_mode").FoldMode; 387 | 388 | var FoldMode = exports.FoldMode = function(markers) { 389 | this.foldingStartMarker = new RegExp("([\\[{])(?:\\s*)$|(" + markers + ")(?:\\s*)(?:#.*)?$"); 390 | }; 391 | oop.inherits(FoldMode, BaseFoldMode); 392 | 393 | (function() { 394 | 395 | this.getFoldWidgetRange = function(session, foldStyle, row) { 396 | var line = session.getLine(row); 397 | var match = line.match(this.foldingStartMarker); 398 | if (match) { 399 | if (match[1]) 400 | return this.openingBracketBlock(session, match[1], row, match.index); 401 | if (match[2]) 402 | return this.indentationBlock(session, row, match.index + match[2].length); 403 | return this.indentationBlock(session, row); 404 | } 405 | }; 406 | 407 | }).call(FoldMode.prototype); 408 | 409 | }); 410 | 411 | ace.define("ace/mode/python",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/python_highlight_rules","ace/mode/folding/pythonic","ace/range"], function(require, exports, module) { 412 | "use strict"; 413 | 414 | var oop = require("../lib/oop"); 415 | var TextMode = require("./text").Mode; 416 | var PythonHighlightRules = require("./python_highlight_rules").PythonHighlightRules; 417 | var PythonFoldMode = require("./folding/pythonic").FoldMode; 418 | var Range = require("../range").Range; 419 | 420 | var Mode = function() { 421 | this.HighlightRules = PythonHighlightRules; 422 | this.foldingRules = new PythonFoldMode("\\:"); 423 | this.$behaviour = this.$defaultBehaviour; 424 | }; 425 | oop.inherits(Mode, TextMode); 426 | 427 | (function() { 428 | 429 | this.lineCommentStart = "#"; 430 | 431 | this.getNextLineIndent = function(state, line, tab) { 432 | var indent = this.$getIndent(line); 433 | 434 | var tokenizedLine = this.getTokenizer().getLineTokens(line, state); 435 | var tokens = tokenizedLine.tokens; 436 | 437 | if (tokens.length && tokens[tokens.length-1].type == "comment") { 438 | return indent; 439 | } 440 | 441 | if (state == "start") { 442 | var match = line.match(/^.*[\{\(\[:]\s*$/); 443 | if (match) { 444 | indent += tab; 445 | } 446 | } 447 | 448 | return indent; 449 | }; 450 | 451 | var outdents = { 452 | "pass": 1, 453 | "return": 1, 454 | "raise": 1, 455 | "break": 1, 456 | "continue": 1 457 | }; 458 | 459 | this.checkOutdent = function(state, line, input) { 460 | if (input !== "\r\n" && input !== "\r" && input !== "\n") 461 | return false; 462 | 463 | var tokens = this.getTokenizer().getLineTokens(line.trim(), state).tokens; 464 | 465 | if (!tokens) 466 | return false; 467 | do { 468 | var last = tokens.pop(); 469 | } while (last && (last.type == "comment" || (last.type == "text" && last.value.match(/^\s+$/)))); 470 | 471 | if (!last) 472 | return false; 473 | 474 | return (last.type == "keyword" && outdents[last.value]); 475 | }; 476 | 477 | this.autoOutdent = function(state, doc, row) { 478 | 479 | row += 1; 480 | var indent = this.$getIndent(doc.getLine(row)); 481 | var tab = doc.getTabString(); 482 | if (indent.slice(-tab.length) == tab) 483 | doc.remove(new Range(row, indent.length-tab.length, row, indent.length)); 484 | }; 485 | 486 | this.$id = "ace/mode/python"; 487 | }).call(Mode.prototype); 488 | 489 | exports.Mode = Mode; 490 | }); (function() { 491 | ace.require(["ace/mode/python"], function(m) { 492 | if (typeof module == "object" && typeof exports == "object" && module) { 493 | module.exports = m; 494 | } 495 | }); 496 | })(); 497 | -------------------------------------------------------------------------------- /src/common/ast-types.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionOperators, LogicalOperators } from './operators'; 2 | import { getTokenLoc, getTokenValue, Token } from './token-types'; 3 | 4 | export type AstNodeType = 5 | | 'assign' 6 | | 'binOp' 7 | | 'const' 8 | | 'logicalOp' 9 | | 'getSingleVar' 10 | | 'setSingleVar' 11 | | 'chainingCalls' 12 | | 'chainingObjectAccess' 13 | | 'funcCall' 14 | | 'funcDef' 15 | | 'arrowFuncDef' 16 | | 'createObject' 17 | | 'createArray' 18 | | 'if' 19 | | 'elif' 20 | | 'for' 21 | | 'while' 22 | | 'tryExcept' 23 | | 'raise' 24 | | 'import' 25 | | 'comment' 26 | | 'return' 27 | | 'continue' 28 | | 'break'; 29 | 30 | export interface NameAlias { 31 | name: string; 32 | alias: string | undefined; 33 | } 34 | 35 | export interface ExceptBody { 36 | error: NameAlias; 37 | body: AstNode[]; 38 | } 39 | 40 | export interface FuncDefNode { 41 | params: string[]; 42 | funcAst: AstBlock; 43 | } 44 | 45 | export interface IsNullCoelsing { 46 | nullCoelsing: boolean | undefined; 47 | } 48 | 49 | export interface ObjectPropertyInfo { 50 | name: AstNode; 51 | value: AstNode; 52 | } 53 | 54 | export abstract class AstNode { 55 | loc: Uint16Array | undefined = undefined; 56 | constructor(public type: AstNodeType) {} 57 | } 58 | 59 | export class AssignNode extends AstNode { 60 | constructor(public target: AstNode, public source: AstNode, public loc: Uint16Array) { 61 | super('assign'); 62 | this.loc = loc; 63 | } 64 | } 65 | 66 | export class ConstNode extends AstNode { 67 | public value: number | string | boolean | null; 68 | 69 | constructor(token: Token) { 70 | super('const'); 71 | this.value = getTokenValue(token); 72 | this.loc = getTokenLoc(token); 73 | } 74 | } 75 | 76 | export class CommentNode extends AstNode { 77 | constructor(public comment: string, public loc: Uint16Array) { 78 | super('comment'); 79 | this.loc = loc; 80 | } 81 | } 82 | 83 | export class ReturnNode extends AstNode { 84 | constructor(public returnValue: AstNode | undefined = undefined, public loc: Uint16Array) { 85 | super('return'); 86 | this.loc = loc; 87 | } 88 | } 89 | 90 | export class RaiseNode extends AstNode { 91 | constructor(public errorName: string, public errorMessageAst: AstNode, public loc: Uint16Array) { 92 | super('raise'); 93 | this.loc = loc; 94 | } 95 | } 96 | 97 | export class ContinueNode extends AstNode { 98 | constructor() { 99 | super('continue'); 100 | } 101 | } 102 | 103 | export class BreakNode extends AstNode { 104 | constructor() { 105 | super('break'); 106 | } 107 | } 108 | 109 | export class SetSingleVarNode extends AstNode { 110 | public name: string; 111 | constructor(token: Token) { 112 | super('setSingleVar'); 113 | this.name = token[0] as string; 114 | this.loc = getTokenLoc(token); 115 | } 116 | } 117 | 118 | export class FunctionCallNode extends AstNode implements IsNullCoelsing { 119 | public nullCoelsing: boolean | undefined = undefined; 120 | 121 | constructor(public name: string, public paramNodes: AstNode[] | null, public loc: Uint16Array) { 122 | super('funcCall'); 123 | this.loc = loc; 124 | } 125 | } 126 | 127 | export class FunctionDefNode extends AstNode implements FuncDefNode { 128 | constructor( 129 | public funcAst: AstBlock, 130 | public params: string[], 131 | public isAsync: boolean, 132 | public loc: Uint16Array 133 | ) { 134 | super('funcDef'); 135 | this.loc = loc; 136 | } 137 | } 138 | 139 | export class ArrowFuncDefNode extends AstNode implements FuncDefNode { 140 | constructor(public funcAst: AstBlock, public params: string[], public loc: Uint16Array) { 141 | super('arrowFuncDef'); 142 | this.loc = loc; 143 | } 144 | } 145 | 146 | export class ElifNode extends AstNode { 147 | constructor( 148 | public conditionNode: AstNode, 149 | public elifBody: AstNode[], 150 | public loc: Uint16Array 151 | ) { 152 | super('elif'); 153 | this.loc = loc; 154 | } 155 | } 156 | 157 | export class IfNode extends AstNode { 158 | constructor( 159 | public conditionNode: AstNode, 160 | public ifBody: AstNode[], 161 | public elifs: ElifNode[] | undefined = undefined, 162 | public elseBody: AstNode[] | undefined = undefined, 163 | public loc: Uint16Array, 164 | ) { 165 | super('if'); 166 | this.loc = loc; 167 | } 168 | } 169 | 170 | export class TryExceptNode extends AstNode { 171 | constructor( 172 | public tryBody: AstNode[], 173 | public exepts: ExceptBody[], 174 | public elseBody: AstNode[] | undefined, 175 | public finallyBody: AstNode[] | undefined, 176 | 177 | public loc: Uint16Array 178 | ) { 179 | super('tryExcept'); 180 | this.loc = loc; 181 | } 182 | } 183 | 184 | export class ForNode extends AstNode { 185 | constructor( 186 | public sourceArray: AstNode, 187 | public itemVarName: string, 188 | public body: AstNode[], 189 | public loc: Uint16Array 190 | ) { 191 | super('for'); 192 | this.loc = loc; 193 | } 194 | } 195 | 196 | export class WhileNode extends AstNode { 197 | constructor(public condition: AstNode, public body: AstNode[], public loc: Uint16Array) { 198 | super('while'); 199 | this.loc = loc; 200 | } 201 | } 202 | 203 | export class ImportNode extends AstNode { 204 | constructor( 205 | public module: NameAlias, 206 | public body: AstBlock, 207 | public parts: NameAlias[] | undefined = undefined, 208 | public loc: Uint16Array 209 | ) { 210 | super('import'); 211 | this.loc = loc; 212 | } 213 | } 214 | 215 | export class GetSingleVarNode extends AstNode implements IsNullCoelsing { 216 | name: string; 217 | nullCoelsing: boolean | undefined = undefined; 218 | 219 | constructor(token: Token, nullCoelsing: boolean | undefined = undefined) { 220 | super('getSingleVar'); 221 | this.name = token[0] as string; 222 | this.nullCoelsing = nullCoelsing; 223 | this.loc = getTokenLoc(token); 224 | } 225 | } 226 | 227 | export class ChainingCallsNode extends AstNode { 228 | constructor(public innerNodes: AstNode[], public loc: Uint16Array) { 229 | super('chainingCalls'); 230 | this.loc = loc; 231 | } 232 | } 233 | 234 | export class CreateObjectNode extends AstNode { 235 | constructor(public props: ObjectPropertyInfo[], public loc: Uint16Array) { 236 | super('createObject'); 237 | this.loc = loc; 238 | } 239 | } 240 | 241 | export class CreateArrayNode extends AstNode { 242 | constructor(public items: AstNode[], public loc: Uint16Array) { 243 | super('createArray'); 244 | this.loc = loc; 245 | } 246 | } 247 | 248 | export class ChainingObjectAccessNode extends AstNode { 249 | constructor( 250 | public indexerBody: AstNode, 251 | public nullCoelsing: boolean | undefined = undefined, 252 | public loc: Uint16Array 253 | ) { 254 | super('chainingObjectAccess'); 255 | this.loc = loc; 256 | } 257 | } 258 | 259 | export interface LogicalNodeItem { 260 | node: AstNode; 261 | op: LogicalOperators | undefined; 262 | } 263 | 264 | export class LogicalOpNode extends AstNode { 265 | constructor(public items: LogicalNodeItem[], public loc: Uint16Array) { 266 | super('logicalOp'); 267 | this.loc = loc; 268 | } 269 | } 270 | 271 | export class BinOpNode extends AstNode { 272 | constructor( 273 | public left: AstNode, 274 | public op: ExpressionOperators, 275 | public right: AstNode, 276 | public loc: Uint16Array 277 | ) { 278 | super('binOp'); 279 | this.loc = loc; 280 | } 281 | } 282 | 283 | export interface AstBlock { 284 | name: string; 285 | type: 'module' | 'func' | 'if' | 'for' | 'while' | 'trycatch'; 286 | funcs: FunctionDefNode[]; 287 | body: AstNode[]; 288 | } 289 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ast-types'; 2 | export * from './token-types'; 3 | export * from './parser-types'; 4 | export * from './operators'; 5 | -------------------------------------------------------------------------------- /src/common/operators.ts: -------------------------------------------------------------------------------- 1 | export enum OperationTypes { 2 | Arithmetic, 3 | Assignment, 4 | Comparison, 5 | Logical, 6 | Membership 7 | } 8 | 9 | export type AssignmentOperators = '=' | '+=' | '-=' | '*=' | '/=' | '++' | '--'; 10 | export type ArithmeticOperators = '+' | '-' | '*' | '/' | '%' | '**' | '//'; 11 | export type ComparisonOperators = '>' | '>=' | '==' | '!=' | '<>' | '<' | '<='; 12 | export type LogicalOperators = 'and' | 'or'; // | "not" | "not in"; 13 | export type MembershipOperators = 'in'; 14 | 15 | export type Operators = 16 | | AssignmentOperators 17 | | ArithmeticOperators 18 | | ComparisonOperators 19 | | LogicalOperators 20 | | MembershipOperators; 21 | 22 | export const OperatorsMap: Map = new Map([ 23 | ['+', OperationTypes.Arithmetic], 24 | ['-', OperationTypes.Arithmetic], 25 | ['*', OperationTypes.Arithmetic], 26 | ['/', OperationTypes.Arithmetic], 27 | ['%', OperationTypes.Arithmetic], 28 | ['**', OperationTypes.Arithmetic], 29 | ['//', OperationTypes.Arithmetic], 30 | 31 | ['>', OperationTypes.Comparison], 32 | ['>=', OperationTypes.Comparison], 33 | ['==', OperationTypes.Comparison], 34 | ['!=', OperationTypes.Comparison], 35 | ['<>', OperationTypes.Comparison], 36 | ['<', OperationTypes.Comparison], 37 | ['<=', OperationTypes.Comparison], 38 | 39 | ['and', OperationTypes.Logical], 40 | ['or', OperationTypes.Logical], 41 | // "not", OperationTypes.Logical], 42 | // "not in", OperationTypes.Logical], 43 | 44 | ['in', OperationTypes.Membership], 45 | 46 | ['=', OperationTypes.Assignment], 47 | ['+=', OperationTypes.Assignment], 48 | ['-=', OperationTypes.Assignment], 49 | ['*=', OperationTypes.Assignment], 50 | ['/=', OperationTypes.Assignment], 51 | ['++', OperationTypes.Assignment], 52 | ['--', OperationTypes.Assignment] 53 | ]); 54 | 55 | export type Primitive = string | number | boolean | null; 56 | 57 | export type ExpressionOperators = 58 | | ArithmeticOperators 59 | | ComparisonOperators 60 | | LogicalOperators 61 | | MembershipOperators; 62 | type ExpressionOperation = (l: Primitive, r: Primitive) => Primitive; 63 | 64 | export const OperationFuncs: Map = new Map< 65 | ExpressionOperators, 66 | ExpressionOperation 67 | >([ 68 | ['+' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '+')) as ExpressionOperation], 69 | ['-' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '-')) as ExpressionOperation], 70 | ['/' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '/')) as ExpressionOperation], 71 | ['*' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '*')) as ExpressionOperation], 72 | ['%' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '%')) as ExpressionOperation], 73 | ['**' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '**')) as ExpressionOperation], 74 | ['//' as ExpressionOperators, ((l, r) => arithmeticOperation(l, r, '//')) as ExpressionOperation], 75 | 76 | ['>' as ExpressionOperators, ((l, r) => comparissonOperation(l, r, '>')) as ExpressionOperation], 77 | [ 78 | '>=' as ExpressionOperators, 79 | ((l, r) => comparissonOperation(l, r, '>=')) as ExpressionOperation 80 | ], 81 | ['<' as ExpressionOperators, ((l, r) => comparissonOperation(l, r, '<')) as ExpressionOperation], 82 | [ 83 | '<=' as ExpressionOperators, 84 | ((l, r) => comparissonOperation(l, r, '<=')) as ExpressionOperation 85 | ], 86 | [ 87 | '==' as ExpressionOperators, 88 | ((l, r) => comparissonOperation(l, r, '==')) as ExpressionOperation 89 | ], 90 | [ 91 | '!=' as ExpressionOperators, 92 | ((l, r) => comparissonOperation(l, r, '!=')) as ExpressionOperation 93 | ], 94 | [ 95 | '<>' as ExpressionOperators, 96 | ((l, r) => comparissonOperation(l, r, '<>')) as ExpressionOperation 97 | ], 98 | 99 | ['and' as ExpressionOperators, ((l, r) => logicalOperation(l, r, 'and')) as ExpressionOperation], 100 | ['or' as ExpressionOperators, ((l, r) => logicalOperation(l, r, 'or')) as ExpressionOperation], 101 | // "not" as ExpressionOperators, ((l, r) => logicalOperation(l, r, "not")) as ExpressionOperation], 102 | // "not in" as ExpressionOperators, ((l, r) => logicalOperation(l, r, "not in")) as ExpressionOperation], 103 | 104 | ['in' as ExpressionOperators, ((l, r) => membershipOperation(l, r, 'in')) as ExpressionOperation] 105 | ]); 106 | 107 | function membershipOperation(l: Primitive, r: Primitive, op: MembershipOperators): Primitive { 108 | if (typeof l === 'string') { 109 | return (l as string).includes(String(r)); 110 | } 111 | 112 | if (Array.isArray(l)) { 113 | return (l as unknown[]).includes(r); 114 | } 115 | 116 | throw new Error(`Unknown operation '${op}'`); 117 | } 118 | 119 | function logicalOperation(l: Primitive, r: Primitive, op: LogicalOperators): Primitive { 120 | switch (op) { 121 | case 'and': 122 | return l && r; 123 | 124 | case 'or': 125 | return l || r; 126 | } 127 | throw new Error(`Unknown operation '${op}'`); 128 | } 129 | 130 | function comparissonOperation(l: Primitive, r: Primitive, op: ComparisonOperators): Primitive { 131 | switch (op) { 132 | case '==': 133 | return l === r; 134 | 135 | case '!=': 136 | return l !== r; 137 | 138 | case '<>': 139 | return l !== r; 140 | 141 | case '>': 142 | return (l as number) > (r as number); 143 | 144 | case '<': 145 | return (l as number) < (r as number); 146 | 147 | case '>=': 148 | return (l as number) >= (r as number); 149 | 150 | case '<=': 151 | return (l as number) <= (r as number); 152 | } 153 | 154 | throw new Error(`Unknown operation '${op}'`); 155 | } 156 | 157 | function arithmeticOperation(l: Primitive, r: Primitive, op: ArithmeticOperators): Primitive { 158 | switch (op) { 159 | case '+': 160 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 161 | return (l as any) + (r as any); 162 | 163 | case '-': 164 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 165 | return (l as any) - (r as any); 166 | 167 | case '*': 168 | return (l as number) * (r as number); 169 | 170 | case '/': 171 | return (l as number) / (r as number); 172 | 173 | case '%': 174 | return (l as number) % (r as number); 175 | 176 | case '**': 177 | return Math.pow(l as number, r as number); 178 | } 179 | 180 | throw new Error(`Unknown operation '${op}'`); 181 | } 182 | -------------------------------------------------------------------------------- /src/common/parser-types.ts: -------------------------------------------------------------------------------- 1 | export interface ParserOptions { 2 | includeComments: boolean; 3 | includeLoc: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/token-types.ts: -------------------------------------------------------------------------------- 1 | import { OperationTypes, Operators, OperatorsMap } from './operators'; 2 | 3 | export enum TokenTypes { 4 | Identifier = 0, 5 | Keyword = 1, 6 | Separator = 2, 7 | Operator = 3, 8 | LiteralNumber = 4, 9 | LiteralBool = 5, 10 | LiteralString = 6, 11 | LiteralNull = 7, 12 | Comment = 8 13 | } 14 | /** 15 | * Token represent a single considered token in a script. Is represented as an array, where element at: 16 | * 0 : value 17 | * 1 : token details. For a memory and performance reasons we use Uint16Array with 5 elements in it: 18 | * [ 19 | * 0 - tokenType number equivalent of @TokenTypes 20 | * 1 - beginLine 21 | * 2 - beginColumn 22 | * 3 - endLine 23 | * 4 - endColumn 24 | * ] 25 | * [(value). Uint16Array[5]([tokenType, beginLine, beginColumn, endLine, endColumn])] 26 | * tokenType 27 | */ 28 | export type Token = [string | number | boolean | null, Uint16Array]; 29 | export type TokenValue = string | number | boolean | null; 30 | 31 | export function isTokenTypeLiteral(tokenType: TokenTypes): boolean { 32 | return ( 33 | tokenType === TokenTypes.LiteralString || 34 | tokenType === TokenTypes.LiteralNumber || 35 | tokenType === TokenTypes.LiteralBool || 36 | tokenType === TokenTypes.LiteralNull 37 | ); 38 | } 39 | 40 | export function getTokenType(token: Token): TokenTypes { 41 | return token[1][0] as TokenTypes; 42 | } 43 | 44 | export function getTokenValue(token: Token | null): TokenValue { 45 | return token ? token[0] : null; 46 | } 47 | 48 | export function getTokenLoc(token: Token): Uint16Array { 49 | return token[1].subarray(1); 50 | } 51 | 52 | export function getStartLine(token: Token): number { 53 | return token[1][1]; 54 | } 55 | 56 | export function getStartColumn(token: Token): number { 57 | return token[1][2]; 58 | } 59 | 60 | export function getEndLine(token: Token): number { 61 | return token[1][3]; 62 | } 63 | 64 | export function getEndColumn(token: Token): number { 65 | return token[1][4]; 66 | } 67 | 68 | export function splitTokensByIndexes(tokens: Token[], sepIndexes: number[]): Token[][] { 69 | const result: Token[][] = []; 70 | 71 | if (!tokens.length) { 72 | return []; 73 | } 74 | 75 | let start = 0; 76 | for (let i = 0; i < sepIndexes.length; i++) { 77 | const ind = sepIndexes[i]; 78 | if (getTokenValue(tokens[start - 1]) === '[') { 79 | start = start - 1; 80 | } 81 | result.push(tokens.slice(start, ind)); 82 | start = ind + 1; 83 | } 84 | 85 | if (getTokenValue(tokens[start - 1]) === '[') { 86 | start = start - 1; 87 | } 88 | result.push(tokens.slice(start, tokens.length)); 89 | return result; 90 | } 91 | 92 | export function splitTokens(tokens: Token[], separator: string): Token[][] { 93 | if (!tokens.length) { 94 | return []; 95 | } 96 | const sepIndexes = findTokenValueIndexes(tokens, value => value === separator); 97 | return splitTokensByIndexes(tokens, sepIndexes); 98 | } 99 | 100 | export function findTokenValueIndex( 101 | tokens: Token[], 102 | predicate: (value: TokenValue) => boolean, 103 | start = 0 104 | ): number { 105 | for (let i = start; i < tokens.length; i++) { 106 | if (getTokenType(tokens[i]) === TokenTypes.LiteralString) { 107 | continue; 108 | } 109 | 110 | if (getTokenValue(tokens[i]) === '(') { 111 | i = skipInnerBrackets(tokens, i, '(', ')'); 112 | } else if (getTokenValue(tokens[i]) === '[') { 113 | i = skipInnerBrackets(tokens, i, '[', ']'); 114 | } else if (getTokenValue(tokens[i]) === '{') { 115 | i = skipInnerBrackets(tokens, i, '{', '}'); 116 | } else if (predicate(getTokenValue(tokens[i]))) { 117 | return i; 118 | } 119 | } 120 | 121 | return -1; 122 | } 123 | 124 | export function findChainingCallTokensIndexes(tokens: Token[]): number[] { 125 | const opIndexes: number[] = []; 126 | 127 | for (let i = 0; i < tokens.length; i++) { 128 | const tValue = getTokenValue(tokens[i]); 129 | const tType = getTokenType(tokens[i]); 130 | 131 | if (tType === TokenTypes.LiteralString) { 132 | continue; 133 | } 134 | 135 | if (tValue === '.') { 136 | opIndexes.push(i); 137 | } else if (tValue === '(') { 138 | i = skipInnerBrackets(tokens, i, '(', ')'); 139 | } else if (tValue === '[' && i === 0) { 140 | i = skipInnerBrackets(tokens, i, '[', ']'); 141 | } else if (tValue === '[' && i !== 0) { 142 | opIndexes.push(i); 143 | i = skipInnerBrackets(tokens, i, '[', ']'); 144 | } else if (tValue === '{') { 145 | i = skipInnerBrackets(tokens, i, '{', '}'); 146 | } 147 | } 148 | 149 | return opIndexes; 150 | } 151 | 152 | export function findTokenValueIndexes( 153 | tokens: Token[], 154 | predicate: (value: TokenValue) => boolean 155 | ): number[] { 156 | const opIndexes: number[] = []; 157 | 158 | for (let i = 0; i < tokens.length; i++) { 159 | const tValue = getTokenValue(tokens[i]); 160 | const tType = getTokenType(tokens[i]); 161 | 162 | if (tType === TokenTypes.LiteralString) { 163 | continue; 164 | } 165 | 166 | if (tValue === '(') { 167 | i = skipInnerBrackets(tokens, i, '(', ')'); 168 | } else if (tValue === '[') { 169 | i = skipInnerBrackets(tokens, i, '[', ']'); 170 | } else if (tValue === '{') { 171 | i = skipInnerBrackets(tokens, i, '{', '}'); 172 | } else if (predicate(tValue)) { 173 | opIndexes.push(i); 174 | } 175 | } 176 | 177 | return opIndexes; 178 | } 179 | 180 | export function findOperators( 181 | tokens: Token[], 182 | operationType: OperationTypes | null = null 183 | ): number[] { 184 | return !operationType 185 | ? findTokenValueIndexes(tokens, value => OperatorsMap.has(value as Operators)) 186 | : findTokenValueIndexes( 187 | tokens, 188 | value => OperatorsMap.get(value as Operators) === operationType 189 | ); 190 | } 191 | 192 | function skipInnerBrackets( 193 | tokens: Token[], 194 | i: number, 195 | openChar: string, 196 | closeChar: string 197 | ): number { 198 | let innerBrackets = 0; 199 | while (getTokenValue(tokens[++i]) !== closeChar || innerBrackets !== 0) { 200 | if (i + 1 >= tokens.length) { 201 | throw new Error(`Closing '${closeChar}' is missing`); 202 | } 203 | 204 | const tokenValue = getTokenValue(tokens[i]); 205 | if (tokenValue === openChar) { 206 | innerBrackets++; 207 | } 208 | if (tokenValue === closeChar) { 209 | innerBrackets--; 210 | } 211 | } 212 | return i; 213 | } 214 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | export function parseDatetimeOrNull(value: string | number | Date): Date | null { 2 | if (!value) { 3 | return null; 4 | } 5 | if (typeof value === 'number') { 6 | return new Date(value); 7 | } 8 | if (value instanceof Date && !isNaN(value.valueOf())) { 9 | return value; 10 | } 11 | // only string values can be converted to Date 12 | if (typeof value !== 'string') { 13 | return null; 14 | } 15 | 16 | const strValue = String(value); 17 | if (!strValue.length) { 18 | return null; 19 | } 20 | 21 | const parseMonth = (mm: string): number => { 22 | if (!mm || !mm.length) { 23 | return NaN; 24 | } 25 | 26 | const m = parseInt(mm, 10); 27 | if (!isNaN(m)) { 28 | return m - 1; 29 | } 30 | 31 | // make sure english months are coming through 32 | if (mm.startsWith('jan')) { 33 | return 0; 34 | } 35 | if (mm.startsWith('feb')) { 36 | return 1; 37 | } 38 | if (mm.startsWith('mar')) { 39 | return 2; 40 | } 41 | if (mm.startsWith('apr')) { 42 | return 3; 43 | } 44 | if (mm.startsWith('may')) { 45 | return 4; 46 | } 47 | if (mm.startsWith('jun')) { 48 | return 5; 49 | } 50 | if (mm.startsWith('jul')) { 51 | return 6; 52 | } 53 | if (mm.startsWith('aug')) { 54 | return 7; 55 | } 56 | if (mm.startsWith('sep')) { 57 | return 8; 58 | } 59 | if (mm.startsWith('oct')) { 60 | return 9; 61 | } 62 | if (mm.startsWith('nov')) { 63 | return 10; 64 | } 65 | if (mm.startsWith('dec')) { 66 | return 11; 67 | } 68 | 69 | return NaN; 70 | }; 71 | 72 | const correctYear = (yy: number): number => { 73 | if (yy < 100) { 74 | return yy < 68 ? yy + 2000 : yy + 1900; 75 | } else { 76 | return yy; 77 | } 78 | }; 79 | 80 | const validDateOrNull = ( 81 | yyyy: number, 82 | month: number, 83 | day: number, 84 | hours: number, 85 | mins: number, 86 | ss: number 87 | ): Date | null => { 88 | if (month > 11 || day > 31 || hours >= 60 || mins >= 60 || ss >= 60) { 89 | return null; 90 | } 91 | 92 | const dd = new Date(yyyy, month, day, hours, mins, ss, 0); 93 | return !isNaN(dd.valueOf()) ? dd : null; 94 | }; 95 | 96 | const strTokens = strValue 97 | .replace('T', ' ') 98 | .toLowerCase() 99 | .split(/[: /-]/); 100 | const dt = strTokens.map(parseFloat); 101 | 102 | // try ISO first 103 | let d = validDateOrNull(dt[0], dt[1] - 1, dt[2], dt[3] || 0, dt[4] || 0, dt[5] || 0); 104 | if (d) { 105 | return d; 106 | } 107 | 108 | // then UK 109 | d = validDateOrNull( 110 | correctYear(dt[2]), 111 | parseMonth(strTokens[1]), 112 | dt[0], 113 | dt[3] || 0, 114 | dt[4] || 0, 115 | dt[5] || 0 116 | ); 117 | if (d) { 118 | return d; 119 | } 120 | 121 | // then US 122 | d = validDateOrNull( 123 | correctYear(dt[2]), 124 | parseMonth(strTokens[0]), 125 | correctYear(dt[1]), 126 | dt[3] || 0, 127 | dt[4] || 0, 128 | dt[5] || 0 129 | ); 130 | if (d) { 131 | return d; 132 | } 133 | 134 | return null; 135 | } 136 | 137 | export function getImportType(name: string): 'jspyModule' | 'jsPackage' | 'json' { 138 | if (name.startsWith('/') || name.startsWith('./')) { 139 | return name.endsWith('.json') ? 'json' : 'jspyModule'; 140 | } 141 | 142 | return 'jsPackage'; 143 | } 144 | 145 | function jspyErrorMessage( 146 | error: string, 147 | module: string, 148 | line: number, 149 | column: number, 150 | message: string 151 | ): string { 152 | return `${error}: ${module}(${line},${column}): ${message}`; 153 | } 154 | 155 | export class JspyTokenizerError extends Error { 156 | constructor( 157 | public module: string, 158 | public line: number, 159 | public column: number, 160 | public message: string 161 | ) { 162 | super(); 163 | this.message = jspyErrorMessage('JspyTokenizerError', module, line, column, message); 164 | Object.setPrototypeOf(this, JspyTokenizerError.prototype); 165 | } 166 | } 167 | 168 | export class JspyParserError extends Error { 169 | constructor( 170 | public module: string, 171 | public line: number, 172 | public column: number, 173 | public message: string 174 | ) { 175 | super(); 176 | this.message = jspyErrorMessage('JspyParserError', module, line, column, message); 177 | Object.setPrototypeOf(this, JspyParserError.prototype); 178 | } 179 | } 180 | 181 | export class JspyEvalError extends Error { 182 | constructor( 183 | public module: string, 184 | public line: number, 185 | public column: number, 186 | public message: string 187 | ) { 188 | super(); 189 | this.message = jspyErrorMessage('JspyEvalError', module, line, column, message); 190 | Object.setPrototypeOf(this, JspyEvalError.prototype); 191 | } 192 | } 193 | 194 | export class JspyError extends Error { 195 | constructor( 196 | public module: string, 197 | public line: number, 198 | public column: number, 199 | public name: string, 200 | public message: string 201 | ) { 202 | super(); 203 | this.message = jspyErrorMessage('JspyError', module || 'name.jspy', line, column, message); 204 | Object.setPrototypeOf(this, JspyError.prototype); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/evaluator/evaluator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowFuncDefNode, 3 | AssignNode, 4 | AstBlock, 5 | AstNode, 6 | BinOpNode, 7 | ChainingCallsNode, 8 | ChainingObjectAccessNode, 9 | ConstNode, 10 | CreateArrayNode, 11 | CreateObjectNode, 12 | ForNode, 13 | FuncDefNode, 14 | FunctionCallNode, 15 | FunctionDefNode, 16 | GetSingleVarNode, 17 | IfNode, 18 | IsNullCoelsing, 19 | LogicalOpNode, 20 | OperationFuncs, 21 | Primitive, 22 | RaiseNode, 23 | ReturnNode, 24 | SetSingleVarNode, 25 | TryExceptNode, 26 | WhileNode 27 | } from '../common'; 28 | import { JspyError, JspyEvalError } from '../common/utils'; 29 | import { BlockContext, cloneContext } from './scope'; 30 | 31 | export class Evaluator { 32 | evalBlock(ast: AstBlock, blockContext: BlockContext): unknown { 33 | let lastResult = null; 34 | 35 | for (const node of ast?.funcs || []) { 36 | const funcDef = node as FunctionDefNode; 37 | 38 | // a child scope needs to be created here 39 | const newScope = blockContext.blockScope; 40 | 41 | newScope.set(funcDef.funcAst.name, (...args: unknown[]): unknown => 42 | this.jspyFuncInvoker(funcDef, blockContext, ...args) 43 | ); 44 | } 45 | 46 | for (let i = 0; i < ast.body.length; i++) { 47 | const node = ast.body[i]; 48 | if (blockContext.cancellationToken.cancel) { 49 | const loc = node.loc || []; 50 | 51 | if (!blockContext.cancellationToken.message) { 52 | blockContext.cancellationToken.message = `Cancelled. ${blockContext.moduleName}: ${loc[0]}, ${loc[1]}`; 53 | } 54 | 55 | return blockContext.cancellationToken.message; 56 | } 57 | 58 | if (node.type === 'comment') { 59 | continue; 60 | } 61 | if (node.type === 'import') { 62 | // we can't use it here, because loader has to be promise 63 | throw new Error(`Import is not supported with 'eval'. Use method 'evalAsync' instead`); 64 | } 65 | try { 66 | lastResult = this.evalNode(node, blockContext); 67 | 68 | if (blockContext.returnCalled) { 69 | const res = blockContext.returnObject; 70 | 71 | // stop processing return 72 | if (ast.type == 'func' || ast.type == 'module') { 73 | blockContext.returnCalled = false; 74 | blockContext.returnObject = null; 75 | } 76 | return res; 77 | } 78 | 79 | if (blockContext.continueCalled) { 80 | break; 81 | } 82 | if (blockContext.breakCalled) { 83 | break; 84 | } 85 | } catch (err) { 86 | const loc = node.loc ? node.loc : [0, 0]; 87 | if (err instanceof JspyError) { 88 | throw err; 89 | } else if (err instanceof JspyEvalError) { 90 | throw err; 91 | } else { 92 | throw new JspyEvalError( 93 | blockContext.moduleName, 94 | loc[0], 95 | loc[1], 96 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 97 | (err as any).message || err 98 | ); 99 | } 100 | } 101 | } 102 | 103 | return lastResult; 104 | } 105 | 106 | jspyFuncInvoker(funcDef: FuncDefNode, context: BlockContext, ...args: unknown[]): unknown { 107 | const ast = Object.assign({}, funcDef.funcAst); 108 | ast.type = 'func'; 109 | 110 | const blockContext = cloneContext(context); 111 | 112 | // set parameters into new scope, based incomming arguments 113 | for (let i = 0; i < funcDef.params?.length || 0; i++) { 114 | const argValue = args?.length > i ? args[i] : null; 115 | blockContext.blockScope.set(funcDef.params[i], argValue); 116 | } 117 | 118 | return this.evalBlock(ast, blockContext); 119 | } 120 | 121 | private invokeFunction( 122 | func: (...args: unknown[]) => unknown, 123 | fps: unknown[], 124 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 125 | loc: { moduleName: string; line: number; column: number } 126 | ): unknown { 127 | return func(...fps); 128 | } 129 | 130 | private evalNode(node: AstNode, blockContext: BlockContext): unknown { 131 | if (node.type === 'import') { 132 | // skip this for now. As modules are implemented externally 133 | return null; 134 | } 135 | 136 | if (node.type === 'comment') { 137 | return null; 138 | } 139 | 140 | if (node.type === 'if') { 141 | const ifNode = node as IfNode; 142 | let doElse = true; 143 | if (this.evalNode(ifNode.conditionNode, blockContext)) { 144 | this.evalBlock( 145 | { name: blockContext.moduleName, type: 'if', body: ifNode.ifBody } as AstBlock, 146 | blockContext 147 | ); 148 | doElse = false; 149 | } else if (ifNode.elifs?.length) { 150 | for (let i = 0; i < ifNode.elifs.length; i++) { 151 | const elIfNode = ifNode.elifs[i]; 152 | 153 | if (this.evalNode(elIfNode.conditionNode, blockContext)) { 154 | this.evalBlock( 155 | { name: blockContext.moduleName, type: 'if', body: elIfNode.elifBody } as AstBlock, 156 | blockContext 157 | ); 158 | doElse = false; 159 | break; 160 | } 161 | } 162 | } 163 | 164 | if (doElse && ifNode.elseBody) { 165 | this.evalBlock( 166 | { name: blockContext.moduleName, type: 'if', body: ifNode.elseBody } as AstBlock, 167 | blockContext 168 | ); 169 | } 170 | 171 | return; 172 | } 173 | 174 | if (node.type === 'raise') { 175 | const raiseNode = node as RaiseNode; 176 | const errorMessage = this.evalNode(raiseNode.errorMessageAst, blockContext) as string; 177 | const err = new JspyError( 178 | blockContext.moduleName, 179 | raiseNode.loc[0], 180 | raiseNode.loc[1], 181 | raiseNode.errorName, 182 | errorMessage 183 | ); 184 | throw err; 185 | } 186 | 187 | if (node.type === 'tryExcept') { 188 | const tryNode = node as TryExceptNode; 189 | try { 190 | this.evalBlock( 191 | { name: blockContext.moduleName, type: 'trycatch', body: tryNode.tryBody } as AstBlock, 192 | blockContext 193 | ); 194 | 195 | if (tryNode.elseBody?.length || 0 > 0) { 196 | this.evalBlock( 197 | { name: blockContext.moduleName, type: 'trycatch', body: tryNode.elseBody } as AstBlock, 198 | blockContext 199 | ); 200 | } 201 | } catch (err) { 202 | const name = err instanceof JspyError ? (err as JspyError).name : typeof err; 203 | const message = 204 | err instanceof JspyError 205 | ? (err as JspyError).message 206 | : // eslint-disable-next-line @typescript-eslint/no-explicit-any 207 | (err as any)?.message ?? String(err); 208 | const moduleName = err instanceof JspyError ? (err as JspyError).module : 0; 209 | const line = err instanceof JspyError ? (err as JspyError).line : 0; 210 | const column = err instanceof JspyError ? (err as JspyError).column : 0; 211 | 212 | const firstExept = tryNode.exepts[0]; 213 | const catchBody = firstExept.body; 214 | const ctx = blockContext; // cloneContext(blockContext); 215 | ctx.blockScope.set(firstExept.error?.alias || 'error', { 216 | name, 217 | message, 218 | line, 219 | column, 220 | moduleName 221 | }); 222 | this.evalBlock( 223 | { name: blockContext.moduleName, type: 'trycatch', body: catchBody } as AstBlock, 224 | ctx 225 | ); 226 | ctx.blockScope.set(firstExept.error?.alias || 'error', null); 227 | } finally { 228 | if (tryNode.finallyBody?.length || 0 > 0) { 229 | this.evalBlock( 230 | { 231 | name: blockContext.moduleName, 232 | type: 'trycatch', 233 | body: tryNode.finallyBody 234 | } as AstBlock, 235 | blockContext 236 | ); 237 | } 238 | } 239 | 240 | return; 241 | } 242 | 243 | if (node.type === 'return') { 244 | const returnNode = node as ReturnNode; 245 | blockContext.returnCalled = true; 246 | blockContext.returnObject = returnNode.returnValue 247 | ? this.evalNode(returnNode.returnValue, blockContext) 248 | : null; 249 | 250 | return blockContext.returnObject; 251 | } 252 | 253 | if (node.type === 'continue') { 254 | blockContext.continueCalled = true; 255 | return; 256 | } 257 | 258 | if (node.type === 'break') { 259 | blockContext.breakCalled = true; 260 | return; 261 | } 262 | 263 | if (node.type === 'for') { 264 | const forNode = node as ForNode; 265 | 266 | const array = this.evalNode(forNode.sourceArray, blockContext) as unknown[] | string; 267 | 268 | for (let i = 0; i < array.length; i++) { 269 | const item = array[i]; 270 | 271 | blockContext.blockScope.set(forNode.itemVarName, item); 272 | this.evalBlock( 273 | { name: blockContext.moduleName, type: 'for', body: forNode.body } as AstBlock, 274 | blockContext 275 | ); 276 | if (blockContext.continueCalled) { 277 | blockContext.continueCalled = false; 278 | } 279 | if (blockContext.breakCalled) { 280 | break; 281 | } 282 | } 283 | 284 | if (blockContext.breakCalled) { 285 | blockContext.breakCalled = false; 286 | } 287 | return; 288 | } 289 | 290 | if (node.type === 'while') { 291 | const whileNode = node as WhileNode; 292 | 293 | while (this.evalNode(whileNode.condition, blockContext)) { 294 | this.evalBlock( 295 | { name: blockContext.moduleName, type: 'while', body: whileNode.body } as AstBlock, 296 | blockContext 297 | ); 298 | 299 | if (blockContext.continueCalled) { 300 | blockContext.continueCalled = false; 301 | } 302 | if (blockContext.breakCalled) { 303 | break; 304 | } 305 | } 306 | if (blockContext.breakCalled) { 307 | blockContext.breakCalled = false; 308 | } 309 | 310 | return; 311 | } 312 | 313 | if (node.type === 'const') { 314 | return (node as ConstNode).value; 315 | } 316 | 317 | if (node.type === 'getSingleVar') { 318 | const name = (node as GetSingleVarNode).name; 319 | 320 | const value = blockContext.blockScope.get((node as GetSingleVarNode).name); 321 | if (value === undefined) { 322 | if (name.charAt(name.length - 1) === ';') { 323 | throw new Error(`Unexpected ';' in the end.`); 324 | } else { 325 | throw new Error(`Variable '${name}' is not defined.`); 326 | } 327 | } 328 | return value; 329 | } 330 | 331 | if (node.type === 'binOp') { 332 | const binOpNode = node as BinOpNode; 333 | const left = this.evalNode(binOpNode.left, blockContext); 334 | const right = this.evalNode(binOpNode.right, blockContext); 335 | const func = OperationFuncs.get(binOpNode.op); 336 | if (typeof func === 'function') return func(left as Primitive, right as Primitive); 337 | else throw new Error('Unknown binary oprastion'); 338 | } 339 | 340 | if (node.type === 'logicalOp') { 341 | const logicalGroups = node as LogicalOpNode; 342 | let ind = 0; 343 | let gResult: unknown = true; 344 | 345 | while (ind < logicalGroups.items.length) { 346 | const eg = logicalGroups.items[ind++]; 347 | 348 | gResult = this.evalNode(eg.node, blockContext); 349 | 350 | if (eg.op === 'and' && !gResult) { 351 | return false; 352 | } 353 | if (eg.op === 'or' && gResult) { 354 | return gResult; 355 | } 356 | } 357 | 358 | return gResult; 359 | } 360 | 361 | if (node.type === 'arrowFuncDef') { 362 | const arrowFuncDef = node as ArrowFuncDefNode; 363 | 364 | return (...args: unknown[]): unknown => 365 | this.jspyFuncInvoker(arrowFuncDef, blockContext, ...args); 366 | } 367 | 368 | if (node.type === 'funcCall') { 369 | const funcCallNode = node as FunctionCallNode; 370 | const func = blockContext.blockScope.get(funcCallNode.name) as ( 371 | ...args: unknown[] 372 | ) => unknown; 373 | if (typeof func !== 'function') { 374 | throw Error(`'${funcCallNode.name}' is not a function or not defined.`); 375 | } 376 | 377 | const pms = funcCallNode.paramNodes?.map(n => this.evalNode(n, blockContext)) || []; 378 | 379 | return this.invokeFunction(func, pms, { 380 | moduleName: blockContext.moduleName, 381 | line: funcCallNode.loc[0], 382 | column: funcCallNode.loc[1] 383 | }); 384 | } 385 | 386 | if (node.type === 'assign') { 387 | const assignNode = node as AssignNode; 388 | 389 | if (assignNode.target.type === 'getSingleVar') { 390 | const node = assignNode.target as SetSingleVarNode; 391 | blockContext.blockScope.set(node.name, this.evalNode(assignNode.source, blockContext)); 392 | } else if (assignNode.target.type === 'chainingCalls') { 393 | const targetNode = assignNode.target as ChainingCallsNode; 394 | 395 | // create a node for all but last property token 396 | // potentially it can go to parser 397 | const targetObjectNode = new ChainingCallsNode( 398 | targetNode.innerNodes.slice(0, targetNode.innerNodes.length - 1), 399 | targetNode.loc 400 | ); 401 | const targetObject = this.evalNode(targetObjectNode, blockContext) as Record< 402 | string, 403 | unknown 404 | >; 405 | 406 | const lastInnerNode = targetNode.innerNodes[targetNode.innerNodes.length - 1]; 407 | 408 | let lastPropertyName = ''; 409 | if (lastInnerNode.type === 'getSingleVar') { 410 | lastPropertyName = (lastInnerNode as GetSingleVarNode).name; 411 | } else if (lastInnerNode.type === 'chainingObjectAccess') { 412 | lastPropertyName = this.evalNode( 413 | (lastInnerNode as ChainingObjectAccessNode).indexerBody, 414 | blockContext 415 | ) as string; 416 | } else { 417 | throw Error('Not implemented Assign operation with chaining calls'); 418 | } 419 | 420 | targetObject[lastPropertyName] = this.evalNode(assignNode.source, blockContext); 421 | } 422 | 423 | return null; 424 | } 425 | 426 | if (node.type === 'chainingCalls') { 427 | return this.resolveChainingCallsNode(node as ChainingCallsNode, blockContext); 428 | } 429 | 430 | if (node.type === 'createObject') { 431 | const createObjectNode = node as CreateObjectNode; 432 | const obj = {} as Record; 433 | 434 | for (const p of createObjectNode.props) { 435 | obj[this.evalNode(p.name, blockContext) as string] = this.evalNode(p.value, blockContext); 436 | } 437 | 438 | return obj; 439 | } 440 | 441 | if (node.type === 'createArray') { 442 | const arrayNode = node as CreateArrayNode; 443 | const res = [] as unknown[]; 444 | 445 | for (const item of arrayNode.items) { 446 | res.push(this.evalNode(item, blockContext)); 447 | } 448 | 449 | return res; 450 | } 451 | } 452 | 453 | private resolveChainingCallsNode(chNode: ChainingCallsNode, blockContext: BlockContext): unknown { 454 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 455 | let startObject = this.evalNode(chNode.innerNodes[0], blockContext) as any; 456 | 457 | for (let i = 1; i < chNode.innerNodes.length; i++) { 458 | const nestedProp = chNode.innerNodes[i]; 459 | 460 | if ((chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing && !startObject) { 461 | startObject = {}; 462 | } 463 | 464 | if (nestedProp.type === 'getSingleVar') { 465 | startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown; 466 | } else if (nestedProp.type === 'chainingObjectAccess') { 467 | const node = nestedProp as ChainingObjectAccessNode; 468 | // startObject = startObject[node.] as unknown; 469 | startObject = startObject[ 470 | this.evalNode(node.indexerBody, blockContext) as string 471 | ] as unknown; 472 | } else if (nestedProp.type === 'funcCall') { 473 | const funcCallNode = nestedProp as FunctionCallNode; 474 | const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown; 475 | 476 | if ( 477 | (func === undefined || func === null) && 478 | (chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing 479 | ) { 480 | startObject = null; 481 | continue; 482 | } 483 | 484 | if (typeof func !== 'function') { 485 | throw Error(`'${funcCallNode.name}' is not a function or not defined.`); 486 | } 487 | const pms = []; 488 | for (const p of funcCallNode.paramNodes || []) { 489 | pms.push(this.evalNode(p, blockContext)); 490 | } 491 | 492 | startObject = this.invokeFunction(func.bind(startObject), pms, { 493 | moduleName: blockContext.moduleName, 494 | line: funcCallNode.loc[0], 495 | column: funcCallNode.loc[0] 496 | }); 497 | } else { 498 | throw Error("Can't resolve chainingCalls node"); 499 | } 500 | } 501 | 502 | return startObject === undefined ? null : startObject; 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/evaluator/evaluatorAsync.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowFuncDefNode, 3 | AssignNode, 4 | AstBlock, 5 | AstNode, 6 | BinOpNode, 7 | ChainingCallsNode, 8 | ChainingObjectAccessNode, 9 | ConstNode, 10 | CreateArrayNode, 11 | CreateObjectNode, 12 | ForNode, 13 | FuncDefNode, 14 | FunctionCallNode, 15 | FunctionDefNode, 16 | GetSingleVarNode, 17 | IfNode, 18 | ImportNode, 19 | IsNullCoelsing, 20 | LogicalOpNode, 21 | OperationFuncs, 22 | Primitive, 23 | RaiseNode, 24 | ReturnNode, 25 | SetSingleVarNode, 26 | TryExceptNode, 27 | WhileNode 28 | } from '../common'; 29 | import { JspyEvalError, JspyError, getImportType } from '../common/utils'; 30 | import { Evaluator } from './evaluator'; 31 | import { BlockContext, cloneContext } from './scope'; 32 | 33 | /** 34 | * This is copy/paste from Evaluator. 35 | * Sadly, we have to copy code around to support both async and non async methods. 36 | * So, any changes to this method, should be replicated in the evaluator.ts 37 | */ 38 | export class EvaluatorAsync { 39 | private moduleParser: (modulePath: string) => Promise = () => 40 | Promise.reject('Module parser is not registered!'); 41 | private jsonFileLoader: (jsonFilePath: string) => Promise = () => Promise.reject('{}'); 42 | private blockContextFactory?: (modulePath: string, ast: AstBlock) => BlockContext; 43 | 44 | registerModuleParser(moduleParser: (modulePath: string) => Promise): EvaluatorAsync { 45 | this.moduleParser = moduleParser; 46 | return this; 47 | } 48 | 49 | registerJsonFileLoader(jsonFileLoader: (modulePath: string) => Promise): EvaluatorAsync { 50 | this.jsonFileLoader = jsonFileLoader; 51 | return this; 52 | } 53 | 54 | registerBlockContextFactory( 55 | blockContextFactory: (modulePath: string, ast: AstBlock) => BlockContext 56 | ): EvaluatorAsync { 57 | this.blockContextFactory = blockContextFactory; 58 | return this; 59 | } 60 | 61 | async evalBlockAsync(ast: AstBlock, blockContext: BlockContext): Promise { 62 | let lastResult = null; 63 | 64 | for (const node of ast?.funcs || []) { 65 | const funcDef = node as FunctionDefNode; 66 | 67 | // a child scope needs to be created here 68 | const newScope = blockContext.blockScope; 69 | 70 | const invoker = funcDef.isAsync 71 | ? async (...args: unknown[]): Promise => 72 | await this.jspyFuncInvokerAsync(funcDef, blockContext, ...args) 73 | : (...args: unknown[]): unknown => 74 | new Evaluator().jspyFuncInvoker(funcDef, blockContext, ...args); 75 | 76 | newScope.set(funcDef.funcAst.name, invoker); 77 | } 78 | 79 | for (let i = 0; i < ast.body.length; i++) { 80 | const node = ast.body[i]; 81 | if (blockContext.cancellationToken.cancel) { 82 | const loc = node.loc || []; 83 | 84 | if (!blockContext.cancellationToken.message) { 85 | blockContext.cancellationToken.message = `Cancelled. ${blockContext.moduleName}: ${loc[0]}, ${loc[1]}`; 86 | } 87 | 88 | return blockContext.cancellationToken.message; 89 | } 90 | 91 | if (node.type === 'comment') { 92 | continue; 93 | } 94 | if (node.type === 'import') { 95 | const importNode = node as ImportNode; 96 | const iType = getImportType(importNode.module.name); 97 | 98 | if (iType === 'json') { 99 | const jsonValue = JSON.parse(await this.jsonFileLoader(importNode.module.name)); 100 | blockContext.blockScope.set( 101 | importNode.module.alias || this.defaultModuleName(importNode.module.name), 102 | jsonValue 103 | ); 104 | continue; 105 | } else if (iType !== 'jspyModule') { 106 | // it is not JSPY import. It is JS and should be handled externally 107 | continue; 108 | } 109 | 110 | if (typeof this.blockContextFactory !== 'function') { 111 | throw new Error('blockContextFactory is not initialized'); 112 | } 113 | 114 | const moduleAst = await this.moduleParser(importNode.module.name); 115 | const moduleBlockContext = this.blockContextFactory(importNode.module.name, moduleAst); 116 | await this.evalBlockAsync(moduleAst, moduleBlockContext); 117 | 118 | let scope = blockContext.blockScope.getScope(); 119 | 120 | if (!importNode.parts?.length) { 121 | // if no parts, then we need to assign to a separate object 122 | scope = {}; 123 | blockContext.blockScope.set( 124 | importNode.module.alias || this.defaultModuleName(importNode.module.name), 125 | scope 126 | ); 127 | } 128 | 129 | this.assignFunctionsToScope( 130 | scope, 131 | moduleBlockContext, 132 | moduleAst, 133 | importNode.parts?.map(p => p.name) 134 | ); 135 | continue; 136 | } 137 | 138 | try { 139 | lastResult = await this.evalNodeAsync(node, blockContext); 140 | if (blockContext.returnCalled) { 141 | const res = blockContext.returnObject; 142 | // stop processing return 143 | if (ast.type == 'func' || ast.type == 'module') { 144 | blockContext.returnCalled = false; 145 | blockContext.returnObject = null; 146 | } 147 | return res; 148 | } 149 | 150 | if (blockContext.continueCalled) { 151 | break; 152 | } 153 | if (blockContext.breakCalled) { 154 | break; 155 | } 156 | } catch (err) { 157 | const loc = node.loc ? node.loc : [0, 0]; 158 | if (err instanceof JspyError) { 159 | throw err; 160 | } else if (err instanceof JspyEvalError) { 161 | throw err; 162 | } else { 163 | throw new JspyEvalError( 164 | blockContext.moduleName, 165 | loc[0], 166 | loc[1], 167 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 168 | (err as any).message || err 169 | ); 170 | } 171 | } 172 | } 173 | 174 | return lastResult; 175 | } 176 | 177 | private assignFunctionsToScope( 178 | scope: Record, 179 | moduleBlockContext: BlockContext, 180 | moduleAst: AstBlock, 181 | parts?: string[] 182 | ): void { 183 | const funcs = moduleAst.funcs.filter(f => !parts || parts.indexOf(f.funcAst?.name) >= 0); 184 | 185 | for (let i = 0; i < funcs.length; i++) { 186 | const funcDef = funcs[i] as FunctionDefNode; 187 | 188 | const invoker = funcDef.isAsync 189 | ? async (...args: unknown[]): Promise => 190 | await this.jspyFuncInvokerAsync(funcDef, moduleBlockContext, ...args) 191 | : (...args: unknown[]): unknown => 192 | new Evaluator().jspyFuncInvoker(funcDef, moduleBlockContext, ...args); 193 | 194 | scope[funcDef.funcAst.name] = invoker; 195 | } 196 | } 197 | 198 | private defaultModuleName(name: string): string { 199 | return name.substring(name.lastIndexOf('/') + 1, name.lastIndexOf('.')); 200 | } 201 | 202 | private async jspyFuncInvokerAsync( 203 | funcDef: FuncDefNode, 204 | context: BlockContext, 205 | ...args: unknown[] 206 | ): Promise { 207 | const ast = Object.assign({}, funcDef.funcAst); 208 | ast.type = 'func'; 209 | 210 | const blockContext = cloneContext(context); 211 | 212 | // set parameters into new scope, based incomming arguments 213 | for (let i = 0; i < funcDef.params?.length || 0; i++) { 214 | const argValue = args?.length > i ? args[i] : null; 215 | blockContext.blockScope.set(funcDef.params[i], argValue); 216 | } 217 | 218 | return await this.evalBlockAsync(ast, blockContext); 219 | } 220 | 221 | private async invokeFunctionAsync( 222 | func: (...args: unknown[]) => unknown, 223 | fps: unknown[], 224 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 225 | loc?: { moduleName: string; line: number; column: number } 226 | ): Promise { 227 | return await func(...fps); 228 | } 229 | 230 | private async evalNodeAsync(node: AstNode, blockContext: BlockContext): Promise { 231 | if (node.type === 'import') { 232 | throw new Error('Import should be defined at the start'); 233 | } 234 | 235 | if (node.type === 'comment') { 236 | return null; 237 | } 238 | 239 | if (node.type === 'if') { 240 | const ifNode = node as IfNode; 241 | let doElse = true; 242 | 243 | if (await this.evalNodeAsync(ifNode.conditionNode, blockContext)) { 244 | await this.evalBlockAsync( 245 | { name: blockContext.moduleName, type: 'if', body: ifNode.ifBody } as AstBlock, 246 | blockContext 247 | ); 248 | doElse = false; 249 | } else if (ifNode.elifs?.length) { 250 | for (let i = 0; i < ifNode.elifs.length; i++) { 251 | const elIfNode = ifNode.elifs[i]; 252 | 253 | if (await this.evalNodeAsync(elIfNode.conditionNode, blockContext)) { 254 | await this.evalBlockAsync( 255 | { name: blockContext.moduleName, type: 'if', body: elIfNode.elifBody } as AstBlock, 256 | blockContext 257 | ); 258 | doElse = false; 259 | break; 260 | } 261 | } 262 | } 263 | 264 | if (doElse && ifNode.elseBody) { 265 | await this.evalBlockAsync( 266 | { name: blockContext.moduleName, type: 'if', body: ifNode.elseBody } as AstBlock, 267 | blockContext 268 | ); 269 | } 270 | 271 | return; 272 | } 273 | 274 | if (node.type === 'raise') { 275 | const raiseNode = node as RaiseNode; 276 | const errorMessage = (await this.evalNodeAsync( 277 | raiseNode.errorMessageAst, 278 | blockContext 279 | )) as string; 280 | const err = new JspyError( 281 | blockContext.moduleName, 282 | raiseNode.loc[0], 283 | raiseNode.loc[1], 284 | raiseNode.errorName, 285 | errorMessage 286 | ); 287 | throw err; 288 | } 289 | 290 | if (node.type === 'tryExcept') { 291 | const tryNode = node as TryExceptNode; 292 | try { 293 | await this.evalBlockAsync( 294 | { name: blockContext.moduleName, type: 'trycatch', body: tryNode.tryBody } as AstBlock, 295 | blockContext 296 | ); 297 | 298 | if (tryNode.elseBody?.length || 0 > 0) { 299 | await this.evalBlockAsync( 300 | { name: blockContext.moduleName, type: 'trycatch', body: tryNode.elseBody } as AstBlock, 301 | blockContext 302 | ); 303 | } 304 | } catch (err) { 305 | // catches here all exceptions. Including JSPY Eval errors 306 | const name = err instanceof JspyError ? (err as JspyError).name : typeof err; 307 | const message = 308 | err instanceof JspyError 309 | ? (err as JspyError).message 310 | : // eslint-disable-next-line @typescript-eslint/no-explicit-any 311 | (err as any)?.message ?? String(err); 312 | const moduleName = err instanceof JspyError ? (err as JspyError).module : 0; 313 | const line = err instanceof JspyError ? (err as JspyError).line : 0; 314 | const column = err instanceof JspyError ? (err as JspyError).column : 0; 315 | 316 | const firstExept = tryNode.exepts[0]; 317 | const catchBody = firstExept.body; 318 | const ctx = blockContext; 319 | ctx.blockScope.set(firstExept.error?.alias || 'error', { 320 | name, 321 | message, 322 | line, 323 | column, 324 | moduleName 325 | }); 326 | await this.evalBlockAsync( 327 | { name: blockContext.moduleName, type: 'trycatch', body: catchBody } as AstBlock, 328 | ctx 329 | ); 330 | ctx.blockScope.set(firstExept.error?.alias || 'error', null); 331 | } finally { 332 | if (tryNode.finallyBody?.length || 0 > 0) { 333 | await this.evalBlockAsync( 334 | { 335 | name: blockContext.moduleName, 336 | type: 'trycatch', 337 | body: tryNode.finallyBody 338 | } as AstBlock, 339 | blockContext 340 | ); 341 | } 342 | } 343 | 344 | return; 345 | } 346 | 347 | if (node.type === 'return') { 348 | const returnNode = node as ReturnNode; 349 | blockContext.returnCalled = true; 350 | blockContext.returnObject = returnNode.returnValue 351 | ? await this.evalNodeAsync(returnNode.returnValue, blockContext) 352 | : null; 353 | 354 | return blockContext.returnObject; 355 | } 356 | 357 | if (node.type === 'continue') { 358 | blockContext.continueCalled = true; 359 | return; 360 | } 361 | 362 | if (node.type === 'break') { 363 | blockContext.breakCalled = true; 364 | return; 365 | } 366 | 367 | if (node.type === 'for') { 368 | const forNode = node as ForNode; 369 | 370 | const array = (await this.evalNodeAsync(forNode.sourceArray, blockContext)) as 371 | | unknown[] 372 | | string; 373 | for (let i = 0; i < array.length; i++) { 374 | const item = array[i]; 375 | blockContext.blockScope.set(forNode.itemVarName, item); 376 | await this.evalBlockAsync( 377 | { name: blockContext.moduleName, type: 'for', body: forNode.body } as AstBlock, 378 | blockContext 379 | ); 380 | if (blockContext.continueCalled) { 381 | blockContext.continueCalled = false; 382 | } 383 | if (blockContext.breakCalled) { 384 | break; 385 | } 386 | } 387 | 388 | if (blockContext.breakCalled) { 389 | blockContext.breakCalled = false; 390 | } 391 | return; 392 | } 393 | 394 | if (node.type === 'while') { 395 | const whileNode = node as WhileNode; 396 | 397 | while (await this.evalNodeAsync(whileNode.condition, blockContext)) { 398 | await this.evalBlockAsync( 399 | { name: blockContext.moduleName, type: 'while', body: whileNode.body } as AstBlock, 400 | blockContext 401 | ); 402 | 403 | if (blockContext.continueCalled) { 404 | blockContext.continueCalled = false; 405 | } 406 | if (blockContext.breakCalled) { 407 | break; 408 | } 409 | } 410 | if (blockContext.breakCalled) { 411 | blockContext.breakCalled = false; 412 | } 413 | 414 | return; 415 | } 416 | 417 | if (node.type === 'const') { 418 | return (node as ConstNode).value; 419 | } 420 | 421 | if (node.type === 'getSingleVar') { 422 | const name = (node as GetSingleVarNode).name; 423 | const value = blockContext.blockScope.get(name); 424 | 425 | if (value === undefined) { 426 | if (name.charAt(name.length - 1) === ';') { 427 | throw new Error(`Unexpected ';' in the end.`); 428 | } else { 429 | throw new Error(`Variable '${name}' is not defined.`); 430 | } 431 | } 432 | return value; 433 | } 434 | 435 | if (node.type === 'binOp') { 436 | const binOpNode = node as BinOpNode; 437 | const left = await this.evalNodeAsync(binOpNode.left, blockContext); 438 | const right = await this.evalNodeAsync(binOpNode.right, blockContext); 439 | 440 | const func = OperationFuncs.get(binOpNode.op); 441 | if (typeof func === 'function') return func(left as Primitive, right as Primitive); 442 | else throw new Error('Unknown binary oprastion'); 443 | } 444 | 445 | if (node.type === 'logicalOp') { 446 | const logicalGroups = node as LogicalOpNode; 447 | let ind = 0; 448 | let gResult: unknown = true; 449 | 450 | while (ind < logicalGroups.items.length) { 451 | const eg = logicalGroups.items[ind++]; 452 | 453 | gResult = await this.evalNodeAsync(eg.node, blockContext); 454 | 455 | if (eg.op === 'and' && !gResult) { 456 | return false; 457 | } 458 | if (eg.op === 'or' && gResult) { 459 | return gResult; 460 | } 461 | } 462 | 463 | return gResult; 464 | } 465 | 466 | if (node.type === 'arrowFuncDef') { 467 | const arrowFuncDef = node as ArrowFuncDefNode; 468 | 469 | return (...args: unknown[]): unknown => 470 | new Evaluator().jspyFuncInvoker(arrowFuncDef, blockContext, ...args); 471 | } 472 | 473 | if (node.type === 'funcCall') { 474 | const funcCallNode = node as FunctionCallNode; 475 | const func = blockContext.blockScope.get(funcCallNode.name) as ( 476 | ...args: unknown[] 477 | ) => unknown; 478 | 479 | if (typeof func !== 'function') { 480 | throw Error(`'${funcCallNode.name}' is not a function or not defined.`); 481 | } 482 | 483 | const pms = []; 484 | for (const p of funcCallNode.paramNodes || []) { 485 | pms.push(await this.evalNodeAsync(p, blockContext)); 486 | } 487 | 488 | return await this.invokeFunctionAsync(func, pms, { 489 | moduleName: blockContext.moduleName, 490 | line: funcCallNode.loc[0], 491 | column: funcCallNode.loc[0] 492 | }); 493 | } 494 | 495 | if (node.type === 'assign') { 496 | const assignNode = node as AssignNode; 497 | 498 | if (assignNode.target.type === 'getSingleVar') { 499 | const node = assignNode.target as SetSingleVarNode; 500 | blockContext.blockScope.set( 501 | node.name, 502 | await this.evalNodeAsync(assignNode.source, blockContext) 503 | ); 504 | } else if (assignNode.target.type === 'chainingCalls') { 505 | const targetNode = assignNode.target as ChainingCallsNode; 506 | 507 | // create a node for all but last property token 508 | // potentially it can go to parser 509 | const targetObjectNode = new ChainingCallsNode( 510 | targetNode.innerNodes.slice(0, targetNode.innerNodes.length - 1), 511 | targetNode.loc 512 | ); 513 | const targetObject = (await this.evalNodeAsync(targetObjectNode, blockContext)) as Record< 514 | string, 515 | unknown 516 | >; 517 | 518 | const lastInnerNode = targetNode.innerNodes[targetNode.innerNodes.length - 1]; 519 | 520 | let lastPropertyName = ''; 521 | if (lastInnerNode.type === 'getSingleVar') { 522 | lastPropertyName = (lastInnerNode as GetSingleVarNode).name; 523 | } else if (lastInnerNode.type === 'chainingObjectAccess') { 524 | lastPropertyName = (await this.evalNodeAsync( 525 | (lastInnerNode as ChainingObjectAccessNode).indexerBody, 526 | blockContext 527 | )) as string; 528 | } else { 529 | throw Error('Not implemented Assign operation with chaining calls'); 530 | } 531 | 532 | targetObject[lastPropertyName] = await this.evalNodeAsync(assignNode.source, blockContext); 533 | } 534 | 535 | return null; 536 | } 537 | 538 | if (node.type === 'chainingCalls') { 539 | return await this.resolveChainingCallsNode(node as ChainingCallsNode, blockContext); 540 | } 541 | 542 | if (node.type === 'createObject') { 543 | const createObjectNode = node as CreateObjectNode; 544 | const obj = {} as Record; 545 | 546 | for (const p of createObjectNode.props) { 547 | obj[(await this.evalNodeAsync(p.name, blockContext)) as string] = await this.evalNodeAsync( 548 | p.value, 549 | blockContext 550 | ); 551 | } 552 | 553 | return obj; 554 | } 555 | 556 | if (node.type === 'createArray') { 557 | const arrayNode = node as CreateArrayNode; 558 | const res = [] as unknown[]; 559 | 560 | for (const item of arrayNode.items) { 561 | res.push(await this.evalNodeAsync(item, blockContext)); 562 | } 563 | 564 | return res; 565 | } 566 | } 567 | 568 | private async resolveChainingCallsNode( 569 | chNode: ChainingCallsNode, 570 | blockContext: BlockContext 571 | ): Promise { 572 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 573 | let startObject = (await this.evalNodeAsync(chNode.innerNodes[0], blockContext)) as any; 574 | 575 | for (let i = 1; i < chNode.innerNodes.length; i++) { 576 | const nestedProp = chNode.innerNodes[i]; 577 | 578 | if ((chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing && !startObject) { 579 | startObject = {}; 580 | } 581 | 582 | if (nestedProp.type === 'getSingleVar') { 583 | startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown; 584 | } else if (nestedProp.type === 'chainingObjectAccess') { 585 | const node = nestedProp as ChainingObjectAccessNode; 586 | // startObject = startObject[node.] as unknown; 587 | startObject = startObject[ 588 | (await this.evalNodeAsync(node.indexerBody, blockContext)) as string 589 | ] as unknown; 590 | } else if (nestedProp.type === 'funcCall') { 591 | const funcCallNode = nestedProp as FunctionCallNode; 592 | const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown; 593 | 594 | if ( 595 | (func === undefined || func === null) && 596 | (chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing 597 | ) { 598 | startObject = null; 599 | continue; 600 | } 601 | 602 | if (typeof func !== 'function') { 603 | throw Error(`'${funcCallNode.name}' is not a function or not defined.`); 604 | } 605 | const pms = []; 606 | for (const p of funcCallNode.paramNodes || []) { 607 | pms.push(await this.evalNodeAsync(p, blockContext)); 608 | } 609 | 610 | startObject = await this.invokeFunctionAsync(func.bind(startObject), pms, { 611 | moduleName: blockContext.moduleName, 612 | line: funcCallNode.loc[0], 613 | column: funcCallNode.loc[0] 614 | }); 615 | } else { 616 | throw Error("Can't resolve chainingCalls node"); 617 | } 618 | } 619 | 620 | return startObject === undefined ? null : startObject; 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /src/evaluator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './evaluator'; 2 | -------------------------------------------------------------------------------- /src/evaluator/scope.ts: -------------------------------------------------------------------------------- 1 | export interface CancellationToken { 2 | cancel?: boolean; 3 | message?: string; 4 | } 5 | 6 | export interface BlockContext { 7 | moduleName: string; 8 | blockScope: Scope; 9 | cancellationToken: CancellationToken; 10 | returnCalled?: boolean; 11 | breakCalled?: boolean; 12 | continueCalled?: boolean; 13 | returnObject?: unknown; 14 | } 15 | 16 | export function cloneContext(context: BlockContext): BlockContext { 17 | return { 18 | moduleName: context.moduleName, 19 | blockScope: context.blockScope.clone(), 20 | // this instance should never change. Otherwise cancel won't work 21 | cancellationToken: context.cancellationToken 22 | } as BlockContext; 23 | } 24 | 25 | export class Scope { 26 | private readonly scope: Record = {}; 27 | 28 | constructor(initialScope: Record) { 29 | this.scope = { ...initialScope }; 30 | } 31 | 32 | getScope(): Record { 33 | return this.scope; 34 | } 35 | 36 | clone(): Scope { 37 | return new Scope(this.scope); 38 | } 39 | set(key: string, value: unknown): void { 40 | this.scope[key] = value; 41 | } 42 | 43 | get(key: string): unknown { 44 | return this.scope[key]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/initialScope.ts: -------------------------------------------------------------------------------- 1 | import { parseDatetimeOrNull } from './common/utils'; 2 | 3 | export const INITIAL_SCOPE = { 4 | jsPython(): string { 5 | return `JSPython v2.1.10 (c) 2022 FalconSoft Ltd. All rights reserved.`; 6 | }, 7 | dateTime: (str: number | string | unknown = null): Date => 8 | parseDatetimeOrNull(str as string) || new Date(), 9 | range: range, 10 | print: (...args: unknown[]): unknown => { 11 | console.log(...args); 12 | return args.length > 0 ? args[0] : null; 13 | }, 14 | isNull: (v: unknown, defValue: unknown = null): boolean | unknown => 15 | defValue === null ? v === null : v || defValue, 16 | isDate: (d: unknown): boolean => d instanceof Date, 17 | isFunction: (v: unknown): boolean => typeof v === 'function', 18 | isString: (v: unknown): boolean => typeof v === 'string', 19 | deleteProperty: (obj: Record, propName: string): boolean => delete obj[propName], 20 | Math: Math, 21 | Object: Object, 22 | Array: Array, 23 | JSON: JSON, 24 | // eslint-disable-next-line @typescript-eslint/no-empty-function 25 | printExecutionContext: (): void => {}, // will be overriden at runtime 26 | // eslint-disable-next-line @typescript-eslint/no-empty-function 27 | getExecutionContext: (): Record => ({}) // will be overriden at runtime 28 | }; 29 | 30 | /** 31 | * This interface needs to be replaced 32 | */ 33 | export interface PackageToImport { 34 | name: string; 35 | properties?: { name: string; as?: string }[]; 36 | as?: string; 37 | } 38 | 39 | function range(start: number, stop = NaN, step = 1): number[] { 40 | const arr: number[] = []; 41 | const isStopNaN = isNaN(stop); 42 | stop = isStopNaN ? start : stop; 43 | start = isStopNaN ? 0 : start; 44 | let i = start; 45 | while (i < stop) { 46 | arr.push(i); 47 | i += step; 48 | } 49 | return arr; 50 | } 51 | -------------------------------------------------------------------------------- /src/interpreter.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Interpreter } from './interpreter'; 3 | 4 | describe('Interpreter', () => { 5 | let e: Interpreter; 6 | 7 | beforeEach(() => { 8 | e = Interpreter.create(); 9 | }); 10 | 11 | it('Test1', () => { 12 | expect(e).toBe(e); 13 | }); 14 | 15 | it('1+2', () => { 16 | expect(e.eval('1+2')).toBe(3); 17 | expect(e.eval('1+2+3')).toBe(6); 18 | }); 19 | 20 | it(`"hello" + " " + 'world'`, () => { 21 | expect(e.eval(`"hello" + " " + 'world'`)).toBe('hello world'); 22 | }); 23 | 24 | it(`"'hello'" + " " + '"world"'`, () => { 25 | expect(e.eval(`"'hello'" + " " + '"world"'`)).toBe(`'hello' "world"`); 26 | }); 27 | 28 | it('(1 + 2) * 3', () => { 29 | expect(e.eval('(1 + 2) * 3')).toBe(9); 30 | expect(e.eval('(1 + 2) * 3 + 5')).toBe(14); 31 | }); 32 | 33 | it('(1 + 2) * (3 + 5)', () => { 34 | expect(e.eval('(1 + 2) * (3 + 5)')).toBe(24); 35 | expect(e.eval('(1 + 2) * (3 + 4) -5')).toBe(16); 36 | 37 | expect(e.eval('2*(3+4)')).toBe(14); 38 | expect(e.eval('2*(3+4)+5')).toBe(19); 39 | }); 40 | 41 | it('1+2*3', () => { 42 | expect(e.eval('1 + 2 * 3')).toBe(1 + 2 * 3); 43 | expect(e.eval('1 + 2 * 3 + 4')).toBe(1 + 2 * 3 + 4); 44 | }); 45 | 46 | it('2 * 3 + 4 * 5', () => { 47 | expect(e.eval('(1 + 2) + (3 + 4) * (5*6)')).toBe(1 + 2 + (3 + 4) * (5 * 6)); 48 | expect(e.eval('(1 + 2) + (3 + 4) * 5')).toBe(1 + 2 + (3 + 4) * 5); 49 | expect(e.eval('2*3+4*5')).toBe(2 * 3 + 4 * 5); 50 | expect(e.eval('2*(3+4)+5*6')).toBe(2 * (3 + 4) + 5 * 6); 51 | }); 52 | 53 | it('2 * 3 + 4 * 5', () => { 54 | expect(e.eval('1+2*1/4')).toBe(1 + (2 * 1) / 4); 55 | expect(e.eval('1+2*1/2+3')).toBe(1 + (2 * 1) / 2 + 3); 56 | }); 57 | 58 | it('1*2/4 + 2*3/6', () => { 59 | expect(e.eval('1*2/4 + 2*3/6')).toBe((1 * 2) / 4 + (2 * 3) / 6); 60 | expect(e.eval('1*2/4 + 2*3/6 - 2.3')).toBe((1 * 2) / 4 + (2 * 3) / 6 - 2.3); 61 | expect(e.eval('7+1*2/4 + 2*3/6 - 2.3')).toBe(7 + (1 * 2) / 4 + (2 * 3) / 6 - 2.3); 62 | }); 63 | 64 | it('5 – (5 * (32 + 4))', () => { 65 | expect(e.eval('5 - (5 * (32 + 4))')).toBe(5 - 5 * (32 + 4)); 66 | expect(e.eval('12 * 5 - (5 * (32 + 4)) + 3')).toBe(12 * 5 - 5 * (32 + 4) + 3); 67 | }); 68 | 69 | it('o.sub1.subValue', () => { 70 | const obj = { o: { v1: 55, sub1: { subValue: 45 } } }; 71 | expect(e.eval('o.v1 + o.sub1.subValue', obj)).toBe(100); 72 | expect(e.eval("o.v1 + o.sub1['sub' + 'Value']", obj)).toBe(100); 73 | expect(e.eval("o.v1 + o['sub1'].subValue", obj)).toBe(100); 74 | }); 75 | 76 | it('assignment o.sub1.subValue', () => { 77 | const obj = { o: { v1: 55, sub1: { subValue: 45 } } }; 78 | expect(e.eval('o.sub1.subValue2 = 10\no.sub1.subValue2', obj)).toBe(10); 79 | }); 80 | 81 | it('func call', () => { 82 | const obj = { add: (x: number, y: number): number => x + y }; 83 | 84 | expect(e.eval('add(2, 3)', obj)).toBe(5); 85 | expect(e.eval('add(2+10, 3)', obj)).toBe(15); 86 | }); 87 | 88 | it(`empty or special param`, () => { 89 | expect(e.eval(`def f(p):\n p\nf('')`)).toBe(``); 90 | expect(e.eval(`def f(p):\n p\nf('"')`)).toBe(`"`); 91 | expect(e.eval(`def f(p):\n p\nf("'")`)).toBe(`'`); 92 | expect(e.eval(`def f(p):\n p\nf(",")`)).toBe(`,`); 93 | expect(e.eval(`def f(p):\n p\nf(" ")`)).toBe(` `); 94 | expect(e.eval(`def f(p):\n p\nf(")")`)).toBe(`)`); 95 | expect(e.eval(`def f(p):\n p\nf("}")`)).toBe(`}`); 96 | }); 97 | 98 | it('Object call', () => { 99 | const obj = { o: { add: (x: number, y: number): number => x + y } }; 100 | 101 | expect(e.eval('o.add(2, 3)', obj)).toBe(5); 102 | expect(e.eval('o.add(2 * 10, 3)', obj)).toBe(23); 103 | expect( 104 | e.eval( 105 | ` 106 | o.add( 107 | 2 * 10, 108 | 3 109 | )`, 110 | obj 111 | ) 112 | ).toBe(23); 113 | }); 114 | 115 | it('Object call2', () => { 116 | const obj = { 117 | o: { 118 | add: (x: number, y: number): number => x + y, 119 | getObject: (p: string): Record => { 120 | return { p }; 121 | } 122 | } 123 | }; 124 | 125 | expect(e.eval('o.getObject(5).p', obj)).toBe(5); 126 | expect(e.eval('x = o.getObject(5)\nx.p * x.p', obj)).toBe(25); 127 | }); 128 | 129 | it('json obj', () => { 130 | expect(e.eval("x = {m1: 1+2*3, m2: 'ee'}\nx.m1")).toBe(7); 131 | expect(e.eval("x = {'m1': 1+2*3}\nx.m1")).toBe(7); 132 | expect(e.eval("x = {['m'+1]: 1+2*3}\nx.m1")).toBe(7); 133 | }); 134 | 135 | it('json ignore last comma', () => { 136 | expect(JSON.stringify(e.eval('[{a:1,}, {a:2},]'))).toBe(JSON.stringify([{ a: 1 }, { a: 2 }])); 137 | }); 138 | 139 | [{ a: 1 }, { a: 2 }]; 140 | 141 | it('json with dynamic key', () => { 142 | expect(e.eval("p = 'prop'\nx = {[p + '_'+1]: 1+2*3}\nx.prop_1")).toBe(7); 143 | expect(e.eval("p = {x:'prop'}\nx = {[p.x + '_'+1]: 1+2*3}\nx.prop_1")).toBe(7); 144 | }); 145 | 146 | it('json single name prop', () => { 147 | expect(e.eval("pp = 't1'\nx = {pp}\nx.pp")).toBe('t1'); 148 | expect(e.eval('pp = 5\nx = {pp, x:10}\nx.pp + x.x')).toBe(15); 149 | }); 150 | 151 | it('json array', () => { 152 | expect(e.eval("x = [{m1: 1+2*3, m2: 'ee'}]\nx.length")).toBe(1); 153 | expect(e.eval('x = [1,2,3]\nx.length')).toBe(3); 154 | expect(e.eval('x = [1,2,3]\nx[1]')).toBe(2); 155 | expect(e.eval('x = [{f1:1, f2:12}, {f1:2, f2:22}, {f1:3, f2:32}]\nx[1].f2')).toBe(22); 156 | expect(e.eval('x = [{f1:1, f2:12}, {f1:2, f2:22}, {f1:3, f2:32}]\nx[1].f2 = 55\nx[1].f2')).toBe( 157 | 55 158 | ); 159 | }); 160 | 161 | it('array map', () => { 162 | const script = ` 163 | def map(n): 164 | { 165 | t1: n * 2, 166 | t2: n * 3 167 | } 168 | 169 | arr = [1,2,3] 170 | arr.map(map) 171 | `; 172 | const res = e.eval(script) as { t1: number; t2: number }[]; 173 | 174 | expect(res.length).toBe(3); 175 | expect(res.map(o => o.t1 + o.t2).join(',')).toBe('5,10,15'); 176 | }); 177 | 178 | it('array map 2', () => { 179 | const script = ` 180 | def map(n): 181 | { 182 | t1: n * 2, 183 | t2: n * 3 184 | } 185 | 186 | def map2(o): 187 | o.t1 + o.t2 188 | 189 | arr = [1,2,3] 190 | 191 | arr 192 | .map(map) 193 | .map(map2) 194 | .join(",") 195 | `; 196 | expect(e.eval(script)).toBe('5,10,15'); 197 | }); 198 | 199 | it('arrow functions', () => { 200 | const script = ` 201 | arr = [1,2,3] 202 | arr 203 | .map(n => 204 | n = n * 2 205 | { 206 | t1: n * 2, 207 | t2: n * 3 208 | } 209 | ) 210 | .map(r => r.t1 * 8) 211 | .join(',') 212 | `; 213 | expect(e.eval(script)).toBe('32,64,96'); 214 | }); 215 | 216 | it('arrow functions with filter', () => { 217 | const script = ` 218 | arr = [1,2,3] 219 | arr.map(n => 220 | n = n * 2 221 | { 222 | t1: n * 2, 223 | t2: n * 3 224 | } 225 | ) 226 | .filter(v => (v.t1 > 10) or (v.t2 > 10)) 227 | .map(r => r.t1 * r.t2) 228 | .join(',') 229 | `; 230 | expect(e.eval(script)).toBe('96,216'); 231 | }); 232 | 233 | it('print empty string', () => { 234 | const script = ` 235 | print( 236 | "" 237 | ) 238 | `; 239 | expect(e.eval(script, { print: (v: string) => v })).toBe(''); 240 | }); 241 | it('if condition', () => { 242 | const script = (p: number): string => ` 243 | x = 1 244 | if x == ${p}: 245 | x = 5 246 | else: 247 | x = 10 248 | x 249 | `; 250 | expect(e.eval(script(1))).toBe(5); 251 | expect(e.eval(script(2))).toBe(10); 252 | }); 253 | 254 | it('if condition', () => { 255 | const script = ` 256 | x = {o1: {ov: 55}} 257 | x.o1.ov1?.someProp or 32 258 | `; 259 | expect(e.eval(script)).toBe(32); 260 | expect(e.eval('x={}\nx?.p1?.ff')).toBe(null); 261 | }); 262 | 263 | it('simple for', () => { 264 | const script = ` 265 | sum = 0 266 | for item in [1,2,3]: 267 | sum = sum + item 268 | sum 269 | `; 270 | expect(e.eval(script)).toBe(6); 271 | }); 272 | 273 | it('simple while', () => { 274 | const script = ` 275 | sum = 0 276 | i = 0 277 | 278 | while i < 5: 279 | sum = sum + i 280 | i = i + 1 281 | 282 | sum 283 | `; 284 | expect(e.eval(script)).toBe(10); 285 | }); 286 | 287 | it('funcCall with null coelsing', () => { 288 | const script = ` 289 | def f(): 290 | null 291 | 292 | f()?.prop or 5 293 | `; 294 | expect(e.eval(script)).toBe(5); 295 | }); 296 | 297 | it('funcCall with params', () => { 298 | const script = ` 299 | def times(a, b): 300 | return a * b 301 | `; 302 | expect(e.eval(script, {}, ['times', 2, 3])).toBe(6); 303 | }); 304 | 305 | it('long comments issue', () => { 306 | const script = ` 307 | async def f2(): 308 | """ 309 | long comment 310 | """ 311 | 5 312 | 313 | f2() 314 | `; 315 | expect(e.eval(script)).toBe(5); 316 | }); 317 | 318 | it('chaining funcCall with null coelsing', () => { 319 | expect(e.eval('p={f: ()=>null}\np?.f()?.sdsd')).toBe(null); 320 | expect(e.eval('p={f: ()=>null}\np?.f()?.sdsd or 5')).toBe(5); 321 | }); 322 | 323 | it('comparison operations', () => { 324 | expect(e.eval('1+2*3==7')).toBe(true); 325 | expect(e.eval('1+2==2')).toBe(false); 326 | }); 327 | 328 | it('comparison operations', () => { 329 | expect(e.eval('1+2*3==7')).toBe(true); 330 | expect(e.eval('1+2==2')).toBe(false); 331 | }); 332 | 333 | // ** migration issue for now 334 | it('simple and operator', async () => { 335 | expect(await e.evaluate('2 == 2 and 3 == 3')).toBe(true); 336 | expect(await e.evaluate('(2 == 2) and (3 == 3) and (5 == 5)')).toBe(true); 337 | expect(await e.evaluate('(2 == 2) and (3 != 3) and (5 == 5)')).toBe(false); 338 | expect(await e.evaluate('(2 != 2) and (3 != 3) and (5 == 5)')).toBe(false); 339 | expect(await e.evaluate('(2 != 2) and (3 == 3) and (5 == 5)')).toBe(false); 340 | expect(await e.evaluate('(2 == 2) and (3 == 3) and (5 != 5)')).toBe(false); 341 | }); 342 | 343 | it('simple or operator', async () => { 344 | expect(await e.evaluate('2 == 2 or 3 == 3')).toBe(true); 345 | expect(await e.evaluate('2 == 2 or 3 == 3 or 5 == 5')).toBe(true); 346 | expect(await e.evaluate('2 != 2 or 3 != 3 or 5 != 5')).toBe(false); 347 | expect(await e.evaluate('2 == 2 or 3 != 3 or 5 != 5')).toBe(true); 348 | expect(await e.evaluate('2 == 2 or 3 == 3 and 5 != 5')).toBe(true); 349 | }); 350 | 351 | it('conditionals', async () => { 352 | expect(await e.evaluate('x = null\nx == null')).toBe(true); 353 | expect(await e.evaluate('x = null\nx?.p1?.p == null')).toBe(true); 354 | expect(await e.evaluate('x = null\nx != null and x.length >0')).toBe(false); 355 | expect(await e.evaluate('x = null\nx?.p1?.p != null and x.length >0')).toBe(false); 356 | }); 357 | 358 | it('arithmetic + comparison', async () => { 359 | expect(await e.evaluate('0.25 == 1/4')).toBe(true); 360 | expect(await e.evaluate('0.25 == 1/2')).toBe(false); 361 | 362 | expect(await e.evaluate('1+2*3 == 5 or 1 > 3')).toBe(false); 363 | expect(await e.evaluate('1+2*3 == 5 or 1 < 3')).toBe(true); 364 | 365 | expect(await e.evaluate('2 == 1/2 + 1/2 and 1/2 + 1/2 == 1')).toBe(false); 366 | expect(await e.evaluate('(2 == 1/2 + 1/2) and (1/2 + 1/2 == 1)')).toBe(false); 367 | expect(await e.evaluate('(2 == (1/2 + 1/2)) and ((1/2 + 1/2) == 1)')).toBe(false); 368 | }); 369 | 370 | it('Negative numbers', async () => { 371 | expect(await e.evaluate('x=-1\nx')).toBe(-1); 372 | expect(await e.evaluate('x=-3.14 + 3\nx')).toBe(-3.14 + 3); 373 | expect(await e.evaluate('-3.14 - 3')).toBe(-3.14 - 3); 374 | expect(await e.evaluate('x=5\nx*-1')).toBe(-5); 375 | expect( 376 | await e.evaluate(` 377 | def f(x): 378 | return x 379 | 380 | f(-5) 381 | `) 382 | ).toBe(-5); 383 | 384 | expect( 385 | await e.evaluate(` 386 | def f(x): 387 | return x 388 | 389 | f(-0.14) 390 | `) 391 | ).toBe(-0.14); 392 | 393 | expect(await e.evaluate('1/2*-1 == -0.5')).toBe(true); 394 | }); 395 | 396 | it('Recursive function - power', async () => { 397 | const script = ` 398 | def power(base, exponent): 399 | if exponent == 0: 400 | return 1 401 | else: 402 | return base * power(base, exponent - 1) 403 | 404 | "5 ** 10 == " + power(5, 10) + " == " + Math.pow(5, 10) 405 | `; 406 | expect(await e.evaluate(script)).toBe('5 ** 10 == 9765625 == 9765625'); 407 | }); 408 | 409 | it('try catch error', async () => { 410 | const script = ` 411 | x = [] 412 | try: 413 | raise Error('Error Message') 414 | x.push(1) 415 | except: 416 | x.push(2) 417 | finally: 418 | x.push(3) 419 | else: 420 | x.push(4) 421 | x 422 | `; 423 | const check = (result: number[]): void => { 424 | expect(result.length).toBe(2); 425 | expect(result[0]).toBe(2); 426 | expect(result[1]).toBe(3); 427 | }; 428 | 429 | check(e.eval(script) as number[]); 430 | check((await e.evaluate(script)) as any); 431 | }); 432 | 433 | it('try catch no error', async () => { 434 | const script = ` 435 | x = [] 436 | try: 437 | x.push(1) 438 | except: 439 | x.push(2) 440 | finally: 441 | x.push(3) 442 | else: 443 | x.push(4) 444 | x 445 | `; 446 | const check = (result: number[]): void => { 447 | expect(result.length).toBe(3); 448 | expect(result[0]).toBe(1); 449 | expect(result[1]).toBe(4); 450 | expect(result[2]).toBe(3); 451 | }; 452 | 453 | check((await e.evaluate(script)) as any); 454 | check(e.eval(script) as number[]); 455 | }); 456 | 457 | it('try catch errorMessage', async () => { 458 | const script = ` 459 | m = '' 460 | try: 461 | raise 'My Message' 462 | x.push(1) 463 | except: 464 | m = error.message 465 | m 466 | `; 467 | 468 | expect(await e.evaluate(script)).toContain('My Message'); 469 | expect(e.eval(script)).toContain('My Message'); 470 | }); 471 | 472 | it('try catch errorMessage with alias', async () => { 473 | const script = ` 474 | m = '' 475 | try: 476 | raise 'My Message' 477 | x.push(1) 478 | except Error err: 479 | m = err.message 480 | m 481 | `; 482 | 483 | expect(await e.evaluate(script)).toContain('My Message'); 484 | expect(e.eval(script)).toContain('My Message'); 485 | }); 486 | 487 | it('try catch errorMessage evaluated', async () => { 488 | const script = ` 489 | m = '' 490 | dd = ' ** ' 491 | try: 492 | raise dd + 'My Message' + dd 493 | x.push(1) 494 | except Error err: 495 | m = err.message 496 | m 497 | `; 498 | 499 | expect(await e.evaluate(script)).toContain(' ** My Message ** '); 500 | expect(e.eval(script)).toContain('My Message'); 501 | }); 502 | 503 | it('try catch errorMessage evaluated 2', async () => { 504 | const script = ` 505 | m = '' 506 | dd = ' ** ' 507 | try: 508 | raise dd 509 | x.push(1) 510 | except Error err: 511 | m = err.message 512 | m 513 | `; 514 | 515 | expect(await e.evaluate(script)).toContain(' ** '); 516 | expect(e.eval(script)).toContain(' ** '); 517 | }); 518 | 519 | it('try catch JS errorMessage', async () => { 520 | const script = ` 521 | m = '' 522 | try: 523 | func1() 524 | except: 525 | m = error.message 526 | m 527 | `; 528 | 529 | const jsErrorMessage = 'JS Error Message'; 530 | const obj = { 531 | func1: (): void => { 532 | throw new Error(jsErrorMessage); 533 | } 534 | }; 535 | expect(await e.evaluate(script, obj)).toContain(jsErrorMessage); 536 | expect(e.eval(script, obj)).toContain(jsErrorMessage); 537 | }); 538 | 539 | it('if with AND', async () => { 540 | const script = ` 541 | x=5 542 | y=10 543 | r = 0 544 | 545 | if x == 5 and y == 10: 546 | r = x + y 547 | 548 | return r 549 | `; 550 | 551 | expect(await e.evaluate(script)).toBe(15); 552 | expect(e.eval(script)).toBe(15); 553 | }); 554 | 555 | it('if with AND nullable objects', async () => { 556 | const script = ` 557 | l = null 558 | r = {price: 5} 559 | 560 | if l?.price != null and r?.price != null: 561 | return 100 562 | 563 | return 1 564 | `; 565 | expect(await e.evaluate(script)).toBe(1); 566 | expect(e.eval(script)).toBe(1); 567 | }); 568 | 569 | it('if with AND in brackets', async () => { 570 | const script = ` 571 | x=5 572 | y=10 573 | r = 0 574 | 575 | if (x == 5) and (y == 10): 576 | r = x + y 577 | 578 | return r 579 | `; 580 | expect(await e.evaluate(script)).toBe(15); 581 | expect(e.eval(script)).toBe(15); 582 | }); 583 | 584 | // incorrect 585 | it('passing value type to the arrow function', async () => { 586 | // const script = ` 587 | // x = [2,3,4,5,6,7,8,9] 588 | // sum = 0 589 | // x.forEach(i => sum = sum + i) 590 | // sum 591 | // `; 592 | }); 593 | 594 | it('passing value type to the for loop', async () => { 595 | const script = ` 596 | x = [2,3,4,5,6,7,8,9] 597 | sum = 0 598 | 599 | for i in x: 600 | sum = sum + i 601 | 602 | sum 603 | `; 604 | expect(await e.evaluate(script)).toBe(44); 605 | expect(e.eval(script)).toBe(44); 606 | }); 607 | 608 | it('passing value type to the while loop', async () => { 609 | const script = ` 610 | x = [2,3,4,5,6,7,8,9] 611 | sum = 0 612 | i=0 613 | while i < x.length: 614 | val = x[i] 615 | sum = sum + val 616 | i = i + 1 617 | 618 | sum 619 | `; 620 | expect(await e.evaluate(script)).toBe(44); 621 | expect(e.eval(script)).toBe(44); 622 | }); 623 | 624 | it('passing value type to the arrow function', async () => { 625 | const script = ` 626 | x = [2,3,4,5,6,7,8,9] 627 | 628 | sum = {value:0} 629 | x.forEach(i => sum.value = sum.value + i) 630 | 631 | sum.value 632 | `; 633 | expect(await e.evaluate(script)).toBe(44); 634 | expect(e.eval(script)).toBe(44); 635 | }); 636 | 637 | it('unknown property is null', async () => { 638 | const script = ` 639 | x = {} 640 | 641 | if x.someValue == null: 642 | return true 643 | else: 644 | return false 645 | `; 646 | expect(await e.evaluate(script)).toBe(true); 647 | expect(e.eval(script)).toBe(true); 648 | }); 649 | 650 | it('boolean value', async () => { 651 | const script = ` 652 | x = 2 == 2 653 | 654 | if x: 655 | return true 656 | else: 657 | return false 658 | `; 659 | expect(await e.evaluate(script)).toBe(true); 660 | expect(e.eval(script)).toBe(true); 661 | }); 662 | 663 | it('null coelsing functions', async () => { 664 | const script = ` 665 | o = {} 666 | 667 | if o?.nonExistentFunctions(23, 43) == null: 668 | return 10 669 | 670 | return 5 671 | `; 672 | expect(await e.evaluate(script)).toBe(10); 673 | expect(e.eval(script)).toBe(10); 674 | }); 675 | 676 | it('return empty', async () => { 677 | const script = ` 678 | if 1 == 1: 679 | return 680 | 681 | return 5 682 | `; 683 | expect(await e.evaluate(script)).toBe(null); 684 | expect(e.eval(script)).toBe(null); 685 | }); 686 | 687 | it('Import', async () => { 688 | const interpreter = Interpreter.create(); 689 | 690 | interpreter.registerModuleLoader(() => { 691 | return Promise.resolve(` 692 | def multiply(x, y): 693 | x * y 694 | 695 | def func1(x, y): 696 | multiply(x, y) + someNumber 697 | 698 | someNumber = 55 699 | `); 700 | }); 701 | 702 | const res = await interpreter.evaluate(` 703 | import '/service.jspy' as obj 704 | 705 | return obj.func1(2, 3) + obj.multiply(2, 3) 706 | `); 707 | 708 | expect(res).toBe(67); 709 | }); 710 | 711 | it('Import and calling function with default value', async () => { 712 | const interpreter = Interpreter.create(); 713 | 714 | interpreter.registerModuleLoader(() => { 715 | return Promise.resolve(` 716 | def multiply(x, y): 717 | x * y 718 | 719 | def func1(x, y): 720 | # if y is null then 100 will be passed 721 | multiply(x, y or 100) + someNumber 722 | 723 | someNumber = 55 724 | `); 725 | }); 726 | 727 | const res = await interpreter.evaluate(` 728 | import './service.jspy' as obj 729 | 730 | return obj.func1(2) + obj.multiply(2, 3) 731 | `); 732 | 733 | expect(res).toBe(261); 734 | }); 735 | 736 | it('Import JSON', async () => { 737 | const interpreter = Interpreter.create(); 738 | 739 | interpreter.registerModuleLoader(() => { 740 | return Promise.resolve(` 741 | {"x": "test1", "n": 22} 742 | `); 743 | }); 744 | 745 | const res = (await interpreter.evaluate(` 746 | import './some.json' as obj 747 | 748 | return obj 749 | `)) as any; 750 | 751 | expect(res.x).toBe('test1'); 752 | expect(res.n).toBe(22); 753 | }); 754 | 755 | it('Import with package loader', async () => { 756 | const interpreter = Interpreter.create(); 757 | 758 | interpreter.registerPackagesLoader( 759 | path => 760 | (path === 'service' 761 | ? { 762 | add: (x: number, y: number): number => x + y, 763 | remove: (x: number, y: number): number => x - y, 764 | times: (x: number, y: number): number => x * y 765 | } 766 | : null) as any 767 | ); 768 | 769 | interpreter.registerModuleLoader(() => { 770 | return Promise.resolve(` 771 | from 'service' import add 772 | 773 | def multiply(x, y): 774 | x * y 775 | 776 | def func1(x, y): 777 | add(x, y) + someNumber 778 | 779 | someNumber = 55 780 | `); 781 | }); 782 | 783 | let res = await interpreter.evaluate(` 784 | import '/service.jspy' as obj 785 | 786 | return obj.func1(2, 3) 787 | `); 788 | 789 | expect(res).toBe(60); 790 | 791 | res = await interpreter.evaluate(` 792 | from '/service.jspy' import func1 793 | 794 | return func1(2, 3) 795 | `); 796 | 797 | expect(res).toBe(60); 798 | }); 799 | 800 | it('semicolon as a string', async () => { 801 | const interpreter = Interpreter.create(); 802 | const res = (await interpreter.evaluate(`"first;second".split(';')`)) as any; 803 | expect(res.length).toBe(2); 804 | }); 805 | 806 | it('literal strings as keyword confusion', async () => { 807 | const interpreter = Interpreter.create(); 808 | 809 | const script = ` 810 | s = '(' 811 | if s == '(': 812 | return 55 813 | 814 | return 99 815 | `; 816 | expect(await interpreter.evalAsync(script)).toBe(55); 817 | expect(interpreter.eval(script)).toBe(55); 818 | }); 819 | 820 | it('literal strings as keyword confusion 2', async () => { 821 | const interpreter = Interpreter.create(); 822 | 823 | const script = ` 824 | s = '()' 825 | if s[0] == '(' and s[s.length - 1] == ')': 826 | return 55 827 | 828 | return 99 829 | `; 830 | expect(await interpreter.evalAsync(script)).toBe(55); 831 | expect(interpreter.eval(script)).toBe(55); 832 | }); 833 | 834 | it('complex chaining call', async () => { 835 | const interpreter = Interpreter.create(); 836 | 837 | const script = ` 838 | p = {f:{}} 839 | p.f.x = 9 840 | p.f.o = {v: 9} 841 | p["test" + p.f.x] = "test9 value" 842 | return p.test9 843 | `; 844 | expect(await interpreter.evalAsync(script)).toBe('test9 value'); 845 | expect(interpreter.eval(script)).toBe('test9 value'); 846 | }); 847 | 848 | it('chaining calls - split', async () => { 849 | const interpreter = Interpreter.create(); 850 | 851 | const script = ` 852 | "12,13,14".split(',')[1] 853 | `; 854 | expect(await interpreter.evalAsync(script)).toBe('13'); 855 | expect(interpreter.eval(script)).toBe('13'); 856 | }); 857 | 858 | it('chaining calls - array indexer', async () => { 859 | const interpreter = Interpreter.create(); 860 | 861 | const script = ` 862 | [ 863 | ["ss1", "ss21", 5], 864 | ["ss2", "test value", 6], 865 | ["ss3", "2020-03-07", 7], 866 | [] 867 | ][1][1] 868 | `; 869 | expect(await interpreter.evalAsync(script)).toBe('test value'); 870 | expect(interpreter.eval(script)).toBe('test value'); 871 | }); 872 | 873 | it('chaining calls - array indexer with ?', async () => { 874 | const interpreter = Interpreter.create(); 875 | 876 | const script = ` 877 | [ 878 | ["ss1", "ss21", 5], 879 | ["ss2", "test value", 6], 880 | ["ss3", "2020-03-07", 7], 881 | [] 882 | ][1]?[1] 883 | `; 884 | expect(await interpreter.evalAsync(script)).toBe('test value'); 885 | expect(interpreter.eval(script)).toBe('test value'); 886 | }); 887 | 888 | it('chaining calls - object indexer with ?', async () => { 889 | const interpreter = Interpreter.create(); 890 | 891 | const script = ` 892 | {x: 5, y: 10}.x 893 | `; 894 | expect(await interpreter.evalAsync(script)).toBe(5); 895 | expect(interpreter.eval(script)).toBe(5); 896 | }); 897 | 898 | it('chaining calls - object indexer with ?', async () => { 899 | const interpreter = Interpreter.create(); 900 | 901 | const script = ` 902 | {x: 5, y: 10, xy: 15}["x"+"y"] 903 | `; 904 | expect(await interpreter.evalAsync(script)).toBe(15); 905 | expect(interpreter.eval(script)).toBe(15); 906 | }); 907 | 908 | it('chaining calls - object indexer with ?', async () => { 909 | const interpreter = Interpreter.create(); 910 | 911 | const script = ` 912 | {value: "1st,2nd,3d"}["value"]?.split(',')?[1] 913 | `; 914 | expect(await interpreter.evalAsync(script)).toBe('2nd'); 915 | expect(interpreter.eval(script)).toBe('2nd'); 916 | }); 917 | 918 | it('string escape chars', async () => { 919 | const interpreter = Interpreter.create(); 920 | expect(interpreter.eval('"12\\"34"')).toBe('12"34'); 921 | expect(interpreter.eval(`"12\\'34"`)).toBe(`12'34`); 922 | expect(interpreter.eval(`"12\\\34"`)).toBe(`12\\34`); 923 | expect(interpreter.eval('"\\"12\\"34\\""')).toBe('"12"34"'); 924 | }); 925 | 926 | it('- elif 1', async () => { 927 | const interpreter = Interpreter.create(); 928 | 929 | const script = ` 930 | x = 5 931 | if x == 6: 932 | x = 20 933 | elif x == 5: 934 | x = 10 935 | else: 936 | x = 30 937 | 938 | return x 939 | `; 940 | expect(await interpreter.evalAsync(script)).toBe(10); 941 | expect(interpreter.eval(script)).toBe(10); 942 | }); 943 | 944 | it('- elif 2', async () => { 945 | const interpreter = Interpreter.create(); 946 | 947 | const script = ` 948 | x = 6 949 | if x == 6: 950 | x = 20 951 | elif x == 5: 952 | x = 10 953 | else: 954 | x = 30 955 | 956 | return x 957 | `; 958 | expect(await interpreter.evalAsync(script)).toBe(20); 959 | expect(interpreter.eval(script)).toBe(20); 960 | }); 961 | 962 | it('- elif 3', async () => { 963 | const interpreter = Interpreter.create(); 964 | 965 | const script = ` 966 | x = 11 967 | if x == 6: 968 | x = 20 969 | elif x == 5: 970 | x = 10 971 | else: 972 | x = 30 973 | return x 974 | `; 975 | expect(await interpreter.evalAsync(script)).toBe(30); 976 | expect(interpreter.eval(script)).toBe(30); 977 | }); 978 | 979 | it('- elif 4', async () => { 980 | const interpreter = Interpreter.create(); 981 | 982 | const script = ` 983 | x = 11 984 | if x == 6: 985 | x = 20 986 | elif x == 11: 987 | x = 11 988 | elif x == 5: 989 | x = 10 990 | else: 991 | x = 30 992 | return x 993 | `; 994 | expect(await interpreter.evalAsync(script)).toBe(11); 995 | expect(interpreter.eval(script)).toBe(11); 996 | }); 997 | 998 | it('return -1', async () => { 999 | const interpreter = Interpreter.create(); 1000 | 1001 | const script = `return -1`; 1002 | expect(await interpreter.evalAsync(script)).toBe(-1); 1003 | expect(interpreter.eval(script)).toBe(-1); 1004 | }); 1005 | 1006 | it('return -3.14', async () => { 1007 | const interpreter = Interpreter.create(); 1008 | 1009 | const script = `return -3.14`; 1010 | expect(await interpreter.evalAsync(script)).toBe(-3.14); 1011 | expect(interpreter.eval(script)).toBe(-3.14); 1012 | }); 1013 | // 1014 | }); 1015 | -------------------------------------------------------------------------------- /src/interpreter.ts: -------------------------------------------------------------------------------- 1 | import { AstBlock, ImportNode, Token } from './common'; 2 | import { getImportType } from './common/utils'; 3 | import { Evaluator } from './evaluator'; 4 | import { EvaluatorAsync } from './evaluator/evaluatorAsync'; 5 | import { BlockContext, Scope } from './evaluator/scope'; 6 | import { INITIAL_SCOPE, PackageToImport } from './initialScope'; 7 | import { Parser } from './parser'; 8 | import { Tokenizer } from './tokenizer'; 9 | 10 | export type PackageLoader = (packageName: string) => Record; 11 | export type ModuleLoader = (filePath: string) => Promise; 12 | 13 | export function jsPython(): Interpreter { 14 | return Interpreter.create(); 15 | } 16 | 17 | export class Interpreter { 18 | private readonly initialScope: Record = { ...INITIAL_SCOPE }; 19 | 20 | private _lastExecutionContext: Record | null = null; 21 | 22 | private packageLoader?: PackageLoader; 23 | private moduleLoader?: ModuleLoader; 24 | 25 | static create(): Interpreter { 26 | return new Interpreter(); 27 | } 28 | 29 | get initialExecutionContext(): Record { 30 | return this.initialScope; 31 | } 32 | 33 | get lastExecutionContext(): Record | null { 34 | return this._lastExecutionContext; 35 | } 36 | 37 | cleanUp(): void { 38 | this._lastExecutionContext = null; 39 | } 40 | 41 | jsPythonInfo(): string { 42 | return INITIAL_SCOPE.jsPython(); 43 | } 44 | 45 | tokenize(script: string): Token[] { 46 | const tokenizer = new Tokenizer(); 47 | return tokenizer.tokenize(script); 48 | } 49 | 50 | parse(script: string, moduleName = 'main.jspy'): AstBlock { 51 | const tokenizer = new Tokenizer(); 52 | const parser = new Parser(); 53 | const jspyAst = parser.parse(tokenizer.tokenize(script), moduleName); 54 | return jspyAst; 55 | } 56 | 57 | eval( 58 | codeOrAst: string | AstBlock, 59 | scope: Record = {}, 60 | entryFunctionName: string | [string, ...unknown[]] = '', 61 | moduleName = 'main.jspy' 62 | ): unknown { 63 | const ast = 64 | typeof codeOrAst === 'string' 65 | ? this.parse(codeOrAst as string, moduleName) 66 | : (codeOrAst as AstBlock); 67 | 68 | const blockContext = { 69 | moduleName: moduleName, 70 | cancellationToken: { cancel: false }, 71 | blockScope: new Scope(scope) 72 | } as BlockContext; 73 | 74 | blockContext.blockScope.set('printExecutionContext', () => 75 | console.log(blockContext.blockScope.getScope()) 76 | ); 77 | blockContext.blockScope.set('getExecutionContext', () => blockContext.blockScope.getScope()); 78 | this._lastExecutionContext = blockContext.blockScope.getScope(); 79 | 80 | const result = new Evaluator().evalBlock(ast, blockContext); 81 | if (!entryFunctionName || !entryFunctionName.length) { 82 | return result; 83 | } else { 84 | const funcName = Array.isArray(entryFunctionName)? entryFunctionName[0] : entryFunctionName as string 85 | const funcParams = Array.isArray(entryFunctionName)? entryFunctionName.slice(1) : [] 86 | const func = blockContext.blockScope.get(funcName); 87 | if (typeof func !== 'function') { 88 | throw Error(`Function ${entryFunctionName} does not exists or not a function`); 89 | } 90 | return func(...funcParams); 91 | } 92 | } 93 | 94 | async evalAsync( 95 | codeOrAst: string | AstBlock, 96 | scope: Record = {}, 97 | entryFunctionName: string | [string, ...unknown[]] = '', 98 | moduleName = 'main.jspy', 99 | ctxInitialized?: (ctx: BlockContext) => void 100 | ): Promise { 101 | const ast = 102 | typeof codeOrAst === 'string' 103 | ? this.parse(codeOrAst as string, moduleName) 104 | : (codeOrAst as AstBlock); 105 | const evaluator = new EvaluatorAsync(); 106 | const blockContext = { 107 | moduleName: moduleName, 108 | cancellationToken: { cancel: false }, 109 | blockScope: new Scope(scope) 110 | } as BlockContext; 111 | 112 | if (typeof ctxInitialized === 'function') { 113 | ctxInitialized(blockContext); 114 | } 115 | 116 | blockContext.blockScope.set('printExecutionContext', () => 117 | console.log(blockContext.blockScope.getScope()) 118 | ); 119 | blockContext.blockScope.set('getExecutionContext', () => blockContext.blockScope.getScope()); 120 | this._lastExecutionContext = blockContext.blockScope.getScope(); 121 | 122 | const result = await evaluator 123 | .registerJsonFileLoader( 124 | async (modulePath: string) => 125 | await (this.moduleLoader 126 | ? this.moduleLoader(modulePath) 127 | : Promise.reject('ModuleLoader is not registered')) 128 | ) 129 | .registerModuleParser(async modulePath => await this.moduleParser(modulePath)) 130 | .registerBlockContextFactory((moduleName, ast: AstBlock) => { 131 | // enrich context 132 | const newContext = this.assignImportContext(ast, scope); 133 | const moduleContext = { 134 | moduleName, 135 | blockScope: new Scope(newContext), 136 | cancellationToken: blockContext.cancellationToken 137 | }; 138 | moduleContext.blockScope.set('printExecutionContext', () => 139 | console.log(moduleContext.blockScope.getScope()) 140 | ); 141 | moduleContext.blockScope.set('getExecutionContext', () => 142 | moduleContext.blockScope.getScope() 143 | ); 144 | return moduleContext; 145 | }) 146 | .evalBlockAsync(ast, blockContext); 147 | 148 | if (!entryFunctionName || !entryFunctionName.length) { 149 | return result; 150 | } else { 151 | const funcName = Array.isArray(entryFunctionName)? entryFunctionName[0] : entryFunctionName as string 152 | const funcParams = Array.isArray(entryFunctionName)? entryFunctionName.slice(1) : [] 153 | 154 | const func = blockContext.blockScope.get(funcName); 155 | if (typeof func !== 'function') { 156 | throw Error(`Function ${entryFunctionName} does not exists or not a function`); 157 | } 158 | return await func(...funcParams); 159 | } 160 | } 161 | 162 | /** 163 | * Compatibility method (with v1). ! 164 | */ 165 | async evaluate( 166 | script: string, 167 | context: Record = {}, 168 | entryFunctionName: string | [string, ...unknown[]] = '', 169 | moduleName = 'main.jspy', 170 | ctxInitialized?: (ctx: BlockContext) => void 171 | ): Promise { 172 | if (!script || !script.length) { 173 | return null; 174 | } 175 | const ast = this.parse(script, moduleName); 176 | 177 | context = context && typeof context === 'object' ? context : {}; 178 | context = this.assignImportContext(ast, context); 179 | 180 | const globalScope = { 181 | ...this.initialScope, 182 | ...context 183 | } as Record; 184 | 185 | return await this.evalAsync(ast, globalScope, entryFunctionName, moduleName, ctxInitialized); 186 | } 187 | 188 | registerPackagesLoader(loader: PackageLoader): Interpreter { 189 | if (typeof loader === 'function') { 190 | this.packageLoader = loader; 191 | } else { 192 | throw Error('PackagesLoader'); 193 | } 194 | return this; 195 | } 196 | 197 | registerModuleLoader(loader: ModuleLoader): Interpreter { 198 | if (typeof loader === 'function') { 199 | this.moduleLoader = loader; 200 | } else { 201 | throw Error('ModuleLoader should be a function'); 202 | } 203 | 204 | return this; 205 | } 206 | 207 | addFunction( 208 | funcName: string, 209 | fn: (...args: unknown[]) => void | unknown | Promise 210 | ): Interpreter { 211 | this.initialScope[funcName] = fn; 212 | return this; 213 | } 214 | 215 | assignGlobalContext(obj: Record): Interpreter { 216 | Object.assign(this.initialScope, obj); 217 | return this; 218 | } 219 | 220 | hasFunction(scripts = '', funcName: string): boolean { 221 | return scripts.indexOf(`def ${funcName}`) > -1; 222 | } 223 | 224 | assignImportContext( 225 | ast: AstBlock, 226 | context: Record 227 | ): Record { 228 | const nodeToPackage = (im: ImportNode): PackageToImport => { 229 | return { 230 | name: im.module.name, 231 | as: im.module.alias, 232 | properties: im.parts?.map(p => ({ name: p.name, as: p.alias })) 233 | } as PackageToImport; 234 | }; 235 | 236 | const importNodes = ast.body.filter(n => n.type === 'import') as ImportNode[]; 237 | 238 | const jsImport = importNodes 239 | .filter(im => getImportType(im.module.name) === 'jsPackage') 240 | .map(im => nodeToPackage(im)); 241 | 242 | if (jsImport.length && this.packageLoader) { 243 | const libraries = this.packageResolver(jsImport); 244 | context = { ...context, ...libraries }; 245 | } 246 | 247 | return context as Record; 248 | } 249 | 250 | private async moduleParser(modulePath: string): Promise { 251 | if (!this.moduleLoader) { 252 | throw new Error('Module Loader is not registered'); 253 | } 254 | 255 | const content = await this.moduleLoader(modulePath); 256 | return this.parse(content, modulePath); 257 | } 258 | 259 | private packageResolver(packages: PackageToImport[]): Record { 260 | if (!this.packageLoader) { 261 | throw Error('Package loader not provided.'); 262 | } 263 | const libraries: Record = {}; 264 | packages.forEach(({ name, as, properties }: PackageToImport) => { 265 | const lib = (this.packageLoader && this.packageLoader(name)) || {}; 266 | if (properties?.length) { 267 | properties.forEach(prop => { 268 | libraries[prop.as || prop.name] = lib[prop.name]; 269 | }); 270 | } else if (as) { 271 | libraries[as] = lib; 272 | } else { 273 | libraries[name] = lib; 274 | } 275 | if (as) { 276 | libraries[as] = lib; 277 | } 278 | }); 279 | return libraries; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser'; 2 | -------------------------------------------------------------------------------- /src/parser/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { BinOpNode, ChainingCallsNode, ChainingObjectAccessNode, ConstNode, ImportNode } from '../common'; 2 | import { Tokenizer } from '../tokenizer'; 3 | import { Parser } from './parser'; 4 | 5 | describe('Parser => ', () => { 6 | it('1+2', async () => { 7 | const ast = new Parser().parse(new Tokenizer().tokenize('1+2')); 8 | expect(ast.body.length).toBe(1); 9 | expect(ast.body[0].type).toBe('binOp'); 10 | const binOp = ast.body[0] as BinOpNode; 11 | expect((binOp.left as ConstNode).value).toBe(1); 12 | expect(binOp.op).toBe('+'); 13 | expect((binOp.right as ConstNode).value).toBe(2); 14 | }); 15 | 16 | it('1+2-3', async () => { 17 | const ast = new Parser().parse(new Tokenizer().tokenize('1 + 2 - 3')); 18 | expect(ast.body.length).toBe(1); 19 | expect(ast.body[0].type).toBe('binOp'); 20 | const binOp = ast.body[0] as BinOpNode; 21 | expect(binOp.left.type).toBe('binOp'); 22 | expect(binOp.op).toBe('-'); 23 | expect((binOp.right as ConstNode).value).toBe(3); 24 | }); 25 | 26 | it('import datapipe-js-utils as utils', async () => { 27 | const script = `import datapipe-js-utils as utils`; 28 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 29 | expect(ast.body.length).toBe(1); 30 | expect(ast.body[0].type).toBe('import'); 31 | const importNode = ast.body[0] as ImportNode; 32 | expect(importNode.module.name).toBe('datapipe-js-utils'); 33 | expect(importNode.module.alias).toBe('utils'); 34 | }); 35 | 36 | it('import datapipe-js-utils', async () => { 37 | const script = `import datapipe-js-utils`; 38 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 39 | expect(ast.body.length).toBe(1); 40 | expect(ast.body[0].type).toBe('import'); 41 | const importNode = ast.body[0] as ImportNode; 42 | expect(importNode.module.name).toBe('datapipe-js-utils'); 43 | expect(importNode.module.alias).toBe(undefined); 44 | }); 45 | 46 | it('from datapipe-js-array import sort, first as f, fullJoin', async () => { 47 | const script = `from datapipe-js-array import sort, first as f, fullJoin`; 48 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 49 | expect(ast.body.length).toBe(1); 50 | expect(ast.body[0].type).toBe('import'); 51 | const importNode = ast.body[0] as ImportNode; 52 | expect(importNode.module.name).toBe('datapipe-js-array'); 53 | expect(importNode.module.alias).toBe(undefined); 54 | expect(importNode.parts).toBeDefined(); 55 | if (importNode.parts) { 56 | expect(importNode.parts.length).toBe(3); 57 | expect(importNode.parts[1].name).toBe('first'); 58 | expect(importNode.parts[1].alias).toBe('f'); 59 | } 60 | }); 61 | 62 | it('chaining calls 1 ', async () => { 63 | const script = `"1,2,3".split(',')[0]`; 64 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 65 | expect(ast.body.length).toBe(1); 66 | expect(ast.body[0].type).toBe("chainingCalls"); 67 | const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; 68 | expect(innerNodes.length).toBe(3); 69 | expect(innerNodes[2].type).toBe("chainingObjectAccess"); 70 | 71 | }); 72 | 73 | it('chaining calls 2 starts with JSON array', async () => { 74 | const script = `["1,2,3"][0].split(',')[0]`; 75 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 76 | expect(ast.body[0].type).toBe("chainingCalls"); 77 | expect((ast.body[0] as ChainingCallsNode).innerNodes.length).toBe(4); 78 | expect((ast.body[0] as ChainingCallsNode).innerNodes[0].type).toBe("createArray"); 79 | expect((ast.body[0] as ChainingCallsNode).innerNodes[1].type).toBe("chainingObjectAccess"); 80 | expect((ast.body[0] as ChainingCallsNode).innerNodes[3].type).toBe("chainingObjectAccess"); 81 | }); 82 | 83 | it('chaining calls 3 start with JSON Object', async () => { 84 | const script = `{value: "1,2,3"}["value"].split(',')[0]`; 85 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 86 | expect(ast.body[0].type).toBe("chainingCalls"); 87 | expect((ast.body[0] as ChainingCallsNode).innerNodes.length).toBe(4); 88 | expect((ast.body[0] as ChainingCallsNode).innerNodes[0].type).toBe("createObject"); 89 | expect((ast.body[0] as ChainingCallsNode).innerNodes[1].type).toBe("chainingObjectAccess"); 90 | expect((ast.body[0] as ChainingCallsNode).innerNodes[3].type).toBe("chainingObjectAccess"); 91 | }); 92 | 93 | it('chaining calls 1 with ? ', async () => { 94 | const script = `"1,2,3".split(',')?[0]`; 95 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 96 | expect(ast.body.length).toBe(1); 97 | const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; 98 | 99 | expect(ast.body[0].type).toBe("chainingCalls"); 100 | expect(innerNodes.length).toBe(3); 101 | expect(innerNodes[2].type).toBe("chainingObjectAccess"); 102 | 103 | expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 104 | expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 105 | expect(!!(innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 106 | }); 107 | 108 | it('chaining calls 2 with ? starts with JSON array', async () => { 109 | const script = `["1,2,3"][0]?.split(',')?[0]`; 110 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 111 | expect(ast.body[0].type).toBe("chainingCalls"); 112 | const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; 113 | expect(innerNodes.length).toBe(4); 114 | expect(innerNodes[0].type).toBe("createArray"); 115 | expect(innerNodes[1].type).toBe("chainingObjectAccess"); 116 | expect(innerNodes[1].type).toBe("chainingObjectAccess"); 117 | expect(innerNodes[3].type).toBe("chainingObjectAccess"); 118 | 119 | expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 120 | expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 121 | expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 122 | expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 123 | }); 124 | 125 | it('chaining calls 3 with ? start with JSON Object', async () => { 126 | const script = `{value: "1,2,3"}["value"]?.split(',')?[0]`; 127 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 128 | expect(ast.body[0].type).toBe("chainingCalls"); 129 | const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; 130 | expect(innerNodes.length).toBe(4); 131 | expect(innerNodes[0].type).toBe("createObject"); 132 | expect(innerNodes[1].type).toBe("chainingObjectAccess"); 133 | expect(innerNodes[3].type).toBe("chainingObjectAccess"); 134 | 135 | expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 136 | expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 137 | expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 138 | expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 139 | }); 140 | 141 | it('chaining calls 4 with ? 2d array access and ?', async () => { 142 | const script = `["1,2,3"][0]?[0]?[0]?.split(',')?[0]`; 143 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 144 | expect(ast.body[0].type).toBe("chainingCalls"); 145 | const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; 146 | expect(innerNodes.length).toBe(6); 147 | expect(innerNodes[0].type).toBe("createArray"); 148 | expect(innerNodes[1].type).toBe("chainingObjectAccess"); 149 | expect(innerNodes[2].type).toBe("chainingObjectAccess"); 150 | expect(innerNodes[3].type).toBe("chainingObjectAccess"); 151 | expect(innerNodes[4].type).toBe("funcCall"); 152 | expect(innerNodes[5].type).toBe("chainingObjectAccess"); 153 | 154 | expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 155 | expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 156 | expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 157 | expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 158 | expect(!!(innerNodes[4] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 159 | expect(!!(innerNodes[5] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 160 | }); 161 | 162 | it('chaining calls 1 with ? ', async () => { 163 | const script = `"1,2,3".split(',')?[0]`; 164 | const ast = new Parser().parse(new Tokenizer().tokenize(script)); 165 | expect(ast.body.length).toBe(1); 166 | const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; 167 | 168 | expect(ast.body[0].type).toBe("chainingCalls"); 169 | expect(innerNodes.length).toBe(3); 170 | expect(innerNodes[2].type).toBe("chainingObjectAccess"); 171 | 172 | expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 173 | expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); 174 | expect(!!(innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(false); 175 | }); 176 | 177 | it('prototype methods call ', async () => { 178 | expect(new Parser().parse(new Tokenizer().tokenize(`t.toString`)).body.length).toBe(1); 179 | expect(new Parser().parse(new Tokenizer().tokenize(`t.toString()`)).body.length).toBe(1); 180 | expect(new Parser().parse(new Tokenizer().tokenize(`t.valueOf`)).body.length).toBe(1); 181 | expect(new Parser().parse(new Tokenizer().tokenize(`t.valueOf()`)).body.length).toBe(1); 182 | }); 183 | 184 | }); 185 | -------------------------------------------------------------------------------- /src/parser/parser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BinOpNode, 3 | ConstNode, 4 | AstBlock, 5 | Token, 6 | AstNode, 7 | Operators, 8 | AssignNode, 9 | TokenTypes, 10 | GetSingleVarNode, 11 | FunctionCallNode, 12 | getTokenType, 13 | getTokenValue, 14 | isTokenTypeLiteral, 15 | getStartLine, 16 | getStartColumn, 17 | getEndColumn, 18 | getEndLine, 19 | findOperators, 20 | splitTokens, 21 | findTokenValueIndex, 22 | FunctionDefNode, 23 | CreateObjectNode, 24 | ObjectPropertyInfo, 25 | CreateArrayNode, 26 | ArrowFuncDefNode, 27 | ExpressionOperators, 28 | IfNode, 29 | ForNode, 30 | WhileNode, 31 | ImportNode, 32 | NameAlias, 33 | ContinueNode, 34 | BreakNode, 35 | ReturnNode, 36 | CommentNode, 37 | getTokenLoc, 38 | OperationTypes, 39 | LogicalNodeItem, 40 | LogicalOperators, 41 | LogicalOpNode, 42 | ComparisonOperators, 43 | TryExceptNode, 44 | ExceptBody, 45 | RaiseNode, 46 | findChainingCallTokensIndexes, 47 | splitTokensByIndexes, 48 | ChainingCallsNode, 49 | ChainingObjectAccessNode, 50 | ElifNode 51 | } from '../common'; 52 | import { JspyParserError } from '../common/utils'; 53 | 54 | class InstructionLine { 55 | readonly tokens: Token[] = []; 56 | 57 | startLine(): number { 58 | return getStartLine(this.tokens[0]); 59 | } 60 | 61 | startColumn(): number { 62 | return getStartColumn(this.tokens[0]); 63 | } 64 | 65 | endLine(): number { 66 | return getEndLine(this.tokens[this.tokens.length - 1]); 67 | } 68 | 69 | endColumn(): number { 70 | return getEndColumn(this.tokens[this.tokens.length - 1]); 71 | } 72 | } 73 | 74 | export class Parser { 75 | private _currentToken: Token | null = null; 76 | private _moduleName = ''; 77 | 78 | /** 79 | * Parses tokens and return Ast - Abstract Syntax Tree for jsPython code 80 | * @param tokens tokens 81 | * @param options parsing options. By default it will exclude comments and include LOC (Line of code) 82 | */ 83 | parse(tokens: Token[], name = 'main.jspy', type = 'module'): AstBlock { 84 | this._moduleName = name; 85 | const ast = { name, type, funcs: [], body: [] } as AstBlock; 86 | 87 | if (!tokens || !tokens.length) { 88 | return ast; 89 | } 90 | 91 | try { 92 | // group all tokens into an Instruction lines. 93 | const instructions = this.tokensToInstructionLines(tokens, 1); 94 | 95 | // process all instructions 96 | this.instructionsToNodes(instructions, ast); 97 | } catch (error) { 98 | const err = error as Error; 99 | const token = this._currentToken ?? ({} as Token); 100 | throw new JspyParserError( 101 | ast.name, 102 | getStartLine(token), 103 | getStartColumn(token), 104 | err.message || String(err) 105 | ); 106 | } 107 | return ast; 108 | } 109 | 110 | private instructionsToNodes(instructions: InstructionLine[], ast: AstBlock): void { 111 | const getBody = (tokens: Token[], startTokenIndex: number): AstNode[] => { 112 | const instructionLines = this.tokensToInstructionLines( 113 | tokens, 114 | getStartLine(tokens[startTokenIndex]) 115 | ); 116 | const bodyAst = { name: ast.name, body: [] as AstNode[], funcs: [] as AstNode[] } as AstBlock; 117 | this.instructionsToNodes(instructionLines, bodyAst); 118 | return bodyAst.body; 119 | }; 120 | 121 | const findIndexes = (tkns: Token[], operation: OperationTypes, result: number[]): boolean => { 122 | result.splice(0, result.length); 123 | findOperators(tkns, operation).forEach(r => result.push(r)); 124 | return !!result.length; 125 | }; 126 | 127 | for (let i = 0; i < instructions.length; i++) { 128 | const instruction = instructions[i]; 129 | 130 | // remove comments 131 | let tt = 0; 132 | while (tt < instruction.tokens.length) { 133 | if (getTokenType(instruction.tokens[tt]) === TokenTypes.Comment) { 134 | instruction.tokens.splice(tt, 1); 135 | } else { 136 | tt++; 137 | } 138 | } 139 | if (!instruction.tokens.length) { 140 | continue; 141 | } 142 | 143 | const firstToken = instruction.tokens[0]; 144 | const secondToken = instruction.tokens.length > 1 ? instruction.tokens[1] : null; 145 | this._currentToken = firstToken; 146 | 147 | const logicOpIndexes: number[] = []; 148 | const assignTokenIndexes: number[] = []; 149 | 150 | if (getTokenType(firstToken) === TokenTypes.Comment) { 151 | ast.body.push( 152 | new CommentNode(getTokenValue(firstToken) as string, getTokenLoc(firstToken)) 153 | ); 154 | } else if ( 155 | getTokenValue(firstToken) === 'def' || 156 | (getTokenValue(firstToken) === 'async' && getTokenValue(secondToken) === 'def') 157 | ) { 158 | const isAsync = getTokenValue(firstToken) === 'async'; 159 | const funcName = getTokenValue(instruction.tokens[isAsync ? 2 : 1]) as string; 160 | const paramsTokens = instruction.tokens.slice( 161 | instruction.tokens.findIndex(tkns => getTokenValue(tkns) === '(') + 1, 162 | instruction.tokens.findIndex(tkns => getTokenValue(tkns) === ')') 163 | ); 164 | 165 | const params = splitTokens(paramsTokens, ',').map(t => getTokenValue(t[0]) as string); 166 | 167 | const endDefOfDef = findTokenValueIndex(instruction.tokens, v => v === ':'); 168 | 169 | if (endDefOfDef === -1) { 170 | throw `Can't find : for def`; 171 | } 172 | 173 | const instructionLines = this.tokensToInstructionLines( 174 | instruction.tokens, 175 | getStartLine(instruction.tokens[endDefOfDef + 1]) 176 | ); 177 | const funcAst = { 178 | name: funcName, 179 | body: [] as AstNode[], 180 | funcs: [] as AstNode[] 181 | } as AstBlock; 182 | this.instructionsToNodes(instructionLines, funcAst); 183 | 184 | ast.funcs.push( 185 | new FunctionDefNode(funcAst, params, isAsync, getTokenLoc(instruction.tokens[0])) 186 | ); 187 | } else if (getTokenValue(firstToken) === 'if') { 188 | const endDefOfDef = findTokenValueIndex(instruction.tokens, v => v === ':'); 189 | 190 | if (endDefOfDef === -1) { 191 | throw `Can't find : for if`; 192 | } 193 | 194 | const ifBody = getBody(instruction.tokens, endDefOfDef + 1); 195 | const conditionTokens = instruction.tokens.slice(1, endDefOfDef); 196 | 197 | const conditionNode = findIndexes(conditionTokens, OperationTypes.Logical, logicOpIndexes) 198 | ? this.groupLogicalOperations(logicOpIndexes, conditionTokens) 199 | : this.createExpressionNode(conditionTokens); 200 | 201 | // elifs 202 | const elifNodes: ElifNode[] = []; 203 | while ( 204 | instructions.length > i + 1 && 205 | getTokenValue(instructions[i + 1].tokens[0]) === 'elif' 206 | ) { 207 | const elifInstruction = instructions[++i]; 208 | 209 | const endOfElif = findTokenValueIndex(elifInstruction.tokens, v => v === ':'); 210 | 211 | const conditionTokens = elifInstruction.tokens.slice(1, endDefOfDef); 212 | 213 | const elifConditionNode = findIndexes( 214 | conditionTokens, 215 | OperationTypes.Logical, 216 | logicOpIndexes 217 | ) 218 | ? this.groupLogicalOperations(logicOpIndexes, conditionTokens) 219 | : this.createExpressionNode(conditionTokens); 220 | 221 | const elifBody = getBody(elifInstruction.tokens, endOfElif + 1); 222 | elifNodes.push( 223 | new ElifNode(elifConditionNode, elifBody, getTokenLoc(elifInstruction.tokens[0])) 224 | ); 225 | } 226 | 227 | // else 228 | let elseBody: AstNode[] | undefined = undefined; 229 | if ( 230 | instructions.length > i + 1 && 231 | getTokenValue(instructions[i + 1].tokens[0]) === 'else' && 232 | getTokenValue(instructions[i + 1].tokens[1]) === ':' 233 | ) { 234 | elseBody = getBody(instructions[i + 1].tokens, 2); 235 | i++; 236 | } 237 | 238 | ast.body.push( 239 | new IfNode(conditionNode, ifBody, elifNodes, elseBody, getTokenLoc(firstToken)) 240 | ); 241 | } else if (getTokenValue(firstToken) === 'try') { 242 | if (getTokenValue(instruction.tokens[1]) !== ':') { 243 | throw `'try' statement should be followed by ':'`; 244 | } 245 | 246 | const tryBody = getBody(instruction.tokens, 2); 247 | const excepts: ExceptBody[] = []; 248 | 249 | let elseBody: AstNode[] | undefined = undefined; 250 | let finallyBody: AstNode[] | undefined = undefined; 251 | 252 | while ( 253 | instructions.length > i + 1 && 254 | (getTokenValue(instructions[i + 1].tokens[0]) === 'else' || 255 | getTokenValue(instructions[i + 1].tokens[0]) === 'except' || 256 | getTokenValue(instructions[i + 1].tokens[0]) === 'finally') 257 | ) { 258 | if (getTokenValue(instructions[i + 1].tokens[0]) === 'else') { 259 | if (elseBody) { 260 | throw new Error(`Only one 'else' is allowed in a 'try'`); 261 | } 262 | 263 | elseBody = getBody(instructions[i + 1].tokens, 2); 264 | } 265 | 266 | if (getTokenValue(instructions[i + 1].tokens[0]) === 'finally') { 267 | if (finallyBody) { 268 | throw new Error(`Only one 'else' is allowed in a 'try'`); 269 | } 270 | 271 | finallyBody = getBody(instructions[i + 1].tokens, 2); 272 | } 273 | 274 | if (getTokenValue(instructions[i + 1].tokens[0]) === 'except') { 275 | const endIndex = findTokenValueIndex(instructions[i + 1].tokens, v => v === ':'); 276 | const except = {} as ExceptBody; 277 | 278 | if (endIndex === 2) { 279 | except.error = { name: getTokenValue(instructions[i + 1].tokens[1]) } as NameAlias; 280 | } else if (endIndex === 3) { 281 | except.error = { 282 | name: getTokenValue(instructions[i + 1].tokens[1]), 283 | alias: getTokenValue(instructions[i + 1].tokens[2]) 284 | } as NameAlias; 285 | } else if (endIndex === 4) { 286 | except.error = { 287 | name: getTokenValue(instructions[i + 1].tokens[1]), 288 | alias: getTokenValue(instructions[i + 1].tokens[3]) 289 | } as NameAlias; 290 | } else if (endIndex !== 1) { 291 | throw new Error( 292 | `Incorrect 'except:' statement. Valid stats: (except: or except Error: or except Error as e:)` 293 | ); 294 | } 295 | 296 | except.body = getBody(instructions[i + 1].tokens, endIndex + 1); 297 | 298 | excepts.push(except); 299 | } 300 | 301 | i++; 302 | } 303 | 304 | if (!excepts.length) { 305 | throw new Error('Except: is missing'); 306 | } 307 | 308 | ast.body.push( 309 | new TryExceptNode(tryBody, excepts, elseBody, finallyBody, getTokenLoc(firstToken)) 310 | ); 311 | } else if (getTokenValue(firstToken) === 'continue') { 312 | ast.body.push(new ContinueNode()); 313 | } else if (getTokenValue(firstToken) === 'break') { 314 | ast.body.push(new BreakNode()); 315 | } else if (getTokenValue(firstToken) === 'return') { 316 | ast.body.push( 317 | new ReturnNode( 318 | instruction.tokens.length > 1 319 | ? this.createExpressionNode(instruction.tokens.slice(1)) 320 | : undefined, 321 | getTokenLoc(firstToken) 322 | ) 323 | ); 324 | } else if (getTokenValue(firstToken) === 'raise') { 325 | if (instruction.tokens.length === 1) { 326 | throw new Error(`Incorrect 'raise' usage. Please specify error name and message `); 327 | } 328 | const errorName = getTokenValue(instruction.tokens[1]) as string; 329 | 330 | // const errorMessage = 331 | // instruction.tokens.length == 5 && 332 | // getTokenValue(instruction.tokens[2]) === '(' && 333 | // getTokenValue(instruction.tokens[4]) === ')' 334 | // ? (getTokenValue(instruction.tokens[3]) as string) 335 | // : undefined; 336 | 337 | const errMsg = this.createExpressionNode(instruction.tokens.slice(1)); 338 | 339 | ast.body.push(new RaiseNode(errorName, errMsg, getTokenLoc(firstToken))); 340 | } else if (getTokenValue(firstToken) === 'for') { 341 | const endDefOfDef = findTokenValueIndex(instruction.tokens, v => v === ':'); 342 | 343 | if (endDefOfDef === -1) { 344 | throw `Can't find : for if`; 345 | } 346 | 347 | const itemVarName = getTokenValue(instruction.tokens[1]) as string; 348 | const sourceArray = this.createExpressionNode(instruction.tokens.slice(3, endDefOfDef)); 349 | const forBody = getBody(instruction.tokens, endDefOfDef + 1); 350 | 351 | ast.body.push(new ForNode(sourceArray, itemVarName, forBody, getTokenLoc(firstToken))); 352 | } else if (getTokenValue(firstToken) === 'while') { 353 | const endDefOfDef = findTokenValueIndex(instruction.tokens, v => v === ':'); 354 | 355 | if (endDefOfDef === -1) { 356 | throw `Can't find : for [while]`; 357 | } 358 | 359 | const conditionTokens = instruction.tokens.slice(1, endDefOfDef); 360 | const conditionNode = findIndexes(conditionTokens, OperationTypes.Logical, logicOpIndexes) 361 | ? this.groupLogicalOperations(logicOpIndexes, conditionTokens) 362 | : this.createExpressionNode(conditionTokens); 363 | 364 | const body = getBody(instruction.tokens, endDefOfDef + 1); 365 | 366 | ast.body.push(new WhileNode(conditionNode, body, getTokenLoc(firstToken))); 367 | } else if (getTokenValue(firstToken) === 'import') { 368 | let asIndex = findTokenValueIndex(instruction.tokens, v => v === 'as'); 369 | if (asIndex < 0) { 370 | asIndex = instruction.tokens.length; 371 | } 372 | 373 | const module = { 374 | name: instruction.tokens 375 | .slice(1, asIndex) 376 | .map(t => getTokenValue(t)) 377 | .join(''), 378 | alias: 379 | instruction.tokens 380 | .slice(asIndex + 1) 381 | .map(t => getTokenValue(t)) 382 | .join('') || undefined 383 | } as NameAlias; 384 | 385 | const body = {} as AstBlock; // empty for now 386 | ast.body.push(new ImportNode(module, body, undefined, getTokenLoc(firstToken))); 387 | } else if (getTokenValue(firstToken) === 'from') { 388 | const importIndex = findTokenValueIndex(instruction.tokens, v => v === 'import'); 389 | if (importIndex < 0) { 390 | throw Error(`'import' must follow 'from'`); 391 | } 392 | 393 | const module = { 394 | name: instruction.tokens 395 | .slice(1, importIndex) 396 | .map(t => getTokenValue(t)) 397 | .join('') 398 | } as NameAlias; 399 | 400 | const parts = splitTokens(instruction.tokens.slice(importIndex + 1), ',').map(t => { 401 | return { 402 | name: getTokenValue(t[0]), 403 | alias: t.length === 3 ? getTokenValue(t[2]) : undefined 404 | } as NameAlias; 405 | }); 406 | 407 | const body = {} as AstBlock; // empty for now 408 | 409 | ast.body.push(new ImportNode(module, body, parts, getTokenLoc(firstToken))); 410 | } else if (findIndexes(instruction.tokens, OperationTypes.Assignment, assignTokenIndexes)) { 411 | const assignTokens = splitTokens(instruction.tokens, '='); 412 | const target = this.createExpressionNode(assignTokens[0]); 413 | const source = this.createExpressionNode(assignTokens[1]); 414 | ast.body.push(new AssignNode(target, source, getTokenLoc(assignTokens[0][0]))); 415 | } else if (findIndexes(instruction.tokens, OperationTypes.Logical, logicOpIndexes)) { 416 | ast.body.push(this.groupLogicalOperations(logicOpIndexes, instruction.tokens)); 417 | } else { 418 | ast.body.push(this.createExpressionNode(instruction.tokens)); 419 | } 420 | } 421 | } 422 | 423 | private sliceWithBrackets(a: Token[], begin: number, end: number): Token[] { 424 | // if expression is in brackets, then we need clean brackets 425 | if (getTokenValue(a[begin]) === '(' && getTokenType(a[begin]) !== TokenTypes.LiteralString) { 426 | begin++; 427 | end--; 428 | } 429 | 430 | return a.slice(begin, end); 431 | } 432 | 433 | private groupComparisonOperations(indexes: number[], tokens: Token[]): AstNode { 434 | const start = 0; 435 | 436 | let leftNode: AstNode | null = null; 437 | for (let i = 0; i < indexes.length; i++) { 438 | const opToken = getTokenValue(tokens[indexes[i]]) as ComparisonOperators; 439 | leftNode = leftNode 440 | ? leftNode 441 | : this.createExpressionNode(this.sliceWithBrackets(tokens, start, indexes[i])); 442 | 443 | const endInd = i + 1 < indexes.length ? indexes[i + 1] : tokens.length; 444 | const rightNode = this.createExpressionNode( 445 | this.sliceWithBrackets(tokens, indexes[i] + 1, endInd) 446 | ); 447 | 448 | leftNode = new BinOpNode(leftNode, opToken, rightNode, getTokenLoc(tokens[0])); 449 | } 450 | 451 | return leftNode as AstNode; 452 | } 453 | 454 | private groupLogicalOperations(logicOp: number[], tokens: Token[]): LogicalOpNode { 455 | let start = 0; 456 | const logicItems: LogicalNodeItem[] = []; 457 | for (let i = 0; i < logicOp.length; i++) { 458 | const opToken = tokens[logicOp[i]]; 459 | const logicalSlice = this.sliceWithBrackets(tokens, start, logicOp[i]); 460 | logicItems.push({ 461 | node: this.createExpressionNode(logicalSlice), 462 | op: getTokenValue(opToken) as LogicalOperators 463 | }); 464 | 465 | start = logicOp[i] + 1; 466 | } 467 | 468 | logicItems.push({ 469 | node: this.createExpressionNode(this.sliceWithBrackets(tokens, start, tokens.length)) 470 | } as LogicalNodeItem); 471 | 472 | const lop = new LogicalOpNode(logicItems, getTokenLoc(tokens[0])); 473 | return lop; 474 | } 475 | 476 | private tokensToInstructionLines(tokens: Token[], startLine: number): InstructionLine[] { 477 | const lines: InstructionLine[] = []; 478 | 479 | let column = 0; 480 | let currentLine = startLine; 481 | let line = new InstructionLine(); 482 | for (let i = 0; i < tokens.length; i++) { 483 | const token = tokens[i]; 484 | const sLine = getStartLine(token); 485 | const sColumn = getStartColumn(token); 486 | const value = getTokenValue(token); 487 | this._currentToken = token; 488 | 489 | if (sLine >= startLine) { 490 | if (currentLine !== sLine) { 491 | currentLine = sLine; 492 | } 493 | 494 | if (column === sColumn && !')}]'.includes(value as string)) { 495 | currentLine = sLine; 496 | lines.push(line); 497 | line = new InstructionLine(); 498 | } 499 | 500 | line.tokens.push(token); 501 | 502 | // first line defines a minimum indent 503 | if (column === 0) { 504 | column = sColumn; 505 | } 506 | 507 | // stop looping through if line has less indent 508 | // it means the corrent block finished 509 | if (sColumn < column) { 510 | break; 511 | } 512 | } 513 | } 514 | 515 | if (line.tokens.length) { 516 | lines.push(line); 517 | } 518 | 519 | return lines; 520 | } 521 | 522 | private createExpressionNode(tokens: Token[]): AstNode { 523 | if (tokens.length === 0) { 524 | throw new Error(`Tokens length can't empty.`); 525 | } 526 | const lastToken = tokens[tokens.length - 1]; 527 | if (getTokenValue(lastToken) === ';' && getTokenType(lastToken) !== TokenTypes.LiteralString) { 528 | throw new Error(`Unexpected symbol ';' in the end`); 529 | } 530 | 531 | this._currentToken = tokens[0]; 532 | 533 | // const or variable 534 | if (tokens.length === 1 || (tokens.length === 2 && getTokenValue(tokens[1]) === '?')) { 535 | const firstToken = tokens[0]; 536 | const tokenType = getTokenType(firstToken); 537 | 538 | if (isTokenTypeLiteral(tokenType)) { 539 | return new ConstNode(firstToken); 540 | } else if (tokenType === TokenTypes.Identifier) { 541 | return new GetSingleVarNode( 542 | firstToken, 543 | (tokens.length === 2 && getTokenValue(tokens[1]) === '?') || undefined 544 | ); 545 | } 546 | 547 | throw Error(`Unhandled single token: '${JSON.stringify(firstToken)}'`); 548 | } 549 | 550 | // arrow function 551 | const arrowFuncParts = splitTokens(tokens, '=>'); 552 | if (arrowFuncParts.length > 1) { 553 | const pArray = 554 | getTokenValue(arrowFuncParts[0][0]) === '(' 555 | ? arrowFuncParts[0].splice(1, arrowFuncParts[0].length - 2) 556 | : arrowFuncParts[0]; 557 | const params = splitTokens(pArray, ',').map(t => getTokenValue(t[0]) as string); 558 | 559 | const instructionLines = this.tokensToInstructionLines(arrowFuncParts[1], 0); 560 | const funcAst = { 561 | name: this._moduleName, 562 | body: [] as AstNode[], 563 | funcs: [] as AstNode[] 564 | } as AstBlock; 565 | this.instructionsToNodes(instructionLines, funcAst); 566 | 567 | return new ArrowFuncDefNode(funcAst, params, getTokenLoc(tokens[0])); 568 | } 569 | 570 | // comparison operations 571 | const comparissonIndexes = findOperators(tokens, OperationTypes.Comparison); 572 | if (comparissonIndexes.length) { 573 | return this.groupComparisonOperations(comparissonIndexes, tokens); 574 | } 575 | 576 | // create arithmetic expression 577 | const ops = findOperators(tokens); 578 | if (ops.length) { 579 | let prevNode: AstNode | null = null; 580 | for (let i = 0; i < ops.length; i++) { 581 | const opIndex = ops[i]; 582 | const op = getTokenValue(tokens[opIndex]) as Operators; 583 | 584 | let nextOpIndex = i + 1 < ops.length ? ops[i + 1] : null; 585 | let nextOp = nextOpIndex !== null ? getTokenValue(tokens[nextOpIndex]) : null; 586 | if (nextOpIndex !== null && (nextOp === '*' || nextOp === '/')) { 587 | let rightNode: AstNode | null = null; 588 | // iterate through all continuous '*', '/' operations 589 | do { 590 | const nextOpIndex2 = i + 2 < ops.length ? ops[i + 2] : null; 591 | 592 | const leftSlice2 = this.sliceWithBrackets(tokens, opIndex + 1, nextOpIndex); 593 | const rightSlice2 = this.sliceWithBrackets( 594 | tokens, 595 | nextOpIndex + 1, 596 | nextOpIndex2 || tokens.length 597 | ); 598 | 599 | const left2 = this.createExpressionNode(leftSlice2); 600 | const right2 = this.createExpressionNode(rightSlice2); 601 | rightNode = new BinOpNode(left2, nextOp, right2, getTokenLoc(tokens[opIndex + 1])); 602 | 603 | i++; 604 | nextOpIndex = i + 1 < ops.length ? ops[i + 1] : null; 605 | nextOp = nextOpIndex !== null ? getTokenValue(tokens[nextOpIndex]) : null; 606 | } while (nextOpIndex !== null && (nextOp === '*' || nextOp === '/')); 607 | 608 | // add up result 609 | if (prevNode === null) { 610 | const leftSlice = this.sliceWithBrackets(tokens, 0, opIndex); 611 | prevNode = this.createExpressionNode(leftSlice); 612 | } 613 | prevNode = new BinOpNode( 614 | prevNode, 615 | op as ExpressionOperators, 616 | rightNode, 617 | getTokenLoc(tokens[0]) 618 | ); 619 | } else { 620 | const leftSlice = prevNode ? [] : this.sliceWithBrackets(tokens, 0, opIndex); 621 | const rightSlice = this.sliceWithBrackets( 622 | tokens, 623 | opIndex + 1, 624 | nextOpIndex || tokens.length 625 | ); 626 | const left: AstNode = prevNode || this.createExpressionNode(leftSlice); 627 | const right = this.createExpressionNode(rightSlice); 628 | prevNode = new BinOpNode(left, op as ExpressionOperators, right, getTokenLoc(tokens[0])); 629 | } 630 | } 631 | 632 | if (prevNode === null) { 633 | throw Error(`Can't create node ...`); 634 | } 635 | 636 | return prevNode; 637 | } 638 | 639 | // create chaining calls 640 | 641 | const inds = findChainingCallTokensIndexes(tokens); 642 | 643 | if (inds.length > 0) { 644 | const chainingGroup = splitTokensByIndexes(tokens, inds); 645 | const innerNodes: AstNode[] = []; 646 | 647 | for (let i = 0; i < chainingGroup.length; i++) { 648 | const chainLinkTokenks = chainingGroup[i]; 649 | 650 | if (i !== 0 && getTokenValue(chainLinkTokenks[0]) === '[') { 651 | const nullCoelsing = getTokenValue(chainLinkTokenks[chainLinkTokenks.length - 1]) === '?'; 652 | if (nullCoelsing) { 653 | chainLinkTokenks.pop(); 654 | } 655 | const paramsTokensSlice = chainLinkTokenks.slice(1, chainLinkTokenks.length - 1); 656 | const paramsNodes = this.createExpressionNode(paramsTokensSlice); 657 | 658 | innerNodes.push( 659 | new ChainingObjectAccessNode( 660 | paramsNodes, 661 | nullCoelsing, 662 | getTokenLoc(chainLinkTokenks[0]) 663 | ) 664 | ); 665 | continue; 666 | } 667 | 668 | innerNodes.push(this.createExpressionNode(chainLinkTokenks)); 669 | } 670 | 671 | return new ChainingCallsNode(innerNodes, getTokenLoc(tokens[0])); 672 | } 673 | 674 | // create function call node 675 | if (tokens.length > 2 && getTokenValue(tokens[1]) === '(') { 676 | const isNullCoelsing = getTokenValue(tokens[tokens.length - 1]) === '?'; 677 | if (isNullCoelsing) { 678 | // remove '?' 679 | tokens.pop(); 680 | } 681 | const name = getTokenValue(tokens[0]) as string; 682 | const paramsTokensSlice = tokens.slice(2, tokens.length - 1); 683 | const paramsTokens = splitTokens(paramsTokensSlice, ','); 684 | const paramsNodes = paramsTokens.map(tkns => this.createExpressionNode(tkns)); 685 | const node = new FunctionCallNode(name, paramsNodes, getTokenLoc(tokens[0])); 686 | node.nullCoelsing = isNullCoelsing || undefined; 687 | return node; 688 | } 689 | 690 | // create Object Node 691 | if (getTokenValue(tokens[0]) === '{' && getTokenValue(tokens[tokens.length - 1]) === '}') { 692 | const keyValueTokens = splitTokens(tokens.splice(1, tokens.length - 2), ','); 693 | const props = [] as ObjectPropertyInfo[]; 694 | for (let i = 0; i < keyValueTokens.length; i++) { 695 | if (!keyValueTokens[i].length) { 696 | continue; 697 | } 698 | const keyValue = splitTokens(keyValueTokens[i], ':'); 699 | if (keyValue.length === 1) { 700 | const pInfo = { 701 | name: new ConstNode(keyValue[0][0]), 702 | value: this.createExpressionNode(keyValue[0]) 703 | } as ObjectPropertyInfo; 704 | 705 | props.push(pInfo); 706 | } else if (keyValue.length === 2) { 707 | let name: AstNode | null = null; 708 | const namePart = keyValue[0]; 709 | 710 | if (namePart.length === 1) { 711 | name = new ConstNode(namePart[0]); 712 | } else if ( 713 | getTokenValue(namePart[0]) === '[' && 714 | getTokenValue(namePart[namePart.length - 1]) === ']' 715 | ) { 716 | name = this.createExpressionNode(namePart.slice(1, namePart.length - 1)); 717 | } else { 718 | throw new Error( 719 | `Incorrect JSON. Can't resolve Key field. That should either constant or expression in []` 720 | ); 721 | } 722 | 723 | const pInfo = { 724 | name, 725 | value: this.createExpressionNode(keyValue[1]) 726 | } as ObjectPropertyInfo; 727 | 728 | props.push(pInfo); 729 | } else { 730 | throw Error('Incorrect JSON'); 731 | } 732 | } 733 | 734 | return new CreateObjectNode(props, getTokenLoc(tokens[0])); 735 | } 736 | 737 | // create Array Node 738 | if (getTokenValue(tokens[0]) === '[' && getTokenValue(tokens[tokens.length - 1]) === ']') { 739 | const items = splitTokens(tokens.splice(1, tokens.length - 2), ',') 740 | .filter(tkns => tkns?.length) 741 | .map(tkns => this.createExpressionNode(tkns)); 742 | 743 | return new CreateArrayNode(items, getTokenLoc(tokens[0])); 744 | } 745 | 746 | throw Error(`Undefined node '${getTokenValue(tokens[0])}'.`); 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /src/tokenizer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokenizer'; 2 | -------------------------------------------------------------------------------- /src/tokenizer/tokenizer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Tokenizer } from './tokenizer'; 2 | 3 | describe('Tokenizer => ', () => { 4 | it('a + b + 55', async () => { 5 | let tokens = new Tokenizer().tokenize('a + b + 55'); 6 | expect(tokens.length).toBe(5); 7 | tokens = new Tokenizer().tokenize('a+b+55'); 8 | expect(tokens.length).toBe(5); 9 | }); 10 | 11 | it('s = 255 + 23 * 45', async () => { 12 | let tokens = new Tokenizer().tokenize('s = 255 + 23 * 45'); 13 | expect(tokens.length).toBe(7); 14 | tokens = new Tokenizer().tokenize('s =255+23*45'); 15 | expect(tokens.length).toBe(7); 16 | }); 17 | 18 | it('s=(255 + 23) * 45', async () => { 19 | let tokens = new Tokenizer().tokenize('s = (255 + 23 ) * 45'); 20 | expect(tokens.length).toBe(9); 21 | tokens = new Tokenizer().tokenize('s=(255 + 23) * 45'); 22 | expect(tokens.length).toBe(9); 23 | tokens = new Tokenizer().tokenize('s=(255 \n +\n 23) \n * 45'); 24 | expect(tokens.length).toBe(9); 25 | }); 26 | 27 | it('if someVar == 20/40:\n someVar = 55', async () => { 28 | let tokens = new Tokenizer().tokenize('if someVar == 20/40:\n someVar = 55'); 29 | expect(tokens.length).toBe(10); 30 | tokens = new Tokenizer().tokenize('if someVar== 20/40:\n someVar=55'); 31 | expect(tokens.length).toBe(10); 32 | tokens = new Tokenizer().tokenize('if someVar==20/40:\n someVar= 55'); 33 | expect(tokens.length).toBe(10); 34 | }); 35 | 36 | it('x="test1"', async () => { 37 | let tokens = new Tokenizer().tokenize('x="test1"'); 38 | expect(tokens.length).toBe(3); 39 | expect(tokens[2][0]).toBe('test1'); 40 | tokens = new Tokenizer().tokenize('x ="test1" '); 41 | expect(tokens.length).toBe(3); 42 | expect(tokens[2][0]).toBe('test1'); 43 | tokens = new Tokenizer().tokenize('x="test1" '); 44 | expect(tokens.length).toBe(3); 45 | expect(tokens[2][0]).toBe('test1'); 46 | }); 47 | 48 | it('x="hello" + " " + "world"', async () => { 49 | let tokens = new Tokenizer().tokenize('x="hello"+" "+"world"'); 50 | expect(tokens.length).toBe(7); 51 | expect(tokens[2][0]).toBe('hello'); 52 | expect(tokens[4][0]).toBe(' '); 53 | expect(tokens[6][0]).toBe('world'); 54 | 55 | tokens = new Tokenizer().tokenize('x="hello" + " "+"world"'); 56 | expect(tokens.length).toBe(7); 57 | expect(tokens[2][0]).toBe('hello'); 58 | expect(tokens[4][0]).toBe(' '); 59 | expect(tokens[5][0]).toBe('+'); 60 | expect(tokens[6][0]).toBe('world'); 61 | tokens = new Tokenizer().tokenize("x='hello' + ' ' + 'world'"); 62 | expect(tokens.length).toBe(7); 63 | expect(tokens[2][0]).toBe('hello'); 64 | expect(tokens[4][0]).toBe(' '); 65 | expect(tokens[6][0]).toBe('world'); 66 | }); 67 | 68 | it('x=""', async () => { 69 | const tokens = new Tokenizer().tokenize('x=""'); 70 | expect(tokens.length).toBe(3); 71 | expect(tokens[2][0]).toBe(''); 72 | }); 73 | 74 | it('x="" # this is comment', async () => { 75 | const tokens = new Tokenizer().tokenize('x="" # this is comment'); 76 | expect(tokens.length).toBe(4); 77 | expect(tokens[3][0]).toBe(' this is comment'); 78 | }); 79 | 80 | it('x= # this is comment \n 5+6', async () => { 81 | const tokens = new Tokenizer().tokenize('x= # this is comment \n 5+6'); 82 | expect(tokens.length).toBe(6); 83 | expect(tokens[4][0]).toBe('+'); 84 | }); 85 | 86 | it('x = 3.14', async () => { 87 | const tokens = new Tokenizer().tokenize('x = 3.14'); 88 | expect(tokens.length).toBe(3); 89 | expect(tokens[0][0]).toBe('x'); 90 | expect(tokens[1][0]).toBe('='); 91 | expect(tokens[2][0]).toBe(3.14); 92 | }); 93 | 94 | it('x = 3.23*3.14', async () => { 95 | const tokens = new Tokenizer().tokenize('x = 3.23*3.14'); 96 | expect(tokens.length).toBe(5); 97 | expect(tokens[2][0]).toBe(3.23); 98 | expect(tokens[3][0]).toBe('*'); 99 | expect(tokens[4][0]).toBe(3.14); 100 | }); 101 | 102 | it('"""test 1"""', async () => { 103 | const tokens = new Tokenizer().tokenize('"""test 1"""'); 104 | expect(tokens.length).toBe(1); 105 | expect(tokens[0][0]).toBe('test 1'); 106 | }); 107 | 108 | it('x="""test 1"""+"d"', async () => { 109 | const tokens = new Tokenizer().tokenize('x="""test 1"""+"d"'); 110 | expect(tokens.length).toBe(5); 111 | expect(tokens[2][0]).toBe('test 1'); 112 | expect(tokens[4][0]).toBe('d'); 113 | }); 114 | 115 | it('return -1', async () => { 116 | const tokens = new Tokenizer().tokenize('return -1'); 117 | expect(tokens.length).toBe(2); 118 | expect(tokens[0][0]).toBe('return'); 119 | expect(tokens[1][0]).toBe(-1); 120 | }); 121 | 122 | /* 123 | it('3 - -2', async () => { 124 | let tokens = new Tokenizer().tokenize('3 - -2') 125 | expect(tokens.length).toBe(3); 126 | expect(tokens[0][0]).toBe('3'); 127 | expect(tokens[1][0]).toBe('-'); 128 | expect(tokens[2][0]).toBe('-2'); 129 | }); 130 | 131 | it('3-2', async () => { 132 | let tokens = new Tokenizer().tokenize('3-2') 133 | expect(tokens.length).toBe(3); 134 | expect(tokens[0][0]).toBe('3'); 135 | expect(tokens[1][0]).toBe('-'); 136 | expect(tokens[2][0]).toBe('2'); 137 | }); 138 | 139 | it('-3+2', async () => { 140 | let tokens = new Tokenizer().tokenize('-3+2') 141 | expect(tokens.length).toBe(3); 142 | expect(tokens[0][0]).toBe('-3'); 143 | expect(tokens[1][0]).toBe('+'); 144 | expect(tokens[2][0]).toBe('2'); 145 | }); 146 | 147 | it('-3.14+2', async () => { 148 | let tokens = new Tokenizer().tokenize('-3.14+2') 149 | expect(tokens.length).toBe(3); 150 | expect(tokens[0][0]).toBe('-3.14'); 151 | expect(tokens[1][0]).toBe('+'); 152 | expect(tokens[2][0]).toBe('2'); 153 | }); 154 | */ 155 | }); 156 | -------------------------------------------------------------------------------- /src/tokenizer/tokenizer.ts: -------------------------------------------------------------------------------- 1 | import { getTokenType, getTokenValue, Token, TokenTypes } from '../common'; 2 | 3 | const SeparatorsMap: Record = { 4 | '\n': ['\n'], 5 | '=': ['=', '==', '=>'], 6 | 7 | '+': ['+', '++', '+='], 8 | '-': ['-', '--', '-='], 9 | '*': ['*', '**', '*='], 10 | '/': ['/', '//', '/='], 11 | 12 | '.': ['.'], 13 | '?': ['?'], 14 | '!': ['!='], 15 | ':': [':'], 16 | ',': [','], 17 | 18 | '>': ['>', '>='], 19 | '<': ['<', '<=', '<>'], 20 | 21 | '(': ['('], 22 | ')': [')'], 23 | '{': ['{'], 24 | '}': ['}'], 25 | '[': ['['], 26 | ']': [']'] 27 | }; 28 | 29 | const escapeChars = ['"', "'", '\\']; 30 | const Keywords: string[] = ['async', 'def', 'for', 'while', 'if', 'return', 'in']; 31 | 32 | export class Tokenizer { 33 | private _startLine = 1; 34 | private _startColumn = 1; 35 | private _currentLine = 1; 36 | private _currentColumn = 1; 37 | private _tokenText = ''; 38 | private _cursor = 0; 39 | private _script = ''; 40 | 41 | private get tokenText(): string { 42 | return this._tokenText; 43 | } 44 | private set tokenText(value: string) { 45 | if (!this._tokenText && value) { 46 | this._startLine = this._currentLine; 47 | this._startColumn = this._currentColumn; 48 | } 49 | this._tokenText = value; 50 | } 51 | 52 | /** 53 | * Splits script code into a tokens 54 | * @param script A jsPython text 55 | */ 56 | tokenize(script: string): Token[] { 57 | if (!script || !script.length) { 58 | return []; 59 | } 60 | 61 | script = script 62 | // eslint-disable-next-line no-control-regex 63 | .replace(new RegExp('\t', 'g'), ' ') // replace all tabs with 2 spaces 64 | // eslint-disable-next-line no-control-regex 65 | .replace(new RegExp('\r', 'g'), ''); // remove all \r symbols 66 | this._script = script; 67 | 68 | this._cursor = 0; 69 | this._startLine = 1; 70 | this._startColumn = 1; 71 | this._currentLine = 1; 72 | this._currentColumn = 1; 73 | 74 | const tokens: Token[] = []; 75 | 76 | let first = true; 77 | // handle initial spaces 78 | while (script[this._cursor] === '\n') { 79 | this.incrementCursor(); 80 | if (first) { 81 | this._currentLine++; 82 | first = false; 83 | } 84 | this._currentColumn = 1; 85 | } 86 | 87 | do { 88 | const symbol = script[this._cursor]; 89 | 90 | if (symbol == ' ' && this.tokenText.length !== 0) { 91 | this.tokenText = this.processToken(this.tokenText, tokens); 92 | continue; 93 | } else if (SeparatorsMap[symbol] !== undefined && !this.isPartOfNumber(symbol, tokens)) { 94 | // handle numbers with floating point e.g. 3.14 95 | this.tokenText = this.processToken(this.tokenText, tokens); 96 | this.tokenText = symbol; 97 | 98 | const sepsMap = SeparatorsMap[symbol]; 99 | 100 | if (sepsMap.length >= 1) { 101 | // process longer operators 102 | while (sepsMap.includes(this.tokenText + script[this._cursor + 1])) { 103 | this.tokenText += script[this.incrementCursor()]; 104 | } 105 | } 106 | this.tokenText = this.processToken(this.tokenText, tokens, false, TokenTypes.Operator); 107 | } else if (symbol === '#') { 108 | let first = true; 109 | while (script[this.incrementCursor()] !== '\n') { 110 | this.tokenText += script[this._cursor]; 111 | 112 | // correct start column 113 | if (first) { 114 | first = false; 115 | this._startColumn = this._startColumn - 1; 116 | } 117 | 118 | if (this._cursor + 1 >= script.length) break; 119 | } 120 | this.tokenText = this.processToken(this.tokenText, tokens, true, TokenTypes.Comment); 121 | } else if (symbol === '"' || symbol === "'") { 122 | // remember either it is single or double quote 123 | const q = symbol; 124 | // we are not expecting token to be added here. 125 | // it should pass a failt to parser 126 | this.tokenText = this.processToken(this.tokenText, tokens); 127 | 128 | // handle """ comment """" 129 | if (script[this._cursor + 1] === q && script[this._cursor + 2] === q) { 130 | const cLine = this._currentLine; 131 | const cColumn = this._currentColumn; 132 | this.incrementCursor(2); 133 | const passCond = true; 134 | while (passCond) { 135 | this.tokenText += script[this.incrementCursor()]; 136 | if ( 137 | this._cursor + 3 >= script.length || 138 | (script[this._cursor + 1] === q && 139 | script[this._cursor + 2] === q && 140 | script[this._cursor + 3] === q) 141 | ) { 142 | break; 143 | } 144 | } 145 | // a special case when multiline string 146 | this._startLine = cLine; 147 | this._startColumn = cColumn; 148 | 149 | this.incrementCursor(3); 150 | } else { 151 | while (script[this.incrementCursor()] !== q) { 152 | if ( 153 | script[this._cursor] === '\\' && 154 | escapeChars.indexOf(script[this._cursor + 1]) >= 0 155 | ) { 156 | this._cursor++; 157 | } 158 | 159 | this.tokenText += script[this._cursor]; 160 | if (this._cursor + 1 >= script.length) { 161 | throw new Error(`Line ${this._startLine}: End of string missing.`); 162 | } 163 | } 164 | 165 | //start column needs to take into account a begining quote, not just a string 166 | this._startColumn--; 167 | } 168 | 169 | // a special case when empty string 170 | if (this.tokenText.length === 0) { 171 | this._startLine = this._currentLine; 172 | this._startColumn = this._currentColumn; 173 | } 174 | this.tokenText = this.processToken(this.tokenText, tokens, true, TokenTypes.LiteralString); 175 | } else if (symbol != ' ') { 176 | this.tokenText += symbol; 177 | } 178 | } while (this.incrementCursor() < script.length); 179 | 180 | this.processToken(this.tokenText, tokens); 181 | 182 | return tokens; 183 | } 184 | 185 | private incrementCursor(count = 1): number { 186 | for (let i = 0; i < count; i++) { 187 | this._cursor = this._cursor + 1; 188 | if (this._script[this._cursor] === '\n') { 189 | this._currentLine++; 190 | this._currentColumn = 0; 191 | } else { 192 | this._currentColumn++; 193 | } 194 | } 195 | 196 | return this._cursor; 197 | } 198 | 199 | private recognizeToken( 200 | tokenText: string, 201 | type: TokenTypes | null = null 202 | ): { value: string | number | boolean | null; type: TokenTypes } { 203 | let value: string | number | boolean | null = tokenText; 204 | 205 | if (type === null) { 206 | if (tokenText === 'null') { 207 | type = TokenTypes.LiteralNull; 208 | value = null; 209 | } else if (tokenText === 'true' || tokenText === 'false') { 210 | type = TokenTypes.LiteralBool; 211 | value = tokenText === 'true'; 212 | } else if (this.parseNumberOrNull(tokenText) !== null) { 213 | type = TokenTypes.LiteralNumber; 214 | value = this.parseNumberOrNull(tokenText); 215 | } else if (Keywords.indexOf(tokenText) >= 0) { 216 | type = TokenTypes.Keyword; 217 | } else { 218 | type = TokenTypes.Identifier; 219 | } 220 | } 221 | 222 | return { 223 | value: value, 224 | type: type 225 | }; 226 | } 227 | 228 | private processToken( 229 | strToken: string, 230 | tokens: Token[], 231 | allowEmptyString = false, 232 | type: TokenTypes | null = null 233 | ): string { 234 | // ignore empty tokens 235 | if ((!strToken.length && !allowEmptyString) || strToken === '\n') return ''; 236 | 237 | const token = this.recognizeToken(strToken, type); 238 | tokens.push([ 239 | token.value, 240 | Uint16Array.of( 241 | token.type as number, 242 | this._startLine, 243 | this._startColumn, 244 | this._currentLine, 245 | this._currentColumn 246 | ) 247 | ] as Token); 248 | return ''; 249 | } 250 | 251 | private parseNumberOrNull(value: string | number): number | null { 252 | if (typeof value === 'number') { 253 | return value; 254 | } 255 | 256 | if (!value || typeof value !== 'string') { 257 | return null; 258 | } 259 | 260 | value = value.trim(); 261 | 262 | // Just to make sure string contains digits only and '.', ','. Otherwise, parseFloat can incorrectly parse into number 263 | for (let i = value.length - 1; i >= 0; i--) { 264 | const d = value.charCodeAt(i); 265 | if (d < 48 || d > 57) { 266 | // '.' - 46 ',' - 44 '-' - 45(but only first char) 267 | if (d !== 46 && d !== 44 && (d !== 45 || i !== 0)) return null; 268 | } 269 | } 270 | 271 | const res = parseFloat(value); 272 | return !isNaN(res) ? res : null; 273 | } 274 | 275 | private isPartOfNumber(symbol: string, currentTokens: Token[]): boolean { 276 | if (symbol === '-' && !this.tokenText.length) { 277 | // '-' needs to be handled e.g. -3; 2 + -2 etc 278 | const prevToken = currentTokens.length !== 0 ? currentTokens[currentTokens.length - 1] : null; 279 | return ( 280 | prevToken === null || 281 | getTokenValue(prevToken) === 'return' || 282 | (getTokenType(prevToken) === TokenTypes.Operator && getTokenValue(prevToken) !== ')') 283 | ); 284 | } else if (symbol === '.' && this.parseNumberOrNull(this.tokenText) !== null) { 285 | return true; 286 | } 287 | return false; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [ 67 | "src/**/*" 68 | ], 69 | "exclude": [ 70 | "node_modules", 71 | "**/*.spec.ts" 72 | ] 73 | } 74 | --------------------------------------------------------------------------------