├── .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 |
Tokenize
41 |
Parse
42 |
Run
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 |
--------------------------------------------------------------------------------