├── .gitignore ├── .npmignore ├── .prettierrc.json ├── AUTHORS ├── LICENSE ├── PATENTS ├── README.md ├── package-lock.json ├── package.json ├── src ├── benchmark │ ├── eval_benchmark.ts │ └── parse_benchmark.ts ├── index.ts ├── lib │ ├── ast.ts │ ├── ast_factory.ts │ ├── constants.ts │ ├── eval.ts │ ├── parser.ts │ └── tokenizer.ts └── test │ ├── eval_test.ts │ ├── parser_test.ts │ └── tokenizer_test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /benchmark 4 | /lib 5 | /test 6 | /.wireit 7 | 8 | /index.* 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": false 4 | } 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Names should be added to this file with this pattern: 2 | # 3 | # For individuals: 4 | # Name 5 | # 6 | # For organizations: 7 | # Organization 8 | # 9 | 10 | Google Inc. <*@google.com> 11 | Justin Fagnani 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013, the Dart project authors. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Dart Project. 5 | 6 | Google hereby grants to you a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this 8 | section) patent license to make, have made, use, offer to sell, sell, 9 | import, transfer, and otherwise run, modify and propagate the contents 10 | of this implementation of Dart, where such license applies only to 11 | those patent claims, both currently owned by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by 13 | this implementation of Dart. This grant does not include claims that 14 | would be infringed only as a consequence of further modification of 15 | this implementation. If you or your agent or exclusive licensee 16 | institute or order or agree to the institution of patent litigation 17 | against any entity (including a cross-claim or counterclaim in a 18 | lawsuit) alleging that this implementation of Dart or any code 19 | incorporated within this implementation of Dart constitutes direct or 20 | contributory patent infringement, or inducement of patent 21 | infringement, then any patent rights granted to you under this License 22 | for this implementation of Dart shall terminate as of the date such 23 | litigation is filed. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jexpr 2 | 3 | ## Overview 4 | 5 | Jexpr is an expression syntax, parser, and evaluator for JS-like expressions. 6 | 7 | Jexpr is designed for libraries that evaluate user-written expressions, such as 8 | HTML templating engines. Jexpr has a relatively rich syntax, supporting 9 | identifiers, operators, property access, method and function calls, and 10 | literals (including arrays and objects), function literals, assignments, 11 | and pipes. 12 | 13 | Example: 14 | 15 | ```js 16 | (person.title + ' ' + person.getFullName()) | uppercase; 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Installation 22 | 23 | ```bash 24 | npm i jexpr 25 | ``` 26 | 27 | ### Usage 28 | 29 | ```ts 30 | import {parse, EvalAstFactory} from 'jexpr'; 31 | 32 | // An EvalAstFactory produces an AST that can be evaluated 33 | const astFactory = new EvalAstFactory(); 34 | 35 | // parse() returns the AST 36 | const expr = parse('(a + b([1, 2, 3]) * c)', astFactory); 37 | 38 | // evaluate() with a scope object 39 | const result = expr.evaluate({ 40 | a: 42, 41 | b: (o: Array) => o.length, 42 | c: 2, 43 | }); 44 | 45 | console.log(result); // 48 46 | ``` 47 | 48 | ## Features 49 | 50 | ### Fast, small parser 51 | 52 | Jexpr is a hand-written, recursive descent, precedence-climbing parser. It's 53 | simple, fast and small. 54 | 55 | ### Pluggable AST factories 56 | 57 | `parse()` takes an AST factory so that different strategies can be used to 58 | produce ASTs. The default factory creates an AST as defined in `lib/ast.js`. 59 | `lib/eval.js` exports an `EvalAstFactory` that produces evaluatable ASTs. 60 | 61 | ### Null-Safety 62 | 63 | Expressions are generally null-safe. If a subexpression yields `null` or 64 | `undefined`, subsequent property access will return null, rather than throwing 65 | an exception. Property access, method invocation and operators are null-safe. 66 | Passing null to a function that doesn't handle null will not be null safe. 67 | 68 | ## Syntax 69 | 70 | ### Property access 71 | 72 | Properties on the model and in the scope are looked up via simple property 73 | names, like `foo`. Property names are looked up first in the top-level 74 | variables, next in the model, then recursively in parent scopes. Properties on 75 | objects can be access with dot notation like `foo.bar`. 76 | 77 | The keyword `this` always refers to the model if there is one, otherwise `this` 78 | is `null`. If you have model properties and top-level variables with the same 79 | name, you can use `this` to refer to the model property. 80 | 81 | ### Literals 82 | 83 | Jexpr supports number, boolean, string, and map literals. Strings 84 | can use either single or double quotes. 85 | 86 | - `null` and `undefined` 87 | - Numbers: `1`, `1.0` 88 | - Booleans: `true`, `false` 89 | - Strings: `'abc'`, `"xyz"` 90 | - Objects: `{ 'a': 1, 'b': 2 }` 91 | - Arrays: `[1, 2, 3]` 92 | 93 | ### Function and method calls 94 | 95 | If a property is a function in the scope, a method on the model, or a method on 96 | an object, it can be invoked with standard function syntax. Functions and 97 | Methods can take arguments. Arguments can be literals or variables. 98 | 99 | Examples: 100 | 101 | - Top-level function: `myFunction()` 102 | - Top-level function with arguments: `myFunction(a, b, 42)` 103 | - Model method: `aMethod()` 104 | - Method on nested-property: `a.b.anotherMethod()` 105 | 106 | ### Operators 107 | 108 | Jexpr supports the following binary and unary operators: 109 | 110 | - Assignment: `=` 111 | - Arithmetic operators: `+`, `-`, `*`, `/`, `%`, unary `+` and `-` 112 | - Comparison operators: `==`, `!=`, `===`, `!==`, `<=`, `<`, `>`, `>=` 113 | - Boolean operators: `&&`, `||`, unary `!` 114 | - Nullish coalescing: `??` 115 | - Pipeline operators: `|` (legacy) and `|>` (modern) 116 | 117 | Expressions do not support bitwise operators such as `&`, `|`, `<<` and `>>`, or 118 | increment/decrement operators (`++` and `--`) 119 | 120 | #### Assignment 121 | 122 | The left-hand-side expression of the assignment operator (`=`) must be one of an 123 | ID, getter or setter, otherwise an exception is thrown. 124 | 125 | ### Maps 126 | 127 | Maps are sets of key/value pairs. The key can either be a quoted string, or an 128 | identifier: 129 | 130 | Examples: 131 | 132 | - `{'a': 1, 'b': 2}` 133 | - `{a: 1, b: 2}` 134 | 135 | ### Array and Object indexing 136 | 137 | Arrays and objects can be accessed via the index operator: [] 138 | 139 | Examples: 140 | 141 | - `items[2]` 142 | - `people['john']` 143 | 144 | ### Function expressions 145 | 146 | Functions can be written with the arrow function syntax. 147 | 148 | Examples: 149 | 150 | - `() => 3` 151 | - `(a, b) => a + b` 152 | 153 | ### Filters and transformers 154 | 155 | A filter is a function that transforms a value into another, used via the pipe 156 | syntax: `value | filter` Any function that takes exactly one argument can be 157 | used as a filter. 158 | 159 | Example: 160 | 161 | If `person.name` is `"John"`, and a top-level function named `uppercase` has 162 | been registered, then `person.name | uppercase` will have the value `"JOHN"`. 163 | 164 | The pipe syntax is used rather than a regular function call so that we can 165 | support two-way bindings through transformers. A transformer is a filter that 166 | has an inverse function. Two-way transformers are not supported yet. 167 | 168 | ## Acknowedgements 169 | 170 | Jexpr is forked from `polymer-expressions` which is no longer officially 171 | maintained by the Polymer team. The JavaScript version of that library was 172 | ported from the 173 | [Dart library](https://github.com/dart-archive/polymer-expressions) of the same 174 | name, originally used in Polymer.dart. 175 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jexpr", 3 | "version": "1.0.0-pre.9", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "jexpr", 9 | "version": "1.0.0-pre.9", 10 | "license": "BSD 3-Clause", 11 | "devDependencies": { 12 | "@types/benchmark": "^2.1.5", 13 | "@types/node": "^20.11.30", 14 | "benchmark": "^2.1.4", 15 | "prettier": "^3.2.5", 16 | "typescript": "^5.4.2", 17 | "wireit": "^0.14.4" 18 | } 19 | }, 20 | "node_modules/@nodelib/fs.scandir": { 21 | "version": "2.1.5", 22 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 23 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 24 | "dev": true, 25 | "dependencies": { 26 | "@nodelib/fs.stat": "2.0.5", 27 | "run-parallel": "^1.1.9" 28 | }, 29 | "engines": { 30 | "node": ">= 8" 31 | } 32 | }, 33 | "node_modules/@nodelib/fs.stat": { 34 | "version": "2.0.5", 35 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 36 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 37 | "dev": true, 38 | "engines": { 39 | "node": ">= 8" 40 | } 41 | }, 42 | "node_modules/@nodelib/fs.walk": { 43 | "version": "1.2.8", 44 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 45 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 46 | "dev": true, 47 | "dependencies": { 48 | "@nodelib/fs.scandir": "2.1.5", 49 | "fastq": "^1.6.0" 50 | }, 51 | "engines": { 52 | "node": ">= 8" 53 | } 54 | }, 55 | "node_modules/@types/benchmark": { 56 | "version": "2.1.5", 57 | "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-2.1.5.tgz", 58 | "integrity": "sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==", 59 | "dev": true 60 | }, 61 | "node_modules/@types/node": { 62 | "version": "20.11.30", 63 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", 64 | "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", 65 | "dev": true, 66 | "dependencies": { 67 | "undici-types": "~5.26.4" 68 | } 69 | }, 70 | "node_modules/anymatch": { 71 | "version": "3.1.3", 72 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 73 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 74 | "dev": true, 75 | "dependencies": { 76 | "normalize-path": "^3.0.0", 77 | "picomatch": "^2.0.4" 78 | }, 79 | "engines": { 80 | "node": ">= 8" 81 | } 82 | }, 83 | "node_modules/benchmark": { 84 | "version": "2.1.4", 85 | "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", 86 | "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", 87 | "dev": true, 88 | "dependencies": { 89 | "lodash": "^4.17.4", 90 | "platform": "^1.3.3" 91 | } 92 | }, 93 | "node_modules/binary-extensions": { 94 | "version": "2.2.0", 95 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 96 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 97 | "dev": true, 98 | "engines": { 99 | "node": ">=8" 100 | } 101 | }, 102 | "node_modules/braces": { 103 | "version": "3.0.2", 104 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 105 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 106 | "dev": true, 107 | "dependencies": { 108 | "fill-range": "^7.0.1" 109 | }, 110 | "engines": { 111 | "node": ">=8" 112 | } 113 | }, 114 | "node_modules/fast-glob": { 115 | "version": "3.3.2", 116 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 117 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 118 | "dev": true, 119 | "dependencies": { 120 | "@nodelib/fs.stat": "^2.0.2", 121 | "@nodelib/fs.walk": "^1.2.3", 122 | "glob-parent": "^5.1.2", 123 | "merge2": "^1.3.0", 124 | "micromatch": "^4.0.4" 125 | }, 126 | "engines": { 127 | "node": ">=8.6.0" 128 | } 129 | }, 130 | "node_modules/fastq": { 131 | "version": "1.17.1", 132 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 133 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 134 | "dev": true, 135 | "dependencies": { 136 | "reusify": "^1.0.4" 137 | } 138 | }, 139 | "node_modules/fill-range": { 140 | "version": "7.0.1", 141 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 142 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 143 | "dev": true, 144 | "dependencies": { 145 | "to-regex-range": "^5.0.1" 146 | }, 147 | "engines": { 148 | "node": ">=8" 149 | } 150 | }, 151 | "node_modules/fsevents": { 152 | "version": "2.3.2", 153 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 154 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 155 | "dev": true, 156 | "hasInstallScript": true, 157 | "optional": true, 158 | "os": [ 159 | "darwin" 160 | ], 161 | "engines": { 162 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 163 | } 164 | }, 165 | "node_modules/glob-parent": { 166 | "version": "5.1.2", 167 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 168 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 169 | "dev": true, 170 | "dependencies": { 171 | "is-glob": "^4.0.1" 172 | }, 173 | "engines": { 174 | "node": ">= 6" 175 | } 176 | }, 177 | "node_modules/graceful-fs": { 178 | "version": "4.2.11", 179 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 180 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 181 | "dev": true 182 | }, 183 | "node_modules/is-binary-path": { 184 | "version": "2.1.0", 185 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 186 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 187 | "dev": true, 188 | "dependencies": { 189 | "binary-extensions": "^2.0.0" 190 | }, 191 | "engines": { 192 | "node": ">=8" 193 | } 194 | }, 195 | "node_modules/is-extglob": { 196 | "version": "2.1.1", 197 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 198 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 199 | "dev": true, 200 | "engines": { 201 | "node": ">=0.10.0" 202 | } 203 | }, 204 | "node_modules/is-glob": { 205 | "version": "4.0.1", 206 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 207 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 208 | "dev": true, 209 | "dependencies": { 210 | "is-extglob": "^2.1.1" 211 | }, 212 | "engines": { 213 | "node": ">=0.10.0" 214 | } 215 | }, 216 | "node_modules/is-number": { 217 | "version": "7.0.0", 218 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 219 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 220 | "dev": true, 221 | "engines": { 222 | "node": ">=0.12.0" 223 | } 224 | }, 225 | "node_modules/jsonc-parser": { 226 | "version": "3.2.1", 227 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", 228 | "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", 229 | "dev": true 230 | }, 231 | "node_modules/lodash": { 232 | "version": "4.17.21", 233 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 234 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 235 | "dev": true 236 | }, 237 | "node_modules/merge2": { 238 | "version": "1.4.1", 239 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 240 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 241 | "dev": true, 242 | "engines": { 243 | "node": ">= 8" 244 | } 245 | }, 246 | "node_modules/micromatch": { 247 | "version": "4.0.5", 248 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 249 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 250 | "dev": true, 251 | "dependencies": { 252 | "braces": "^3.0.2", 253 | "picomatch": "^2.3.1" 254 | }, 255 | "engines": { 256 | "node": ">=8.6" 257 | } 258 | }, 259 | "node_modules/normalize-path": { 260 | "version": "3.0.0", 261 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 262 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 263 | "dev": true, 264 | "engines": { 265 | "node": ">=0.10.0" 266 | } 267 | }, 268 | "node_modules/picomatch": { 269 | "version": "2.3.1", 270 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 271 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 272 | "dev": true, 273 | "engines": { 274 | "node": ">=8.6" 275 | }, 276 | "funding": { 277 | "url": "https://github.com/sponsors/jonschlinkert" 278 | } 279 | }, 280 | "node_modules/platform": { 281 | "version": "1.3.6", 282 | "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", 283 | "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", 284 | "dev": true 285 | }, 286 | "node_modules/prettier": { 287 | "version": "3.2.5", 288 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 289 | "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 290 | "dev": true, 291 | "bin": { 292 | "prettier": "bin/prettier.cjs" 293 | }, 294 | "engines": { 295 | "node": ">=14" 296 | }, 297 | "funding": { 298 | "url": "https://github.com/prettier/prettier?sponsor=1" 299 | } 300 | }, 301 | "node_modules/proper-lockfile": { 302 | "version": "4.1.2", 303 | "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", 304 | "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", 305 | "dev": true, 306 | "dependencies": { 307 | "graceful-fs": "^4.2.4", 308 | "retry": "^0.12.0", 309 | "signal-exit": "^3.0.2" 310 | } 311 | }, 312 | "node_modules/queue-microtask": { 313 | "version": "1.2.3", 314 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 315 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 316 | "dev": true, 317 | "funding": [ 318 | { 319 | "type": "github", 320 | "url": "https://github.com/sponsors/feross" 321 | }, 322 | { 323 | "type": "patreon", 324 | "url": "https://www.patreon.com/feross" 325 | }, 326 | { 327 | "type": "consulting", 328 | "url": "https://feross.org/support" 329 | } 330 | ] 331 | }, 332 | "node_modules/retry": { 333 | "version": "0.12.0", 334 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 335 | "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", 336 | "dev": true, 337 | "engines": { 338 | "node": ">= 4" 339 | } 340 | }, 341 | "node_modules/reusify": { 342 | "version": "1.0.4", 343 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 344 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 345 | "dev": true, 346 | "engines": { 347 | "iojs": ">=1.0.0", 348 | "node": ">=0.10.0" 349 | } 350 | }, 351 | "node_modules/run-parallel": { 352 | "version": "1.2.0", 353 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 354 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 355 | "dev": true, 356 | "funding": [ 357 | { 358 | "type": "github", 359 | "url": "https://github.com/sponsors/feross" 360 | }, 361 | { 362 | "type": "patreon", 363 | "url": "https://www.patreon.com/feross" 364 | }, 365 | { 366 | "type": "consulting", 367 | "url": "https://feross.org/support" 368 | } 369 | ], 370 | "dependencies": { 371 | "queue-microtask": "^1.2.2" 372 | } 373 | }, 374 | "node_modules/signal-exit": { 375 | "version": "3.0.7", 376 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 377 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 378 | "dev": true 379 | }, 380 | "node_modules/to-regex-range": { 381 | "version": "5.0.1", 382 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 383 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 384 | "dev": true, 385 | "dependencies": { 386 | "is-number": "^7.0.0" 387 | }, 388 | "engines": { 389 | "node": ">=8.0" 390 | } 391 | }, 392 | "node_modules/typescript": { 393 | "version": "5.4.2", 394 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", 395 | "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", 396 | "dev": true, 397 | "bin": { 398 | "tsc": "bin/tsc", 399 | "tsserver": "bin/tsserver" 400 | }, 401 | "engines": { 402 | "node": ">=14.17" 403 | } 404 | }, 405 | "node_modules/undici-types": { 406 | "version": "5.26.5", 407 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 408 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 409 | "dev": true 410 | }, 411 | "node_modules/wireit": { 412 | "version": "0.14.4", 413 | "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.14.4.tgz", 414 | "integrity": "sha512-WNAXEw2cJs1nSRNJNRcPypARZNumgtsRTJFTNpd6turCA6JZ6cEwl4ZU3C1IHc/3IaXoPu9LdxcI5TBTdD6/pg==", 415 | "dev": true, 416 | "workspaces": [ 417 | "vscode-extension", 418 | "website" 419 | ], 420 | "dependencies": { 421 | "braces": "^3.0.2", 422 | "chokidar": "^3.5.3", 423 | "fast-glob": "^3.2.11", 424 | "jsonc-parser": "^3.0.0", 425 | "proper-lockfile": "^4.1.2" 426 | }, 427 | "bin": { 428 | "wireit": "bin/wireit.js" 429 | }, 430 | "engines": { 431 | "node": ">=14.14.0" 432 | } 433 | }, 434 | "node_modules/wireit/node_modules/chokidar": { 435 | "version": "3.6.0", 436 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 437 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 438 | "dev": true, 439 | "dependencies": { 440 | "anymatch": "~3.1.2", 441 | "braces": "~3.0.2", 442 | "glob-parent": "~5.1.2", 443 | "is-binary-path": "~2.1.0", 444 | "is-glob": "~4.0.1", 445 | "normalize-path": "~3.0.0", 446 | "readdirp": "~3.6.0" 447 | }, 448 | "engines": { 449 | "node": ">= 8.10.0" 450 | }, 451 | "funding": { 452 | "url": "https://paulmillr.com/funding/" 453 | }, 454 | "optionalDependencies": { 455 | "fsevents": "~2.3.2" 456 | } 457 | }, 458 | "node_modules/wireit/node_modules/readdirp": { 459 | "version": "3.6.0", 460 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 461 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 462 | "dev": true, 463 | "dependencies": { 464 | "picomatch": "^2.2.1" 465 | }, 466 | "engines": { 467 | "node": ">=8.10.0" 468 | } 469 | } 470 | }, 471 | "dependencies": { 472 | "@nodelib/fs.scandir": { 473 | "version": "2.1.5", 474 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 475 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 476 | "dev": true, 477 | "requires": { 478 | "@nodelib/fs.stat": "2.0.5", 479 | "run-parallel": "^1.1.9" 480 | } 481 | }, 482 | "@nodelib/fs.stat": { 483 | "version": "2.0.5", 484 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 485 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 486 | "dev": true 487 | }, 488 | "@nodelib/fs.walk": { 489 | "version": "1.2.8", 490 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 491 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 492 | "dev": true, 493 | "requires": { 494 | "@nodelib/fs.scandir": "2.1.5", 495 | "fastq": "^1.6.0" 496 | } 497 | }, 498 | "@types/benchmark": { 499 | "version": "2.1.5", 500 | "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-2.1.5.tgz", 501 | "integrity": "sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==", 502 | "dev": true 503 | }, 504 | "@types/node": { 505 | "version": "20.11.30", 506 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", 507 | "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", 508 | "dev": true, 509 | "requires": { 510 | "undici-types": "~5.26.4" 511 | } 512 | }, 513 | "anymatch": { 514 | "version": "3.1.3", 515 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 516 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 517 | "dev": true, 518 | "requires": { 519 | "normalize-path": "^3.0.0", 520 | "picomatch": "^2.0.4" 521 | } 522 | }, 523 | "benchmark": { 524 | "version": "2.1.4", 525 | "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", 526 | "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", 527 | "dev": true, 528 | "requires": { 529 | "lodash": "^4.17.4", 530 | "platform": "^1.3.3" 531 | } 532 | }, 533 | "binary-extensions": { 534 | "version": "2.2.0", 535 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 536 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 537 | "dev": true 538 | }, 539 | "braces": { 540 | "version": "3.0.2", 541 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 542 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 543 | "dev": true, 544 | "requires": { 545 | "fill-range": "^7.0.1" 546 | } 547 | }, 548 | "fast-glob": { 549 | "version": "3.3.2", 550 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 551 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 552 | "dev": true, 553 | "requires": { 554 | "@nodelib/fs.stat": "^2.0.2", 555 | "@nodelib/fs.walk": "^1.2.3", 556 | "glob-parent": "^5.1.2", 557 | "merge2": "^1.3.0", 558 | "micromatch": "^4.0.4" 559 | } 560 | }, 561 | "fastq": { 562 | "version": "1.17.1", 563 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 564 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 565 | "dev": true, 566 | "requires": { 567 | "reusify": "^1.0.4" 568 | } 569 | }, 570 | "fill-range": { 571 | "version": "7.0.1", 572 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 573 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 574 | "dev": true, 575 | "requires": { 576 | "to-regex-range": "^5.0.1" 577 | } 578 | }, 579 | "fsevents": { 580 | "version": "2.3.2", 581 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 582 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 583 | "dev": true, 584 | "optional": true 585 | }, 586 | "glob-parent": { 587 | "version": "5.1.2", 588 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 589 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 590 | "dev": true, 591 | "requires": { 592 | "is-glob": "^4.0.1" 593 | } 594 | }, 595 | "graceful-fs": { 596 | "version": "4.2.11", 597 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 598 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 599 | "dev": true 600 | }, 601 | "is-binary-path": { 602 | "version": "2.1.0", 603 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 604 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 605 | "dev": true, 606 | "requires": { 607 | "binary-extensions": "^2.0.0" 608 | } 609 | }, 610 | "is-extglob": { 611 | "version": "2.1.1", 612 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 613 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 614 | "dev": true 615 | }, 616 | "is-glob": { 617 | "version": "4.0.1", 618 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 619 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 620 | "dev": true, 621 | "requires": { 622 | "is-extglob": "^2.1.1" 623 | } 624 | }, 625 | "is-number": { 626 | "version": "7.0.0", 627 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 628 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 629 | "dev": true 630 | }, 631 | "jsonc-parser": { 632 | "version": "3.2.1", 633 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", 634 | "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", 635 | "dev": true 636 | }, 637 | "lodash": { 638 | "version": "4.17.21", 639 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 640 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 641 | "dev": true 642 | }, 643 | "merge2": { 644 | "version": "1.4.1", 645 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 646 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 647 | "dev": true 648 | }, 649 | "micromatch": { 650 | "version": "4.0.5", 651 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 652 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 653 | "dev": true, 654 | "requires": { 655 | "braces": "^3.0.2", 656 | "picomatch": "^2.3.1" 657 | } 658 | }, 659 | "normalize-path": { 660 | "version": "3.0.0", 661 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 662 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 663 | "dev": true 664 | }, 665 | "picomatch": { 666 | "version": "2.3.1", 667 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 668 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 669 | "dev": true 670 | }, 671 | "platform": { 672 | "version": "1.3.6", 673 | "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", 674 | "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", 675 | "dev": true 676 | }, 677 | "prettier": { 678 | "version": "3.2.5", 679 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 680 | "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 681 | "dev": true 682 | }, 683 | "proper-lockfile": { 684 | "version": "4.1.2", 685 | "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", 686 | "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", 687 | "dev": true, 688 | "requires": { 689 | "graceful-fs": "^4.2.4", 690 | "retry": "^0.12.0", 691 | "signal-exit": "^3.0.2" 692 | } 693 | }, 694 | "queue-microtask": { 695 | "version": "1.2.3", 696 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 697 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 698 | "dev": true 699 | }, 700 | "retry": { 701 | "version": "0.12.0", 702 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 703 | "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", 704 | "dev": true 705 | }, 706 | "reusify": { 707 | "version": "1.0.4", 708 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 709 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 710 | "dev": true 711 | }, 712 | "run-parallel": { 713 | "version": "1.2.0", 714 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 715 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 716 | "dev": true, 717 | "requires": { 718 | "queue-microtask": "^1.2.2" 719 | } 720 | }, 721 | "signal-exit": { 722 | "version": "3.0.7", 723 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 724 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 725 | "dev": true 726 | }, 727 | "to-regex-range": { 728 | "version": "5.0.1", 729 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 730 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 731 | "dev": true, 732 | "requires": { 733 | "is-number": "^7.0.0" 734 | } 735 | }, 736 | "typescript": { 737 | "version": "5.4.2", 738 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", 739 | "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", 740 | "dev": true 741 | }, 742 | "undici-types": { 743 | "version": "5.26.5", 744 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 745 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 746 | "dev": true 747 | }, 748 | "wireit": { 749 | "version": "0.14.4", 750 | "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.14.4.tgz", 751 | "integrity": "sha512-WNAXEw2cJs1nSRNJNRcPypARZNumgtsRTJFTNpd6turCA6JZ6cEwl4ZU3C1IHc/3IaXoPu9LdxcI5TBTdD6/pg==", 752 | "dev": true, 753 | "requires": { 754 | "braces": "^3.0.2", 755 | "chokidar": "^3.5.3", 756 | "fast-glob": "^3.2.11", 757 | "jsonc-parser": "^3.0.0", 758 | "proper-lockfile": "^4.1.2" 759 | }, 760 | "dependencies": { 761 | "chokidar": { 762 | "version": "3.6.0", 763 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 764 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 765 | "dev": true, 766 | "requires": { 767 | "anymatch": "~3.1.2", 768 | "braces": "~3.0.2", 769 | "fsevents": "~2.3.2", 770 | "glob-parent": "~5.1.2", 771 | "is-binary-path": "~2.1.0", 772 | "is-glob": "~4.0.1", 773 | "normalize-path": "~3.0.0", 774 | "readdirp": "~3.6.0" 775 | } 776 | }, 777 | "readdirp": { 778 | "version": "3.6.0", 779 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 780 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 781 | "dev": true, 782 | "requires": { 783 | "picomatch": "^2.2.1" 784 | } 785 | } 786 | } 787 | } 788 | } 789 | } 790 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jexpr", 3 | "version": "1.0.0-pre.9", 4 | "description": "A simple expression parser and evaluator", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/justinfagnani/jexpr.git" 8 | }, 9 | "type": "module", 10 | "main": "index.js", 11 | "exports": { 12 | ".": "./index.js", 13 | "./lib/*": "./lib/*.js" 14 | }, 15 | "files": [ 16 | "index.*", 17 | "lib/*", 18 | "src/*.ts", 19 | "src/lib/*" 20 | ], 21 | "scripts": { 22 | "build": "wireit", 23 | "test": "wireit", 24 | "prepublish": "npm test", 25 | "benchmark": "wireit", 26 | "format": "prettier \"**/*.{json,md,ts}\" --write" 27 | }, 28 | "author": "Justin Fagnani", 29 | "license": "BSD 3-Clause", 30 | "devDependencies": { 31 | "@types/benchmark": "^2.1.5", 32 | "@types/node": "^20.11.30", 33 | "benchmark": "^2.1.4", 34 | "prettier": "^3.2.5", 35 | "typescript": "^5.4.2", 36 | "wireit": "^0.14.4" 37 | }, 38 | "wireit": { 39 | "build": { 40 | "command": "tsc --pretty", 41 | "files": [ 42 | "src/**/*.ts", 43 | "tsconfig.json" 44 | ], 45 | "output": [ 46 | "index.{js,js.map,d.ts,d.ts.map}", 47 | "lib" 48 | ], 49 | "clean": "if-file-deleted" 50 | }, 51 | "test": { 52 | "command": "node --test-reporter spec --test test/*_test.js", 53 | "dependencies": [ 54 | "build" 55 | ], 56 | "files": [], 57 | "output": [] 58 | }, 59 | "benchmark": { 60 | "command": "node benchmark/eval_benchmark", 61 | "dependencies": [ 62 | "build" 63 | ], 64 | "files": [], 65 | "output": [] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/benchmark/eval_benchmark.ts: -------------------------------------------------------------------------------- 1 | import Benchmark, {Event} from 'benchmark'; 2 | import {EvalAstFactory} from '../lib/eval.js'; 3 | import {Parser} from '../lib/parser.js'; 4 | 5 | const suite = new Benchmark.Suite(); 6 | 7 | const astFactory = new EvalAstFactory(); 8 | const identifierExpr = new Parser('foo', astFactory).parse()!; 9 | const complexExpr = new Parser('(a + b([1, 2, 3]) * c)', astFactory).parse()!; 10 | 11 | suite 12 | .add('eval identifier', function () { 13 | return identifierExpr.evaluate({foo: 'bar'}); 14 | }) 15 | .add('native identifier', function () { 16 | const f = function (o: {foo: string}) { 17 | return o.foo; 18 | }; 19 | const result = f({foo: 'bar'}); 20 | return result; 21 | }) 22 | .add('eval complex', function () { 23 | const result = complexExpr.evaluate({ 24 | a: 42, 25 | b: function (o: Array) { 26 | return o.length; 27 | }, 28 | c: 2, 29 | }); 30 | return result; 31 | }) 32 | .add('native complex', function () { 33 | var f = function (a: number, b: (a: Array) => number, c: number) { 34 | return a + b([1, 2, 3]) * c; 35 | }; 36 | var result = f( 37 | 42, 38 | function (o) { 39 | return o.length; 40 | }, 41 | 2, 42 | ); 43 | return result; 44 | }) 45 | .on('cycle', function (event: Event) { 46 | console.log(String(event.target)); 47 | }) 48 | .run({async: true}); 49 | -------------------------------------------------------------------------------- /src/benchmark/parse_benchmark.ts: -------------------------------------------------------------------------------- 1 | import Benchmark, {Event} from 'benchmark'; 2 | import {parse} from '../lib/parser.js'; 3 | import {DefaultAstFactory as AstFactory} from '../lib/ast_factory.js'; 4 | 5 | const suite = new Benchmark.Suite(); 6 | const astFactory = new AstFactory(); 7 | 8 | suite 9 | .add('parse identifier', function () { 10 | return parse('foo', astFactory); 11 | }) 12 | .add('parse complex', function () { 13 | return parse('(a + b([1, 2, 3]) * c)', astFactory); 14 | }) 15 | .on('cycle', function (event: Event) { 16 | console.log(String(event.target)); 17 | }) 18 | .run({async: true}); 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/parser.js'; 2 | export * from './lib/ast_factory.js'; 3 | export * from './lib/eval.js'; 4 | -------------------------------------------------------------------------------- /src/lib/ast.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Portions Copyright (c) 2013, the Dart project authors. 4 | */ 5 | 6 | export type Expression = 7 | | Literal 8 | | Empty 9 | | ID 10 | | Unary 11 | | Binary 12 | | Getter 13 | | Invoke 14 | | Paren 15 | | Index 16 | | Ternary 17 | | Map 18 | | List 19 | | ArrowFunction; 20 | 21 | export type LiteralValue = string | number | boolean | null | undefined; 22 | 23 | export interface Literal { 24 | type: 'Literal'; 25 | value: LiteralValue; 26 | } 27 | 28 | export interface Empty { 29 | type: 'Empty'; 30 | } 31 | 32 | export interface ID { 33 | type: 'ID'; 34 | value: string; 35 | } 36 | 37 | export interface Unary { 38 | type: 'Unary'; 39 | operator: string; 40 | child: Expression; 41 | } 42 | 43 | export interface Binary { 44 | type: 'Binary'; 45 | operator: string; 46 | left: Expression; 47 | right: Expression; 48 | } 49 | 50 | export interface Getter { 51 | type: 'Getter'; 52 | receiver: Expression; 53 | name: string; 54 | } 55 | 56 | export interface Invoke { 57 | type: 'Invoke'; 58 | receiver: Expression; 59 | method?: string; 60 | arguments?: Array; 61 | } 62 | 63 | export interface Paren { 64 | type: 'Paren'; 65 | child: Expression; 66 | } 67 | 68 | export interface Index { 69 | type: 'Index'; 70 | receiver: Expression; 71 | argument?: Expression; 72 | } 73 | 74 | export interface Ternary { 75 | type: 'Ternary'; 76 | condition: Expression; 77 | trueExpr: Expression; 78 | falseExpr: Expression; 79 | } 80 | 81 | export interface Map { 82 | type: 'Map'; 83 | entries?: {[key: string]: Expression | undefined}; 84 | } 85 | 86 | export interface List { 87 | type: 'List'; 88 | items?: Array; 89 | } 90 | 91 | export interface ArrowFunction { 92 | type: 'ArrowFunction'; 93 | params: Array; 94 | body: Expression; 95 | } 96 | -------------------------------------------------------------------------------- /src/lib/ast_factory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Portions Copyright (c) 2013, the Dart project authors. 4 | */ 5 | 6 | import * as ast from './ast.js'; 7 | 8 | export interface AstFactory { 9 | empty(): E; 10 | literal(value: ast.LiteralValue): E; 11 | id(name: string): E; 12 | unary(operator: string, expression: E): E; 13 | binary(left: E, op: string, right: E | undefined): E; 14 | getter(receiver: E, name: string): E; 15 | invoke( 16 | receiver: E, 17 | method: string | undefined, 18 | args: Array | undefined, 19 | ): E; 20 | paren(child: E | undefined): E; 21 | index(receiver: E, argument: E | undefined): E; 22 | ternary(condition: E, trueExpr: E | undefined, falseExpr: E | undefined): E; 23 | map(entries: {[key: string]: E | undefined} | undefined): E; 24 | list(items: Array | undefined): E; 25 | arrowFunction(params: Array, body: E | undefined): E; 26 | } 27 | 28 | export class DefaultAstFactory implements AstFactory { 29 | empty(): ast.Empty { 30 | return {type: 'Empty'}; 31 | } 32 | 33 | // TODO(justinfagnani): just use a JS literal? 34 | literal(value: ast.LiteralValue): ast.Literal { 35 | return { 36 | type: 'Literal', 37 | value, 38 | }; 39 | } 40 | 41 | id(value: string): ast.ID { 42 | return { 43 | type: 'ID', 44 | value, 45 | }; 46 | } 47 | 48 | unary(operator: string, child: ast.Expression): ast.Unary { 49 | return { 50 | type: 'Unary', 51 | operator, 52 | child, 53 | }; 54 | } 55 | 56 | binary( 57 | left: ast.Expression, 58 | operator: string, 59 | right: ast.Expression, 60 | ): ast.Binary { 61 | return { 62 | type: 'Binary', 63 | operator, 64 | left, 65 | right, 66 | }; 67 | } 68 | 69 | getter(receiver: ast.Expression, name: string): ast.Getter { 70 | return { 71 | type: 'Getter', 72 | receiver, 73 | name, 74 | }; 75 | } 76 | 77 | invoke( 78 | receiver: ast.Expression, 79 | method: string | undefined, 80 | args: Array | undefined, 81 | ): ast.Invoke { 82 | // TODO(justinfagnani): do this assertion in the parser 83 | if (args === undefined) { 84 | throw new Error('args'); 85 | } 86 | return { 87 | type: 'Invoke', 88 | receiver, 89 | method, 90 | arguments: args, 91 | }; 92 | } 93 | 94 | paren(child: ast.Expression): ast.Paren { 95 | return { 96 | type: 'Paren', 97 | child, 98 | }; 99 | } 100 | 101 | index( 102 | receiver: ast.Expression, 103 | argument: ast.Expression | undefined, 104 | ): ast.Index { 105 | return { 106 | type: 'Index', 107 | receiver, 108 | argument, 109 | }; 110 | } 111 | 112 | ternary( 113 | condition: ast.Expression, 114 | trueExpr: ast.Expression, 115 | falseExpr: ast.Expression, 116 | ): ast.Ternary { 117 | return { 118 | type: 'Ternary', 119 | condition, 120 | trueExpr, 121 | falseExpr, 122 | }; 123 | } 124 | 125 | map(entries: {[key: string]: ast.Expression}): ast.Map { 126 | return { 127 | type: 'Map', 128 | entries, 129 | }; 130 | } 131 | 132 | list(items: Array): ast.List { 133 | return { 134 | type: 'List', 135 | items, 136 | }; 137 | } 138 | 139 | arrowFunction( 140 | params: Array, 141 | body: ast.Expression, 142 | ): ast.ArrowFunction { 143 | return { 144 | type: 'ArrowFunction', 145 | params, 146 | body, 147 | }; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Portions Copyright (c) 2013, the Dart project authors. 4 | */ 5 | 6 | export const KEYWORDS = ['this']; 7 | export const UNARY_OPERATORS = ['+', '-', '!']; 8 | export const BINARY_OPERATORS = [ 9 | '=', 10 | '+', 11 | '-', 12 | '*', 13 | '/', 14 | '%', 15 | '^', 16 | '==', 17 | '!=', 18 | '>', 19 | '<', 20 | '>=', 21 | '<=', 22 | '||', 23 | '&&', 24 | '??', 25 | '&', 26 | '===', 27 | '!==', 28 | '|', 29 | '|>', 30 | ]; 31 | 32 | export const PRECEDENCE: Record = { 33 | '!': 0, 34 | ':': 0, 35 | ',': 0, 36 | ')': 0, 37 | ']': 0, 38 | '}': 0, 39 | 40 | '|>': 1, 41 | '?': 2, 42 | '??': 3, 43 | '||': 4, 44 | '&&': 5, 45 | '|': 6, 46 | '^': 7, 47 | '&': 8, 48 | 49 | // equality 50 | '!=': 9, 51 | '==': 9, 52 | '!==': 9, 53 | '===': 9, 54 | 55 | // relational 56 | '>=': 10, 57 | '>': 10, 58 | '<=': 10, 59 | '<': 10, 60 | 61 | // additive 62 | '+': 11, 63 | '-': 11, 64 | 65 | // multiplicative 66 | '%': 12, 67 | '/': 12, 68 | '*': 12, 69 | 70 | // postfix 71 | '(': 13, 72 | '[': 13, 73 | '.': 13, 74 | '{': 13, // not sure this is correct 75 | }; 76 | 77 | export const POSTFIX_PRECEDENCE = 13; 78 | -------------------------------------------------------------------------------- /src/lib/eval.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Portions Copyright (c) 2013, the Dart project authors. 4 | */ 5 | 6 | import * as ast from './ast.js'; 7 | import {AstFactory} from './ast_factory.js'; 8 | 9 | const {hasOwn, fromEntries} = Object; 10 | 11 | const _BINARY_OPERATORS: Record any> = { 12 | '+': (a: any, b: any) => a + b, 13 | '-': (a: any, b: any) => a - b, 14 | '*': (a: any, b: any) => a * b, 15 | '/': (a: any, b: any) => a / b, 16 | '%': (a: any, b: any) => a % b, 17 | '==': (a: any, b: any) => a == b, 18 | '!=': (a: any, b: any) => a != b, 19 | '===': (a: any, b: any) => a === b, 20 | '!==': (a: any, b: any) => a !== b, 21 | '>': (a: any, b: any) => a > b, 22 | '>=': (a: any, b: any) => a >= b, 23 | '<': (a: any, b: any) => a < b, 24 | '<=': (a: any, b: any) => a <= b, 25 | '||': (a: any, b: any) => a || b, 26 | '&&': (a: any, b: any) => a && b, 27 | '??': (a: any, b: any) => a ?? b, 28 | '|': (a: any, f: (a: any) => any) => f(a), 29 | '|>': (a: any, f: (a: any) => any) => f(a), 30 | }; 31 | 32 | const _UNARY_OPERATORS: Record any> = { 33 | '+': (a: any) => a, 34 | '-': (a: any) => -a, 35 | '!': (a: any) => !a, 36 | }; 37 | 38 | export interface Scope { 39 | [key: string]: any; 40 | } 41 | 42 | export interface Evaluatable { 43 | evaluate(scope: Scope): any; 44 | getIds(idents: string[]): string[]; 45 | } 46 | 47 | export type Expression = 48 | | Literal 49 | | Empty 50 | | ID 51 | | Unary 52 | | Binary 53 | | Getter 54 | | Invoke 55 | | Index 56 | | Ternary 57 | | Map 58 | | List 59 | | ArrowFunction; 60 | 61 | export interface Literal extends Evaluatable { 62 | type: 'Literal'; 63 | value: ast.LiteralValue; 64 | } 65 | export interface Empty extends Evaluatable { 66 | type: 'Empty'; 67 | } 68 | export interface ID extends Evaluatable { 69 | type: 'ID'; 70 | value: string; 71 | } 72 | export interface Unary extends Evaluatable { 73 | type: 'Unary'; 74 | operator: string; 75 | child: Expression; 76 | } 77 | export interface Binary extends Evaluatable { 78 | type: 'Binary'; 79 | operator: string; 80 | left: Expression; 81 | right: Expression; 82 | } 83 | export interface Getter extends Evaluatable { 84 | type: 'Getter'; 85 | receiver: Expression; 86 | name: string; 87 | } 88 | export interface Invoke extends Evaluatable { 89 | type: 'Invoke'; 90 | receiver: Expression; 91 | method: string | undefined; 92 | arguments: Array | undefined; 93 | } 94 | export interface Index extends Evaluatable { 95 | type: 'Index'; 96 | receiver: Expression; 97 | argument: Expression; 98 | } 99 | export interface Ternary extends Evaluatable { 100 | type: 'Ternary'; 101 | condition: Expression; 102 | trueExpr: Expression; 103 | falseExpr: Expression; 104 | } 105 | export interface Map extends Evaluatable { 106 | type: 'Map'; 107 | entries: {[key: string]: Expression | undefined} | undefined; 108 | } 109 | export interface List extends Evaluatable { 110 | type: 'List'; 111 | items: Array | undefined; 112 | } 113 | export interface ArrowFunction extends Evaluatable { 114 | type: 'ArrowFunction'; 115 | params: Array; 116 | body: Expression; 117 | } 118 | 119 | export class EvalAstFactory implements AstFactory { 120 | empty(): Empty { 121 | // TODO(justinfagnani): return null instead? 122 | return { 123 | type: 'Empty', 124 | evaluate(scope) { 125 | return scope; 126 | }, 127 | getIds(idents) { 128 | return idents; 129 | }, 130 | }; 131 | } 132 | 133 | // TODO(justinfagnani): just use a JS literal? 134 | literal(v: string): Literal { 135 | return { 136 | type: 'Literal', 137 | value: v, 138 | evaluate(_scope) { 139 | return this.value; 140 | }, 141 | getIds(idents) { 142 | return idents; 143 | }, 144 | }; 145 | } 146 | 147 | id(v: string): ID { 148 | return { 149 | type: 'ID', 150 | value: v, 151 | evaluate(scope) { 152 | // TODO(justinfagnani): this prevents access to properties named 'this' 153 | if (this.value === 'this') return scope; 154 | return scope?.[this.value]; 155 | }, 156 | getIds(idents) { 157 | idents.push(this.value); 158 | return idents; 159 | }, 160 | }; 161 | } 162 | 163 | unary(op: string, expr: Expression): Unary { 164 | const f = _UNARY_OPERATORS[op]; 165 | return { 166 | type: 'Unary', 167 | operator: op, 168 | child: expr, 169 | evaluate(scope) { 170 | return f(this.child.evaluate(scope)); 171 | }, 172 | getIds(idents) { 173 | return this.child.getIds(idents); 174 | }, 175 | }; 176 | } 177 | 178 | binary(l: Expression, op: string, r: Expression): Binary { 179 | const f = _BINARY_OPERATORS[op]; 180 | return { 181 | type: 'Binary', 182 | operator: op, 183 | left: l, 184 | right: r, 185 | evaluate(scope) { 186 | if (this.operator === '=') { 187 | if ( 188 | this.left.type !== 'ID' && 189 | this.left.type !== 'Getter' && 190 | this.left.type !== 'Index' 191 | ) { 192 | throw new Error(`Invalid assignment target: ${this.left}`); 193 | } 194 | const value = this.right.evaluate(scope); 195 | let receiver: object | undefined = undefined; 196 | let property!: string; 197 | if (this.left.type === 'Getter') { 198 | receiver = this.left.receiver.evaluate(scope); 199 | property = this.left.name; 200 | } else if (this.left.type === 'Index') { 201 | receiver = this.left.receiver.evaluate(scope); 202 | property = this.left.argument.evaluate(scope); 203 | } else if (this.left.type === 'ID') { 204 | // TODO: the id could be a parameter 205 | receiver = scope; 206 | property = this.left.value; 207 | } 208 | return receiver === undefined 209 | ? undefined 210 | : ((receiver as any)[property] = value); 211 | } 212 | return f(this.left.evaluate(scope), this.right.evaluate(scope)); 213 | }, 214 | getIds(idents) { 215 | this.left.getIds(idents); 216 | this.right.getIds(idents); 217 | return idents; 218 | }, 219 | }; 220 | } 221 | 222 | getter(g: Expression, n: string): Getter { 223 | return { 224 | type: 'Getter', 225 | receiver: g, 226 | name: n, 227 | evaluate(scope) { 228 | return this.receiver.evaluate(scope)?.[this.name]; 229 | }, 230 | getIds(idents) { 231 | this.receiver.getIds(idents); 232 | return idents; 233 | }, 234 | }; 235 | } 236 | 237 | invoke(receiver: Expression, method: string, args: Expression[]): Invoke { 238 | if (method != null && typeof method !== 'string') { 239 | throw new Error('method not a string'); 240 | } 241 | return { 242 | type: 'Invoke', 243 | receiver: receiver, 244 | method: method, 245 | arguments: args, 246 | evaluate(scope) { 247 | const receiver = this.receiver.evaluate(scope); 248 | // TODO(justinfagnani): this might be wrong in cases where we're 249 | // invoking a top-level function rather than a method. If method is 250 | // defined on a nested scope, then we should probably set _this to null. 251 | const _this = this.method ? receiver : scope?.['this'] ?? scope; 252 | const f = this.method ? receiver?.[method] : receiver; 253 | const args = this.arguments ?? []; 254 | const argValues = args.map((a) => a?.evaluate(scope)); 255 | return f?.apply?.(_this, argValues); 256 | }, 257 | getIds(idents) { 258 | this.receiver.getIds(idents); 259 | this.arguments?.forEach((a) => a?.getIds(idents)); 260 | return idents; 261 | }, 262 | }; 263 | } 264 | 265 | paren(e: Expression): Expression { 266 | return e; 267 | } 268 | 269 | index(e: Expression, a: Expression): Index { 270 | return { 271 | type: 'Index', 272 | receiver: e, 273 | argument: a, 274 | evaluate(scope) { 275 | return this.receiver.evaluate(scope)?.[this.argument.evaluate(scope)]; 276 | }, 277 | getIds(idents) { 278 | this.receiver.getIds(idents); 279 | return idents; 280 | }, 281 | }; 282 | } 283 | 284 | ternary(c: Expression, t: Expression, f: Expression): Ternary { 285 | return { 286 | type: 'Ternary', 287 | condition: c, 288 | trueExpr: t, 289 | falseExpr: f, 290 | evaluate(scope) { 291 | const c = this.condition.evaluate(scope); 292 | if (c) { 293 | return this.trueExpr.evaluate(scope); 294 | } else { 295 | return this.falseExpr.evaluate(scope); 296 | } 297 | }, 298 | getIds(idents) { 299 | this.condition.getIds(idents); 300 | this.trueExpr.getIds(idents); 301 | this.falseExpr.getIds(idents); 302 | return idents; 303 | }, 304 | }; 305 | } 306 | 307 | map(entries: {[key: string]: Expression} | undefined): Map { 308 | return { 309 | type: 'Map', 310 | entries: entries, 311 | evaluate(scope) { 312 | const map = {}; 313 | if (entries && this.entries) { 314 | for (const key in entries) { 315 | const val = this.entries[key]; 316 | if (val) { 317 | (map as any)[key] = val.evaluate(scope); 318 | } 319 | } 320 | } 321 | return map; 322 | }, 323 | getIds(idents) { 324 | if (entries && this.entries) { 325 | for (const key in entries) { 326 | const val = this.entries[key]; 327 | if (val) { 328 | val.getIds(idents); 329 | } 330 | } 331 | } 332 | return idents; 333 | }, 334 | }; 335 | } 336 | 337 | // TODO(justinfagnani): if the list is deeply literal 338 | list(l: Array | undefined): List { 339 | return { 340 | type: 'List', 341 | items: l, 342 | evaluate(scope) { 343 | return this.items?.map((a) => a?.evaluate(scope)); 344 | }, 345 | getIds(idents) { 346 | this.items?.forEach((i) => i?.getIds(idents)); 347 | return idents; 348 | }, 349 | }; 350 | } 351 | 352 | arrowFunction(params: string[], body: Expression): Expression { 353 | return { 354 | type: 'ArrowFunction', 355 | params, 356 | body, 357 | evaluate(scope) { 358 | const params = this.params; 359 | const body = this.body; 360 | return function (...args: any[]) { 361 | // Create a nested scope for the function body with a proxy to getting 362 | // and setting parameters on a paramsObj, and setting other 363 | // identifiers on the scope. Without a proxy, attempting to set 364 | // properties on the outer scope would actually set them on the 365 | // inner scope due to JavaScript's assignment semantics. 366 | const paramsObj = fromEntries(params.map((p, i) => [p, args[i]])); 367 | const newScope = new Proxy(scope ?? {}, { 368 | set(target, prop, value) { 369 | if (hasOwn(paramsObj, prop)) { 370 | paramsObj[prop as string] = value; 371 | } 372 | return (target[prop as string] = value); 373 | }, 374 | get(target, prop) { 375 | if (hasOwn(paramsObj, prop)) { 376 | return paramsObj[prop as string]; 377 | } 378 | return target[prop as string]; 379 | }, 380 | }); 381 | return body.evaluate(newScope); 382 | }; 383 | }, 384 | getIds(idents) { 385 | // Only return the _free_ variables in the body. Since arrow function 386 | // parameters are the only way to introduce new variable names, we can 387 | // assume that any variable in the body that isn't a parameter is free. 388 | return this.body 389 | .getIds(idents) 390 | .filter((id) => !this.params.includes(id)); 391 | }, 392 | }; 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/lib/parser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Portions Copyright (c) 2013, the Dart project authors. 4 | */ 5 | 6 | import {ID, Invoke, Expression} from './ast.js'; 7 | import {AstFactory} from './ast_factory.js'; 8 | import { 9 | BINARY_OPERATORS, 10 | KEYWORDS, 11 | POSTFIX_PRECEDENCE, 12 | UNARY_OPERATORS, 13 | } from './constants.js'; 14 | import {Kind, Token, Tokenizer} from './tokenizer.js'; 15 | 16 | export const parse = ( 17 | expr: string, 18 | astFactory: AstFactory, 19 | ): E | undefined => new Parser(expr, astFactory).parse(); 20 | 21 | export class Parser { 22 | private _kind?: Kind; 23 | private _tokenizer: Tokenizer; 24 | private _ast: AstFactory; 25 | private _token?: Token; 26 | private _value?: string; 27 | 28 | constructor(input: string, astFactory: AstFactory) { 29 | this._tokenizer = new Tokenizer(input); 30 | this._ast = astFactory; 31 | } 32 | 33 | parse(): N | undefined { 34 | this._advance(); 35 | return this._parseExpression(); 36 | } 37 | 38 | private _advance(kind?: Kind, value?: string) { 39 | if (!this._matches(kind, value)) { 40 | throw new Error( 41 | `Expected kind ${kind} (${value}), was ${this._token?.kind} (${this._token?.value})`, 42 | ); 43 | } 44 | const t = this._tokenizer.nextToken(); 45 | this._token = t; 46 | this._kind = t?.kind; 47 | this._value = t?.value; 48 | } 49 | 50 | _matches(kind?: Kind, value?: string) { 51 | return !((kind && this._kind !== kind) || (value && this._value !== value)); 52 | } 53 | 54 | private _parseExpression(): N | undefined { 55 | if (!this._token) return this._ast.empty(); 56 | const expr = this._parseUnary(); 57 | return expr === undefined ? undefined : this._parsePrecedence(expr, 0); 58 | } 59 | 60 | // _parsePrecedence and _parseBinary implement the precedence climbing 61 | // algorithm as described in: 62 | // http://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method 63 | private _parsePrecedence(left: N | undefined, precedence: number) { 64 | if (left === undefined) { 65 | throw new Error('Expected left to be defined.'); 66 | } 67 | while (this._token) { 68 | if (this._matches(Kind.GROUPER, '(')) { 69 | const args = this._parseArguments(); 70 | left = this._ast.invoke(left, undefined, args); 71 | } else if (this._matches(Kind.GROUPER, '[')) { 72 | const indexExpr = this._parseIndex(); 73 | left = this._ast.index(left, indexExpr); 74 | } else if (this._matches(Kind.DOT)) { 75 | this._advance(); 76 | const right = this._parseUnary(); 77 | left = this._makeInvokeOrGetter(left, right); 78 | } else if (this._matches(Kind.KEYWORD)) { 79 | break; 80 | } else if ( 81 | this._matches(Kind.OPERATOR) && 82 | this._token.precedence >= precedence 83 | ) { 84 | left = 85 | this._value === '?' 86 | ? this._parseTernary(left) 87 | : this._parseBinary(left, this._token); 88 | } else { 89 | break; 90 | } 91 | } 92 | return left; 93 | } 94 | 95 | private _makeInvokeOrGetter(left: N, right: N | undefined) { 96 | if (right === undefined) { 97 | throw new Error('expected identifier'); 98 | } 99 | if (right.type === 'ID') { 100 | return this._ast.getter(left, (right as ID).value); 101 | } else if ( 102 | right.type === 'Invoke' && 103 | (right as Invoke).receiver.type === 'ID' 104 | ) { 105 | const method = (right as Invoke).receiver as ID; 106 | return this._ast.invoke( 107 | left, 108 | method.value, 109 | (right as Invoke).arguments as any, 110 | ); 111 | } else { 112 | throw new Error(`expected identifier: ${right}`); 113 | } 114 | } 115 | 116 | private _parseBinary(left: N, op: Token) { 117 | if (BINARY_OPERATORS.indexOf(op.value) === -1) { 118 | throw new Error(`unknown operator: ${op.value}`); 119 | } 120 | this._advance(); 121 | let right = this._parseUnary(); 122 | while ( 123 | (this._kind === Kind.OPERATOR || 124 | this._kind === Kind.DOT || 125 | this._kind === Kind.GROUPER) && 126 | this._token!.precedence > op.precedence 127 | ) { 128 | right = this._parsePrecedence(right, this._token!.precedence); 129 | } 130 | return this._ast.binary(left, op.value, right); 131 | } 132 | 133 | private _parseUnary(): N | undefined { 134 | if (this._matches(Kind.OPERATOR)) { 135 | const value = this._value; 136 | this._advance(); 137 | // handle unary + and - on numbers as part of the literal, not as a 138 | // unary operator 139 | if (value === '+' || value === '-') { 140 | if (this._matches(Kind.INTEGER)) { 141 | return this._parseInteger(value); 142 | } else if (this._matches(Kind.DECIMAL)) { 143 | return this._parseDecimal(value); 144 | } 145 | } 146 | if (UNARY_OPERATORS.indexOf(value!) === -1) 147 | throw new Error(`unexpected token: ${value}`); 148 | const expr = this._parsePrecedence( 149 | this._parsePrimary(), 150 | POSTFIX_PRECEDENCE, 151 | ); 152 | return this._ast.unary(value!, expr); 153 | } 154 | return this._parsePrimary(); 155 | } 156 | 157 | private _parseTernary(condition: N) { 158 | this._advance(Kind.OPERATOR, '?'); 159 | const trueExpr = this._parseExpression(); 160 | this._advance(Kind.COLON); 161 | const falseExpr = this._parseExpression(); 162 | return this._ast.ternary(condition, trueExpr, falseExpr); 163 | } 164 | 165 | private _parsePrimary() { 166 | switch (this._kind) { 167 | case Kind.KEYWORD: 168 | const keyword = this._value!; 169 | if (keyword === 'this') { 170 | this._advance(); 171 | // TODO(justin): return keyword node 172 | return this._ast.id(keyword); 173 | } else if (KEYWORDS.indexOf(keyword) !== -1) { 174 | throw new Error(`unexpected keyword: ${keyword}`); 175 | } 176 | throw new Error(`unrecognized keyword: ${keyword}`); 177 | case Kind.IDENTIFIER: 178 | return this._parseInvokeOrIdentifier(); 179 | case Kind.STRING: 180 | return this._parseString(); 181 | case Kind.INTEGER: 182 | return this._parseInteger(); 183 | case Kind.DECIMAL: 184 | return this._parseDecimal(); 185 | case Kind.GROUPER: 186 | if (this._value === '(') { 187 | return this._parseParenOrFunction(); 188 | } else if (this._value === '{') { 189 | return this._parseMap(); 190 | } else if (this._value === '[') { 191 | return this._parseList(); 192 | } 193 | return undefined; 194 | case Kind.COLON: 195 | throw new Error('unexpected token ":"'); 196 | default: 197 | return undefined; 198 | } 199 | } 200 | 201 | private _parseList() { 202 | const items: (N | undefined)[] = []; 203 | do { 204 | this._advance(); 205 | if (this._matches(Kind.GROUPER, ']')) break; 206 | items.push(this._parseExpression()); 207 | } while (this._matches(Kind.COMMA)); 208 | this._advance(Kind.GROUPER, ']'); 209 | return this._ast.list(items); 210 | } 211 | 212 | private _parseMap() { 213 | const entries: {[key: string]: N | undefined} = {}; 214 | do { 215 | this._advance(); 216 | if (this._matches(Kind.GROUPER, '}')) break; 217 | const key = this._value!; 218 | if (this._matches(Kind.STRING) || this._matches(Kind.IDENTIFIER)) { 219 | this._advance(); 220 | } 221 | this._advance(Kind.COLON); 222 | entries[key] = this._parseExpression(); 223 | } while (this._matches(Kind.COMMA)); 224 | this._advance(Kind.GROUPER, '}'); 225 | return this._ast.map(entries); 226 | } 227 | 228 | private _parseInvokeOrIdentifier() { 229 | const value = this._value; 230 | if (value === 'true') { 231 | this._advance(); 232 | return this._ast.literal(true); 233 | } 234 | if (value === 'false') { 235 | this._advance(); 236 | return this._ast.literal(false); 237 | } 238 | if (value === 'null') { 239 | this._advance(); 240 | return this._ast.literal(null); 241 | } 242 | if (value === 'undefined') { 243 | this._advance(); 244 | return this._ast.literal(undefined); 245 | } 246 | const identifier = this._parseIdentifier(); 247 | const args = this._parseArguments(); 248 | return !args ? identifier : this._ast.invoke(identifier, undefined, args); 249 | } 250 | 251 | private _parseIdentifier() { 252 | if (!this._matches(Kind.IDENTIFIER)) { 253 | throw new Error(`expected identifier: ${this._value}`); 254 | } 255 | const value = this._value; 256 | this._advance(); 257 | return this._ast.id(value!); 258 | } 259 | 260 | private _parseArguments() { 261 | if (!this._matches(Kind.GROUPER, '(')) { 262 | return undefined; 263 | } 264 | const args: Array = []; 265 | do { 266 | this._advance(); 267 | if (this._matches(Kind.GROUPER, ')')) { 268 | break; 269 | } 270 | const expr = this._parseExpression(); 271 | args.push(expr); 272 | } while (this._matches(Kind.COMMA)); 273 | this._advance(Kind.GROUPER, ')'); 274 | return args; 275 | } 276 | 277 | private _parseIndex() { 278 | // console.assert(this._matches(Kind.GROUPER, '[')); 279 | this._advance(); 280 | const expr = this._parseExpression(); 281 | this._advance(Kind.GROUPER, ']'); 282 | return expr; 283 | } 284 | 285 | private _parseParenOrFunction() { 286 | const expressions = this._parseArguments(); 287 | if (this._matches(Kind.ARROW)) { 288 | this._advance(); 289 | const body = this._parseExpression(); 290 | const params = expressions?.map((e) => (e as ID).value) ?? []; 291 | return this._ast.arrowFunction(params, body); 292 | } else { 293 | return this._ast.paren(expressions![0]); 294 | } 295 | } 296 | 297 | private _parseString() { 298 | const value = this._ast.literal(this._value!); 299 | this._advance(); 300 | return value; 301 | } 302 | 303 | private _parseInteger(prefix: string = '') { 304 | const value = this._ast.literal(parseInt(`${prefix}${this._value}`, 10)); 305 | this._advance(); 306 | return value; 307 | } 308 | 309 | private _parseDecimal(prefix: string = '') { 310 | const value = this._ast.literal(parseFloat(`${prefix}${this._value}`)); 311 | this._advance(); 312 | return value; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/lib/tokenizer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Portions Copyright (c) 2013, the Dart project authors. 4 | */ 5 | 6 | import {KEYWORDS, POSTFIX_PRECEDENCE, PRECEDENCE} from './constants.js'; 7 | 8 | const _TWO_CHAR_OPS = ['==', '!=', '<=', '>=', '||', '&&', '??', '|>']; 9 | const _THREE_CHAR_OPS = ['===', '!==']; 10 | 11 | export interface Token { 12 | kind: Kind; 13 | value: string; 14 | precedence: number; 15 | } 16 | 17 | export enum Kind { 18 | STRING = 1, 19 | IDENTIFIER = 2, 20 | DOT = 3, 21 | COMMA = 4, 22 | COLON = 5, 23 | INTEGER = 6, 24 | DECIMAL = 7, 25 | OPERATOR = 8, 26 | GROUPER = 9, 27 | KEYWORD = 10, 28 | ARROW = 11, 29 | } 30 | 31 | export const token = (kind: Kind, value: string, precedence: number = 0) => ({ 32 | kind, 33 | value, 34 | precedence, 35 | }); 36 | 37 | const _isWhitespace = (ch: number) => 38 | ch === 9 /* \t */ || 39 | ch === 10 /* \n */ || 40 | ch === 13 /* \r */ || 41 | ch === 32; /* space */ 42 | 43 | // TODO(justinfagnani): allow code points > 127 44 | const _isIdentOrKeywordStart = (ch: number) => 45 | ch === 95 /* _ */ || 46 | ch === 36 /* $ */ || 47 | // ch &= ~32 puts ch into the range [65,90] [A-Z] only if ch was already in 48 | // the that range or in the range [97,122] [a-z]. We must mutate ch only after 49 | // checking other characters, thus the comma operator. 50 | ((ch &= ~32), 65 /* A */ <= ch && ch <= 90); /* Z */ 51 | 52 | // TODO(justinfagnani): allow code points > 127 53 | const _isIdentifier = (ch: number) => 54 | _isIdentOrKeywordStart(ch) || _isNumber(ch); 55 | 56 | const _isKeyword = (str: string) => KEYWORDS.indexOf(str) !== -1; 57 | 58 | const _isQuote = (ch: number) => ch === 34 /* " */ || ch === 39; /* ' */ 59 | 60 | const _isNumber = (ch: number) => 48 /* 0 */ <= ch && ch <= 57; /* 9 */ 61 | 62 | const _isOperator = (ch: number) => 63 | ch === 43 /* + */ || 64 | ch === 45 /* - */ || 65 | ch === 42 /* * */ || 66 | ch === 47 /* / */ || 67 | ch === 33 /* ! */ || 68 | ch === 38 /* & */ || 69 | ch === 37 /* % */ || 70 | ch === 60 /* < */ || 71 | ch === 61 /* = */ || 72 | ch === 62 /* > */ || 73 | ch === 63 /* ? */ || 74 | ch === 94 /* ^ */ || 75 | ch === 124; /* | */ 76 | 77 | const _isGrouper = (ch: number) => 78 | ch === 40 /* ( */ || 79 | ch === 41 /* ) */ || 80 | ch === 91 /* [ */ || 81 | ch === 93 /* ] */ || 82 | ch === 123 /* { */ || 83 | ch === 125; /* } */ 84 | 85 | const _escapeString = (str: string) => 86 | str.replace(/\\(.)/g, (_match, group) => { 87 | switch (group) { 88 | case 'n': 89 | return '\n'; 90 | case 'r': 91 | return '\r'; 92 | case 't': 93 | return '\t'; 94 | case 'b': 95 | return '\b'; 96 | case 'f': 97 | return '\f'; 98 | default: 99 | return group; 100 | } 101 | }); 102 | 103 | export class Tokenizer { 104 | private _input: string; 105 | private _index = -1; 106 | private _tokenStart = 0; 107 | private _next?: number; 108 | 109 | constructor(input: string) { 110 | this._input = input; 111 | this._advance(); 112 | } 113 | 114 | nextToken() { 115 | while (_isWhitespace(this._next!)) { 116 | this._advance(true); 117 | } 118 | if (_isQuote(this._next!)) return this._tokenizeString(); 119 | if (_isIdentOrKeywordStart(this._next!)) { 120 | return this._tokenizeIdentOrKeyword(); 121 | } 122 | if (_isNumber(this._next!)) return this._tokenizeNumber(); 123 | if (this._next === 46 /* . */) return this._tokenizeDot(); 124 | if (this._next === 44 /* , */) return this._tokenizeComma(); 125 | if (this._next === 58 /* : */) return this._tokenizeColon(); 126 | if (_isOperator(this._next!)) return this._tokenizeOperator(); 127 | if (_isGrouper(this._next!)) return this._tokenizeGrouper(); 128 | // no match, should be end of input 129 | this._advance(); 130 | if (this._next !== undefined) { 131 | throw new Error(`Expected end of input, got ${this._next}`); 132 | } 133 | return undefined; 134 | } 135 | 136 | private _advance(resetTokenStart?: boolean) { 137 | this._index++; 138 | if (this._index < this._input.length) { 139 | this._next = this._input.charCodeAt(this._index); 140 | if (resetTokenStart === true) { 141 | this._tokenStart = this._index; 142 | } 143 | } else { 144 | this._next = undefined; 145 | } 146 | } 147 | 148 | private _getValue(lookahead: number = 0) { 149 | const v = this._input.substring(this._tokenStart, this._index + lookahead); 150 | if (lookahead === 0) { 151 | this._clearValue(); 152 | } 153 | return v; 154 | } 155 | 156 | private _clearValue() { 157 | this._tokenStart = this._index; 158 | } 159 | 160 | private _tokenizeString() { 161 | const _us = 'unterminated string'; 162 | const quoteChar = this._next; 163 | this._advance(true); 164 | while (this._next !== quoteChar) { 165 | if (this._next === undefined) throw new Error(_us); 166 | if (this._next === 92 /* \ */) { 167 | this._advance(); 168 | if (this._next === undefined) throw new Error(_us); 169 | } 170 | this._advance(); 171 | } 172 | const t = token(Kind.STRING, _escapeString(this._getValue())); 173 | this._advance(); 174 | return t; 175 | } 176 | 177 | private _tokenizeIdentOrKeyword() { 178 | // This do/while loops assumes _isIdentifier(this._next!), so it must only 179 | // be called if _isIdentOrKeywordStart(this._next!) has returned true. 180 | do { 181 | this._advance(); 182 | } while (_isIdentifier(this._next!)); 183 | const value = this._getValue(); 184 | const kind = _isKeyword(value) ? Kind.KEYWORD : Kind.IDENTIFIER; 185 | return token(kind, value); 186 | } 187 | 188 | private _tokenizeNumber() { 189 | // This do/while loops assumes _isNumber(this._next!), so it must only 190 | // be called if _isNumber(this._next!) has returned true. 191 | do { 192 | this._advance(); 193 | } while (_isNumber(this._next!)); 194 | if (this._next === 46 /* . */) return this._tokenizeDot(); 195 | return token(Kind.INTEGER, this._getValue()); 196 | } 197 | 198 | private _tokenizeDot() { 199 | this._advance(); 200 | if (_isNumber(this._next!)) return this._tokenizeFraction(); 201 | this._clearValue(); 202 | return token(Kind.DOT, '.', POSTFIX_PRECEDENCE); 203 | } 204 | 205 | private _tokenizeComma() { 206 | this._advance(true); 207 | return token(Kind.COMMA, ','); 208 | } 209 | 210 | private _tokenizeColon() { 211 | this._advance(true); 212 | return token(Kind.COLON, ':'); 213 | } 214 | 215 | private _tokenizeFraction() { 216 | // This do/while loops assumes _isNumber(this._next!), so it must only 217 | // be called if _isNumber(this._next!) has returned true. 218 | do { 219 | this._advance(); 220 | } while (_isNumber(this._next!)); 221 | return token(Kind.DECIMAL, this._getValue()); 222 | } 223 | 224 | private _tokenizeOperator() { 225 | this._advance(); 226 | let op = this._getValue(2); 227 | 228 | if (_THREE_CHAR_OPS.indexOf(op) !== -1) { 229 | this._advance(); 230 | this._advance(); 231 | } else { 232 | op = this._getValue(1); 233 | if (op === '=>') { 234 | this._advance(); 235 | return token(Kind.ARROW, op); 236 | } 237 | if (_TWO_CHAR_OPS.indexOf(op) !== -1) { 238 | this._advance(); 239 | } 240 | } 241 | op = this._getValue(); 242 | return token(Kind.OPERATOR, op, PRECEDENCE[op]); 243 | } 244 | 245 | private _tokenizeGrouper() { 246 | const value = String.fromCharCode(this._next!); 247 | const t = token(Kind.GROUPER, value, PRECEDENCE[value]); 248 | this._advance(true); 249 | return t; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/test/eval_test.ts: -------------------------------------------------------------------------------- 1 | import {test, describe as suite} from 'node:test'; 2 | import assert from 'node:assert'; 3 | 4 | import * as evaluate from '../lib/eval.js'; 5 | import * as parser from '../lib/parser.js'; 6 | 7 | const Parser = parser.Parser; 8 | 9 | const astFactory = new evaluate.EvalAstFactory(); 10 | 11 | function expectEval(s: string, expected: any, scope?: evaluate.Scope) { 12 | const expr = new Parser(s, astFactory).parse(); 13 | const result = expr!.evaluate(scope!); 14 | assert.deepEqual(result, expected); 15 | } 16 | 17 | function doEval(s: string, scope?: evaluate.Scope) { 18 | const expr = new Parser(s, astFactory).parse(); 19 | return expr!.evaluate(scope!); 20 | } 21 | 22 | suite('eval', function () { 23 | test('should return the model for an empty expression', function () { 24 | expectEval('', {foo: 'bar'}, {foo: 'bar'}); 25 | }); 26 | 27 | test('should handle the "this" keyword', function () { 28 | expectEval('this', {foo: 'bar'}, {foo: 'bar'}); 29 | expectEval('this.name', 'foo', {name: 'foo'}); 30 | expectEval('this["a"]', 'x', {a: 'x'}); 31 | }); 32 | 33 | test('should return a literal int', function () { 34 | expectEval('1', 1); 35 | expectEval('+1', 1); 36 | expectEval('-1', -1); 37 | }); 38 | 39 | test('should return a literal double', function () { 40 | expectEval('1.2', 1.2); 41 | expectEval('+1.2', 1.2); 42 | expectEval('-1.2', -1.2); 43 | }); 44 | 45 | test('should return a literal string', function () { 46 | expectEval('"hello"', 'hello'); 47 | expectEval("'hello'", 'hello'); 48 | }); 49 | 50 | test('should return a literal boolean', function () { 51 | expectEval('true', true); 52 | expectEval('false', false); 53 | }); 54 | 55 | test('should return a literal null', function () { 56 | expectEval('null', null); 57 | }); 58 | 59 | test('should return a literal list', function () { 60 | expectEval('[1, 2, 3]', [1, 2, 3]); 61 | }); 62 | 63 | test('should return a literal map', function () { 64 | expectEval('{"a": 1}', {a: 1}); 65 | expectEval('{a: 1}', {a: 1}); 66 | }); 67 | 68 | test('should access properties of a literal map', function () { 69 | expectEval('{"a": 1}.a', 1); 70 | expectEval('{a: 1}.a', 1); 71 | }); 72 | 73 | test('should return undefined for deeply nil properties of the scope', function () { 74 | expectEval('data.nullable', undefined, {}); 75 | expectEval('data.nullable', undefined, {data: null}); 76 | expectEval('data.nullable.nulled', undefined, {data: null}); 77 | expectEval('data.nullable.nulled.right', undefined, {data: null}); 78 | // test the opposite case 79 | expectEval('data.nonnullable.nonnulled.right', true, { 80 | data: {nonnullable: {nonnulled: {right: true}}}, 81 | }); 82 | }); 83 | 84 | test('should evaluate unary operators', function () { 85 | expectEval('+a', 2, {a: 2}); 86 | expectEval('-a', -2, {a: 2}); 87 | expectEval('!a', false, {a: true}); 88 | }); 89 | 90 | test('should evaluate ID assignment', function () { 91 | const scope = {foo: 0}; 92 | expectEval('foo = 3', 3, scope); 93 | assert.equal(scope.foo, 3); 94 | }); 95 | 96 | test('should evaluate getter assignment', function () { 97 | const scope = {foo: {bar: 0}}; 98 | expectEval('foo.bar = 3', 3, scope); 99 | assert.equal(scope.foo.bar, 3); 100 | }); 101 | 102 | test('should evaluate index assignment', function () { 103 | const scope = {foo: {bar: 0}}; 104 | expectEval("foo['bar'] = 3", 3, scope); 105 | assert.equal(scope.foo.bar, 3); 106 | }); 107 | 108 | test('should evaluate binary operators', function () { 109 | expectEval('1 + 2', 3); 110 | expectEval('2 - 1', 1); 111 | expectEval('4 / 2', 2); 112 | expectEval('2 * 3', 6); 113 | expectEval('5 % 2', 1); 114 | expectEval('5 % -2', 1); 115 | expectEval('-5 % 2', -1); 116 | 117 | expectEval('1 == 1', true); 118 | expectEval('1 == 2', false); 119 | expectEval('1 == null', false); 120 | expectEval('1 != 1', false); 121 | expectEval('1 != 2', true); 122 | expectEval('1 != null', true); 123 | 124 | const x = {}; 125 | const y = {}; 126 | expectEval('x === y', true, {x: x, y: x}); 127 | expectEval('x !== y', false, {x: x, y: x}); 128 | expectEval('x === y', false, {x: x, y: y}); 129 | expectEval('x !== y', true, {x: x, y: y}); 130 | 131 | expectEval('1 > 1', false); 132 | expectEval('1 > 2', false); 133 | expectEval('2 > 1', true); 134 | expectEval('1 >= 1', true); 135 | expectEval('1 >= 2', false); 136 | expectEval('2 >= 1', true); 137 | expectEval('1 < 1', false); 138 | expectEval('1 < 2', true); 139 | expectEval('2 < 1', false); 140 | expectEval('1 <= 1', true); 141 | expectEval('1 <= 2', true); 142 | expectEval('2 <= 1', false); 143 | 144 | expectEval('true || true', true); 145 | expectEval('true || false', true); 146 | expectEval('false || true', true); 147 | expectEval('false || false', false); 148 | 149 | expectEval('true && true', true); 150 | expectEval('true && false', false); 151 | expectEval('false && true', false); 152 | expectEval('false && false', false); 153 | 154 | expectEval('x ?? 2', 1, {x: 1}); 155 | expectEval('x ?? 2', 2); 156 | 157 | expectEval('x | f', 2, {x: 1, f: (x: number) => x * 2}); 158 | expectEval('x |> f', 2, {x: 1, f: (x: number) => x * 2}); 159 | expectEval('x |> a.f', 2, {x: 1, a: {f: (x: number) => x * 2}}); 160 | expectEval('x |> f |> g', 3, { 161 | x: 1, 162 | f: (x: number) => x * 2, 163 | g: (x: number) => x + 1, 164 | }); 165 | }); 166 | 167 | test('should evaulate ternary operators', function () { 168 | expectEval('true ? 1 : 2', 1); 169 | expectEval('false ? 1 : 2', 2); 170 | expectEval('true ? true ? 1 : 2 : 3', 1); 171 | expectEval('true ? false ? 1 : 2 : 3', 2); 172 | expectEval('false ? true ? 1 : 2 : 3', 3); 173 | expectEval('false ? 1 : true ? 2 : 3', 2); 174 | expectEval('false ? 1 : false ? 2 : 3', 3); 175 | expectEval('null ? 1 : 2', 2); 176 | }); 177 | 178 | test('should evaluate function literals', function () { 179 | assert.equal(doEval('() => 3')(), 3); 180 | assert.equal(doEval('(a, b) => a + b')(1, 2), 3); 181 | expectEval('((a, b) => a + b)(1, 2)', 3); 182 | expectEval('arr.map((a) => a * 2)', [2, 4, 6], {arr: [1, 2, 3]}); 183 | const scope = {foo: 3}; 184 | doEval('() => this.foo = 4', scope)(); 185 | assert.equal(scope.foo, 4); 186 | assert.equal(doEval('(a) => a', {a: 5})(3), 3); 187 | assert.equal(doEval('(a) => ((a) => a)(2)', {a: 5})(3), 2); 188 | expectEval('(() => this.foo)()', 3, {foo: 3}); 189 | }); 190 | 191 | test('should call functions in scope', function () { 192 | const foo = { 193 | x: function () { 194 | return 42; 195 | }, 196 | y: function (i: any, j: any) { 197 | return i * j; 198 | }, 199 | name: 'fred', 200 | }; 201 | expectEval('x()', foo.x(), foo); 202 | expectEval('name', foo.name, foo); 203 | expectEval('y(5, 10)', 50, foo); 204 | }); 205 | 206 | test('should call functions with `this` as scope', function () { 207 | const o = { 208 | foo: 'bar', 209 | checkThis(this: {foo: 'bar'}) { 210 | return this.foo === 'bar'; 211 | }, 212 | }; 213 | expectEval('checkThis()', true, o); 214 | }); 215 | 216 | test('should call functions with `this` in nested scopes', function () { 217 | const model = {}; 218 | const o = { 219 | this: model, 220 | getThis(this: any) { 221 | return this; 222 | }, 223 | }; 224 | const scope = Object.create(o); 225 | expectEval('getThis()', model, scope); 226 | }); 227 | 228 | test('should call methods with `this` as receiver', function () { 229 | const scope = { 230 | foo: { 231 | getThis(this: any) { 232 | return this; 233 | }, 234 | }, 235 | }; 236 | expectEval('foo.getThis()', scope.foo, scope); 237 | }); 238 | 239 | test('should invoke chained methods', function () { 240 | const foo = { 241 | a: function () { 242 | return function () { 243 | return 1; 244 | }; 245 | }, 246 | x: function () { 247 | return 42; 248 | }, 249 | name: 'fred', 250 | }; 251 | expectEval('name.length', foo.name.length, foo); 252 | expectEval('x().toString()', foo.x().toString(), foo); 253 | expectEval('name.substring(2)', foo.name.substring(2), foo); 254 | expectEval('a()()', 1, foo); 255 | }); 256 | 257 | test('should not error on undefined methods', function () { 258 | const foo = { 259 | name: 'fred', 260 | }; 261 | expectEval('x()', undefined, foo); 262 | expectEval('x().toString()', undefined, foo); 263 | expectEval('x(name)', undefined, foo); 264 | expectEval('x()()', undefined, foo); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /src/test/parser_test.ts: -------------------------------------------------------------------------------- 1 | import {test, describe as suite} from 'node:test'; 2 | import assert from 'node:assert'; 3 | 4 | import * as ast_factory from '../lib/ast_factory.js'; 5 | import * as parser from '../lib/parser.js'; 6 | 7 | const Parser = parser.Parser; 8 | const astFactory = new ast_factory.DefaultAstFactory(); 9 | 10 | function expectParse(s: string, e: any) { 11 | const p = new Parser(s, astFactory).parse(); 12 | assert.deepEqual(p, e); 13 | } 14 | 15 | suite('Parser', function () { 16 | test('can be constructed', function () { 17 | new Parser('', astFactory); 18 | }); 19 | 20 | test('should parse an empty expression', function () { 21 | expectParse('', astFactory.empty()); 22 | }); 23 | 24 | test('should parse an identifier', function () { 25 | expectParse('abc', astFactory.id('abc')); 26 | }); 27 | 28 | test('should parse a string literal', function () { 29 | expectParse('"abc"', astFactory.literal('abc')); 30 | }); 31 | 32 | test('should parse a bool literal', function () { 33 | expectParse('true', astFactory.literal(true)); 34 | expectParse('false', astFactory.literal(false)); 35 | }); 36 | 37 | test('should parse a null literal', function () { 38 | expectParse('null', astFactory.literal(null)); 39 | }); 40 | 41 | test('should parse an undefined literal', function () { 42 | expectParse('undefined', astFactory.literal(undefined)); 43 | }); 44 | 45 | test('should parse an integer literal', function () { 46 | expectParse('123', astFactory.literal(123)); 47 | }); 48 | 49 | test('should parse a double literal', function () { 50 | expectParse('1.23', astFactory.literal(1.23)); 51 | }); 52 | 53 | test('should parse a positive double literal', function () { 54 | expectParse('+1.23', astFactory.literal(1.23)); 55 | }); 56 | 57 | test('should parse a negative double literal', function () { 58 | expectParse('-1.23', astFactory.literal(-1.23)); 59 | }); 60 | 61 | test('should parse unary operators', function () { 62 | expectParse(`!a`, astFactory.unary('!', astFactory.id('a'))); 63 | expectParse(`-a`, astFactory.unary('-', astFactory.id('a'))); 64 | }); 65 | 66 | test('should parse binary operators', function () { 67 | const operators = [ 68 | '=', 69 | '+', 70 | '-', 71 | '*', 72 | '/', 73 | '%', 74 | '^', 75 | '==', 76 | '!=', 77 | '>', 78 | '<', 79 | '>=', 80 | '<=', 81 | '||', 82 | '&&', 83 | '??', 84 | '&', 85 | '===', 86 | '!==', 87 | '|', 88 | '??', 89 | ]; 90 | for (const i in operators) { 91 | const op = operators[i]; 92 | expectParse( 93 | `a ${op} b`, 94 | astFactory.binary(astFactory.id('a'), op, astFactory.id('b')), 95 | ); 96 | expectParse( 97 | `1 ${op} 2`, 98 | astFactory.binary(astFactory.literal(1), op, astFactory.literal(2)), 99 | ); 100 | expectParse( 101 | `this ${op} null`, 102 | astFactory.binary(astFactory.id('this'), op, astFactory.literal(null)), 103 | ); 104 | } 105 | }); 106 | 107 | test('shoud parse arrow functions', function () { 108 | expectParse('() => x', astFactory.arrowFunction([], astFactory.id('x'))); 109 | expectParse( 110 | '(a) => a', 111 | astFactory.arrowFunction(['a'], astFactory.id('a')), 112 | ); 113 | expectParse( 114 | '(a, b) => a + b', 115 | astFactory.arrowFunction( 116 | ['a', 'b'], 117 | astFactory.binary(astFactory.id('a'), '+', astFactory.id('b')), 118 | ), 119 | ); 120 | expectParse( 121 | 'fn(() => x)', 122 | astFactory.invoke(astFactory.id('fn'), undefined, [ 123 | astFactory.arrowFunction([], astFactory.id('x')), 124 | ]), 125 | ); 126 | expectParse( 127 | 'fn ?? () => x', 128 | astFactory.binary( 129 | astFactory.id('fn'), 130 | '??', 131 | astFactory.arrowFunction([], astFactory.id('x')), 132 | ), 133 | ); 134 | expectParse( 135 | '(() => x)()', 136 | astFactory.invoke( 137 | astFactory.paren(astFactory.arrowFunction([], astFactory.id('x'))), 138 | undefined, 139 | [], 140 | ), 141 | ); 142 | }); 143 | 144 | // test('should thrown on unknown operators', () { 145 | // expect(() => parse('a ?? b'), throwsParseException); 146 | // expect(() => parse('a &&& b'), throwsParseException); 147 | // expect(() => parse('a ==== b'), throwsParseException); 148 | // }); 149 | 150 | test('should give multiply higher associativity than plus', function () { 151 | expectParse( 152 | 'a + b * c', 153 | astFactory.binary( 154 | astFactory.id('a'), 155 | '+', 156 | astFactory.binary(astFactory.id('b'), '*', astFactory.id('c')), 157 | ), 158 | ); 159 | expectParse( 160 | 'a * b + c', 161 | astFactory.binary( 162 | astFactory.binary(astFactory.id('a'), '*', astFactory.id('b')), 163 | '+', 164 | astFactory.id('c'), 165 | ), 166 | ); 167 | }); 168 | 169 | test('should parse a dot operator', function () { 170 | expectParse('a.b', astFactory.getter(astFactory.id('a'), 'b')); 171 | }); 172 | 173 | test('should parse chained dot operators', function () { 174 | expectParse( 175 | 'a.b.c', 176 | astFactory.getter(astFactory.getter(astFactory.id('a'), 'b'), 'c'), 177 | ); 178 | }); 179 | 180 | test('should give dot high associativity', function () { 181 | expectParse( 182 | 'a * b.c', 183 | astFactory.binary( 184 | astFactory.id('a'), 185 | '*', 186 | astFactory.getter(astFactory.id('b'), 'c'), 187 | ), 188 | ); 189 | }); 190 | 191 | test('should parse a function with no arguments', function () { 192 | expectParse('a()', astFactory.invoke(astFactory.id('a'), undefined, [])); 193 | }); 194 | 195 | test('should parse a single function argument', function () { 196 | expectParse( 197 | 'a(b)', 198 | astFactory.invoke(astFactory.id('a'), undefined, [astFactory.id('b')]), 199 | ); 200 | }); 201 | 202 | test('should parse a function call as a subexpression', function () { 203 | expectParse( 204 | 'a() + 1', 205 | astFactory.binary( 206 | astFactory.invoke(astFactory.id('a'), undefined, []), 207 | '+', 208 | astFactory.literal(1), 209 | ), 210 | ); 211 | }); 212 | 213 | test('should parse multiple function arguments', function () { 214 | expectParse( 215 | 'a(b, c)', 216 | astFactory.invoke(astFactory.id('a'), undefined, [ 217 | astFactory.id('b'), 218 | astFactory.id('c'), 219 | ]), 220 | ); 221 | }); 222 | 223 | test('should parse nested function calls', function () { 224 | expectParse( 225 | 'a(b(c))', 226 | astFactory.invoke(astFactory.id('a'), undefined, [ 227 | astFactory.invoke(astFactory.id('b'), undefined, [astFactory.id('c')]), 228 | ]), 229 | ); 230 | }); 231 | 232 | test('should parse an empty method call', function () { 233 | expectParse('a.b()', astFactory.invoke(astFactory.id('a'), 'b', [])); 234 | }); 235 | 236 | test('should parse a method call with a single argument', function () { 237 | expectParse( 238 | 'a.b(c)', 239 | astFactory.invoke(astFactory.id('a'), 'b', [astFactory.id('c')]), 240 | ); 241 | }); 242 | 243 | test('should parse a method call with multiple arguments', function () { 244 | expectParse( 245 | 'a.b(c, d)', 246 | astFactory.invoke(astFactory.id('a'), 'b', [ 247 | astFactory.id('c'), 248 | astFactory.id('d'), 249 | ]), 250 | ); 251 | }); 252 | 253 | test('should parse chained method calls', function () { 254 | expectParse( 255 | 'a.b().c()', 256 | astFactory.invoke( 257 | astFactory.invoke(astFactory.id('a'), 'b', []), 258 | 'c', 259 | [], 260 | ), 261 | ); 262 | }); 263 | 264 | test('should parse chained function calls', function () { 265 | expectParse( 266 | 'a()()', 267 | astFactory.invoke( 268 | astFactory.invoke(astFactory.id('a'), undefined, []), 269 | undefined, 270 | [], 271 | ), 272 | ); 273 | }); 274 | 275 | test('should parse parenthesized expression', function () { 276 | expectParse('(a)', astFactory.paren(astFactory.id('a'))); 277 | expectParse( 278 | '(( 3 * ((1 + 2)) ))', 279 | astFactory.paren( 280 | astFactory.paren( 281 | astFactory.binary( 282 | astFactory.literal(3), 283 | '*', 284 | astFactory.paren( 285 | astFactory.paren( 286 | astFactory.binary( 287 | astFactory.literal(1), 288 | '+', 289 | astFactory.literal(2), 290 | ), 291 | ), 292 | ), 293 | ), 294 | ), 295 | ), 296 | ); 297 | }); 298 | 299 | test('should parse an index operator', function () { 300 | expectParse( 301 | 'a[b]', 302 | astFactory.index(astFactory.id('a'), astFactory.id('b')), 303 | ); 304 | expectParse( 305 | 'a.b[c]', 306 | astFactory.index( 307 | astFactory.getter(astFactory.id('a'), 'b'), 308 | astFactory.id('c'), 309 | ), 310 | ); 311 | }); 312 | 313 | test('should parse chained index operators', function () { 314 | expectParse( 315 | 'a[][]', 316 | astFactory.index( 317 | astFactory.index(astFactory.id('a'), undefined), 318 | undefined, 319 | ), 320 | ); 321 | }); 322 | 323 | test('should parse multiple index operators', function () { 324 | expectParse( 325 | 'a[b] + c[d]', 326 | astFactory.binary( 327 | astFactory.index(astFactory.id('a'), astFactory.id('b')), 328 | '+', 329 | astFactory.index(astFactory.id('c'), astFactory.id('d')), 330 | ), 331 | ); 332 | }); 333 | 334 | test('should parse ternary operators', function () { 335 | expectParse( 336 | 'a ? b : c', 337 | astFactory.ternary( 338 | astFactory.id('a'), 339 | astFactory.id('b'), 340 | astFactory.id('c'), 341 | ), 342 | ); 343 | expectParse( 344 | 'a.a ? b.a : c.a', 345 | astFactory.ternary( 346 | astFactory.getter(astFactory.id('a'), 'a'), 347 | astFactory.getter(astFactory.id('b'), 'a'), 348 | astFactory.getter(astFactory.id('c'), 'a'), 349 | ), 350 | ); 351 | // expect(() => parse('a + 1 ? b + 1 :: c.d + 3'), throwsParseException); 352 | }); 353 | 354 | test('ternary operators have lowest associativity', function () { 355 | expectParse( 356 | 'a == b ? c + d : e - f', 357 | astFactory.ternary( 358 | astFactory.binary(astFactory.id('a'), '==', astFactory.id('b')), 359 | astFactory.binary(astFactory.id('c'), '+', astFactory.id('d')), 360 | astFactory.binary(astFactory.id('e'), '-', astFactory.id('f')), 361 | ), 362 | ); 363 | 364 | expectParse( 365 | 'a.x == b.y ? c + d : e - f', 366 | astFactory.ternary( 367 | astFactory.binary( 368 | astFactory.getter(astFactory.id('a'), 'x'), 369 | '==', 370 | astFactory.getter(astFactory.id('b'), 'y'), 371 | ), 372 | astFactory.binary(astFactory.id('c'), '+', astFactory.id('d')), 373 | astFactory.binary(astFactory.id('e'), '-', astFactory.id('f')), 374 | ), 375 | ); 376 | }); 377 | 378 | test('should parse a filter chain', function () { 379 | expectParse( 380 | 'a | b | c', 381 | astFactory.binary( 382 | astFactory.binary(astFactory.id('a'), '|', astFactory.id('b')), 383 | '|', 384 | astFactory.id('c'), 385 | ), 386 | ); 387 | }); 388 | 389 | test('should parse map literals', function () { 390 | expectParse("{'a': 1}", astFactory.map({a: astFactory.literal(1)})); 391 | expectParse('{a: 1}', astFactory.map({a: astFactory.literal(1)})); 392 | expectParse( 393 | "{'a': 1, 'b': 2 + 3}", 394 | astFactory.map({ 395 | a: astFactory.literal(1), 396 | b: astFactory.binary(astFactory.literal(2), '+', astFactory.literal(3)), 397 | }), 398 | ); 399 | expectParse( 400 | "{'a': foo()}", 401 | astFactory.map({ 402 | a: astFactory.invoke(astFactory.id('foo'), undefined, []), 403 | }), 404 | ); 405 | expectParse( 406 | "{'a': foo('a')}", 407 | astFactory.map({ 408 | a: astFactory.invoke(astFactory.id('foo'), undefined, [ 409 | astFactory.literal('a'), 410 | ]), 411 | }), 412 | ); 413 | }); 414 | 415 | test('should parse map literals with method calls', function () { 416 | expectParse( 417 | "{'a': 1}.length", 418 | astFactory.getter(astFactory.map({a: astFactory.literal(1)}), 'length'), 419 | ); 420 | }); 421 | 422 | test('should parse list literals', function () { 423 | expectParse( 424 | '[1, "a", b]', 425 | astFactory.list([ 426 | astFactory.literal(1), 427 | astFactory.literal('a'), 428 | astFactory.id('b'), 429 | ]), 430 | ); 431 | expectParse( 432 | '[[1, 2], [3, 4]]', 433 | astFactory.list([ 434 | astFactory.list([astFactory.literal(1), astFactory.literal(2)]), 435 | astFactory.list([astFactory.literal(3), astFactory.literal(4)]), 436 | ]), 437 | ); 438 | }); 439 | }); 440 | -------------------------------------------------------------------------------- /src/test/tokenizer_test.ts: -------------------------------------------------------------------------------- 1 | import {test, describe as suite} from 'node:test'; 2 | import assert from 'node:assert'; 3 | 4 | import * as constants from '../lib/constants.js'; 5 | import {Kind, token as makeToken, Token, Tokenizer} from '../lib/tokenizer.js'; 6 | 7 | const STRING = Kind.STRING; 8 | const IDENTIFIER = Kind.IDENTIFIER; 9 | const DOT = Kind.DOT; 10 | const COMMA = Kind.COMMA; 11 | const COLON = Kind.COLON; 12 | const INTEGER = Kind.INTEGER; 13 | const DECIMAL = Kind.DECIMAL; 14 | const OPERATOR = Kind.OPERATOR; 15 | const GROUPER = Kind.GROUPER; 16 | const KEYWORD = Kind.KEYWORD; 17 | const ARROW = Kind.ARROW; 18 | const POSTFIX_PRECEDENCE = constants.POSTFIX_PRECEDENCE; 19 | const PRECEDENCE = constants.PRECEDENCE; 20 | 21 | function tokenize(s: string) { 22 | const tokenizer = new Tokenizer(s); 23 | const tokens: Token[] = []; 24 | let token: Token | undefined; 25 | while ((token = tokenizer.nextToken()) != null) { 26 | tokens.push(token); 27 | } 28 | return tokens; 29 | } 30 | 31 | function expectTokens(s: string, expected: Token[]) { 32 | const tokens = tokenize(s); 33 | assert.deepEqual(tokens, expected); 34 | } 35 | 36 | function t(kind: Kind, value: string, precedence?: number): Token { 37 | return makeToken(kind, value, precedence); 38 | } 39 | 40 | suite('tokenizer', function () { 41 | test('should tokenize an empty expression', function () { 42 | expectTokens('', []); 43 | }); 44 | 45 | test('should tokenize an identifier', function () { 46 | expectTokens('abc', [t(IDENTIFIER, 'abc')]); 47 | expectTokens('ABC', [t(IDENTIFIER, 'ABC')]); 48 | expectTokens('_abc', [t(IDENTIFIER, '_abc')]); 49 | expectTokens('a_b', [t(IDENTIFIER, 'a_b')]); 50 | expectTokens('$abc', [t(IDENTIFIER, '$abc')]); 51 | expectTokens('a$', [t(IDENTIFIER, 'a$')]); 52 | expectTokens('a1', [t(IDENTIFIER, 'a1')]); 53 | }); 54 | 55 | test('should tokenize two identifiers', function () { 56 | expectTokens('abc def', [t(IDENTIFIER, 'abc'), t(IDENTIFIER, 'def')]); 57 | }); 58 | 59 | test('should tokenize a double quoted String', function () { 60 | expectTokens('"abc"', [t(STRING, 'abc')]); 61 | }); 62 | 63 | test('should tokenize a single quoted String', function () { 64 | expectTokens("'abc'", [t(STRING, 'abc')]); 65 | }); 66 | 67 | test('should tokenize a String with escaping', function () { 68 | expectTokens('"a\\c\\\\d\\\'\\""', [t(STRING, 'ac\\d\'"')]); 69 | }); 70 | 71 | test('should tokenize a dot operator', function () { 72 | expectTokens('a.b', [ 73 | t(IDENTIFIER, 'a'), 74 | t(DOT, '.', POSTFIX_PRECEDENCE), 75 | t(IDENTIFIER, 'b'), 76 | ]); 77 | expectTokens('ab.cd', [ 78 | t(IDENTIFIER, 'ab'), 79 | t(DOT, '.', POSTFIX_PRECEDENCE), 80 | t(IDENTIFIER, 'cd'), 81 | ]); 82 | expectTokens('ab.cd()', [ 83 | t(IDENTIFIER, 'ab'), 84 | t(DOT, '.', POSTFIX_PRECEDENCE), 85 | t(IDENTIFIER, 'cd'), 86 | t(GROUPER, '(', PRECEDENCE['(']), 87 | t(GROUPER, ')', PRECEDENCE[')']), 88 | ]); 89 | expectTokens('ab.cd(e)', [ 90 | t(IDENTIFIER, 'ab'), 91 | t(DOT, '.', POSTFIX_PRECEDENCE), 92 | t(IDENTIFIER, 'cd'), 93 | t(GROUPER, '(', PRECEDENCE['(']), 94 | t(IDENTIFIER, 'e'), 95 | t(GROUPER, ')', PRECEDENCE[')']), 96 | ]); 97 | }); 98 | 99 | test('should tokenize a unary plus operator', function () { 100 | expectTokens('+a', [t(OPERATOR, '+', PRECEDENCE['+']), t(IDENTIFIER, 'a')]); 101 | }); 102 | 103 | test('should tokenize a one-character operator', function () { 104 | expectTokens('a + b', [ 105 | t(IDENTIFIER, 'a'), 106 | t(OPERATOR, '+', PRECEDENCE['+']), 107 | t(IDENTIFIER, 'b'), 108 | ]); 109 | }); 110 | 111 | test('should tokenize a two-character operator', function () { 112 | expectTokens('a && b', [ 113 | t(IDENTIFIER, 'a'), 114 | t(OPERATOR, '&&', PRECEDENCE['&&']), 115 | t(IDENTIFIER, 'b'), 116 | ]); 117 | }); 118 | 119 | test('should tokenize a three-character operator', function () { 120 | expectTokens('a !== b', [ 121 | t(IDENTIFIER, 'a'), 122 | t(OPERATOR, '!==', PRECEDENCE['!==']), 123 | t(IDENTIFIER, 'b'), 124 | ]); 125 | }); 126 | 127 | test('should tokenize a ternary operator', function () { 128 | expectTokens('a ? b : c', [ 129 | t(IDENTIFIER, 'a'), 130 | t(OPERATOR, '?', PRECEDENCE['?']), 131 | t(IDENTIFIER, 'b'), 132 | t(COLON, ':', PRECEDENCE[':']), 133 | t(IDENTIFIER, 'c'), 134 | ]); 135 | }); 136 | 137 | test('should tokenize keywords', function () { 138 | expectTokens('this', [t(KEYWORD, 'this')]); 139 | }); 140 | 141 | test('should tokenize groups', function () { 142 | expectTokens('a(b)[]{}', [ 143 | t(IDENTIFIER, 'a'), 144 | t(GROUPER, '(', PRECEDENCE['(']), 145 | t(IDENTIFIER, 'b'), 146 | t(GROUPER, ')', PRECEDENCE[')']), 147 | t(GROUPER, '[', PRECEDENCE['[']), 148 | t(GROUPER, ']', PRECEDENCE[']']), 149 | t(GROUPER, '{', PRECEDENCE['{']), 150 | t(GROUPER, '}', PRECEDENCE['}']), 151 | ]); 152 | }); 153 | 154 | test('should tokenize argument lists', function () { 155 | expectTokens('(a, b)', [ 156 | t(GROUPER, '(', PRECEDENCE['(']), 157 | t(IDENTIFIER, 'a'), 158 | t(COMMA, ',', PRECEDENCE[',']), 159 | t(IDENTIFIER, 'b'), 160 | t(GROUPER, ')', PRECEDENCE[')']), 161 | ]); 162 | }); 163 | 164 | test('should tokenize arrow functions', function () { 165 | // console.log(tokenize('() => x')); 166 | expectTokens('() => x', [ 167 | t(GROUPER, '(', PRECEDENCE['(']), 168 | t(GROUPER, ')', PRECEDENCE[')']), 169 | t(ARROW, '=>'), 170 | t(IDENTIFIER, 'x'), 171 | ]); 172 | expectTokens('(a, b) => x', [ 173 | t(GROUPER, '(', PRECEDENCE['(']), 174 | t(IDENTIFIER, 'a'), 175 | t(COMMA, ',', PRECEDENCE[',']), 176 | t(IDENTIFIER, 'b'), 177 | t(GROUPER, ')', PRECEDENCE[')']), 178 | t(ARROW, '=>'), 179 | t(IDENTIFIER, 'x'), 180 | ]); 181 | }); 182 | 183 | test('should tokenize maps', function () { 184 | expectTokens(`{'a': b}`, [ 185 | t(GROUPER, '{', PRECEDENCE['{']), 186 | t(STRING, 'a'), 187 | t(COLON, ':', PRECEDENCE[':']), 188 | t(IDENTIFIER, 'b'), 189 | t(GROUPER, '}', PRECEDENCE['}']), 190 | ]); 191 | expectTokens(`{a: b}`, [ 192 | t(GROUPER, '{', PRECEDENCE['{']), 193 | t(IDENTIFIER, 'a'), 194 | t(COLON, ':', PRECEDENCE[':']), 195 | t(IDENTIFIER, 'b'), 196 | t(GROUPER, '}', PRECEDENCE['}']), 197 | ]); 198 | }); 199 | 200 | test('should tokenize lists', function () { 201 | expectTokens(`[1, 'a', b]`, [ 202 | t(GROUPER, '[', PRECEDENCE['[']), 203 | t(INTEGER, '1'), 204 | t(COMMA, ',', PRECEDENCE[',']), 205 | t(STRING, 'a'), 206 | t(COMMA, ',', PRECEDENCE[',']), 207 | t(IDENTIFIER, 'b'), 208 | t(GROUPER, ']', PRECEDENCE[']']), 209 | ]); 210 | }); 211 | 212 | test('should tokenize integers', function () { 213 | expectTokens('123', [t(INTEGER, '123')]); 214 | expectTokens('+123', [ 215 | t(OPERATOR, '+', PRECEDENCE['+']), 216 | t(INTEGER, '123'), 217 | ]); 218 | expectTokens('-123', [ 219 | t(OPERATOR, '-', PRECEDENCE['-']), 220 | t(INTEGER, '123'), 221 | ]); 222 | }); 223 | 224 | test('should tokenize decimals', function () { 225 | expectTokens('1.23', [t(DECIMAL, '1.23')]); 226 | expectTokens('+1.23', [ 227 | t(OPERATOR, '+', PRECEDENCE['+']), 228 | t(DECIMAL, '1.23'), 229 | ]); 230 | expectTokens('-1.23', [ 231 | t(OPERATOR, '-', PRECEDENCE['-']), 232 | t(DECIMAL, '1.23'), 233 | ]); 234 | }); 235 | 236 | test('should tokenize booleans as identifiers', function () { 237 | expectTokens('true', [t(IDENTIFIER, 'true')]); 238 | expectTokens('false', [t(IDENTIFIER, 'false')]); 239 | }); 240 | 241 | test('should tokenize name.substring(2)', function () { 242 | expectTokens('name.substring(2)', [ 243 | t(IDENTIFIER, 'name'), 244 | t(DOT, '.', POSTFIX_PRECEDENCE), 245 | t(IDENTIFIER, 'substring'), 246 | t(GROUPER, '(', PRECEDENCE['(']), 247 | t(INTEGER, '2'), 248 | t(GROUPER, ')', PRECEDENCE[')']), 249 | ]); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "isolatedModules": false, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitThis": true, 12 | "removeComments": false, 13 | "preserveConstEnums": true, 14 | "outDir": ".", 15 | "declaration": true, 16 | "sourceMap": true, 17 | "pretty": true, 18 | "allowSyntheticDefaultImports": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": ["src/**/*.ts"], 22 | "exclude": [] 23 | } 24 | --------------------------------------------------------------------------------