├── .cirrus.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── dependabot.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── Makefile ├── README.md ├── babel.config.js ├── docs ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── dev-notes.md ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── config │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── config.tests.js │ ├── package.json │ └── src │ │ └── index.js ├── plugin-prettier │ ├── .npmignore │ ├── README.md │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── prettier-plugin.test.js.snap │ │ └── prettier-plugin.test.js │ │ └── index.js ├── plugin │ ├── .npmignore │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.js │ │ └── loader.js └── zcomp │ ├── .npmignore │ ├── README.md │ ├── package.json │ └── src │ ├── commands │ ├── build.js │ ├── index.js │ ├── repl.js │ └── run.js │ └── compiler │ ├── __tests__ │ ├── __snapshots__ │ │ └── transpile.test.js.snap │ ├── transpile.test.js │ └── types.test.js │ ├── gen.js │ ├── parse.js │ ├── tokenize.js │ └── types.js └── yarn.lock /.cirrus.yml: -------------------------------------------------------------------------------- 1 | Tests_task: 2 | node_modules_cache: 3 | folder: node_modules 4 | fingerprint_script: | 5 | cat package.json 6 | cat yarn.lock 7 | populate_script: make install 8 | bootstrap_script: yarn lerna bootstrap 9 | script: make 10 | matrix: 11 | - container: 12 | image: node:latest 13 | matrix: 14 | - name: "Tests@Node:Latest" 15 | - name: "CodeCoverage@Node:Latest" 16 | upload_script: | 17 | curl -L -o ./codecov https://codecov.io/bash 18 | chmod +x ./codecov 19 | ./codecov 20 | env: 21 | CODE_COVERAGE_RUN: true 22 | CODECOV_TOKEN: ENCRYPTED[230fd82953b0b0ed3fbde809dbad5bc8cf92a3b88e03d5b86c5124e01a890a12fde06130fb35af71c19ec4485d4519e3] 23 | code_coverage_artifacts: 24 | path: ./coverage/** 25 | - container: 26 | image: node:dubnium 27 | name: "Tests@Node:Dubnium" 28 | - container: 29 | image: node:erbium 30 | name: "Tests@Node:Erbium" 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/**/lib 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: ["eslint:recommended", "prettier"], 8 | globals: { 9 | Atomics: "readonly", 10 | SharedArrayBuffer: "readonly", 11 | }, 12 | parser: "babel-eslint", 13 | parserOptions: { 14 | ecmaVersion: 11, 15 | sourceType: "module", 16 | }, 17 | rules: { 18 | "no-useless-escape": 0, 19 | "no-fallthrough": 0, 20 | }, 21 | plugins: ["jest"], 22 | }; 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: eslint-plugin-jest 10 | versions: 11 | - 24.1.3 12 | - 24.1.5 13 | - 24.1.9 14 | - 24.2.0 15 | - 24.2.1 16 | - 24.3.4 17 | - 24.3.5 18 | - dependency-name: eslint 19 | versions: 20 | - 7.18.0 21 | - 7.19.0 22 | - 7.20.0 23 | - 7.23.0 24 | - 7.24.0 25 | - dependency-name: y18n 26 | versions: 27 | - 4.0.1 28 | - 4.0.2 29 | - dependency-name: "@types/jest" 30 | versions: 31 | - 26.0.20 32 | - 26.0.22 33 | - dependency-name: "@babel/preset-env" 34 | versions: 35 | - 7.12.11 36 | - 7.12.13 37 | - 7.12.16 38 | - 7.12.17 39 | - 7.13.0 40 | - 7.13.12 41 | - 7.13.5 42 | - 7.13.9 43 | - dependency-name: husky 44 | versions: 45 | - 4.3.8 46 | - 5.0.9 47 | - 5.1.1 48 | - 5.1.2 49 | - 5.1.3 50 | - 5.2.0 51 | - dependency-name: ini 52 | versions: 53 | - 1.3.8 54 | - dependency-name: "@babel/plugin-proposal-optional-chaining" 55 | versions: 56 | - 7.12.13 57 | - 7.12.16 58 | - 7.12.17 59 | - 7.12.7 60 | - 7.13.0 61 | - dependency-name: eslint-config-prettier 62 | versions: 63 | - 7.2.0 64 | - 8.0.0 65 | - dependency-name: node-notifier 66 | versions: 67 | - 8.0.1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /tmp 6 | node_modules 7 | coverage 8 | package.json~ 9 | packages/**/lib 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lerna.json 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | yarn build 3 | yarn test 4 | .PHONY: build 5 | 6 | install: 7 | yarn install 8 | .PHONY: install 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zcomp 2 | 3 | The Z transpiler/interpreter 4 | 5 | [![Version](https://img.shields.io/npm/v/@zlanguage/zcomp.svg)](https://www.npmjs.com/package/@zlanguage/zcomp) 6 | [![Downloads/week](https://img.shields.io/npm/dw/@zlanguage/zcomp.svg)](https://www.npmjs.com/package/@zlanguage/zcomp) 7 | [![License](https://img.shields.io/npm/l/@zlanguage/zcomp.svg)](https://github.com/zlanguage/zcomp/blob/master/LICENSE) 8 | 9 | ## Z 10 | 11 | A transpiled language that can be evaluated as a script (for testing) or transpiled to human-readable JS (for production code). Z supports modules, functions, closure, error handling, and many more features you would expect from a modern language. Is it ready for production code? I'd wait a few months before that. 12 | 13 | ## ZComp 14 | 15 | The Z Compiler (ZComp) can be installed with: 16 | 17 | ```sh 18 | $ npm install -g @zlanguage/zcomp 19 | ``` 20 | 21 | Then, you should install the zstdlib (a runtime library): 22 | 23 | ```sh 24 | $ npm install -g @zlanguage/zstdlib 25 | ``` 26 | 27 | Finally, navigate to the directory you're using Z in, and type: 28 | 29 | ```sh 30 | $ npm install @zlanguage/zstdlib 31 | ``` 32 | 33 | This installs the Z standard library locally in just the paackage you need it for. 34 | 35 | ### Use the Compiler 36 | 37 | Transpile Z Code: 38 | 39 | ```sh 40 | $ zcomp transpile [path of Z to transpile] [path of where to output the transpiled JS] 41 | ``` 42 | 43 | Run Z Code: 44 | 45 | ```sh 46 | $ zcomp run [path of Z to run] 47 | ``` 48 | 49 | ## Docs 50 | 51 | The docs are on the offical [Z Website](https://zlanguage.github.io/). 52 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "10", 8 | }, 9 | }, 10 | ], 11 | ], 12 | plugins: [ 13 | "@babel/proposal-optional-chaining", 14 | "@babel/proposal-nullish-coalescing-operator", 15 | "@babel/proposal-class-properties", 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.6.2] 2 | 3 | - Fixed some regressions from 0.6.0 4 | 5 | ## [0.6.0/0.6.1] - 2020-05-07 6 | 7 | - Switched off oclif and onto commander 8 | - The watch, dirt, and transpile commands have been removed for now 9 | - transpile is now called build, and functions the same 10 | - Removed chai 11 | - Code cleanup 12 | 13 | ## [0.4.4] - 2019-10-12 14 | 15 | ### Added 16 | 17 | - Infix macros 18 | - Overloaded macros 19 | - `$Z` macro to access gloabl namespace 20 | 21 | ## [0.4.0] - 2019-10-01 22 | 23 | ### Added 24 | 25 | - Macro system and `include` 26 | 27 | ## [0.3.20] - 2019-09-24 28 | 29 | ### Fixed 30 | 31 | - Escaped characters in string literals 32 | - Repl dependency loading 33 | 34 | ## [0.3.19] - 2019-09-23 35 | 36 | ### Fixed 37 | 38 | - Extractors and Predicates 39 | 40 | ## [0.3.14] - 2019-09-21 41 | 42 | ### Added 43 | 44 | - Symbol and RegExp literals 45 | 46 | ## [0.3.7] - 2019-09-01 47 | 48 | ### Added 49 | 50 | - `:l` command in REPL 51 | 52 | ## [0.3.3] - 2019-08-21 53 | 54 | ### Added 55 | 56 | - `static` traits 57 | 58 | ## [0.3.2] - 2019-08-20 59 | 60 | ### Added 61 | 62 | - Enums, `where` and `derive`, and traits 63 | 64 | ## [0.3.0] - 2019-08-18 65 | 66 | ### Added 67 | 68 | - Keyword arguments via implicit objects 69 | 70 | ## [0.2.22] - 2019-08-18 71 | 72 | ### Added 73 | 74 | - Extractors 75 | - Predicates 76 | - Blocks in `match` expressions 77 | 78 | ## [0.2.20] - 2019-08-17 79 | 80 | ### Added 81 | 82 | - Top-level `get` 83 | - Inferred goroutines 84 | 85 | ## [0.2.13] - 2019-08-13 86 | 87 | ### Added 88 | 89 | - Standalone `get` 90 | 91 | ## [0.2.10] - 2019-08-12 92 | 93 | ### Added 94 | 95 | - Channels/Goroutines Added 96 | 97 | ## [0.2.8] - 2019-08-11 98 | 99 | ### Added 100 | 101 | - `wdir` command 102 | 103 | ## [0.2.7] - 2019-08-07 104 | 105 | ### Added 106 | 107 | - Ternary Operator 108 | 109 | ## [0.2.6] - 2019-08-06 110 | 111 | ### Added 112 | 113 | - Left and right associative operators 114 | 115 | ### Fixed 116 | 117 | - Invocations can be used with operators 118 | 119 | ## [0.2.5] - 2019-08-05 120 | 121 | ### Added 122 | 123 | - REPL result highlighting 124 | 125 | ### Fixed 126 | 127 | - Operator evaluation 128 | 129 | ## [0.2.3] - 2019-08-04 130 | 131 | ### Added 132 | 133 | - `watch` command 134 | 135 | ### Fixed 136 | 137 | - REPL works properly 138 | 139 | ## [0.2.0] - 2019-08-04 140 | 141 | ### Changed 142 | 143 | - Loop Expressions are now much more effecient 144 | 145 | ## [0.1.16] - 2019-08-02 146 | 147 | ### Added 148 | 149 | - Loop Expressions 150 | 151 | ## [0.1.11] - 2019-07-31 152 | 153 | ### Added 154 | 155 | - Exponent Operator 156 | 157 | ## [0.1.10] - 2019-07-30 158 | 159 | ### Added 160 | 161 | - Order of Operations 162 | 163 | ### Fixed 164 | 165 | - Negative Numbers 166 | - Left-Right evaluation of infix operators 167 | 168 | ## [0.1.9] - 2019-07-30 169 | 170 | ### Added 171 | 172 | - Enhanced Number Literals 173 | 174 | ## [0.1.8] - 2019-07-29 175 | 176 | ### Added 177 | 178 | - Variable Types (Runtime) 179 | - Return Types (Runtime) 180 | 181 | ## [0.1.4] - 2019-07-29 182 | 183 | ### Added 184 | 185 | - Enter/exit blocks 186 | - Parameter type checks 187 | 188 | ### Fixed 189 | 190 | - Infix Syntax 191 | 192 | ## [0.1.3] - 2019-07-27 193 | 194 | ### Added 195 | 196 | - Infix Syntax 197 | 198 | ## [0.1.2] - 2019-07-27 199 | 200 | ### Added 201 | 202 | - Implicit blocks for arrays 203 | 204 | ### Added 205 | 206 | - Dollar Directives 207 | - Compile-time Metadata 208 | 209 | ## [0.1.1] - 2019-07-27 210 | 211 | ### Added 212 | 213 | - Dollar Directives 214 | - Compile-time Metadata 215 | 216 | ## [0.1.0] - 2019-07-26 217 | 218 | ### Added 219 | 220 | - Documentation 221 | - Recursive Directory Compiling (`dirt` command) 222 | - Stateless Compilation 223 | 224 | ## [0.0.18] - 2019-07-23 225 | 226 | ### Added 227 | 228 | - Pattern Matching 229 | 230 | ## [0.0.13] - 2019-07-22 231 | 232 | ### Added 233 | 234 | - Rest/Spread 235 | - Function Syntatic Shorthands: 236 | - Return Statement Ommision 237 | - Implicit Parameters 238 | 239 | ## [0.0.8] - 2019-07-20 240 | 241 | ### Added 242 | 243 | - Standard Library Imports 244 | - More Memory-Effecient Name-Mangaling 245 | - Loser Reserved Word Policy 246 | - Else-if 247 | 248 | ## [0.0.5] - 2019-07-19 249 | 250 | ### Added 251 | 252 | - Error Handling 253 | - Destructuring 254 | - Smart Imports 255 | - `run` command 256 | 257 | ## [0.0.0] - 2019-07-19 258 | 259 | ### Added 260 | 261 | - Basic Compiler, featuring: 262 | - Variables 263 | - Functions 264 | - Arrays & Objects 265 | - Control Flow 266 | - Modules 267 | - `transpile` command 268 | 269 | [0.0.0]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.0.0 270 | [0.0.5]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.0.5 271 | [0.0.8]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.0.8 272 | [0.0.13]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.0.13 273 | [0.0.18]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.0.18 274 | [0.1.0]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.0 275 | [0.1.1]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.1 276 | [0.1.2]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.2 277 | [0.1.3]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.3 278 | [0.1.4]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.4 279 | [0.1.8]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.8 280 | [0.1.9]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.9 281 | [0.1.10]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.10 282 | [0.1.11]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.11 283 | [0.1.16]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.1.16 284 | [0.2.0]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.0 285 | [0.2.3]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.3 286 | [0.2.5]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.5 287 | [0.2.6]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.6 288 | [0.2.7]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.7 289 | [0.2.8]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.8 290 | [0.2.10]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.10 291 | [0.2.13]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.13 292 | [0.2.20]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.20 293 | [0.2.22]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.2.22 294 | [0.3.0]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.0 295 | [0.3.2]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.2 296 | [0.3.3]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.3 297 | [0.3.7]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.7 298 | [0.3.14]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.14 299 | [0.3.19]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.19 300 | [0.3.20]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.3.20 301 | [0.4.0]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.4.0 302 | [0.4.4]: https://www.npmjs.com/package/@zlanguage/zcomp/v/0.4.4 303 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nathanbreslow@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Hey! Welcome to the Z Compiler, we love contributors! Here is a basic guide on how to develop the code. 4 | Note: This guide is only for Unix and macOS users. 5 | 6 | ## Install build dependencies 7 | 8 | ```shell 9 | make install 10 | ``` 11 | 12 | ## Build the package 13 | 14 | ```shell 15 | make 16 | ``` 17 | 18 | ## Run tests 19 | 20 | ```shell 21 | make test 22 | ``` 23 | 24 | ## Test and collect coverage 25 | 26 | ```shell 27 | make coverage 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/dev-notes.md: -------------------------------------------------------------------------------- 1 | # Dev Notes 2 | 3 | ## Tokenization regex capturing groups 4 | 5 | [1] Whitespace 6 | [2] Comment 7 | [3] String 8 | [4] Keyword 9 | [5] Name 10 | [6] Number 11 | [7] Punctuator 12 | [8] RegExp 13 | 14 | ## Reference to how Z mangles identifiers for debugging 15 | 16 | ```javascript 17 | const symbolMap = { 18 | "+": "$plus", 19 | "-": "$minus", 20 | "*": "$star", 21 | "/": "$slash", 22 | "^": "$carot", 23 | "?": "$question", 24 | "=": "$eq", 25 | "<": "$lt", 26 | ">": "$gt", 27 | "\\": "$backslash", 28 | "&": "$and", 29 | "|": "$or", 30 | "%": "$percent", 31 | "'": "$quote", 32 | "!": "$exclam", 33 | }; 34 | ``` 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: !!require("process").env.CI, 3 | coverageDirectory: "coverage", 4 | testEnvironment: "node", 5 | modulePathIgnorePatterns: ["packages/.*/lib"], 6 | }; 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.6.2", 6 | "npmClient": "yarn", 7 | "npmClientArgs": [ 8 | "--no-lockfile" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zlanguage/monorepo", 3 | "homepage": "https://github.com/zlanguage/zcomp", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/zlanguage/zcomp.git" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "yarn lerna run build", 11 | "clean": "rimraf packages/**/lib", 12 | "lerna": "lerna", 13 | "lint": "eslint packages/**/*.js", 14 | "prettier": "prettier --write **/*.{js,json,md}", 15 | "test": "jest", 16 | "bootstrap": "lerna bootstrap" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "pretty-quick" 21 | } 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.12.1", 25 | "@babel/core": "^7.12.3", 26 | "@babel/plugin-proposal-class-properties": "^7.12.1", 27 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 28 | "@babel/plugin-proposal-optional-chaining": "^7.12.1", 29 | "@babel/preset-env": "^7.12.1", 30 | "@types/jest": "^26.0.14", 31 | "@zlanguage/zstdlib": "^0.4.1", 32 | "babel-eslint": "^10.1.0", 33 | "babel-jest": "^26.6.0", 34 | "eslint": "^7.11.0", 35 | "eslint-config-prettier": "^8.1.0", 36 | "eslint-plugin-jest": "^24.1.0", 37 | "husky": "^4.3.0", 38 | "jest": "^26.6.0", 39 | "lerna": "^3.22.1", 40 | "prettier": "^2.1.2", 41 | "pretty-quick": "^3.1.0", 42 | "rimraf": "^3.0.2" 43 | }, 44 | "engines": { 45 | "node": ">=10.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/config/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /packages/config/README.md: -------------------------------------------------------------------------------- 1 | # `@zlanguage/config` 2 | 3 | The configuration loader. 4 | -------------------------------------------------------------------------------- /packages/config/__tests__/config.tests.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import rimraf from "rimraf"; 3 | import loadConfig from "../src"; 4 | 5 | beforeAll(() => { 6 | if (!fs.existsSync("fixtures-in-progress")) { 7 | fs.mkdirSync("fixtures-in-progress"); 8 | } else { 9 | rimraf("fixtures-in-progress"); 10 | fs.mkdirSync("fixtures-in-progress"); 11 | } 12 | }); 13 | 14 | afterAll(() => { 15 | rimraf.sync("fixtures-in-progress"); 16 | }); 17 | 18 | describe("loading from zconfig.json", () => { 19 | let fakeConfigJson = { plugins: ["fake-plugin"] }; 20 | 21 | beforeEach(() => { 22 | fs.writeFileSync( 23 | "fixtures-in-progress/zconfig.json", 24 | JSON.stringify(fakeConfigJson) 25 | ); 26 | }); 27 | 28 | it("loads the config", () => { 29 | process.chdir("fixtures-in-progress"); 30 | expect(loadConfig()).toEqual(fakeConfigJson); 31 | process.chdir(".."); 32 | }); 33 | }); 34 | 35 | describe("loading from package.json", () => { 36 | let fakePackageJson = { zConfig: { plugins: ["fake-plugin"] } }; 37 | 38 | beforeEach(() => { 39 | fs.writeFileSync( 40 | "fixtures-in-progress/package.json", 41 | JSON.stringify(fakePackageJson) 42 | ); 43 | }); 44 | 45 | it("loads the config", () => { 46 | process.chdir("fixtures-in-progress"); 47 | expect(loadConfig()).toEqual(fakePackageJson.zConfig); 48 | process.chdir(".."); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zlanguage/config", 3 | "version": "0.6.2", 4 | "description": "The configuration handler.", 5 | "keywords": [ 6 | "z", 7 | "language", 8 | "compiler", 9 | "config" 10 | ], 11 | "author": "Reece Dunham ", 12 | "homepage": "https://github.com/zlanguage/zcomp", 13 | "license": "MIT", 14 | "main": "lib/index.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/zlanguage/zcomp.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/zlanguage/zcomp/issues" 21 | }, 22 | "scripts": { 23 | "build": "babel --config-file ../../babel.config.js src --out-dir lib" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.12.1", 27 | "@babel/core": "^7.12.3" 28 | }, 29 | "dependencies": { 30 | "deep-extend": "^0.6.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/config/src/index.js: -------------------------------------------------------------------------------- 1 | import merge from "deep-extend"; 2 | import fs from "fs"; 3 | 4 | export const defaultConfig = { 5 | plugins: [], 6 | }; 7 | 8 | /** 9 | * Loads the configuration file. 10 | * 11 | * @returns {defaultConfig} The user's configuration. 12 | */ 13 | export default function loadConfig() { 14 | let config = defaultConfig; 15 | let json = {}; 16 | 17 | if (fs.existsSync("package.json")) { 18 | json = JSON.parse(fs.readFileSync("package.json")); 19 | let o = json.zOptions; 20 | if (!!o) { 21 | config = merge(config, o); 22 | } 23 | } 24 | 25 | if (fs.existsSync("zconfig.json")) { 26 | json = JSON.parse(fs.readFileSync("zconfig.json")); 27 | if (!!json) { 28 | config = merge(config, json); 29 | } 30 | } 31 | 32 | return config; 33 | } 34 | -------------------------------------------------------------------------------- /packages/plugin-prettier/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /packages/plugin-prettier/README.md: -------------------------------------------------------------------------------- 1 | # `@zlanguage/plugin-prettier` 2 | 3 | A plugin that runs Prettier on outputted code. 4 | -------------------------------------------------------------------------------- /packages/plugin-prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zlanguage/plugin-prettier", 3 | "version": "0.6.2", 4 | "description": "A plugin that runs Prettier on outputted code.", 5 | "keywords": [ 6 | "z", 7 | "language", 8 | "compiler", 9 | "plugin" 10 | ], 11 | "author": "Reece Dunham ", 12 | "homepage": "https://github.com/zlanguage/zcomp", 13 | "license": "MIT", 14 | "main": "lib/index.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/zlanguage/zcomp.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/zlanguage/zcomp/issues" 21 | }, 22 | "scripts": { 23 | "build": "babel --config-file ../../babel.config.js src --out-dir lib" 24 | }, 25 | "dependencies": { 26 | "@zlanguage/plugin": "^0.6.2", 27 | "@zlanguage/config": "^0.6.2" 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.12.1", 31 | "@babel/core": "^7.12.3" 32 | }, 33 | "peerDependencies": { 34 | "prettier": "1.x || 2.x" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/plugin-prettier/src/__tests__/__snapshots__/prettier-plugin.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`prettier plugin outputs code with prettier correctly 1`] = `undefined`; 4 | 5 | exports[`prettier plugin outputs normal code 1`] = ` 6 | "\\"use strict\\"; 7 | 8 | var $Z = require(\\"@zlanguage/zstdlib\\"); 9 | var matcher = require(\\"@zlanguage/zstdlib/src/js/matcher\\"); 10 | 11 | var $eq = $Z.$eq; 12 | var isObject = $Z.isObject; 13 | var typeOf = $Z.typeOf; 14 | var stone = $Z.stone; 15 | var log = $Z.log; 16 | var copy = $Z.copy; 17 | var assertBool = $Z.assertBool; 18 | var $plus = $Z.$plus; 19 | var $minus = $Z.$minus; 20 | var $star = $Z.$star; 21 | var $slash = $Z.$slash; 22 | var $percent = $Z.$percent; 23 | var $carot = $Z.$carot; 24 | var pow = $Z.pow; 25 | var $lt = $Z.$lt; 26 | var $gt$eq = $Z.$gt$eq; 27 | var $gt = $Z.$gt; 28 | var $lt$eq = $Z.$lt$eq; 29 | var not = $Z.not; 30 | var $plus$plus = $Z.$plus$plus; 31 | var m = $Z.m; 32 | var both = $Z.both; 33 | var either = $Z.either; 34 | var and = $Z.and; 35 | var or = $Z.or; 36 | var JS = $Z.JS; 37 | var assertType = $Z.assertType; 38 | var typeGeneric = $Z.typeGeneric; 39 | var chan = $Z.chan; 40 | var send = $Z.send; 41 | var to = $Z.to; 42 | var til = $Z.til; 43 | var by = $Z.by; 44 | var curry = $Z.curry; 45 | var $or$gt = $Z.$or$gt; 46 | var $gt$gt = $Z.$gt$gt; 47 | var $lt$lt = $Z.$lt$lt; 48 | var handleErr = $Z.handleErr; 49 | var traits = stone(require(\\"@zlanguage/zstdlib/src/js/traits\\")); 50 | var {Show, Enum} = traits; 51 | function Red() { 52 | 53 | 54 | return Show(Enum({ 55 | type() { return \\"Color\\"; }, 56 | get constructor() { return Red; }, 57 | get parent() { return Color; }, 58 | get fields() { return []; }, 59 | 60 | \\"=\\"(other) { 61 | return other.constructor === Red; 62 | } 63 | })); 64 | } 65 | 66 | Red.extract = function (val) { 67 | if (val.constructor === Red) { 68 | return []; 69 | } 70 | return undefined; 71 | }; 72 | 73 | function Orange() { 74 | 75 | 76 | return Show(Enum({ 77 | type() { return \\"Color\\"; }, 78 | get constructor() { return Orange; }, 79 | get parent() { return Color; }, 80 | get fields() { return []; }, 81 | 82 | \\"=\\"(other) { 83 | return other.constructor === Orange; 84 | } 85 | })); 86 | } 87 | 88 | Orange.extract = function (val) { 89 | if (val.constructor === Orange) { 90 | return []; 91 | } 92 | return undefined; 93 | }; 94 | 95 | function Yellow() { 96 | 97 | 98 | return Show(Enum({ 99 | type() { return \\"Color\\"; }, 100 | get constructor() { return Yellow; }, 101 | get parent() { return Color; }, 102 | get fields() { return []; }, 103 | 104 | \\"=\\"(other) { 105 | return other.constructor === Yellow; 106 | } 107 | })); 108 | } 109 | 110 | Yellow.extract = function (val) { 111 | if (val.constructor === Yellow) { 112 | return []; 113 | } 114 | return undefined; 115 | }; 116 | 117 | function Green() { 118 | 119 | 120 | return Show(Enum({ 121 | type() { return \\"Color\\"; }, 122 | get constructor() { return Green; }, 123 | get parent() { return Color; }, 124 | get fields() { return []; }, 125 | 126 | \\"=\\"(other) { 127 | return other.constructor === Green; 128 | } 129 | })); 130 | } 131 | 132 | Green.extract = function (val) { 133 | if (val.constructor === Green) { 134 | return []; 135 | } 136 | return undefined; 137 | }; 138 | 139 | function Blue() { 140 | 141 | 142 | return Show(Enum({ 143 | type() { return \\"Color\\"; }, 144 | get constructor() { return Blue; }, 145 | get parent() { return Color; }, 146 | get fields() { return []; }, 147 | 148 | \\"=\\"(other) { 149 | return other.constructor === Blue; 150 | } 151 | })); 152 | } 153 | 154 | Blue.extract = function (val) { 155 | if (val.constructor === Blue) { 156 | return []; 157 | } 158 | return undefined; 159 | }; 160 | 161 | function Purple() { 162 | 163 | 164 | return Show(Enum({ 165 | type() { return \\"Color\\"; }, 166 | get constructor() { return Purple; }, 167 | get parent() { return Color; }, 168 | get fields() { return []; }, 169 | 170 | \\"=\\"(other) { 171 | return other.constructor === Purple; 172 | } 173 | })); 174 | } 175 | 176 | Purple.extract = function (val) { 177 | if (val.constructor === Purple) { 178 | return []; 179 | } 180 | return undefined; 181 | }; 182 | 183 | var Color = { 184 | order: [Red, Orange, Yellow, Green, Blue, Purple], 185 | Red, 186 | Orange, 187 | Yellow, 188 | Green, 189 | Blue, 190 | Purple 191 | }; 192 | Red()[\\"succ\\"]()[\\"toString\\"](); 193 | " 194 | `; 195 | -------------------------------------------------------------------------------- /packages/plugin-prettier/src/__tests__/prettier-plugin.test.js: -------------------------------------------------------------------------------- 1 | import gen from "../../../zcomp/src/compiler/gen"; 2 | import parse from "../../../zcomp/src/compiler/parse"; 3 | import tokenize from "../../../zcomp/src/compiler/tokenize"; 4 | import PrettierPlugin from "../../../plugin-prettier/src/index"; 5 | 6 | const normalCode = ` 7 | importstd traits 8 | 9 | def { Show, Enum }: traits 10 | 11 | enum Color { 12 | Red, 13 | Orange, 14 | Yellow, 15 | Green, 16 | Blue, 17 | Purple 18 | } derives (Show, Enum) 19 | 20 | Red().succ().toString()`; 21 | 22 | function transpileZ(z, plugins = []) { 23 | return gen(parse(tokenize(z)), true, plugins); 24 | } 25 | 26 | describe("prettier plugin", () => { 27 | it("outputs normal code", () => { 28 | expect(transpileZ(normalCode)).toMatchSnapshot(); 29 | }); 30 | 31 | it("outputs code with prettier correctly", () => { 32 | expect(transpileZ(normalCode, [new PrettierPlugin()])).toMatchSnapshot(); 33 | }); 34 | 35 | it("has the correct metadata", () => { 36 | expect(new PrettierPlugin().name).toBe("zcomp-plugin-prettier"); 37 | }); 38 | 39 | it("has a difference with the prettier plugin", () => { 40 | expect(transpileZ(normalCode, [new PrettierPlugin()])).not.toBe( 41 | transpileZ(normalCode) 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/plugin-prettier/src/index.js: -------------------------------------------------------------------------------- 1 | import Plugin from "@zlanguage/plugin"; 2 | import prettier from "prettier"; 3 | import loadConfig from "@zlanguage/config"; 4 | 5 | /** 6 | * A plugin that runs Prettier on outputted code. 7 | */ 8 | export default class PrettierPlugin extends Plugin { 9 | constructor() { 10 | super(); 11 | this.name = "zcomp-plugin-prettier"; 12 | 13 | this.listen("outputGeneratedCode", this.handleCode); 14 | } 15 | 16 | /** 17 | * Handles the code. 18 | * 19 | * @param {{ code: string }} data The code. 20 | */ 21 | handleCode(data) { 22 | const { code } = data; 23 | return prettier.format(code, PrettierPlugin.prettierConfig); 24 | } 25 | 26 | /** 27 | * The Prettier configuration. 28 | */ 29 | static prettierConfig = loadConfig().prettierOptions ?? { parser: "babel" }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/plugin/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /packages/plugin/README.md: -------------------------------------------------------------------------------- 1 | # `@zlanguage/plugin` 2 | 3 | The plugin API. 4 | -------------------------------------------------------------------------------- /packages/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zlanguage/plugin", 3 | "version": "0.6.2", 4 | "description": "The plugin API.", 5 | "keywords": [ 6 | "z", 7 | "language", 8 | "compiler" 9 | ], 10 | "author": "Reece Dunham ", 11 | "homepage": "https://github.com/zlanguage/zcomp", 12 | "license": "MIT", 13 | "main": "lib/index.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/zlanguage/zcomp.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/zlanguage/zcomp/issues" 20 | }, 21 | "scripts": { 22 | "build": "babel --config-file ../../babel.config.js src --out-dir lib" 23 | }, 24 | "devDependencies": { 25 | "@babel/cli": "^7.12.1", 26 | "@babel/core": "^7.12.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/plugin/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Z plugin. 3 | */ 4 | export default class Plugin { 5 | /** 6 | * Set up the plugin. 7 | * CAUTION: you will want to call super if you override this. 8 | */ 9 | constructor() { 10 | this.name = "Plugin"; 11 | this.listeners = {}; 12 | } 13 | 14 | /** 15 | * A list of valid events you can listen for. 16 | */ 17 | static listenerTypes = ["cliStartup", "outputGeneratedCode"]; 18 | 19 | /** 20 | * Listen for an event, and run the callback once it gets triggered. 21 | * 22 | * @param {string} event The name of the event. 23 | * @param {(data: any) => void} callback The function to run once the event happens. 24 | */ 25 | listen(event, callback) { 26 | if (!Plugin.listenerTypes.includes(event)) { 27 | console.log( 28 | `[WARN] [${this.name}] - I don't know how to listen for ${event}!` 29 | ); 30 | } 31 | if (Array.isArray(this.listeners[event])) { 32 | this.listeners[event].push(callback); 33 | } else { 34 | this.listeners[event] = [callback]; 35 | } 36 | } 37 | 38 | /** 39 | * Internal function to dispatch events. 40 | * DO NOT CALL FROM YOUR PLUGIN!! 41 | * 42 | * @param {string} name The event name. 43 | * @param {any} data The event data. 44 | */ 45 | _eventbus_announce(name, data) { 46 | if (Array.isArray(this.listeners[name])) { 47 | this.listeners[name].forEach((eventCallback) => { 48 | return eventCallback(data); 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/plugin/src/loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load the plugins requested by the configuration. 3 | * 4 | * @param {*} config The configuration. 5 | * @returns {import("./index").default[]} A list of plugin objects. 6 | */ 7 | export default function loadPlugins(config) { 8 | let pluginObjects = []; 9 | const pluginPaths = config.plugins ?? []; 10 | 11 | pluginPaths.forEach((p) => { 12 | pluginObjects.push(new require(p)); 13 | }); 14 | 15 | return pluginObjects; 16 | } 17 | -------------------------------------------------------------------------------- /packages/zcomp/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /packages/zcomp/README.md: -------------------------------------------------------------------------------- 1 | # zcomp 2 | 3 | The Z transpiler/interpreter 4 | 5 | [![Version](https://img.shields.io/npm/v/@zlanguage/zcomp.svg)](https://www.npmjs.com/package/@zlanguage/zcomp) 6 | [![Downloads/week](https://img.shields.io/npm/dw/@zlanguage/zcomp.svg)](https://www.npmjs.com/package/@zlanguage/zcomp) 7 | [![License](https://img.shields.io/npm/l/@zlanguage/zcomp.svg)](https://github.com/zlanguage/zcomp/blob/master/LICENSE) 8 | 9 | # Z 10 | 11 | A transpiled language that can be evaluated as a script (for testing) or transpiled to human-readable JS (for production code). Z supports modules, functions, closure, error handling, and many more features you would expect from a modern language. Is it ready for production code? I'd wait a few months before that. 12 | 13 | ## ZComp 14 | 15 | The Z Compiler (ZComp) can be installed with: 16 | 17 | ```sh 18 | $ npm install -g @zlanguage/zcomp 19 | ``` 20 | 21 | Then, you should install the zstdlib (a mix of a runtime and standard library): 22 | 23 | ```sh 24 | $ npm install -g @zlanguage/zstdlib 25 | ``` 26 | 27 | Finally, navigate to the directory you're using Z in, and type: 28 | 29 | ```sh 30 | $ npm install @zlanguage/zstdlib 31 | ``` 32 | 33 | This installs the Z standard library locally in just the paackage you need it for. 34 | 35 | ## Use the Compiler 36 | 37 | Transpile Z Code: 38 | 39 | ```sh 40 | $ zcomp transpile [path of Z to transpile] [path of where to output the transpiled JS] 41 | ``` 42 | 43 | Run Z Code: 44 | 45 | ```sh 46 | $ zcomp run [path of Z to run] 47 | ``` 48 | 49 | Transpile one directory to another: 50 | 51 | ```sh 52 | $ zcomp dirt [path of directory with Z] [path of "out" directory] 53 | ``` 54 | 55 | Watch a file for changes, and transpile when the file is changed: 56 | 57 | ```sh 58 | $ zcomp watch [path of Z to watch and transpile] [path of where to output the transpiled JS] 59 | ``` 60 | 61 | ## Docs 62 | 63 | The docs are on the offical [Z Website](https://zlanguage.github.io/). 64 | -------------------------------------------------------------------------------- /packages/zcomp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zlanguage/zcomp", 3 | "description": "The Z language transpiler.", 4 | "version": "0.6.2", 5 | "author": "N8", 6 | "bin": { 7 | "zcomp": "node lib/commands/index.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/zlanguage/zcomp/issues" 11 | }, 12 | "dependencies": { 13 | "@zlanguage/zstdlib": "^0.4.1", 14 | "commander": "^6.1.0", 15 | "globby": "^11.0.1", 16 | "vlq": "^1.0.1", 17 | "@zlanguage/config": "^0.6.2", 18 | "@zlanguage/plugin": "^0.6.2" 19 | }, 20 | "engines": { 21 | "node": ">=10.0.0" 22 | }, 23 | "homepage": "https://github.com/zlanguage/zcomp", 24 | "keywords": [ 25 | "Z", 26 | "language", 27 | "compiler" 28 | ], 29 | "license": "MIT", 30 | "main": "src/commands/index.js", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/zlanguage/zcomp.git" 34 | }, 35 | "scripts": { 36 | "build": "babel --config-file ../../babel.config.js src --out-dir lib" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.12.1", 40 | "@babel/core": "^7.12.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/zcomp/src/commands/build.js: -------------------------------------------------------------------------------- 1 | import tokenize from "../compiler/tokenize"; 2 | import parse from "../compiler/parse"; 3 | import gen from "../compiler/gen"; 4 | import fs from "fs"; 5 | 6 | export default function main(file, { outFile }, plugins) { 7 | if (!outFile) { 8 | outFile = file.replace(/(.+).zlang/, "$1.js"); 9 | } 10 | fs.readFile(file, (err, data) => { 11 | if (err) { 12 | return console.log(err); 13 | } 14 | let res; 15 | try { 16 | res = gen(parse(tokenize(data.toString()))); 17 | plugins.forEach((p) => { 18 | res = 19 | p._eventbus_announce("outputGeneratedCode", { 20 | code: res, 21 | }) ?? res; 22 | }); 23 | } catch (err) { 24 | console.log(err); 25 | } 26 | fs.writeFile(outFile, res, (err) => { 27 | if (err) { 28 | console.log(err); 29 | } 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/zcomp/src/commands/index.js: -------------------------------------------------------------------------------- 1 | import { program } from "commander"; 2 | import loadConfig from "@zlanguage/config"; 3 | import loadPlugins from "@zlanguage/plugin/loader"; 4 | 5 | program.version("0.6.0"); 6 | program.name("zcomp"); 7 | program.description("The Z programming language transpiler."); 8 | 9 | const config = loadConfig(); 10 | const plugins = loadPlugins(config); 11 | 12 | // Allow plugins to attach custom commands 13 | plugins.forEach((plugin) => { 14 | plugin._eventbus_announce("cliStartup", { 15 | cli: program, 16 | }); 17 | }); 18 | 19 | program 20 | .command("repl") 21 | .description("Start a Z REPL.") 22 | .action(require("./repl")); 23 | 24 | program 25 | .command("run ") 26 | .description("Run a Z source file.") 27 | .action(require("./run")); 28 | 29 | program 30 | .command("build ") 31 | .description("Build a Z source file into a JavaScript file.") 32 | .option("-o, --out-file", "The name of the file to output the code to.") 33 | .action((file, opts) => require("./build")(file, opts, plugins)); 34 | 35 | program.parse(process.argv); 36 | -------------------------------------------------------------------------------- /packages/zcomp/src/commands/repl.js: -------------------------------------------------------------------------------- 1 | import readline from "readline"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import tokenize from "../compiler/tokenize"; 5 | import parse from "../compiler/parse"; 6 | import gen from "../compiler/gen"; 7 | import process from "process"; 8 | 9 | const lineBreakers = ["(", "{", "["]; 10 | 11 | function getLines(rl, opener) { 12 | const symMap = { 13 | "(": ")", 14 | "{": "}", 15 | "[": "]", 16 | }; 17 | const closer = symMap[opener]; 18 | let open = 1; 19 | let res = ""; 20 | const string = /("(?:[^"\\]|\\(?:[nr"\\]|u\{[0-9A-F]{4,6}\}))*")/g; 21 | return new Promise((resolve) => { 22 | rl.question("..", function recieve(code) { 23 | res += code + "\n"; 24 | code 25 | .replace(string, "") 26 | .split("") 27 | .filter((char) => [opener, closer].includes(char)) 28 | .forEach((char) => { 29 | if (char === opener) { 30 | open += 1; 31 | } else { 32 | open -= 1; 33 | } 34 | }); 35 | if (open > 0) { 36 | rl.question("..", recieve); 37 | } else { 38 | resolve(res); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | export default function main() { 45 | const rl = readline.createInterface({ 46 | input: process.stdin, 47 | output: process.stdout, 48 | }); 49 | 50 | const commands = []; 51 | const loaded = {}; 52 | 53 | rl.question("zrepl>", async function ask(code) { 54 | process.exit(0); 55 | if (code.startsWith(":")) { 56 | const command = code[1]; 57 | let rest = code.slice(2).trim(); 58 | 59 | switch (command) { 60 | case "l": 61 | if (!rest.includes(".")) { 62 | rest = rest + ".zlang"; 63 | } 64 | fs.readFile(path.join(process.cwd(), rest), (err, data) => { 65 | if (err) { 66 | return console.log(err); 67 | } 68 | if (Object.keys(loaded).includes(rest)) { 69 | const [from, to] = loaded[rest]; 70 | commands.splice( 71 | from, 72 | to - from + 1, 73 | ...data.toString().split("\n") 74 | ); 75 | const dif = data.toString().split("\n").length - (to - from + 1); 76 | if (dif > 0) { 77 | loaded[rest] = [from, to + dif]; 78 | } 79 | } else { 80 | loaded[rest] = [ 81 | commands.length, 82 | commands.length - 1 + data.toString().split("\n").length, 83 | ]; 84 | data 85 | .toString() 86 | .split("\n") 87 | .forEach((ln) => { 88 | commands.push(ln); 89 | }); 90 | } 91 | }); 92 | break; 93 | default: 94 | console.log(`Invalid REPL command: ${command}.`); 95 | } 96 | } else { 97 | if (lineBreakers.some((lb) => code.endsWith(lb))) { 98 | code += await getLines(rl, code[code.length - 1]); 99 | } 100 | 101 | let alreadyLogged = false; 102 | 103 | if ( 104 | /^[A-Za-z_+\-/*%&|?^=<>'!][A-Za-z_0-9+\-/*%&|?^<>='!.]*$/.test(code) || 105 | code.startsWith("[") || 106 | code.startsWith('"') || 107 | code.startsWith("`") || 108 | code.startsWith("@") || 109 | /^((?:0[box])?-?\d[\d_]*(?:\.[\d_]+)?(?:e\-?[\d_]+)?[a-z]*)$/.test(code) 110 | ) { 111 | alreadyLogged = true; 112 | code = `log(${code})`; 113 | } 114 | 115 | try { 116 | let res = eval( 117 | gen(parse(tokenize(commands.concat(code).join("\n")), false)) 118 | ); 119 | if (res && res.then) { 120 | res = await res; 121 | } 122 | if (res !== "use strict" && !alreadyLogged) { 123 | console.log(res); 124 | } 125 | if ( 126 | (code.includes(":") && !code.startsWith("log([")) || 127 | code.trim().startsWith("import") || 128 | code.trim().startsWith("importstd") || 129 | code.trim().startsWith("enum") || 130 | code.trim().startsWith("macro") || 131 | code.trim().startsWith("include") || 132 | code.trim().startsWith("includestd") 133 | ) { 134 | commands.push(code); 135 | } 136 | } catch (err) { 137 | console.log( 138 | err.message ? "Error:" : "", 139 | err.message ? err.message : err 140 | ); 141 | } 142 | } 143 | rl.question("zrepl>", ask); 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /packages/zcomp/src/commands/run.js: -------------------------------------------------------------------------------- 1 | import tokenize from "../compiler/tokenize"; 2 | import parse from "../compiler/parse"; 3 | import gen from "../compiler/gen"; 4 | import fs from "fs"; 5 | 6 | export default function main(path) { 7 | fs.readFile(path, (err, data) => { 8 | if (err) { 9 | return console.log(err); 10 | } 11 | const deps = []; 12 | const res = []; 13 | const ast = parse(tokenize(data.toString())); 14 | ast.forEach((statement, index) => { 15 | if (statement.type === "import" && statement.wunth.includes(".zlang")) { 16 | deps.push(statement.wunth.replace(/"/g, "")); 17 | const r = statement.wunth 18 | .replace(/([^\/]+)\.zlang/, "_tmp_$1.js") 19 | .replace(/"/g, ""); 20 | res.push(r); 21 | ast[index].wunth = `"${r}"`; 22 | } 23 | }); 24 | if (deps.length === 0) { 25 | const tmppath = path.replace(/([^\/]+)\.zlang/, "_tmp_$1.js"); 26 | fs.writeFile(tmppath, gen(ast), (err) => { 27 | if (err) { 28 | return console.log(err); 29 | } 30 | const moduleFilename = module.filename; 31 | const _filename = __filename; 32 | const _dirname = __dirname; 33 | module.filename = tmppath; 34 | __filename = tmppath; 35 | __dirname = tmppath.replace(/([^\/]+)\.(.+)/, ""); 36 | try { 37 | eval(gen(ast)); 38 | } catch (err) { 39 | console.log(err); 40 | } 41 | module.filename = moduleFilename; 42 | __filename = _filename; 43 | __dirname = _dirname; 44 | fs.unlink(tmppath, (err) => { 45 | if (err) { 46 | console.log(err); 47 | } 48 | }); 49 | }); 50 | } 51 | let complete = 0; 52 | let errFound = false; 53 | deps.forEach((dep, index) => { 54 | if (errFound) { 55 | return; 56 | } 57 | const r = res[index]; 58 | dep = dep.replace(/^(\.\/|\/)/, ""); 59 | fs.readFile(dep, (err, data) => { 60 | if (err) { 61 | return console.log(err); 62 | } 63 | let transpiled; 64 | try { 65 | transpiled = gen(parse(tokenize(data.toString()), false)); 66 | } catch (err) { 67 | console.log(`Error in file ${dep}:`); 68 | console.log(err.toString()); 69 | errFound = true; 70 | res.forEach((r) => { 71 | if (fs.existsSync(r)) { 72 | fs.unlink(r, (err) => { 73 | if (err) { 74 | console.log(err); 75 | } 76 | }); 77 | } 78 | }); 79 | return; 80 | } 81 | fs.writeFile(r, transpiled, (err) => { 82 | if (err) { 83 | return console.log(err); 84 | } 85 | complete += 1; 86 | if (complete === deps.length) { 87 | const tmppath = path.replace(/([^\/]+)\.zlang/, "_tmp_$1.js"); 88 | fs.writeFile(tmppath, gen(ast), (err) => { 89 | if (err) { 90 | return console.log(err); 91 | } 92 | const moduleFilename = module.filename; 93 | const _filename = __filename; 94 | const _dirname = __dirname; 95 | module.filename = tmppath; 96 | __filename = tmppath; 97 | __dirname = tmppath.replace(/([^\/]+)\.(.+)/, ""); 98 | try { 99 | eval(gen(ast)); 100 | } catch (err) { 101 | console.log(err); 102 | } 103 | module.filename = moduleFilename; 104 | __filename = _filename; 105 | __dirname = _dirname; 106 | fs.unlink(tmppath, (err) => { 107 | if (err) { 108 | console.log(err); 109 | } 110 | }); 111 | res.forEach((r) => { 112 | fs.unlink(r, (err) => { 113 | if (err) { 114 | console.log(err); 115 | } 116 | }); 117 | }); 118 | }); 119 | } 120 | }); 121 | }); 122 | }); 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/__tests__/__snapshots__/transpile.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Transpiling snapshot tests ($Z Macro) should transpile log($Z) to the snapshot 1`] = `"log($Z);"`; 4 | 5 | exports[`Transpiling snapshot tests (advanced function & features) should transpile .val to the snapshot 1`] = `"function (obj) {return obj[\\"val\\"];};"`; 6 | 7 | exports[`Transpiling snapshot tests (advanced function & features) should transpile Math.pow(@, @) to the snapshot 1`] = `"curry(function ($, $$) {return Math[\\"pow\\"]($, $$);});"`; 8 | 9 | exports[`Transpiling snapshot tests (advanced function & features) should transpile func () { exit { log("Hello World") } } to the snapshot 1`] = `"function (){ try { {} } finally { if (!(log(\\"Hello World\\"))) { throw new Error(\\"Exit failed\\") } } };"`; 10 | 11 | exports[`Transpiling snapshot tests (advanced function & features) should transpile func (x number!) number! { 12 | return x 13 | } to the snapshot 1`] = `"function (x) {if (!($eq(typeOf(x), \\"number\\"))) { throw new Error(\\"Enter failed\\") }return assertType(\\"number\\", x);};"`; 14 | 15 | exports[`Transpiling snapshot tests (advanced function & features) should transpile x number!: 3 to the snapshot 1`] = `"x = assertType(\\"number\\", 3);"`; 16 | 17 | exports[`Transpiling snapshot tests (comments) should transpile # Hola to the snapshot 1`] = `""`; 18 | 19 | exports[`Transpiling snapshot tests (control flow) should transpile if foo { log(bar) } else if fooey { log(baree) } else { log(foobar) } to the snapshot 1`] = `"if (assertBool(foo)) {log(bar);} else {if (assertBool(fooey)) {log(baree);} else {log(foobar);}}"`; 20 | 21 | exports[`Transpiling snapshot tests (control flow) should transpile if foo { log(bar) } to the snapshot 1`] = `"if (assertBool(foo)) {log(bar);}"`; 22 | 23 | exports[`Transpiling snapshot tests (control flow) should transpile log(bar) if foo to the snapshot 1`] = `"if (assertBool(foo)) {log(bar);}"`; 24 | 25 | exports[`Transpiling snapshot tests (control flow) should transpile loop { log("YAY") } to the snapshot 1`] = `"while (true) {log(\\"YAY\\");}"`; 26 | 27 | exports[`Transpiling snapshot tests (enums) should transpile enum Foo { 28 | Bar(x: number!), 29 | Baz(y: _!, z: number!) 30 | } derives (Nada) where { 31 | foobar () {} 32 | } to the snapshot 1`] = `"function Bar(x) {if($eq(Object.keys((x == null) ? { [Symbol()]: 0 } : x).sort(), [\\"x\\"].sort())) {({ x } = x);}if (typeOf(x) !== \\"number\\") {throw new Error(\\"Foo.Bar.x must be of type number. However, you passed \\" + x + \\" to Foo.Bar which is not of type number.\\");}return Nada({type() { return \\"Foo\\"; },get constructor() { return Bar; },get parent() { return Foo; },get fields() { return [\\"x\\"]; },get x(){ return x; },\\"=\\"(other) {return other.constructor === Bar && $eq(x, other.x);}});}Bar.extract = function (val) {if (val.constructor === Bar) {return [val.x];}return undefined;};function Baz(y, z) {if($eq(Object.keys((y == null) ? { [Symbol()]: 0 } : y).sort(), [\\"y\\", \\"z\\"].sort())) {({ y, z } = y);}if (typeOf(z) !== \\"number\\") {throw new Error(\\"Foo.Baz.z must be of type number. However, you passed \\" + z + \\" to Foo.Baz which is not of type number.\\");}return Nada({type() { return \\"Foo\\"; },get constructor() { return Baz; },get parent() { return Foo; },get fields() { return [\\"y\\", \\"z\\"]; },get y(){ return y; },get z(){ return z; },\\"=\\"(other) {return other.constructor === Baz && $eq(y, other.y) && $eq(z, other.z);}});}Baz.extract = function (val) {if (val.constructor === Baz) {return [val.y, val.z];}return undefined;};var Foo = {order: [Bar, Baz],Bar,Baz};Foo.foobar = function () {};"`; 33 | 34 | exports[`Transpiling snapshot tests (enums) should transpile enum Point(x: number!, y: number!) where { 35 | nada () {} 36 | } to the snapshot 1`] = `"function Point(x, y) {if($eq(Object.keys((x == null) ? { [Symbol()]: 0 } : x).sort(), [\\"x\\", \\"y\\"].sort())) {({ x, y } = x);}if (typeOf(x) !== \\"number\\") {throw new Error(\\"Point.Point.x must be of type number. However, you passed \\" + x + \\" to Point.Point which is not of type number.\\");}if (typeOf(y) !== \\"number\\") {throw new Error(\\"Point.Point.y must be of type number. However, you passed \\" + y + \\" to Point.Point which is not of type number.\\");}return {type() { return \\"Point\\"; },get constructor() { return Point; },get parent() { return Point; },get fields() { return [\\"x\\", \\"y\\"]; },get x(){ return x; },get y(){ return y; },\\"=\\"(other) {return other.constructor === Point && $eq(x, other.x) && $eq(y, other.y);}};}Point.extract = function (val) {if (val.constructor === Point) {return [val.x, val.y];}return undefined;};Point.order = [Point];Point.nada = function () {};"`; 37 | 38 | exports[`Transpiling snapshot tests (error handling) should transpile try { 39 | raise "FOOEY" 40 | } on err { 41 | settle err 42 | } to the snapshot 1`] = `"try {throw new Error(\\"FOOEY\\");} catch (err) {err[\\"settled\\"] = true;if (assertBool($eq(err[\\"settled\\"], undefined))) {throw new Error(\\"Error err not settled.\\")}}"`; 43 | 44 | exports[`Transpiling snapshot tests (goroutines) should transpile copy(go func () { 45 | get fooey() 46 | }) to the snapshot 1`] = `"copy(async function () {await fooey()._from();});"`; 47 | 48 | exports[`Transpiling snapshot tests (goroutines) should transpile get foo to the snapshot 1`] = `"var $main = async function () {await foo._from();};$main();"`; 49 | 50 | exports[`Transpiling snapshot tests (goroutines) should transpile go { 51 | get fooey() 52 | } to the snapshot 1`] = `"(async function () {await fooey()._from();})();"`; 53 | 54 | exports[`Transpiling snapshot tests (include) should transpile includestd "imperative" to the snapshot 1`] = `""`; 55 | 56 | exports[`Transpiling snapshot tests (metadata) should transpile operator +: 1000 to the snapshot 1`] = `"/* operator $plus = 1000 */"`; 57 | 58 | exports[`Transpiling snapshot tests (modules) should transpile export fooey to the snapshot 1`] = `"module.exports = stone(fooey);"`; 59 | 60 | exports[`Transpiling snapshot tests (modules) should transpile import foo to the snapshot 1`] = `"var foo = stone(require(\\"foo\\"));"`; 61 | 62 | exports[`Transpiling snapshot tests (modules) should transpile import foo: "./foo" to the snapshot 1`] = `"var foo = stone(require(\\"./foo\\"));"`; 63 | 64 | exports[`Transpiling snapshot tests (modules) should transpile import ramda.src.map to the snapshot 1`] = `"var map = stone(require(\\"ramda/src/map\\"));"`; 65 | 66 | exports[`Transpiling snapshot tests (modules) should transpile importstd gr to the snapshot 1`] = `"var gr = stone(require(\\"@zlanguage/zstdlib/src/js/gr\\"));"`; 67 | 68 | exports[`Transpiling snapshot tests (variable declarations) should transpile def x: 0 to the snapshot 1`] = `"var x = 0;"`; 69 | 70 | exports[`Transpiling snapshot tests (variable declarations) should transpile def x: 0, y: 0 to the snapshot 1`] = `"var x = 0, y = 0;"`; 71 | 72 | exports[`Transpiling snapshot tests (variable declarations) should transpile hoist x: 0 to the snapshot 1`] = `"var x = 0;"`; 73 | 74 | exports[`Transpiling snapshot tests (variable declarations) should transpile hoist x: 0, y: 0 to the snapshot 1`] = `"var x = 0, y = 0;"`; 75 | 76 | exports[`Transpiling snapshot tests (variable declarations) should transpile let x: 0 to the snapshot 1`] = `"let x = 0;"`; 77 | 78 | exports[`Transpiling snapshot tests (variable declarations) should transpile let x: 0, y: 0 to the snapshot 1`] = `"let x = 0, y = 0;"`; 79 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/__tests__/transpile.test.js: -------------------------------------------------------------------------------- 1 | import tokenize from "../tokenize"; 2 | import parse from "../parse"; 3 | import gen from "../gen"; 4 | 5 | const evalTests = { 6 | "math and string manipulations": { 7 | "3 + 3": 6, 8 | "2 - 2": 0, 9 | "2 - 9": -7, 10 | "3 * 7": 21, 11 | "6 / 2": 3, 12 | "3 + 2 * 7": 17, 13 | "3 / 3 - 2": -1, 14 | "0.5 + 0.25": 0.75, 15 | "0.125 - 0.25 * 0.5": 0, 16 | "0.125 / 4 * 10": 0.3125, 17 | "-3.2 + -3": -6.2, 18 | "2 ^ 3": 8, 19 | "2 pow 3": 8, 20 | "2 ^ 3 ^ 3": 512, 21 | "2 pow 3 pow 3": 134217728, 22 | "1 pow 0": 1, 23 | "2 * 3 ^ 3 + 2": 56, 24 | "5 % 2": 1, 25 | "5 % 2 + 1": 2, 26 | "[1, 2, 3] ++ [4, 5, 6]": [1, 2, 3, 4, 5, 6], 27 | '"Hello" ++ " World"': "Hello World", 28 | '"[" ++ [1, 2, 3] ++ "]"': "[1,2,3]", 29 | }, 30 | "relational and boolean logic operators": { 31 | "true and false": false, 32 | "true or false": true, 33 | "true and false or false": false, 34 | "3 = 3": true, 35 | "0.1 + 0.2 = 0.3": true, 36 | "2 = 3": false, 37 | '"Hello" = "Hello"': true, 38 | '"Hello" = "World"': false, 39 | 'not("Hello" = "World")': true, 40 | "[1, 2, 3] = (1, 2, 3)": true, 41 | "[[1], [2], [3]] = ([1], [2], [3])": true, 42 | "[1, 2, 4] = {1, 2, 3}": false, 43 | '["x": 3, "y": 4] = ["x": 3, "y": 4]': true, 44 | '["x": 3, "y": 5] = ["x": 3, "y": 4]': false, 45 | "3 > 2": true, 46 | "2 > 3": false, 47 | "2 < 3": true, 48 | "3 < 2": false, 49 | "3 >= 3": true, 50 | "3 >= 2": true, 51 | "3 >= 4": false, 52 | "3 <= 3": true, 53 | "3 <= 4": true, 54 | "3 <= 2": false, 55 | "copy(if (true) 3 else 5)": 3, 56 | "copy(if (false) 3 else 5)": 5, 57 | "copy(if (true) 3 else if (false) 7 else 4)": 3, 58 | "copy(if (false) 3 else if (true) 7 else 4)": 7, 59 | "copy(if (false) 3 else if (false) 7 else 4)": 4, 60 | }, 61 | "refinements, subscripts, and invocations": { 62 | "copy(func(){}())": undefined, 63 | "copy(console.log)": console.log, 64 | 'copy(console["log"])': console.log, 65 | "copy(console.log.call)": console.log.call, 66 | 'copy(console["log"].call)': console.log.call, 67 | "copy(console..log..call)": console.log.call, 68 | "copy(console..foo..call)": undefined, 69 | 'copy("Hello").slice(0, -1)': "Hell", 70 | "copy(undefined..x..y)": undefined, 71 | '"Hello"..slice(0, -1)': "Hell", 72 | }, 73 | functions: { 74 | "copy(func x! + 2)(2)": 4, 75 | "copy(func () @@iterator)()": Symbol.iterator, 76 | [`copy(func (x number!, y number!) number! { 77 | return x + y 78 | })(3, 4)`]: 7, 79 | }, 80 | enums: { 81 | [`enum Point(x: number!, y: number!) 82 | Point(3, 4).x 83 | `]: 3, 84 | [`enum Point(x: number!, y: number!) 85 | Point(y: 3, x: 4).y 86 | `]: 3, 87 | [`importstd traits 88 | def { Show, Enum }: traits 89 | enum Color { 90 | Red, 91 | Orange, 92 | Yellow, 93 | Green, 94 | Blue, 95 | Purple 96 | } derives (Show, Enum) 97 | Red().succ().toString()`]: "Orange()", 98 | }, 99 | "loop expressions": { 100 | "[] ++ loop(x <- 1...10) x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 101 | "[] ++ loop(x <- 1 to 10, if x % 2 = 0) x * 2": [4, 8, 12, 16, 20], 102 | "[] ++ loop(x <- 1 to 10, y: x * 2, if y % 4 = 0) x": [2, 4, 6, 8, 10], 103 | "[] ++ loop(x <- 1 to 3, if x % 2 = 0, y <- 1 to 3, z: x * y) z": [2, 4, 6], 104 | }, 105 | "pattern matching": { 106 | [`match 3 { 107 | 3 => "yes", 108 | _ => "no" 109 | }`]: "yes", 110 | [`match 4 { 111 | 3 => "yes", 112 | _ => "no" 113 | }`]: "no", 114 | [`match [1, 2, 3] { 115 | (number!n, ...xs) => xs, 116 | _ => "no" 117 | }`]: [2, 3], 118 | [`match 3 { 119 | number! => "A number.", 120 | string! => "A string.", 121 | array! => "An array", 122 | _ => "Something else." 123 | }`]: "A number.", 124 | [`match "hola" { 125 | number! => "A number.", 126 | string! => "A string.", 127 | array! => "An array", 128 | _ => "Something else." 129 | }`]: "A string.", 130 | [`match [1, 2, 3] { 131 | number! => "A number.", 132 | string! => "A string.", 133 | array! => "An array", 134 | _ => "Something else." 135 | }`]: "An array", 136 | [`match \`a\` { 137 | number! => "A number.", 138 | string! => "A string.", 139 | array! => "An array", 140 | _ => "Something else." 141 | }`]: "Something else.", 142 | [`match ["x": 3, "y": 4] { 143 | { x: number!, y: number! } => "A point-like object.", 144 | _ => "no" 145 | }`]: "A point-like object.", 146 | }, 147 | macros: { 148 | [`macro $hello () { 149 | return ~{ 150 | "Hey" 151 | }~ 152 | } 153 | copy($hello)`]: "Hey", 154 | [`macro $join(~l:expr, ~r:expr) { 155 | return ~{ 156 | {{~l}} ++ " " ++ {{~r}} 157 | }~ 158 | } 159 | copy($join "Hello" "World")`]: "Hello World", 160 | [`macro $proc(~body:block) { 161 | return ~{ 162 | func () { 163 | {{~body}} 164 | } 165 | }~ 166 | } 167 | copy($proc { 168 | return 3 169 | })()`]: 3, 170 | [`macro operator ->(~param:expr, ~body:expr) { 171 | return ~{ 172 | func ({{~param}}) { 173 | return {{~body}} 174 | } 175 | }~ 176 | } 177 | copy(x -> x * 2)(5)`]: 10, 178 | [`macro $do (~body:block, while, ~cond:expr) { 179 | return ~{ 180 | loop { 181 | {{~body}} 182 | if not({{~cond}}) { 183 | break 184 | } 185 | } 186 | }~ 187 | } 188 | 189 | macro $do (~body:block, until, ~cond:expr) { 190 | return ~{ 191 | loop { 192 | {{~body}} 193 | if {{~cond}} { 194 | break 195 | } 196 | } 197 | }~ 198 | } 199 | 200 | macro $do (~body:block, unless, ~cond:expr) { 201 | return ~{ 202 | if not({{~cond}}) { 203 | {{~body}} 204 | } 205 | }~ 206 | }`]: "use strict", 207 | [`macro $switch (...{case, ~body:block},) { 208 | 209 | }`]: "use strict", 210 | }, 211 | }; 212 | 213 | const transpileTests = { 214 | "variable declarations": [ 215 | "let x: 0", 216 | "let x: 0, y: 0", 217 | "def x: 0", 218 | "def x: 0, y: 0", 219 | "hoist x: 0", 220 | "hoist x: 0, y: 0", 221 | ], 222 | modules: [ 223 | "import foo", 224 | 'import foo: "./foo"', 225 | "import ramda.src.map", 226 | "importstd gr", 227 | "export fooey", 228 | ], 229 | "$Z Macro": ["log($Z)"], 230 | include: ['includestd "imperative"'], 231 | "control flow": [ 232 | "if foo { log(bar) }", 233 | "log(bar) if foo", 234 | "if foo { log(bar) } else if fooey { log(baree) } else { log(foobar) }", 235 | 'loop { log("YAY") }', 236 | ], 237 | "error handling": [ 238 | `try { 239 | raise "FOOEY" 240 | } on err { 241 | settle err 242 | }`, 243 | ], 244 | goroutines: [ 245 | `copy(go func () { 246 | get fooey() 247 | })`, 248 | `go { 249 | get fooey() 250 | }`, 251 | "get foo", 252 | ], 253 | comments: ["# Hola"], 254 | enums: [ 255 | `enum Foo { 256 | Bar(x: number!), 257 | Baz(y: _!, z: number!) 258 | } derives (Nada) where { 259 | foobar () {} 260 | } `, 261 | `enum Point(x: number!, y: number!) where { 262 | nada () {} 263 | }`, 264 | ], 265 | "advanced function & features": [ 266 | "x number!: 3", 267 | `func (x number!) number! { 268 | return x 269 | }`, 270 | 'func () { exit { log("Hello World") } }', 271 | "Math.pow(@, @)", 272 | ".val", 273 | ], 274 | metadata: ["operator +: 1000"], 275 | }; 276 | 277 | function evalZ(z) { 278 | return eval(gen(parse(tokenize(z)))); 279 | } 280 | 281 | describe("Evaluation tests", () => { 282 | Object.entries(evalTests).forEach(([testName, tests]) => { 283 | describe(testName, () => { 284 | Object.entries(tests).forEach(([expr, res]) => { 285 | it(`should evaluate ${expr} as ${ 286 | res == null ? res : res.toString() 287 | }`, () => { 288 | expect(evalZ(expr)).toStrictEqual(res); 289 | }); 290 | }); 291 | }); 292 | }); 293 | }); 294 | 295 | function transpileZ(z) { 296 | return gen(parse(tokenize(z)), false); 297 | } 298 | 299 | describe("Transpiling snapshot tests", () => { 300 | Object.entries(transpileTests).forEach(([testName, tests]) => { 301 | describe(`(${testName})`, () => { 302 | tests.forEach((test) => { 303 | it(`should transpile ${test} to the snapshot`, () => { 304 | expect( 305 | transpileZ(test) 306 | .split("\n") 307 | .map((x) => x.trim()) 308 | .join("") 309 | ).toMatchSnapshot(); 310 | }); 311 | }); 312 | }); 313 | }); 314 | }); 315 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/__tests__/types.test.js: -------------------------------------------------------------------------------- 1 | import tokenize from "../tokenize"; 2 | import parse from "../parse"; 3 | import gen from "../gen"; 4 | const testStr = "let x: 0"; 5 | const testTok = tokenize(testStr)(); 6 | const testAst = parse(tokenize(testStr)); 7 | const testGened = gen(parse(tokenize(testStr))); 8 | 9 | describe("Tokenization", () => { 10 | it("should take one (non-optional) parameter", () => { 11 | expect(tokenize.length).toBe(1); 12 | }); 13 | 14 | it("should return a function", () => { 15 | expect(tokenize(testStr)).toBeInstanceOf(Function); 16 | }); 17 | 18 | it("should have returned a function that returns an object", () => { 19 | expect(testTok).toBeInstanceOf(Object); 20 | }); 21 | 22 | it("should have returned a function that returns a proper token", () => { 23 | expect( 24 | Object.prototype.hasOwnProperty.call(testTok, "id") || 25 | Object.prototype.hasOwnProperty.call(testTok, "lineNumber") || 26 | Object.prototype.hasOwnProperty.call(testTok, "columnNumber") || 27 | Object.prototype.hasOwnProperty.call(testTok, "columnWidth") 28 | ).toBe(true); 29 | }); 30 | 31 | it("should produce error AST", () => { 32 | expect(tokenize("✔️")()).toEqual({ 33 | id: "(error)", 34 | lineNumber: 0, 35 | columnNumber: 0, 36 | columnTo: 0, 37 | string: "", 38 | string: "✔️", 39 | }); 40 | }); 41 | 42 | it("should return undefined if comment AST is disabled", () => { 43 | expect(tokenize("# testing")()).toBeUndefined(); 44 | }); 45 | 46 | it("should return comment AST if it is enabled", () => { 47 | expect(tokenize("# testing 2", true)()).toStrictEqual({ 48 | columnNumber: 0, 49 | columnTo: 11, 50 | comment: "# testing 2", 51 | id: "(comment)", 52 | lineNumber: 0, 53 | }); 54 | }); 55 | }); 56 | 57 | describe("Parsing", () => { 58 | it("should take one parameter: the token generator", () => { 59 | expect(parse.length).toBe(1); 60 | }); 61 | 62 | it("should return an array of objects", () => { 63 | testAst.forEach((ast) => expect(ast).toBeInstanceOf(Object)); 64 | }); 65 | }); 66 | 67 | describe("Code generation", () => { 68 | it("should take one formal, non optional parameter: the AST", () => { 69 | expect(gen.length).toBe(1); 70 | }); 71 | 72 | it("should return a string", () => { 73 | expect(typeof testGened === "string").toBe(true); 74 | }); 75 | 76 | it("should return a string that imports the Z Standard Library", () => { 77 | expect(testGened).toContain('var $Z = require("@zlanguage/zstdlib")'); 78 | }); 79 | 80 | it("should return a string that sets strict mode to true", () => { 81 | expect(testGened.startsWith(`"use strict"`)).toBe(true); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/gen.js: -------------------------------------------------------------------------------- 1 | import runtime from "@zlanguage/zstdlib"; 2 | import { AstNode, validTypes } from "./types"; 3 | 4 | export const prims = Object.keys(runtime); 5 | 6 | /** 7 | * The prelude. 8 | */ 9 | export let res = `"use strict"; 10 | 11 | var $Z = require("@zlanguage/zstdlib"); 12 | var matcher = require("@zlanguage/zstdlib/src/js/matcher"); 13 | 14 | ${prims.map((name) => `var ${name} = $Z.${name};`).join("\n")} 15 | `; 16 | 17 | let index = 0; 18 | 19 | /** 20 | * The currently focused element. 21 | * 22 | * @type {AstNode} 23 | */ 24 | let curr; 25 | 26 | /** 27 | * The amount of spaces before any statements generated after a change of this value. 28 | * 29 | * @type {number} 30 | */ 31 | let padstart = 0; 32 | 33 | /** 34 | * Adds 2 spaces before any statements. 35 | */ 36 | function indent() { 37 | padstart += 2; 38 | } 39 | 40 | /** 41 | * Decreases indent by 2. 42 | */ 43 | function outdent() { 44 | padstart -= 2; 45 | } 46 | 47 | /** 48 | * Stringifies an expression. 49 | * 50 | * @param {string | number | AstNode | any[]} thing The expression. 51 | */ 52 | function zStringify(thing) { 53 | if (typeof thing === "string") { 54 | if (thing.startsWith("@@")) { 55 | return `Symbol.${thing.replace("@@", "")}`; 56 | } 57 | if (thing.startsWith("@")) { 58 | return `Symbol.for("${thing.replace("@", "")}")`; 59 | } 60 | if (thing.startsWith('"')) { 61 | return ( 62 | '"' + 63 | thing.slice(1, -1).replace(/\\|\n|\t|\r|"/g, (p) => { 64 | switch (p) { 65 | case '"': { 66 | return '\\"'; 67 | } 68 | case "\n": { 69 | return "\\n"; 70 | } 71 | case "\\": { 72 | return "\\\\"; 73 | } 74 | case "\t": { 75 | return "\\t"; 76 | } 77 | case "\r": { 78 | return "\\r"; 79 | } 80 | } 81 | }) + 82 | '"' 83 | ); 84 | } 85 | return thing; 86 | } else if (isExpr(thing)) { 87 | let anchor = curr; 88 | curr = thing; 89 | let res = genExpr(); 90 | curr = anchor; 91 | return res; 92 | } else if (typeof thing === "number") { 93 | return thing.toString(); 94 | } else if (thing && thing.species === "Object") { 95 | indent(); 96 | const anchor = curr; 97 | const obj = thing 98 | .map(([k, v]) => { 99 | return `[${(() => { 100 | curr = k; 101 | return genExpr(); 102 | })()}]: ${(() => { 103 | curr = v; 104 | return genExpr(); 105 | })()}`; // [computed property]: val 106 | }) 107 | .map((x) => x.padStart(padstart + x.length)) 108 | .join(",\n"); 109 | curr = anchor; 110 | outdent(); 111 | return "{\n" + obj + "\n" + "}".padStart(padstart + 1); 112 | } else if (Array.isArray(thing)) { 113 | // Empty object literal: [:] 114 | if (thing.toString() === ":") { 115 | return "{}"; 116 | } 117 | const anchor = curr; 118 | const arr = thing 119 | .map((expr) => { 120 | curr = expr; 121 | return genExpr(); 122 | }) 123 | .join(", "); 124 | curr = anchor; 125 | return `[${arr}]`; 126 | } else { 127 | return thing; 128 | } 129 | } 130 | 131 | /** 132 | * Generates a parameter list. 133 | * 134 | * @returns {string} The parameter list as a string. 135 | */ 136 | function genParameterList() { 137 | let r = curr.wunth.map((parameter) => { 138 | curr = parameter; 139 | return genExpr(); 140 | }); 141 | return r.join(", "); 142 | } 143 | 144 | // For conditional refinements. 145 | let condrList = []; 146 | 147 | /** 148 | * Generates a chained expression. 149 | */ 150 | function genExprTwoth() { 151 | let r = ""; 152 | switch (curr.type) { 153 | case "refinement": { 154 | r += `["${curr.wunth}"]`; 155 | r += genTwoth(); 156 | break; 157 | } 158 | case "subscript": { 159 | r += `[${zStringify(curr.wunth)}]`; 160 | r += genTwoth(); 161 | break; 162 | } 163 | case "condrefinement": { 164 | // Record the chained conditional refinement. 165 | condrList.push(curr.zeroth); 166 | const upToNow = [ 167 | condrList[0], 168 | ...condrList.slice(1).map((r) => `["${r}"]`), 169 | ].join(""); 170 | r += ` && ${upToNow}["${curr.wunth}"]`; 171 | r += genTwoth(); 172 | break; 173 | } 174 | case "invocation": { 175 | r += "("; 176 | let anchor = curr; 177 | r += genParameterList(); 178 | r += ")"; 179 | curr = anchor; 180 | r += genTwoth(); 181 | break; 182 | } 183 | case "assignment": { 184 | r += " = "; 185 | curr = curr.wunth; 186 | r += genExpr(); 187 | break; 188 | } 189 | } 190 | return r; 191 | } 192 | 193 | /** 194 | * Traverse an expression, add all the different parts together. 195 | * 196 | * @returns {string} The twoth. 197 | */ 198 | function genTwoth() { 199 | let r = ""; 200 | if (curr.twoth) { 201 | curr = curr.twoth; 202 | if (curr.type !== "condrefinement") { 203 | // Restart the conditional refinement list after a conditional refinement chain ends. 204 | condrList = []; 205 | } 206 | r += genExprTwoth(); 207 | } 208 | return r; 209 | } 210 | 211 | /** 212 | * Check if a node is an expression. 213 | * 214 | * @param {AstNode} astNode The node. 215 | * @returns {boolean} If the node is an expression. 216 | */ 217 | export function isExpr(obj) { 218 | return ( 219 | obj && 220 | [ 221 | "subscript", 222 | "refinement", 223 | "invocation", 224 | "assignment", 225 | "function", 226 | "spread", 227 | "match", 228 | "range", 229 | "loopexpr", 230 | "ifexpr", 231 | "goroutine", 232 | "get", 233 | "condrefinement", 234 | "condsubscript", 235 | ].includes(obj.type) 236 | ); 237 | } 238 | 239 | /** 240 | * Generates array and object destrucutring. 241 | * 242 | * @param {{ species?: string }} arr The array. 243 | */ 244 | function genDestructuring(arr) { 245 | if (arr && arr.species && arr.species.startsWith("Destructuring")) { 246 | // Handle destrucutring 247 | switch (arr.species.slice(13)) { 248 | case "[Array]": 249 | return `[${arr.map(genDestructuring).join(", ")}]`; 250 | case "[Object]": { 251 | let r = "{"; 252 | r += arr 253 | .map((dstruct) => { 254 | if (dstruct.type === "assignment") { 255 | // Handle destructured aliases { key: name } 256 | return `${dstruct.zeroth} : ${dstruct.wunth}`; 257 | } 258 | return dstruct; 259 | }) 260 | .join(", "); 261 | r += "}"; 262 | return r; 263 | } 264 | } 265 | } else if (isExpr(arr)) { 266 | // Other left-hand assignments are left alone. 267 | let anchor = curr; 268 | curr = arr; 269 | let res = genExpr(); 270 | curr = anchor; 271 | return res; 272 | } 273 | return arr; 274 | } 275 | 276 | /** 277 | * Utility function to detect namespaced extractors. 278 | * 279 | * @param {AstNode?} thing The AST node. 280 | * @returns {boolean} 281 | */ 282 | export function typeIn(thing, type) { 283 | if (thing === undefined) { 284 | return false; 285 | } 286 | return ( 287 | thing.type === type || 288 | typeIn(thing.zeroth, type) || 289 | typeIn(thing.wunth, type) || 290 | typeIn(thing.twoth, type) 291 | ); 292 | } 293 | 294 | /** 295 | * Transforms a pattern into calls to Z's matcher library. 296 | * 297 | * @param {*} pat The pattern. 298 | * @returns {string} A call to the matcher library. 299 | */ 300 | function stringifyPat(pat) { 301 | if (typeof pat === "string" && pat.includes("$exclam")) { 302 | // Detect type 303 | const parts = pat.split("$exclam").map((x) => x); 304 | if (parts.length === 1) { 305 | // Detect type wildcard 306 | return `matcher.type("${parts[0]}"")`; 307 | } else { 308 | return `matcher.type("${parts[0]}", "${parts[1]}")`; 309 | } 310 | } 311 | if (typeof pat === "string" && pat.endsWith("$question")) { 312 | return `matcher.predicate(${pat.replace(/\$question$/, "")})`; 313 | } 314 | if (zStringify(pat).endsWith('?"]')) { 315 | return `matcher.predicate(${zStringify(pat).replace(/\?"]/, '"]')})`; 316 | } 317 | if (pat.species === "Destructuring[Array]") { 318 | return `matcher.arr(${pat.map(stringifyPat).join(", ")})`; 319 | } 320 | if (/^[a-z_]$/.test(pat[0])) { 321 | return `matcher.wildcard("${pat}")`; 322 | } 323 | if (pat.species === "Destructuring[Object]") { 324 | return `matcher.obj(${pat 325 | .map((patpart) => { 326 | if (patpart.type !== "assignment") { 327 | throw new Error("Object pattern matching expression requires value."); 328 | } 329 | const key = patpart.zeroth.startsWith('"') 330 | ? patpart.zeroth 331 | : `"${patpart.zeroth}"`; 332 | return `matcher.prop(${key}, ${stringifyPat(patpart.wunth)})`; 333 | }) 334 | .join(", ")})`; 335 | } 336 | if (pat.type === "spread") { 337 | return `matcher.rest("${pat.wunth}")`; 338 | } 339 | if (pat.type === "range") { 340 | return `matcher.range(${pat.zeroth}, ${pat.wunth})`; 341 | } 342 | // Detect extractor 343 | if (pat.type === "invocation" || typeIn(pat, "invocation")) { 344 | if (pat.zeroth === "to" || pat.zeroth === "til" || pat.zeroth === "by") { 345 | const range = zStringify(pat); 346 | return `matcher.range(${range}[0], ${range}[${range}.length - 1])`; 347 | } 348 | let destruct = pat.wunth; 349 | let temp = pat.twoth; 350 | // Support namespaced extractors 351 | while (temp) { 352 | if (temp.type === "invocation" && temp.twoth === undefined) { 353 | destruct = temp.wunth; 354 | let cursor = pat; 355 | while (cursor.twoth && cursor.twoth.twoth) { 356 | cursor = cursor.twoth; 357 | } 358 | cursor.twoth = undefined; 359 | break; 360 | } 361 | temp = temp.twoth; 362 | } 363 | destruct.species = "Destructuring[Array]"; 364 | return `matcher.extractor(${zStringify( 365 | pat.type === "invocation" ? pat.zeroth : pat 366 | )}, ${stringifyPat(destruct)})`; 367 | } 368 | // Otherwise, use stringification to produce a value. 369 | return zStringify(pat); 370 | } 371 | 372 | /** 373 | * Handle loop expressions (eg. `loop (x <- xs) x * 2`). 374 | * 375 | * @param {AstNode} loopexpr The loop expression AST node. 376 | */ 377 | function genLoopStatements(loopexpr) { 378 | const loops = []; 379 | // Figure out the final expression. 380 | const finalRes = (() => { 381 | let anchor = curr; 382 | curr = loopexpr.wunth; 383 | let r = genExpr(); 384 | curr = anchor; 385 | return r; 386 | })(); 387 | // Iterate over the expressions in the parens loop(_) 388 | loopexpr.zeroth.forEach((expr) => { 389 | // Detect generator 390 | if (expr.type === "invocation" && expr.zeroth === "$lt$minus") { 391 | loops.push( 392 | new AstNode({ 393 | type: "of", 394 | zeroth: expr.wunth[0], 395 | wunth: expr.wunth[1], 396 | twoth: [], 397 | predicates: [], 398 | }) 399 | ); 400 | } else if (expr.type === "assignment") { 401 | // Generators can have assignments attached to them. 402 | loops[loops.length - 1].twoth.push(expr); 403 | } else if (expr.type === "predicate") { 404 | // And predicates too. 405 | loops[loops.length - 1].predicates.push(expr); 406 | } 407 | }); 408 | let r = ""; 409 | loops.forEach((loop) => { 410 | // Figure out the two parts of the generator. 411 | const iteree = (function () { 412 | let anchor = curr; 413 | curr = loop.zeroth; 414 | let res = genExpr(); 415 | curr = anchor; 416 | return res; 417 | })(); 418 | const iterable = (function () { 419 | let anchor = curr; 420 | curr = loop.wunth; 421 | let res = genExpr(); 422 | curr = anchor; 423 | return res; 424 | })(); 425 | r += `for (const ${iteree} of ${iterable}) {\n`; 426 | indent(); 427 | let anchor = curr; 428 | // Add the assignments. 429 | loop.twoth.forEach((assignment) => { 430 | let anchor = curr; 431 | curr = new AstNode({ 432 | type: "def", 433 | zeroth: [assignment], 434 | }); 435 | r += genStatement() + "\n"; 436 | curr = anchor; 437 | }); 438 | // Add the predicates. 439 | let conds = loop.predicates 440 | .map((predicate) => { 441 | let anchor = curr; 442 | curr = predicate.zeroth; 443 | let r = genExpr(); 444 | curr = anchor; 445 | return r; 446 | }) 447 | .join(" && "); 448 | if (conds === "") { 449 | conds = "true"; 450 | } 451 | let condstr = `if (${conds}) `; 452 | r += condstr.padStart(condstr.length + padstart); 453 | curr = anchor; 454 | }); 455 | // Add the final result. 456 | let inner = `res.push(${finalRes})\n`; 457 | r += inner; 458 | // Add all the closing brackets 459 | loops.forEach(() => { 460 | outdent(); 461 | r += "\n" + "}".padStart(padstart + 1); 462 | }); 463 | return r; 464 | } 465 | 466 | /** 467 | * Produces a array of match expressions to pass to matchers. 468 | * 469 | * @param {any[]} matches The match expressions. 470 | * @returns {string} An array of match expressions. 471 | */ 472 | function genMatcherArr(matches) { 473 | let r = ""; 474 | r += matches 475 | .map(([pat, expr]) => { 476 | let res = "["; 477 | res += stringifyPat(pat); 478 | res += ", "; 479 | let anchor = curr; 480 | curr = expr; 481 | indent(); 482 | res += genExpr(); 483 | outdent(); 484 | curr = anchor; 485 | res += "]"; 486 | return res.padStart(res.length + padstart + 2); 487 | }) 488 | .join(",\n"); 489 | return r; 490 | } 491 | 492 | /** 493 | * Generates an expression. 494 | * 495 | * @returns {string} The built expression. 496 | */ 497 | function genExpr() { 498 | let r = ""; 499 | if (isExpr(curr)) { 500 | switch (curr.type) { 501 | case "subscript": 502 | r += `${zStringify(curr.zeroth)}[${zStringify(curr.wunth)}]`; 503 | r += genTwoth(); 504 | break; 505 | case "refinement": 506 | r += `${zStringify(curr.zeroth)}["${curr.wunth}"]`; 507 | r += genTwoth(); 508 | break; 509 | case "condrefinement": 510 | r += `${zStringify(curr.zeroth)} && ${zStringify(curr.zeroth)}["${ 511 | curr.wunth 512 | }"]`; 513 | // Remember refinement in the chain. 514 | condrList.push(curr.zeroth); 515 | r += genTwoth(); 516 | break; 517 | case "invocation": { 518 | r += `${curr.zeroth}(`; 519 | let anchor = curr; 520 | r += genParameterList(); 521 | r += ")"; 522 | curr = anchor; 523 | r += genTwoth(); 524 | break; 525 | } 526 | case "assignment": 527 | r += `${genDestructuring(curr.zeroth)} = `; 528 | curr = curr.wunth; 529 | r += genExpr(); 530 | break; 531 | // Goroutines translate to async functions. 532 | case "goroutine": 533 | r += "async "; 534 | case "function": 535 | (function () { 536 | r += `function (`; 537 | let anchor = curr; 538 | const list = curr.zeroth 539 | .map((param) => { 540 | curr = param; 541 | return genExpr(); 542 | }) 543 | .join(", "); 544 | r += list; 545 | r += ")"; 546 | curr = anchor.wunth; 547 | // Account for exit statements. 548 | if (curr[curr.length - 1] && curr[curr.length - 1].type === "exit") { 549 | let anchor = curr; 550 | r += `{ try { ${genBlock()} }`; 551 | curr = curr[curr.length - 1]; 552 | let conds = (function () { 553 | let r = ""; 554 | let anchor = curr; 555 | r += curr.zeroth 556 | .map((condition) => { 557 | curr = condition; 558 | return genExpr(); 559 | }) 560 | .join(" && "); 561 | curr = anchor; 562 | return `if (!(${r})) { throw new Error("Exit failed") }`; 563 | })(); 564 | r += ` finally { ${conds} } }`; 565 | curr = anchor; 566 | } else { 567 | r += genBlock(); 568 | } 569 | curr = anchor; 570 | r += genTwoth(); 571 | })(); 572 | break; 573 | case "spread": 574 | r += `...${zStringify(curr.wunth)}`; 575 | break; 576 | case "match": 577 | r += `matcher([\n`; 578 | r += genMatcherArr(curr.wunth); 579 | r += `])(${zStringify(curr.zeroth)})`; 580 | break; 581 | // Range literals translate directly to arrays. 582 | case "range": { 583 | const from = curr.zeroth; 584 | const to = curr.wunth; 585 | r += `Array($plus($minus(${zStringify(to)}, ${zStringify( 586 | from 587 | )}), 1)).fill().map(function (_, index) { return $plus(index, ${zStringify( 588 | from 589 | )}) })`; 590 | break; 591 | } 592 | // List expressions become IIFEs 593 | case "loopexpr": 594 | r += "function(){\n const res = [];\n"; 595 | r += genLoopStatements(curr) 596 | .split("\n") 597 | .map((str) => str.padStart(padstart + 2 + str.length)) 598 | .join("\n"); 599 | r += "\n return res;\n}()"; 600 | break; 601 | // If expressions become ternary operators. 602 | case "ifexpr": 603 | { 604 | let anchor = curr; 605 | curr = anchor.zeroth; 606 | r += `(${genExpr()}) ? `; 607 | curr = anchor.wunth; 608 | r += genExpr(); 609 | curr = anchor.twoth; 610 | r += ` : ${genExpr()}`; 611 | curr = anchor; 612 | } 613 | break; 614 | // get(arg) translates to "await (arg)._from" 615 | case "get": 616 | r += "await "; 617 | { 618 | let anchor = curr; 619 | curr = curr.zeroth; 620 | r += genExpr(); 621 | curr = anchor; 622 | } 623 | r += "._from()"; 624 | } 625 | } else { 626 | // Some standalone things, ie. objects and numbers do not need special handling 627 | r += zStringify(curr); 628 | } 629 | return r; 630 | } 631 | 632 | let generateStatement = Object.create(null); 633 | 634 | generateStatement.let = () => { 635 | let r = `let `; 636 | let assignmentArray = []; 637 | let anchor = curr; 638 | curr.zeroth.forEach((assignment) => { 639 | curr = assignment; 640 | assignmentArray.push(genExpr()); 641 | }); 642 | r += assignmentArray.join(", "); 643 | curr = anchor; 644 | return r + ";"; 645 | }; 646 | 647 | generateStatement.def = () => { 648 | let r = `var `; 649 | let assignmentArray = []; 650 | let anchor = curr; 651 | curr.zeroth.forEach((assignment) => { 652 | curr = assignment; 653 | assignmentArray.push(genExpr()); 654 | }); 655 | r += assignmentArray.join(", "); 656 | curr = anchor; 657 | return r + ";"; 658 | }; 659 | 660 | generateStatement.import = () => { 661 | return `var ${genDestructuring(curr.zeroth)} = stone(require(${ 662 | curr.wunth 663 | }));`; // All imports are immutable 664 | }; 665 | 666 | generateStatement.export = () => { 667 | let r = `module.exports = stone(`; // All expors are also immutable. 668 | let anchor = curr; 669 | curr = curr.zeroth; 670 | r += genExpr(); 671 | curr = anchor; 672 | r += ");"; 673 | return r; 674 | }; 675 | 676 | generateStatement.if = () => { 677 | let r = "if (assertBool("; // An if condition must be a boolean 678 | let anchor = curr; 679 | curr = curr.zeroth; 680 | r += genExpr(); 681 | r += "))"; 682 | curr = anchor.wunth; 683 | r += genBlock(); 684 | if (anchor.twoth !== undefined) { 685 | r += " else"; 686 | curr = anchor.twoth; 687 | r += genBlock(); 688 | } 689 | curr = anchor; 690 | return r; 691 | }; 692 | 693 | generateStatement.loop = () => { 694 | let r = "while (true)"; 695 | let anchor = curr; 696 | curr = curr.zeroth; 697 | r += genBlock(); 698 | curr = anchor; 699 | return r; 700 | }; 701 | 702 | generateStatement.break = () => { 703 | return "break;"; 704 | }; 705 | 706 | generateStatement.return = () => { 707 | let r = "return "; 708 | let anchor = curr; 709 | curr = curr.zeroth; 710 | r += genExpr(); 711 | r += ";"; 712 | curr = anchor; 713 | return r; 714 | }; 715 | 716 | generateStatement.try = () => { 717 | let r = "try"; 718 | let anchor = curr; 719 | curr = curr.zeroth; 720 | r += genBlock(); 721 | r += ` catch (${anchor.wunth})`; 722 | curr = anchor.twoth; 723 | r += genBlock([ 724 | `if (assertBool($eq(${anchor.wunth}["settled"], undefined))) {`, 725 | ` throw new Error("Error ${anchor.wunth} not settled.")`, 726 | "}", // Check to make sure that the error object caught has been settled via the settle statement. 727 | ]); 728 | curr = anchor; 729 | return r; 730 | }; 731 | 732 | generateStatement.raise = () => { 733 | return `throw new Error(${zStringify(curr.zeroth)});`; 734 | }; 735 | 736 | generateStatement.settle = () => { 737 | return `${curr.zeroth}["settled"] = true;`; 738 | }; 739 | 740 | generateStatement.meta = () => { 741 | return `/* meta ${curr.zeroth} = ${'"' + curr.wunth + '"'} */`; // Meta statements become comments just so you know that they are there. 742 | }; 743 | 744 | generateStatement.enter = () => { 745 | let r = ""; 746 | let anchor = curr; 747 | r += curr.zeroth 748 | .map((condition) => { 749 | curr = condition; 750 | return genExpr(); 751 | }) 752 | .join(" && "); // Gather conditions 753 | curr = anchor; 754 | return `if (!(${r})) { throw new Error("Enter failed") }`; 755 | }; 756 | 757 | generateStatement.exit = () => { 758 | return ""; 759 | }; 760 | 761 | generateStatement.operator = () => { 762 | return `/* operator ${curr.zeroth} = ${curr.wunth} */`; // Like meta, operator translates into a comment so you know it's there. 763 | }; 764 | 765 | generateStatement.go = () => { 766 | let r = "(async function ()"; 767 | let anchor = curr; 768 | curr = curr.zeroth; 769 | r += genBlock(); 770 | r += ")();"; 771 | curr = anchor; 772 | return r; 773 | }; 774 | 775 | /** 776 | * Generates the immutable properties for an enum declaration. 777 | * 778 | * @param {string[]} fields The enum's properties. 779 | * @returns {string} A JS code string which defines the properties. 780 | */ 781 | export function generateGetters(fields) { 782 | return fields 783 | .map((field) => `get ${field}(){ return ${field}; }`) 784 | .join(",\n\t\t"); 785 | } 786 | 787 | /** 788 | * Generates the equals method for an enum declaration. 789 | * 790 | * @param {any[]} fields The fields in the enum. 791 | * @returns {string} A JS code string which defines the equals method. 792 | */ 793 | export function generateEquals(type, fields) { 794 | return `"="(other) { 795 | return other.constructor === ${type}${ 796 | fields.length > 0 ? " && " : "" 797 | }${fields.map((field) => `$eq(${field}, other.${field})`).join(" && ")}; 798 | }`; 799 | } 800 | 801 | // Generates the static methods of an enum from a `where` block. 802 | function generateStatics(type, static_item = {}) { 803 | let res = ""; 804 | Object.entries(static_item).forEach(([key, func]) => { 805 | let anchor = curr; 806 | curr = func; 807 | const f = genExpr(); 808 | curr = anchor; 809 | res += ` 810 | ${type}.${key} = ${f}; 811 | `; 812 | }); 813 | return res; 814 | } 815 | 816 | // Generates the overarching parent type for an enum. 817 | function generateParent(type, parts, static_item = {}) { 818 | let res = `var ${type} = { 819 | order: ${zStringify(parts)}, 820 | ${parts.join(",\n\t")} 821 | };`; 822 | res += generateStatics(type, static_item); 823 | return res; 824 | } 825 | 826 | // Generates type checks for an enum's fields. 827 | function generateTypeChecks(typeChecks, parent, child) { 828 | let r = ""; 829 | typeChecks 830 | .filter(([, type]) => type !== "_$exclam") 831 | .forEach(([field, type]) => { 832 | type = type.replace(/\$exclam$/, ""); 833 | r += ` 834 | if (typeOf(${field}) !== "${type}") { 835 | throw new Error("${parent}.${child}.${field} must be of type ${type}. However, you passed " + ${field} + " to ${parent}.${child} which is not of type ${type}."); 836 | } 837 | `; 838 | }); 839 | return r; 840 | } 841 | 842 | // Will wrap and expression with functions. 843 | function wrapFuncs(funcs, str) { 844 | if (!Array.isArray(funcs) || funcs.length === 0) { 845 | return str; 846 | } 847 | const open = 848 | funcs 849 | .map((func) => { 850 | let anchor = curr; 851 | curr = func; 852 | let res = genExpr(); 853 | curr = anchor; 854 | return res; 855 | }) 856 | .join("(") + "("; 857 | const close = ")".repeat(funcs.length); 858 | return open + str + close; 859 | } 860 | 861 | generateStatement.enum = () => { 862 | let r = ""; 863 | const parentType = curr.zeroth; 864 | const types = []; 865 | curr.wunth.forEach((type) => { 866 | let fields = []; 867 | const typeChecks = []; 868 | // Record type checks. ex: enum Point(x: number!, y: number!) 869 | if (type.type === "invocation") { 870 | fields = type.wunth; 871 | if ( 872 | Array.isArray(fields[0]) && 873 | fields[0].some((field) => Array.isArray(field)) 874 | ) { 875 | fields = fields[0].map((field) => { 876 | if (Array.isArray(field)) { 877 | field[0] = field[0].replace(/"/g, ""); 878 | typeChecks.push(field); 879 | return field[0]; 880 | } 881 | return field; 882 | }); 883 | } 884 | type = type.zeroth; 885 | } 886 | types.push(type); 887 | // Generate the enum constructor function. 888 | r += `function ${type}(${fields.join(", ")}) { 889 | ${ 890 | fields[0] // Support keyword arguments to enums. 891 | ? ` 892 | if($eq(Object.keys((${fields[0]} == null) ? { [Symbol()]: 0 } : ${ 893 | fields[0] 894 | }).sort(), ${zStringify(fields.map((field) => `"${field}"`))}.sort())) { 895 | ({ ${fields.join(", ")} } = ${fields[0]}); 896 | } 897 | ` 898 | : "" 899 | } 900 | ${generateTypeChecks(typeChecks, parentType, type)} 901 | return ${ 902 | wrapFuncs( 903 | curr.derives, 904 | `{ 905 | type() { return "${parentType}"; }, 906 | get constructor() { return ${type}; }, 907 | get parent() { return ${parentType}; }, 908 | get fields() { return ${zStringify(fields.map((field) => `"${field}"`))}; }, 909 | ${generateGetters(fields)}${fields.length > 0 ? "," : ""} 910 | ${generateEquals(type, fields)} 911 | }` 912 | ) + ";\n}\n" 913 | }\n`; 914 | r += `${type}.extract = function (val) { 915 | if (val.constructor === ${type}) { 916 | return [${fields.map((field) => `val.${field}`).join(", ")}]; 917 | } 918 | return undefined; 919 | }; 920 | 921 | `; 922 | }); 923 | // Support for singleton enums. 924 | if (curr.wunth[0] === parentType || curr.wunth[0].zeroth === parentType) { 925 | r += `${parentType}.order = ${zStringify(types)};\n`; 926 | r += generateStatics(parentType, curr.twoth); 927 | if (curr.staticDerives && curr.staticDerives.length > 0) { 928 | r += `\n${parentType} = ${wrapFuncs(curr.staticDerives, parentType)}`; 929 | } 930 | } else { 931 | // Normal enums 932 | r += generateParent(parentType, types, curr.twoth); 933 | if (curr.staticDerives && curr.staticDerives.length > 0) { 934 | r += `\n${parentType} = ${wrapFuncs(curr.staticDerives, parentType)}`; 935 | } 936 | } 937 | return r; 938 | }; 939 | 940 | generateStatement.hoist = () => { 941 | let r = `var `; 942 | let assignmentArray = []; 943 | let anchor = curr; 944 | curr.zeroth.forEach((assignment) => { 945 | curr = assignment; 946 | assignmentArray.push(genExpr()); 947 | }); 948 | r += assignmentArray.join(", "); 949 | curr = anchor; 950 | return r + ";"; 951 | }; 952 | 953 | generateStatement.invocation = () => {}; 954 | 955 | /** 956 | * Generate a block. 957 | * 958 | * @param {string[]} cleanup Anything that should go at the end of the block. 959 | * @returns {string} The block. 960 | */ 961 | function genBlock(cleanup) { 962 | let r = " {\n"; 963 | indent(); 964 | let anchor = curr; 965 | curr.forEach((statement) => { 966 | if (Array.isArray(statement) && statement.spreadOut) { 967 | statement.forEach((subStatement) => { 968 | curr = subStatement; 969 | r += genStatement(); 970 | r += "\n"; 971 | }); 972 | return; 973 | } 974 | curr = statement; 975 | r += genStatement(); 976 | r += "\n"; 977 | }); 978 | curr = anchor; 979 | if (cleanup !== undefined) { 980 | r += cleanup.map((x) => x.padStart(x.length + padstart)).join("\n"); // Indent the cleanup. 981 | r += "\n"; 982 | } 983 | outdent(); 984 | r += "}".padStart(1 + padstart); 985 | return r; 986 | } 987 | 988 | /** 989 | * Generates the next statement in the queue. 990 | * 991 | * @returns {string} The generated JavaScript for the statement. 992 | */ 993 | function genStatement() { 994 | let res; 995 | if (curr.type === undefined || curr.type === null) { 996 | return ""; 997 | } 998 | if (isExpr(curr)) { 999 | res = genExpr() + ";"; 1000 | } else { 1001 | res = generateStatement[curr.type].call(); 1002 | } 1003 | condrList = []; 1004 | return res.padStart(padstart + res.length); 1005 | } 1006 | 1007 | /** 1008 | * Generates code for all AST Nodes given. 1009 | * 1010 | * @param {AstNode[]} ast The AST Nodes. 1011 | * @returns {string} The generated JavaScript. 1012 | */ 1013 | function genStatements(ast) { 1014 | let r = ""; 1015 | while (index < ast.length) { 1016 | curr = ast[index]; 1017 | if (Array.isArray(curr)) { 1018 | curr.forEach((statement) => { 1019 | curr = statement; 1020 | r += genStatement(); 1021 | r += "\n"; 1022 | }); 1023 | index += 1; 1024 | continue; 1025 | } 1026 | if (curr !== undefined) { 1027 | r += genStatement(); 1028 | r += "\n"; 1029 | } 1030 | index += 1; 1031 | } 1032 | return r; 1033 | } 1034 | 1035 | /** 1036 | * Generate JS code based on the given Z AST nodes. 1037 | * 1038 | * @param {AstNode} ast The AST Node. 1039 | * @param {boolean?} usePrelude If the prelude should be included. 1040 | * @param {any[]} plugins The plugins. 1041 | */ 1042 | module.exports = Object.freeze((ast, usePrelude = true, plugins = []) => { 1043 | index = 0; 1044 | padstart = 0; 1045 | let generatedContent = usePrelude 1046 | ? res + genStatements(ast) 1047 | : genStatements(ast); 1048 | plugins.forEach((plugin) => { 1049 | generatedContent = plugin._eventbus_announce("outputGeneratedCode", { 1050 | code: generatedContent, 1051 | }); 1052 | }); 1053 | return generatedContent; 1054 | }); 1055 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/parse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import gen from "./gen"; 3 | import tokenize from "./tokenize"; 4 | import { copy } from "@zlanguage/zstdlib"; 5 | import fs from "fs"; 6 | import { AstNode, validTypes } from "./types"; 7 | 8 | /** 9 | * List of all generated warnings. 10 | * 11 | * @type {string[]} 12 | */ 13 | let warnings = []; 14 | 15 | // An API for registering error objects will later be displayed. 16 | const errors = (() => { 17 | let index = 0; 18 | let errors = []; 19 | return { 20 | empty() { 21 | errors = []; 22 | }, 23 | push(val) { 24 | if (errors[index] === undefined) { 25 | errors[index] = val; 26 | } 27 | }, 28 | next() { 29 | index += 1; 30 | }, 31 | restart() { 32 | index = 0; 33 | }, 34 | get length() { 35 | return errors.reduce((t) => t + 1, 0); 36 | }, 37 | forEach(cb) { 38 | errors.forEach(cb); 39 | }, 40 | first() { 41 | return errors.find((x) => x); 42 | }, 43 | }; 44 | })(); 45 | 46 | let macros = {}; 47 | let isMacro = false; 48 | let prevTok; 49 | let tok; 50 | let nextTok; 51 | let tokList; 52 | 53 | /** 54 | * The current token. 55 | * 56 | * @type {number} 57 | */ 58 | let index = 0; 59 | 60 | /** 61 | * Metadata collected via `meta` statements. 62 | */ 63 | let metadata = { 64 | ddsdir: process.cwd(), // Default place where the dollar directives are coming from. 65 | }; 66 | 67 | /** 68 | * Valid left-associative operator names (all built-in). 69 | * To is reserved for future use. 70 | */ 71 | export const validNameOps = ["and", "or", "to", "til", "by"]; 72 | 73 | /** 74 | * Unary operations involving keywords. 75 | * `get(arg)` - JavaScript: `await (arg).from()`. 76 | * `static(arg)` - Derives a trait on the enum itself. 77 | */ 78 | export const unOps = ["get", "static", "sym", "bsym"]; 79 | 80 | /** 81 | * Operators that can be broken over newlines, ex: `|>`. 82 | */ 83 | export const validStartLineOps = ["$or$gt"]; 84 | 85 | /** 86 | * All of Z's operators. 87 | */ 88 | export const ops = { 89 | and: -333, 90 | or: -333, 91 | $eq: -222, 92 | $lt: -111, 93 | $gt: -111, 94 | $gt$eq: -111, 95 | $lt$eq: -111, 96 | $plus$plus: 111, 97 | $plus: 111, 98 | $minus: 111, 99 | $star: 222, 100 | $slash: 222, 101 | $percent: 222, 102 | $carot: 333, 103 | by: 444, 104 | to: 555, 105 | til: 555, 106 | }; 107 | 108 | /** 109 | * Reverse of the above (also for debugging). 110 | */ 111 | export const reverseSymMap = { 112 | $plus: "+", 113 | $minus: "-", 114 | $star: "*", 115 | $slash: "/", 116 | $carot: "^", 117 | $question: "?", 118 | $eq: "=", 119 | $lt: "<", 120 | $gt: ">", 121 | $backslash: "\\", 122 | $and: "&", 123 | $or: "|", 124 | $percent: "%", 125 | $quote: "'", 126 | $exclam: "!", 127 | }; 128 | 129 | /** 130 | * Checks if identifier is valid JS name. 131 | */ 132 | const isValidName = /^[A-Za-z_$][A-za-z_$0-9]*$/; 133 | 134 | /** 135 | * Checks if an identifier consists only of symbols. (ie. +, -, ?!?) but not +x 136 | */ 137 | const validOpName = RegExp( 138 | `^(${Object.keys(reverseSymMap) 139 | .filter((key) => key.startsWith("$")) 140 | .map((key) => key.replace(/\$/g, "\\$")) 141 | .join("|")})+$` 142 | ); 143 | 144 | /** 145 | * Makes an error AST Node. 146 | * 147 | * @returns {AstNode} The AST Node. 148 | */ 149 | function error(message) { 150 | return new AstNode({ 151 | id: "(error)", 152 | zeroth: `Error: "${message}" at ${formatCurrLine()}.`, 153 | }); 154 | } 155 | 156 | /** 157 | * Records a warning. 158 | * 159 | * @param {string} message The warning message. 160 | */ 161 | function warn(message) { 162 | warnings.push(`Warning: "${message}" at ${formatCurrLine()}.`); 163 | } 164 | 165 | /** 166 | * Makes an error AST Node without the quotes. 167 | * 168 | * @returns {AstNode} The AST Node. 169 | */ 170 | function nqerror(message) { 171 | return new AstNode({ 172 | id: "(error)", 173 | zeroth: `Error: ${message} at ${formatCurrLine()}.`, 174 | }); 175 | } 176 | 177 | /** 178 | * Formats the current token position: "{lineNumber}:{columnNumber}:{columnTo}". 179 | * 180 | * @returns {string} The formatted current token position. 181 | */ 182 | function formatCurrLine() { 183 | let lastTok; 184 | // Find the last available token. 185 | if (tok) { 186 | lastTok = tok; 187 | } else { 188 | for (let idx = index; lastTok === undefined; idx -= 1) { 189 | lastTok = tokList[idx]; 190 | } 191 | } 192 | return `${lastTok.lineNumber + 1}:${lastTok.columnNumber}:${ 193 | lastTok.columnTo 194 | }`; 195 | } 196 | 197 | /** 198 | * Move ahead one token, while checking the current token (not the incoming one) has a certain ID. 199 | * 200 | * @type {string?} id The ID to check for. 201 | */ 202 | function advance(id) { 203 | isTok(id); 204 | index += 1; 205 | prevTok = tok; 206 | tok = nextTok; 207 | nextTok = tokList[index + 1]; 208 | } 209 | 210 | /** 211 | * Fall back one token. 212 | */ 213 | function fallback() { 214 | index -= 1; 215 | nextTok = tok; 216 | tok = prevTok; 217 | prevTok = tokList[index - 1]; 218 | } 219 | 220 | /** 221 | * Check that the token has a certain ID. 222 | */ 223 | function isTok(id) { 224 | if (tok && id !== undefined && id !== tok.id) { 225 | errors.push(error(`Expected "${id}" but got "${tok.id}".`)); 226 | } 227 | if (tok === undefined && id !== undefined) { 228 | errors.push(error(`Expected "${id}" but got nothing.`)); 229 | } 230 | } 231 | 232 | // See if a specific type of expression (type) is a subexpression of (thing) 233 | function typeIn(thing, type) { 234 | if (thing === undefined) { 235 | return false; 236 | } 237 | return ( 238 | thing.type === type || 239 | typeIn(thing.zeroth, type) || 240 | typeIn(thing.wunth, type) || 241 | typeIn(thing.twoth, type) 242 | ); 243 | } 244 | 245 | // Converts an array of assignments into an object. 246 | function arrayToObj(arr) { 247 | if (!Array.isArray(arr)) { 248 | return arr; 249 | } 250 | if (!arr.some((field) => field && field.type === "assignment")) { 251 | return arr; 252 | } 253 | // Prepare the object 254 | const obj = arr 255 | .map((field) => { 256 | // Allow for [name] -> ["name": name] syntax 257 | if (typeof field === "string" || typeof field === "number") { 258 | return new AstNode({ 259 | type: "assignment", 260 | zeroth: `"` + demangle(field) + `"`, 261 | wunth: field, 262 | }); 263 | } 264 | return field; 265 | }) 266 | .map(({ zeroth, wunth }) => [zeroth, wunth]) 267 | .map(([zeroth, wunth]) => [arrayToObj(zeroth), arrayToObj(wunth)]); 268 | obj.species = "Object"; 269 | return obj; 270 | } 271 | 272 | // Is there an expression ahead of the current token? 273 | function isExprAhead() { 274 | return ( 275 | nextTok !== undefined && 276 | ["(", "[", ".", "..", ":"].includes(nextTok.id) && 277 | (["(", "["].includes(nextTok.id) 278 | ? tok.lineNumber === nextTok.lineNumber 279 | : true) 280 | ); // Invocations and subscripts must be on the same line. 281 | } 282 | 283 | // Implicit parameter check. 284 | function isImplicit(str) { 285 | return typeof str === "string" && str.endsWith("$exclam"); 286 | } 287 | 288 | /** 289 | * Check if a node is an expression. 290 | * 291 | * @param {AstNode} astNode The node. 292 | * @returns {boolean} If the node is an expression. 293 | */ 294 | export function isExpr(astNode) { 295 | if (!astNode) { 296 | return false; 297 | } 298 | return validTypes.includes(astNode.type); 299 | } 300 | 301 | // Searches an expression for implicit parameters and returns the ones it finds. 302 | function findImplicits(ast) { 303 | if ( 304 | (typeof ast !== "object" && typeof ast !== "string") || 305 | ast === null || 306 | ast.type === "match" 307 | ) { 308 | return []; 309 | } 310 | const implicits = []; 311 | if (isImplicit(ast)) { 312 | implicits.push(ast); 313 | } 314 | if (isImplicit(ast.zeroth)) { 315 | implicits.push(ast.zeroth); 316 | } 317 | if (isImplicit(ast.wunth)) { 318 | implicits.push(ast.wunth); 319 | } 320 | if (isImplicit(ast.twoth)) { 321 | implicits.push(ast.twoth); 322 | } 323 | if (Array.isArray(ast)) { 324 | ast.forEach((part) => { 325 | implicits.push(...findImplicits(part)); 326 | }); 327 | } 328 | switch (ast.type) { 329 | case "invocation": 330 | ast.wunth.forEach((param) => { 331 | if (isImplicit(param)) { 332 | implicits.push(param); 333 | } else if (isExpr(param)) { 334 | implicits.push(...findImplicits(param)); 335 | } 336 | }); 337 | break; 338 | } 339 | if (isExpr(ast.zeroth) || Array.isArray(ast.zeroth)) { 340 | implicits.push(...findImplicits(ast.zeroth)); 341 | } 342 | if (isExpr(ast.wunth) || Array.isArray(ast.wunth)) { 343 | implicits.push(...findImplicits(ast.wunth)); 344 | } 345 | if (isExpr(ast.twoth) || Array.isArray(ast.twoth)) { 346 | implicits.push(...findImplicits(ast.twoth)); 347 | } 348 | return Array.from(new Set(implicits)); 349 | } 350 | 351 | // Finds the wildcards in a pattern. 352 | function findWildcards(pat) { 353 | const wildcards = []; 354 | switch (typeof pat) { 355 | case "string": 356 | if (pat.includes("$exclam")) { 357 | const [, wildcard] = pat.split("$exclam"); 358 | if (wildcard) { 359 | wildcards.push(wildcard); 360 | } 361 | } else if (/^[a-z_]$/.test(pat[0]) && !pat.includes("$question")) { 362 | wildcards.push(pat); 363 | } 364 | break; 365 | case "object": 366 | if (pat.type === "spread") { 367 | wildcards.push(pat.wunth); 368 | } else if (pat.species === "Destructuring[Array]") { 369 | pat.forEach((patpart) => { 370 | wildcards.push(...findWildcards(patpart)); 371 | }); 372 | } else if (pat.type === "invocation") { 373 | pat.wunth.species = "Destructuring[Array]"; 374 | if (pat.twoth) { 375 | wildcards.push(...findWildcards(pat.twoth)); 376 | } else { 377 | wildcards.push(...findWildcards(pat.wunth)); 378 | } 379 | } else if (pat.species === "Destructuring[Object]") { 380 | const pats = arrayToObj([...pat]).map(([, v]) => v); 381 | pats.forEach((patpart) => { 382 | wildcards.push(...findWildcards(patpart)); 383 | }); 384 | } else if (pat.type === "refinement" || pat.type === "subscript") { 385 | wildcards.push(...findWildcards(pat.twoth)); 386 | } 387 | break; 388 | } 389 | return Array.from(new Set(wildcards)); 390 | } 391 | 392 | // Adds return-type assertions to a return statement. 393 | function convertReturns(returnType, statement) { 394 | if (statement.type === "return") { 395 | return new AstNode({ 396 | type: "return", 397 | zeroth: new AstNode({ 398 | type: "invocation", 399 | zeroth: "assertType", 400 | wunth: [returnType, statement.zeroth], 401 | }), 402 | }); 403 | } 404 | return wrapReturn(returnType, statement); 405 | } 406 | 407 | /** 408 | * Add return-type assertions to a whole function. 409 | * 410 | * @param {string} returnType The return type. 411 | * @param {AstNode} block The block AST Node. 412 | * @returns {AstNode} The transformed AST Node. 413 | */ 414 | function wrapReturn(returnType, block) { 415 | if (!block) { 416 | return block; 417 | } 418 | if (!returnType.startsWith(`"`)) { 419 | // Stringify return type for use with assertType function. 420 | returnType = `"${returnType}"`; 421 | } 422 | if (Array.isArray(block)) { 423 | block = block.map((statement, index) => 424 | convertReturns(returnType, statement, index) 425 | ); 426 | } 427 | if (block.zeroth) { 428 | if (Array.isArray(block.zeroth)) { 429 | block.zeroth = block.zeroth.map((statement, index) => 430 | convertReturns(returnType, statement, index) 431 | ); 432 | } else { 433 | block.zeroth = wrapReturn(returnType, block.zeroth); 434 | } 435 | } 436 | if (block.wunth) { 437 | if (Array.isArray(block.wunth)) { 438 | block.wunth = block.wunth.map((statement, index) => 439 | convertReturns(returnType, statement, index) 440 | ); 441 | } else { 442 | block.wunth = wrapReturn(returnType, block.wunth); 443 | } 444 | } 445 | if (block.twoth) { 446 | if (Array.isArray(block.twoth)) { 447 | block.twoth = block.twoth.map((statement, index) => 448 | convertReturns(returnType, statement, index) 449 | ); 450 | } else { 451 | block.twoth = wrapReturn(returnType, block.twoth); 452 | } 453 | } 454 | return block; 455 | } 456 | 457 | // Configures an expression in a near identical manner to expr() 458 | function configureExpr(type, zeroth, wunth, twoth) { 459 | if (twoth !== undefined) { 460 | return { 461 | type, 462 | zeroth, 463 | wunth, 464 | twoth, 465 | }; 466 | } 467 | if (wunth !== undefined && type !== undefined) { 468 | return { 469 | type, 470 | zeroth, 471 | wunth, 472 | }; 473 | } 474 | // Return raw value if isn't a complete token 475 | return zeroth; 476 | } 477 | 478 | /** 479 | * Determines the precedence of an operator. 480 | * 481 | * @param {number?} op The operator's ID. 482 | * @returns {number} The result. 483 | */ 484 | function opPred(op) { 485 | // Does the operator have valid precedence? 486 | if (ops[op] !== undefined) { 487 | // If so, return that. 488 | return ops[op]; 489 | } 490 | // Otherwise, return a default of 1. 491 | return 1; 492 | } 493 | 494 | /** 495 | * Checks if an operator can be left-associative. 496 | * 497 | * @param {string} op The operator to check. 498 | * @returns {boolean} If the operator can be left-associative. 499 | */ 500 | function isValidOp(op) { 501 | // Is it part of a predefined list of left-associative operators? 502 | if (validNameOps.includes(op)) { 503 | return true; 504 | } 505 | // Is it all symbols? 506 | if (validOpName.test(op)) { 507 | return true; 508 | } 509 | // Otherwise, it's a right-associative operator. 510 | return false; 511 | } 512 | 513 | /** 514 | * Changes an expression according to the rules of operator precedence. 515 | * 516 | * @param {AstNode?} obj The AST Node. 517 | * @returns {AstNode} The modified AST Node. 518 | * @example 519 | * Takes: +(3, -(2, 7)) 520 | * Returns: +(-(3, 2), 7) 521 | */ 522 | function swapLeftToRight(obj) { 523 | // Is the object not an object? 524 | if (!obj) { 525 | return obj; 526 | } 527 | // Is it already in proper oder? 528 | if (obj.leftToRight) { 529 | return obj; 530 | } 531 | // Is it not a function call? 532 | if (obj.type !== "invocation") { 533 | return obj; 534 | } 535 | // Does it fit the shape needed to switch operators? 536 | if (!obj.wunth) { 537 | return obj; 538 | } 539 | if (!obj.wunth[1]) { 540 | return obj; 541 | } 542 | if (obj.wunth[1].type !== "invocation") { 543 | return obj; 544 | } 545 | // Is the operator in question (the second one) right associative? 546 | if (!isValidOp(obj.wunth[1].zeroth)) { 547 | return obj; 548 | } 549 | // Does the operator in question have greater precednece than the current operator being evaluated second? 550 | if (opPred(obj.wunth[1].zeroth) > opPred(obj.zeroth)) { 551 | return obj; 552 | } 553 | // Otherwise, switch the order in which the operators are evaluated. 554 | const nested = obj.wunth[1]; 555 | const outer = obj; 556 | return new AstNode({ 557 | type: "invocation", 558 | zeroth: nested.zeroth, 559 | wunth: [ 560 | new AstNode({ 561 | type: "invocation", 562 | zeroth: outer.zeroth, 563 | wunth: [outer.wunth[0], nested.wunth[0]], 564 | }), 565 | nested.wunth[1], 566 | ], 567 | leftToRight: true, 568 | }); 569 | } 570 | 571 | /** 572 | * Orders an expression left-to-right using the above function. 573 | * 574 | * @param {AstNode?} obj The AST Node. 575 | * @returns {AstNode} The AST Node. 576 | */ 577 | function leftToRight(obj) { 578 | if (!obj) { 579 | return obj; 580 | } 581 | if (obj.type === "invocation") { 582 | obj = swapLeftToRight(obj); 583 | } 584 | if (obj.zeroth && obj.zeroth.type === "invocation") { 585 | obj.zeroth = leftToRight(obj.zeroth); 586 | } 587 | if (obj.wunth && obj.wunth.type === "invocation") { 588 | obj.wunth = leftToRight(obj.wunth); 589 | } 590 | if (Array.isArray(obj.wunth)) { 591 | obj.wunth = obj.wunth.map(leftToRight); 592 | } 593 | if (obj.twoth && obj.twoth.type === "invocation") { 594 | obj.twoth = leftToRight(obj.twoth); 595 | } 596 | return obj; 597 | } 598 | 599 | /** 600 | * Parses a collection literal. 601 | */ 602 | function parseCol(start, end, sep = ",") { 603 | let res = []; 604 | advance(start); 605 | if (tok && tok.id !== end) { 606 | // eslint-disable-next-line 607 | while (true) { 608 | const toPush = expr(); 609 | // Check for errors in the expression about to be added to the up-and-coming collection. 610 | if (!findAndThrow(toPush)) { 611 | res.push(toPush); 612 | } 613 | advance(); 614 | if (tok && tok.id === sep) { 615 | advance(sep); 616 | } else { 617 | break; 618 | } 619 | } 620 | isTok(end); 621 | } 622 | return res; 623 | } 624 | 625 | /** 626 | * Checks if a statement has a `get` expresison in it. 627 | * 628 | * @param {AstNode} statement 629 | * @returns {boolean} 630 | */ 631 | function hasGet(statement) { 632 | if (statement == null || typeof statement !== "object") { 633 | return false; 634 | } 635 | if (statement.type === "function") { 636 | return false; 637 | } 638 | if (statement.type === "go") { 639 | return false; 640 | } 641 | if (statement.type === "get") { 642 | return true; 643 | } 644 | for (const part of [statement.zeroth, statement.wunth, statement.twoth]) { 645 | if (hasGet(part)) { 646 | return true; 647 | } 648 | if (Array.isArray(part)) { 649 | return isGoroutine(part); 650 | } 651 | } 652 | return false; 653 | } 654 | 655 | // Used for seeing if a function can be treated as a goroutine 656 | function isGoroutine(ast) { 657 | return ast.some((statement) => (hasGet(statement) ? true : false)); 658 | } 659 | 660 | // Regexes for demangling identifiers for debugging 661 | const demanglers = Object.keys(reverseSymMap).map((x) => { 662 | const res = new RegExp(`\\${x}`, "g"); 663 | res.str = reverseSymMap[x]; 664 | return res; 665 | }); 666 | 667 | // Reverses the mangling performed in tokenization. 668 | export function demangle(str) { 669 | demanglers.forEach((demangler) => { 670 | str = str.replace(demangler, demangler.str); 671 | }); 672 | return str; 673 | } 674 | 675 | // Detects if an alphanumeric identifier could have been intended as an operator. 676 | function warnBadOp(str) { 677 | if (str.id.endsWith("$eq") || str.id.endsWith("$exclam")) { 678 | return; 679 | } 680 | if (isValidOp(str.id)) { 681 | return; 682 | } 683 | const syms = Object.keys(ops).filter((op) => op.startsWith("$")); 684 | syms.forEach((sym) => { 685 | if (str.id.includes(sym)) { 686 | warn( 687 | `You may have intended to use ${demangle( 688 | sym 689 | )} as an operator in this context. To do so, separate the operator and it's operands. ${ 690 | str.source 691 | } is interpreted as an alphanumeric identifier without spaces.` 692 | ); 693 | } 694 | }); 695 | } 696 | 697 | // Allows for expressions with more than two parts to use operators. 698 | function mkChain(type, zeroth, wunth, twoth) { 699 | // Detect normal operator 700 | if ( 701 | ((type === "refinement" || type === "condrefinement") && 702 | twoth.type === "invocation" && 703 | twoth.zeroth !== wunth) || 704 | (type === "subscript" && 705 | twoth.type === "invocation" && 706 | twoth.zeroth !== "]") || 707 | (type === "invocation" && 708 | twoth.type === "invocation" && 709 | twoth.zeroth !== ")") 710 | ) { 711 | // Detect partial application 712 | if (twoth.zeroth === "curry") { 713 | const oldType = type; 714 | const oldZeroth = zeroth; 715 | const oldWunth = wunth; 716 | type = "invocation"; 717 | zeroth = "curry"; 718 | wunth = twoth.wunth; 719 | wunth[0].wunth[0].zeroth = new AstNode({ 720 | type: oldType, 721 | zeroth: oldZeroth, 722 | wunth: oldWunth, 723 | twoth: wunth[0].wunth[0].zeroth, 724 | }); 725 | twoth = undefined; 726 | } else { 727 | // Otherwise, fix it like normal 728 | const operator = twoth.zeroth; 729 | const realTwoth = twoth.wunth[0]; 730 | const rightHand = twoth.wunth[1]; 731 | const resolution = mkChain(type, zeroth, wunth, realTwoth); 732 | type = "invocation"; 733 | zeroth = operator; 734 | wunth = [resolution, rightHand]; 735 | twoth = undefined; 736 | } 737 | } 738 | return configureExpr(type, zeroth, wunth, twoth); 739 | } 740 | 741 | function expr({ infix = true } = {}) { 742 | let zeroth, wunth, twoth, type; 743 | // Is the token a literal? If so, prepare to return it's value. 744 | if (tok !== undefined) { 745 | switch (tok.id) { 746 | case "(string)": 747 | case "(template)": 748 | // Strings are encased in quotes 749 | zeroth = `"${tok.string}"`; 750 | break; 751 | case "(number)": 752 | // Numbers are translated to raw numbers 753 | zeroth = tok.number; 754 | break; 755 | case "(regexp)": 756 | zeroth = RegExp(tok.string, tok.flags); 757 | break; 758 | case "(": 759 | // Parse destructuring 760 | // Parens are never part of tuple literals - Z dosen't have them 761 | zeroth = parseCol("(", ")"); 762 | zeroth.species = "Destructuring[Array]"; 763 | // Allow for () {} syntax. () {} = (func (){}) 764 | if (nextTok && nextTok.id === "{") { 765 | zeroth.push({ 766 | type: "function", 767 | zeroth: [], 768 | wunth: block(), 769 | }); 770 | } 771 | break; 772 | case "{": 773 | // Object destructuring 774 | zeroth = parseCol("{", "}"); 775 | zeroth.forEach((d, index) => { 776 | if (validOpName.test(d)) { 777 | zeroth[index] = { 778 | type: "assignment", 779 | zeroth: `"${demangle(d)}"`, 780 | wunth: d, 781 | }; 782 | } 783 | }); 784 | zeroth.species = "Destructuring[Object]"; 785 | break; 786 | case "[": 787 | // Parse Array 788 | zeroth = parseCol("[", "]"); 789 | zeroth = arrayToObj(zeroth); // Support parsing of object literals. 790 | break; 791 | // Expressions starting with refinements are functions in disguise. .x = func x!.x 792 | case ".": { 793 | // Put "." in nextTok for refinement 794 | fallback(); 795 | tok = { alphanumeric: true, id: "(throwaway)" }; 796 | const chain = expr({ infix: false }); 797 | // Configure the function. 798 | type = "function"; 799 | zeroth = ["obj"]; 800 | wunth = [ 801 | { 802 | type: "return", 803 | zeroth: new AstNode({ 804 | type: "refinement", 805 | zeroth: "obj", 806 | wunth: chain.wunth, 807 | twoth: chain.twoth, 808 | }), 809 | }, 810 | ]; 811 | break; 812 | } 813 | case "(keyword)": 814 | switch (tok.string) { 815 | // For statically deriving traits 816 | case "static": 817 | type = "static"; 818 | advance("(keyword)"); 819 | zeroth = tok.id; 820 | break; 821 | // Denotes a goroutine 822 | case "go": 823 | advance("(keyword)"); 824 | type = "goroutine"; 825 | // Function parsing 826 | case "func": { 827 | if (type !== "goroutine") { 828 | type = "function"; 829 | } 830 | // Keep track of parameter types. 831 | const typeChecks = []; 832 | zeroth = []; // Zeroth will serve as the parameter list 833 | // Figure out functions parameter list. 834 | // Look for implicit parameters 835 | // If it's a normal function: 836 | if (nextTok && nextTok.id === "(") { 837 | advance(); 838 | advance(); 839 | // Detect empty parameter list 840 | if (tok && tok.id !== ")") { 841 | // Figure out the parameter list 842 | let i = 0; 843 | while (i < 100) { 844 | if (tok && tok.id === "(keyword)") { 845 | errors.push(error("Unexpected keyword in parameter list.")); 846 | } 847 | const nextParam = expr(); // Any valid expression can be used in parameter position 848 | if (!findAndThrow(nextParam)) { 849 | zeroth.push(nextParam); 850 | } 851 | // Check for runtime type checks 852 | if (nextTok && nextTok.alphanumeric) { 853 | if (isValidName.test(nextParam)) { 854 | typeChecks.push([nextParam, nextTok.id]); 855 | } 856 | advance(); 857 | } 858 | if (nextTok && nextTok.id === ")") { 859 | // The parameter list has finished 860 | advance(); // Prepare for block 861 | break; 862 | } 863 | if (nextTok && nextTok.id === ",") { 864 | //Let's skip to the next parameter 865 | advance(); 866 | advance(); 867 | } 868 | i += 1; 869 | } 870 | // Maximum 100 parameters. 871 | if (i === 100) { 872 | return error("Unclosed function parameter list."); 873 | } 874 | } 875 | // Turn the type-checks into an enter statement. (If there are any) 876 | if (nextTok && nextTok.id === "{") { 877 | wunth = [ 878 | ...typeChecks.map(([param, type]) => ({ 879 | type: "enter", 880 | zeroth: [ 881 | new AstNode({ 882 | type: "invocation", 883 | zeroth: "$eq", 884 | wunth: [ 885 | new AstNode({ 886 | type: "invocation", 887 | zeroth: type.includes("$gt") 888 | ? "typeGeneric" 889 | : "typeOf", 890 | wunth: [param], 891 | }), // Handle generics types. 892 | `"${type 893 | .replace(/\$exclam$/, "") 894 | .replace(/\$gt/g, ">") 895 | .replace(/\$lt/g, "<")}"`, 896 | ], 897 | }), 898 | ], 899 | })), 900 | ...block(), 901 | ]; 902 | } else if ( 903 | tok && 904 | tok.id === ")" && 905 | nextTok && 906 | nextTok.alphanumeric && 907 | nextTok.id.endsWith("$exclam") 908 | ) { 909 | // Or handle a return type 910 | advance(")"); 911 | const returnType = tok.id.replace(/\$exclam$/, ""); 912 | wunth = [ 913 | ...typeChecks.map(([param, type]) => ({ 914 | type: "enter", 915 | zeroth: [ 916 | new AstNode({ 917 | type: "invocation", 918 | zeroth: "$eq", 919 | wunth: [ 920 | new AstNode({ 921 | type: "invocation", 922 | zeroth: type.includes("$gt") 923 | ? "typeGeneric" 924 | : "typeOf", 925 | wunth: [param], 926 | }), 927 | `"${type 928 | .replace(/\$exclam$/, "") 929 | .replace(/\$gt/g, ">") 930 | .replace(/\$lt/g, "<")}"`, 931 | ], 932 | }), 933 | ], 934 | })), 935 | ...wrapReturn(returnType, block()), 936 | ]; // Wrap the function body's return statements with type assertions. 937 | } else { 938 | // Single-expression function: func (params) expr 939 | advance(")"); 940 | wunth = [ 941 | new AstNode({ 942 | type: "return", 943 | zeroth: expr(), 944 | }), 945 | ]; 946 | } 947 | } else { 948 | // Find implicit parameters in a function and add them to the parameter list 949 | advance("(keyword)"); 950 | const implicitExpr = expr(); 951 | zeroth = findImplicits(implicitExpr); 952 | wunth = [ 953 | new AstNode({ 954 | type: "return", 955 | zeroth: implicitExpr, 956 | }), 957 | ]; 958 | } 959 | // Support for goroutine inference 960 | if (isGoroutine(wunth)) { 961 | type = "goroutine"; 962 | } 963 | if (isExprAhead()) { 964 | twoth = expr(); 965 | } 966 | break; 967 | } 968 | // Parse match 969 | case "match": { 970 | type = "match"; 971 | advance("(keyword)"); 972 | zeroth = expr(); 973 | advance(); 974 | advance("{"); 975 | wunth = []; 976 | let i = 0; 977 | while (tok && tok.id !== "}" && i < 100) { 978 | const pat = expr(); 979 | // Find wildcards in the match expression. A wildcard is a lowercase alphanumeric identifier 980 | const wildcards = findWildcards(pat); 981 | advance(); 982 | let res; 983 | if (nextTok && nextTok.id !== "{") { 984 | advance("$eq$gt"); 985 | // Single expression result: pat => expr 986 | res = new AstNode({ 987 | type: "function", 988 | zeroth: wildcards, 989 | wunth: [ 990 | new AstNode({ 991 | type: "return", 992 | zeroth: expr(), 993 | }), 994 | ], 995 | }); 996 | } else { 997 | isTok("$eq$gt"); 998 | // Block result: pat => block 999 | res = new AstNode({ 1000 | type: "function", 1001 | zeroth: wildcards, 1002 | wunth: block(), 1003 | }); 1004 | } 1005 | advance(); 1006 | if (tok && tok.id !== "}") { 1007 | advance(","); 1008 | } 1009 | wunth.push([pat, res]); 1010 | i += 1; 1011 | } 1012 | if (i === 100) { 1013 | return error("Unclosed match expression."); 1014 | } 1015 | break; 1016 | } 1017 | case "loop": 1018 | // Parse loop 1019 | type = "loopexpr"; 1020 | advance("(keyword)"); 1021 | advance("("); 1022 | zeroth = []; 1023 | while (tok && tok.id !== ")") { 1024 | // Allow for predicates inside loop expression 1025 | if (tok.id === "(keyword)" && tok.string === "if") { 1026 | advance("(keyword)"); 1027 | zeroth.push( 1028 | new AstNode({ 1029 | type: "predicate", 1030 | zeroth: expr(), 1031 | }) 1032 | ); 1033 | } else { 1034 | // Otherwise, just add a normal expresison. Invalid expressions are ignored. 1035 | zeroth.push(expr()); 1036 | } 1037 | advance(); 1038 | if (tok && tok.id === ",") { 1039 | advance(","); 1040 | } else { 1041 | break; 1042 | } 1043 | } 1044 | advance(")"); 1045 | // Body of loop expression: loop(gens, predicates, assignments) body 1046 | wunth = expr(); 1047 | break; 1048 | case "if": 1049 | // Parse ternary operator. 1050 | advance("(keyword)"); 1051 | type = "ifexpr"; 1052 | advance("("); 1053 | zeroth = expr(); 1054 | advance(); 1055 | advance(")"); 1056 | wunth = expr(); 1057 | advance(); 1058 | if (tok && tok.string !== "else") { 1059 | return error("Ternary operator requires else clause."); 1060 | } 1061 | advance("(keyword)"); 1062 | twoth = expr(); 1063 | break; 1064 | case "get": 1065 | // Get is a unary operation. 1066 | advance("(keyword)"); 1067 | type = "get"; 1068 | zeroth = expr(); 1069 | if ( 1070 | nextTok && 1071 | nextTok.id === "(keyword)" && 1072 | nextTok.string === "else" 1073 | ) { 1074 | advance(); 1075 | const oldZeroth = zeroth; 1076 | type = "invocation"; 1077 | zeroth = "handleErr"; 1078 | wunth = [ 1079 | new AstNode({ 1080 | type: "get", 1081 | zeroth: oldZeroth, 1082 | }), 1083 | new AstNode({ 1084 | type: "function", 1085 | zeroth: ["err"], 1086 | wunth: block(), 1087 | }), 1088 | ]; 1089 | } 1090 | break; 1091 | default: 1092 | zeroth = tok.string; 1093 | } 1094 | break; 1095 | case "...": 1096 | // Parse spread / splat 1097 | type = "spread"; 1098 | advance(); 1099 | zeroth = "..."; 1100 | wunth = expr(); 1101 | break; 1102 | case "@": 1103 | if (nextTok && nextTok.alphanumeric === true) { 1104 | advance(); 1105 | zeroth = "@" + tok.source; 1106 | } else if (nextTok && nextTok.id === "@") { 1107 | advance(); 1108 | advance(); 1109 | zeroth = "@@" + tok.source; 1110 | } else { 1111 | zeroth = "@"; 1112 | } 1113 | break; 1114 | case "$": { 1115 | advance(); 1116 | if (tok.id === "Z") { 1117 | zeroth = "$Z"; 1118 | break; 1119 | } 1120 | // isMacro = true; 1121 | if (macros[tok.id] === undefined) { 1122 | return error(`Undefined macro ${tok.id}.`); 1123 | } 1124 | const result = paramMacro()[0]; 1125 | fallback(); 1126 | if (result && result.type) { 1127 | ({ type, zeroth, wunth, twoth } = result); 1128 | } else { 1129 | zeroth = result; 1130 | } 1131 | // isMacro = false; 1132 | // advance(); 1133 | break; 1134 | } 1135 | case "(error)": 1136 | // Return invalid tokens 1137 | return error(`Unexpected token(s) ${tok.string}`); 1138 | default: 1139 | // As for alphanumerics, they just translated to their string values. 1140 | try { 1141 | if ( 1142 | tok.alphanumeric || 1143 | (prevTok.id === "[" && tok.id === ":") || 1144 | tok.id === ")" || 1145 | tok.id === "]" || 1146 | tok.id === "@" || 1147 | tok.id === "}" 1148 | ) { 1149 | // Check if the identifier was intended as an operator. 1150 | warnBadOp(tok); 1151 | zeroth = tok.id; 1152 | } else { 1153 | // Other non-alphanumeric tokens become errors 1154 | return error(`Unexpected token ${tok.id}`); 1155 | } 1156 | } catch (_) { 1157 | return error(`Unexpected token ${tok.id}`); 1158 | } 1159 | break; 1160 | } 1161 | } 1162 | // Are there more tokens left? (And is it not a spread/splat) 1163 | if (nextTok !== undefined && type !== "spread") { 1164 | // If so, there might be more to the expression 1165 | switch (nextTok.id) { 1166 | case ".": 1167 | // Parse a refinement 1168 | advance(); 1169 | advance(); 1170 | type = "refinement"; 1171 | if (!tok) { 1172 | return error( 1173 | `Infix operation refinement requires right-hand side for property access. Only left-hand provided.` 1174 | ); 1175 | } 1176 | // Negative numbers with decimals support 1177 | if (tok.number !== undefined && typeof zeroth === "number") { 1178 | zeroth = Number(`${zeroth}.${tok.number}`); 1179 | if (isExprAhead()) { 1180 | const temp = expr(); 1181 | wunth = temp.wunth; 1182 | twoth = temp.twoth; 1183 | ({ type, zeroth, wunth, twoth } = mkChain( 1184 | type, 1185 | zeroth, 1186 | wunth, 1187 | twoth 1188 | )); 1189 | } 1190 | } else { 1191 | if (!tok.alphanumeric && tok.id !== "(keyword)") { 1192 | return error( 1193 | `Refinement expects valid identifier but instead got ${tok.id}.` 1194 | ); 1195 | } 1196 | wunth = tok.id === "(keyword)" ? tok.string : tok.id; // And the property being accessed 1197 | //Update twoth with next part of expression. 1198 | if (isExprAhead()) { 1199 | twoth = expr(); 1200 | ({ type, zeroth, wunth, twoth } = mkChain( 1201 | type, 1202 | zeroth, 1203 | wunth, 1204 | twoth 1205 | )); // Support operators with refinments. 1206 | } 1207 | } 1208 | break; 1209 | case "..": 1210 | // Optional chaining. 1211 | advance(); 1212 | advance(); 1213 | type = "condrefinement"; 1214 | if (!tok) { 1215 | return error( 1216 | `Infix operation refinement requires right-hand side for property access. Only left-hand provided.` 1217 | ); 1218 | } 1219 | if (!tok.alphanumeric && tok.id !== "(keyword)") { 1220 | return error( 1221 | `Refinement expects valid identifier but instead got ${tok.id}.` 1222 | ); 1223 | } 1224 | wunth = tok.id === "(keyword)" ? tok.string : tok.id; 1225 | if (isExprAhead()) { 1226 | twoth = expr(); 1227 | ({ type, zeroth, wunth, twoth } = mkChain( 1228 | type, 1229 | zeroth, 1230 | wunth, 1231 | twoth 1232 | )); // Support operators for optional chaining. 1233 | } 1234 | break; 1235 | case "...": 1236 | // Parse range 1237 | // Currently, range only can have a one-token prefix. 1238 | if (tok.lineNumber === nextTok.lineNumber) { 1239 | type = "range"; 1240 | advance(); 1241 | advance(); 1242 | wunth = expr(); 1243 | } 1244 | break; 1245 | case "(": 1246 | // It's an invocation 1247 | advance(); 1248 | type = "invocation"; 1249 | wunth = parseCol("(", ")"); 1250 | if ( 1251 | wunth.length !== 0 && 1252 | wunth.every((param) => param && param.type === "assignment") 1253 | ) { 1254 | wunth = [ 1255 | wunth.map(({ zeroth, wunth }) => [`"` + zeroth + `"`, wunth]), 1256 | ]; 1257 | wunth[0].species = "Object"; 1258 | } 1259 | // Allow for partial application 1260 | if (wunth.includes("@")) { 1261 | // Record the function being partially applied. 1262 | const wrappedFunc = zeroth; 1263 | type = "function"; 1264 | zeroth = []; 1265 | wunth 1266 | .filter((param) => param === "@") 1267 | .forEach((_, index) => { 1268 | zeroth[index] = "$".repeat(index + 1); 1269 | }); 1270 | let currParamDollarAmt = 1; 1271 | wunth = [ 1272 | new AstNode({ 1273 | type: "return", 1274 | zeroth: { 1275 | type: "invocation", 1276 | zeroth: wrappedFunc, 1277 | wunth: wunth.map((param, index) => { 1278 | if (param === "@") { 1279 | const res = "$".repeat(currParamDollarAmt); 1280 | currParamDollarAmt += 1; 1281 | return res; 1282 | } 1283 | return param; 1284 | }), 1285 | }, 1286 | }), 1287 | ]; 1288 | const result = configureExpr(type, zeroth, wunth, twoth); 1289 | type = "invocation"; 1290 | zeroth = "curry"; 1291 | wunth = [result]; 1292 | } 1293 | // Is there a refinement after the end of the method call? A subscript? ANOTHER method call? 1294 | if (isExprAhead()) { 1295 | // If so, record it in twoth 1296 | twoth = expr(); 1297 | ({ type, zeroth, wunth, twoth } = mkChain( 1298 | type, 1299 | zeroth, 1300 | wunth, 1301 | twoth 1302 | )); // Support operators with invocations. 1303 | } 1304 | break; 1305 | case "[": 1306 | // Parse subscript 1307 | advance(); 1308 | advance(); 1309 | type = "subscript"; 1310 | wunth = expr(); // Unlike refinements, subscripts allow ANY expression for property access 1311 | advance(); 1312 | isTok("]"); 1313 | // Continue to the next part of the expression 1314 | if (isExprAhead()) { 1315 | twoth = expr(); 1316 | ({ type, zeroth, wunth, twoth } = mkChain( 1317 | type, 1318 | zeroth, 1319 | wunth, 1320 | twoth 1321 | )); 1322 | } 1323 | break; 1324 | case ":": 1325 | // Parse assignment 1326 | advance(); 1327 | advance(); 1328 | type = "assignment"; 1329 | wunth = expr(); // Parse right-hand side 1330 | break; 1331 | } 1332 | } 1333 | // Handle operator 1334 | if ( 1335 | nextTok && 1336 | nextTok.alphanumeric && 1337 | (validStartLineOps.includes(nextTok.id) 1338 | ? true 1339 | : nextTok.lineNumber === tok.lineNumber) && 1340 | !nextTok.id.endsWith("$exclam") && 1341 | nextTok.id !== "$eq$gt" && 1342 | (isValidOp(nextTok.id) || nextTok.id === "pow") && 1343 | infix 1344 | ) { 1345 | // Check if operator may be invalid or be intended to mean something else. For example, the operator +2 probably was meant to mean + 2 1346 | warnBadOp(nextTok); 1347 | advance(); 1348 | advance(); 1349 | let res = new AstNode({ 1350 | type: "invocation", 1351 | zeroth: prevTok.id, 1352 | wunth: [configureExpr(type, zeroth, wunth, twoth), expr()].filter( 1353 | (param) => param !== undefined 1354 | ), 1355 | }); 1356 | return leftToRight(res); 1357 | } 1358 | // Typed assignment: variable type!: expr 1359 | if ( 1360 | nextTok && 1361 | nextTok.alphanumeric && 1362 | nextTok.id.endsWith("$exclam") && 1363 | tokList[index + 2] && 1364 | tokList[index + 2].id !== "," && 1365 | tokList[index + 2].id !== ")" 1366 | ) { 1367 | type = "assignment"; 1368 | const typeToAssert = nextTok.id.replace(/\$exclam$/, ""); 1369 | advance(); 1370 | advance(); 1371 | advance(":"); 1372 | wunth = { 1373 | type: "invocation", 1374 | zeroth: "assertType", 1375 | wunth: ['"' + typeToAssert + '"', expr()], 1376 | }; 1377 | } 1378 | if (twoth !== undefined) { 1379 | return new AstNode({ 1380 | type, 1381 | zeroth, 1382 | wunth, 1383 | twoth, 1384 | }); 1385 | } 1386 | if (wunth !== undefined && type !== undefined) { 1387 | return new AstNode({ 1388 | type, 1389 | zeroth, 1390 | wunth, 1391 | }); 1392 | } 1393 | // Some unary operations capture zeroth only 1394 | if (unOps.includes(type)) { 1395 | return new AstNode({ 1396 | type, 1397 | zeroth, 1398 | }); 1399 | } 1400 | // Return raw value if isn't a complete token 1401 | return zeroth; 1402 | } 1403 | 1404 | // Object to store functions, each which parses a statement. 1405 | let parseStatement = Object.create(null); 1406 | 1407 | parseStatement.let = (name = "Let") => { 1408 | let letStatement = new AstNode({ 1409 | type: "let", 1410 | zeroth: [], 1411 | }); 1412 | advance("(keyword)"); 1413 | let assignment; 1414 | assignment = expr(); 1415 | if (assignment.type !== "assignment") { 1416 | // Z uses : for assignment, but it's a common mistake to try and use "=" 1417 | if (assignment.type === "invocation" && assignment.zeroth === "$eq") { 1418 | warn("The assignment operator in Z is :, but you used =."); 1419 | } 1420 | return error(`${name} statement expects assignment.`); 1421 | } 1422 | letStatement.zeroth.push(assignment); 1423 | // Parse additional assignments. 1424 | while (nextTok && nextTok.id === ",") { 1425 | advance(); 1426 | advance(","); 1427 | assignment = expr(); 1428 | if (assignment.type !== "assignment") { 1429 | if (assignment.type === "invocation" && assignment.zeroth === "$eq") { 1430 | return error("The assignment operator in Z is :, but you used =."); 1431 | } 1432 | return error(`${name} statement expects assignment.`); 1433 | } 1434 | letStatement.zeroth.push(assignment); 1435 | } 1436 | return letStatement; 1437 | }; 1438 | 1439 | // Def is like const 1440 | parseStatement.def = () => { 1441 | const res = parseStatement.let("Def"); 1442 | res.type = "def"; 1443 | return res; 1444 | }; 1445 | 1446 | parseStatement.import = () => { 1447 | const importStatement = new AstNode({ 1448 | type: "import", 1449 | }); 1450 | advance("(keyword)"); 1451 | let imported = expr(); 1452 | // Test for shorthand imports: 1453 | if (isValidName.test(imported)) { 1454 | // import {name} = import {name}: "{name}" 1455 | importStatement.zeroth = imported; 1456 | importStatement.wunth = `"${imported}"`; 1457 | } else if (typeIn(imported, "refinement")) { 1458 | // import some.thing = import thing: "some/thing" 1459 | let path = `"${imported.zeroth}/${imported.wunth}`; 1460 | let modName = ""; 1461 | if (imported.twoth) { 1462 | imported = imported.twoth; 1463 | // Traverse refinements to extract import path. 1464 | // eslint-disable-next-line 1465 | while (true) { 1466 | path += `/${imported.wunth}`; 1467 | if (imported.twoth === undefined) { 1468 | modName = imported.wunth; 1469 | path += `"`; 1470 | break; 1471 | } 1472 | imported = imported.twoth; 1473 | } 1474 | } else { 1475 | // Only one refinement in import path. 1476 | modName = imported.wunth; 1477 | path += `"`; 1478 | } 1479 | importStatement.zeroth = modName; 1480 | importStatement.wunth = path; 1481 | } else { 1482 | if (imported.type !== "assignment") { 1483 | return error("Invalid import statement."); 1484 | } else if (!imported.wunth.startsWith('"')) { 1485 | return error("Import statement expects string."); 1486 | } 1487 | // Simplistic import: import {name}: "{path}" 1488 | importStatement.zeroth = imported.zeroth; 1489 | importStatement.wunth = imported.wunth; 1490 | } 1491 | return importStatement; 1492 | }; 1493 | 1494 | parseStatement.export = () => { 1495 | const exportStatement = new AstNode({ 1496 | type: "export", 1497 | }); 1498 | advance("(keyword)"); 1499 | const exported = expr(); 1500 | if (exported.type === "assignment") { 1501 | return error("Cannot export an assignment."); 1502 | } 1503 | exportStatement.zeroth = exported; 1504 | return exportStatement; 1505 | }; 1506 | 1507 | parseStatement.if = (extraAdv) => { 1508 | const ifStatement = { 1509 | type: "if", 1510 | }; 1511 | advance("(keyword)"); 1512 | if (tok.id === "(") { 1513 | warn( 1514 | "If conditions in Z don't have parentheses around them. This could lead to a misinterpretation of the entire if statement. Try `if cond {` rather than `if (cond) {`." 1515 | ); 1516 | } 1517 | ifStatement.zeroth = expr(); 1518 | ifStatement.wunth = block(extraAdv); 1519 | // "else if" is a special case, you cannot omit the braces after else (or if) otherwise. 1520 | if (nextTok && nextTok.id === "(keyword)" && nextTok.string === "else") { 1521 | advance("}"); 1522 | // Parse else-if 1523 | if (nextTok && nextTok.id === "(keyword)" && nextTok.string === "if") { 1524 | advance("(keyword)"); 1525 | ifStatement.twoth = [statement()]; 1526 | } else { 1527 | ifStatement.twoth = block(extraAdv); 1528 | } 1529 | } 1530 | return ifStatement; 1531 | }; 1532 | 1533 | parseStatement.loop = () => { 1534 | const loopStatement = { 1535 | type: "loop", 1536 | }; 1537 | loopStatement.zeroth = block(); 1538 | return loopStatement; 1539 | }; 1540 | 1541 | parseStatement.return = () => { 1542 | const returnStatement = new AstNode({ 1543 | type: "return", 1544 | }); 1545 | advance("(keyword)"); 1546 | returnStatement.zeroth = expr(); 1547 | if (returnStatement.zeroth.id === "(error)") { 1548 | warn( 1549 | "You may have a bare return statement in your code. These are not allowed in Z. Use `return undefined` instead." 1550 | ); 1551 | } 1552 | return returnStatement; 1553 | }; 1554 | 1555 | // This takes care of the "}" at the end of blocks (Here for legacy reasons). 1556 | parseStatement["}"] = () => { 1557 | advance(); 1558 | }; 1559 | 1560 | parseStatement.break = () => { 1561 | return new AstNode({ 1562 | type: "break", 1563 | }); 1564 | }; 1565 | 1566 | parseStatement.try = () => { 1567 | const tryStatement = new AstNode({ 1568 | type: "try", 1569 | }); 1570 | tryStatement.zeroth = block(); 1571 | // Check for "on" clause 1572 | if (nextTok && nextTok.id === "(keyword)" && nextTok.string === "on") { 1573 | advance(); 1574 | advance("(keyword)"); 1575 | tryStatement.wunth = expr(); 1576 | tryStatement.twoth = block(); 1577 | } else { 1578 | return error("Try block needs an 'on' clause."); 1579 | } 1580 | return tryStatement; 1581 | }; 1582 | 1583 | // settle statements exist to make sure you take care of errors. 1584 | parseStatement.settle = () => { 1585 | const settleStatement = new AstNode({ 1586 | type: "settle", 1587 | }); 1588 | advance("(keyword)"); 1589 | settleStatement.zeroth = expr(); 1590 | return settleStatement; 1591 | }; 1592 | 1593 | parseStatement.raise = () => { 1594 | const raiseStatement = new AstNode({ 1595 | type: "raise", 1596 | }); 1597 | advance("(keyword)"); 1598 | raiseStatement.zeroth = expr(); 1599 | return raiseStatement; 1600 | }; 1601 | 1602 | parseStatement.importstd = () => { 1603 | advance("(keyword)"); 1604 | return new AstNode({ 1605 | type: "import", 1606 | zeroth: tok.id, 1607 | wunth: `"@zlanguage/zstdlib/src/js/${tok.id}"`, // The npm package where the standard library lives. 1608 | }); 1609 | }; 1610 | 1611 | parseStatement.meta = () => { 1612 | advance("(keyword)"); 1613 | const name = tok.id; 1614 | advance(); 1615 | advance(":"); 1616 | if (tok.id !== "(string)") { 1617 | return error("Meta requires string and value."); 1618 | } 1619 | const value = tok.string; 1620 | // Store metadata for passing to dollar directives later 1621 | metadata[name] = value; 1622 | return new AstNode({ 1623 | type: "meta", 1624 | zeroth: name, 1625 | wunth: value, 1626 | }); 1627 | }; 1628 | 1629 | parseStatement.enter = () => { 1630 | return new AstNode({ 1631 | type: "enter", 1632 | zeroth: block(), 1633 | }); 1634 | }; 1635 | 1636 | parseStatement.exit = () => { 1637 | return new AstNode({ 1638 | type: "exit", 1639 | zeroth: block(), 1640 | }); 1641 | }; 1642 | 1643 | parseStatement.operator = () => { 1644 | advance("(keyword)"); 1645 | const op = tok.id; 1646 | advance(); 1647 | advance(":"); 1648 | const pred = tok.number; 1649 | // Record the operator's new precedence 1650 | ops[op] = pred; 1651 | return new AstNode({ 1652 | type: "operator", 1653 | zeroth: op, 1654 | wunth: pred, 1655 | }); 1656 | }; 1657 | 1658 | // hoist is like var 1659 | parseStatement.hoist = () => { 1660 | const res = parseStatement.let("Hoist"); 1661 | res.type = "hoist"; 1662 | return res; 1663 | }; 1664 | parseStatement.enum = () => { 1665 | advance("(keyword)"); 1666 | const res = { 1667 | type: "enum", 1668 | }; 1669 | if (nextTok.id === "(") { 1670 | // enum Point(x: number!, y: number!) 1671 | const name = expr(); 1672 | res.zeroth = name.zeroth; 1673 | res.wunth = [name]; 1674 | } else { 1675 | /* enum Maybe { 1676 | Some(thing), 1677 | None 1678 | } 1679 | enum Color { 1680 | Red, 1681 | Green, 1682 | Blue 1683 | }*/ 1684 | res.zeroth = tok.id; 1685 | advance(); 1686 | res.wunth = parseCol("{", "}"); 1687 | // Check for invalid expressions in the enum body. 1688 | if ( 1689 | !res.wunth.every( 1690 | (part) => typeof part === "string" || part.type === "invocation" 1691 | ) 1692 | ) { 1693 | return error( 1694 | "Only parenless constructors and normal constructors are allowed in enum declarations." 1695 | ); 1696 | } 1697 | } 1698 | // Detect errors in the fields of the constructors of the enum. 1699 | if ( 1700 | !res.wunth 1701 | .filter((part) => part.type === "invocation") 1702 | .every((invok) => 1703 | invok.wunth.every( 1704 | (part) => Array.isArray(part) || typeof part === "string" 1705 | ) 1706 | ) 1707 | ) { 1708 | return error( 1709 | `Error in enum constructor parameter list. Enum name: ${res.zeroth}.` 1710 | ); 1711 | } 1712 | // Deal with derives and static derives. 1713 | if (nextTok && nextTok.id === "(keyword)" && nextTok.string === "derives") { 1714 | advance(); 1715 | advance("(keyword)"); 1716 | const derives = parseCol("(", ")"); 1717 | res.derives = derives.filter((derive) => derive.type !== "static"); 1718 | res.staticDerives = derives 1719 | .filter((derive) => derive.type === "static") 1720 | .map((derive) => derive.zeroth); 1721 | } 1722 | // The "where" block, where static methods are declared. 1723 | if (nextTok && nextTok.id === "(keyword)" && nextTok.string === "where") { 1724 | advance(); 1725 | advance("(keyword)"); 1726 | advance("{"); 1727 | res.twoth = {}; 1728 | if (tok.id !== "}") { 1729 | while (true) { 1730 | // Get the function name 1731 | const key = tok.id; 1732 | advance(); 1733 | // Get the function body 1734 | const temp = expr(); 1735 | const func = new AstNode({ 1736 | type: "function", 1737 | zeroth: temp.slice(0, -1), 1738 | wunth: temp[temp.length - 1].wunth, 1739 | }); 1740 | res.twoth[key] = func; 1741 | advance("}"); 1742 | if (!tok) { 1743 | break; 1744 | } 1745 | if (tok.id === "}") { 1746 | break; 1747 | } 1748 | } 1749 | } 1750 | isTok("}"); 1751 | } 1752 | return res; 1753 | }; 1754 | 1755 | parseStatement.go = function () { 1756 | return new AstNode({ 1757 | type: "go", 1758 | zeroth: block(), 1759 | }); 1760 | }; 1761 | 1762 | /** 1763 | * The block symbols. 1764 | */ 1765 | export const blockSyms = { 1766 | "(": ")", 1767 | "[": "]", 1768 | "{": "}", 1769 | }; 1770 | 1771 | parseStatement.macro = function () { 1772 | advance("(keyword)"); 1773 | if (tok.id === "(keyword)" && tok.string === "operator") { 1774 | advance("(keyword)"); 1775 | } else { 1776 | advance("$"); 1777 | } 1778 | const macroName = tok.id; 1779 | advance(); 1780 | advance("("); 1781 | const params = []; 1782 | let symAmt = 1; 1783 | while (tok.id !== ")") { 1784 | (function param(p = params, term = ")") { 1785 | if (tok.id === "~") { 1786 | advance("~"); 1787 | const capture = { 1788 | name: tok.id, 1789 | }; 1790 | advance(); 1791 | advance(":"); 1792 | capture.captureType = tok.id; 1793 | advance(); 1794 | if (tok.id !== term) { 1795 | advance(","); 1796 | } 1797 | p.push(capture); 1798 | } else if (tok.id === "...") { 1799 | advance("..."); 1800 | const opener = tok.id; 1801 | const closer = blockSyms[opener]; 1802 | const capture = []; 1803 | let comma = false; 1804 | advance(); 1805 | while (tok.id !== closer) { 1806 | param(capture, closer); 1807 | if (prevTok.id === "," && tok.id === closer) { 1808 | comma = true; 1809 | } 1810 | } 1811 | advance(); 1812 | if (tok.id === ",") { 1813 | advance(); 1814 | } 1815 | p.push({ 1816 | name: "rest", 1817 | captureType: "rest", 1818 | start: opener, 1819 | end: closer, 1820 | comma, 1821 | captureGroups: capture, 1822 | }); 1823 | } else { 1824 | p.push({ 1825 | name: tok.alphanumeric ? tok.id : tok.string, 1826 | paramName: "$_".repeat(symAmt), 1827 | captureType: "skip", 1828 | }); 1829 | symAmt += 1; 1830 | advance(); 1831 | if (tok.id !== term) { 1832 | advance(","); 1833 | } 1834 | } 1835 | })(); 1836 | } 1837 | const macroBody = eval( 1838 | gen([ 1839 | { 1840 | type: "invocation", 1841 | zeroth: "copy", 1842 | wunth: [ 1843 | { 1844 | type: "function", 1845 | zeroth: params.map((param) => 1846 | param.paramName ? param.paramName : param.name 1847 | ), 1848 | wunth: block(), 1849 | }, 1850 | ], 1851 | }, 1852 | ]) 1853 | ); 1854 | if (Object.keys(macros).includes(macroName)) { 1855 | const oldMacro = macros[macroName]; 1856 | if (oldMacro.divergencePoint === undefined) { 1857 | const otherParams = oldMacro.params; 1858 | otherParams.forEach((param, index) => { 1859 | if (param.captureType === "skip" && param.name !== params[index].name) { 1860 | macros[macroName] = { 1861 | divergencePoint: index, 1862 | options: [param.name, params[index].name], 1863 | paths: [ 1864 | oldMacro, 1865 | { 1866 | params, 1867 | macro: macroBody, 1868 | }, 1869 | ], 1870 | }; 1871 | } 1872 | }); 1873 | } else { 1874 | macros[macroName].options.push( 1875 | params[macros[macroName].divergencePoint].name 1876 | ); 1877 | macros[macroName].paths.push({ 1878 | params, 1879 | macro: macroBody, 1880 | }); 1881 | } 1882 | return {}; 1883 | } 1884 | macros[macroName] = { 1885 | params, 1886 | macro: macroBody, 1887 | }; 1888 | return {}; 1889 | }; 1890 | // Keywords that can be used as valid statements or expressions. 1891 | const exprKeywords = Object.freeze(["func", "match", "get"]); 1892 | 1893 | function statement() { 1894 | // For normal statement keywords 1895 | if (tok && tok.id === "(keyword)" && !exprKeywords.includes(tok.string)) { 1896 | const parser = parseStatement[tok.string]; 1897 | if (typeof parser !== "function") { 1898 | return error("Invalid use of keyword."); 1899 | } 1900 | return parser(); 1901 | } else { 1902 | // Otherwise, it's some kind of expressions 1903 | let res = expr(); 1904 | 1905 | if (res !== undefined && res.id === "(error)") { 1906 | return res; 1907 | } 1908 | if ( 1909 | typeIn(res, "assignment") || 1910 | typeIn(res, "invocation") || 1911 | typeIn(res, "get") || 1912 | typeIn(res, "match") || 1913 | typeIn(res, "function") || 1914 | (res && res.species === "Destructuring[Array]") 1915 | ) { 1916 | // If res is a valid standalone expression, return it. 1917 | return res; 1918 | } else { 1919 | if (res !== undefined && !isMacro) { 1920 | // Otherwise, report an error. 1921 | return error( 1922 | `Invalid expression ${res}, expression must be an assignment or invocation if it does not involve a keyword.` 1923 | ); 1924 | } 1925 | return res; 1926 | } 1927 | } 1928 | } 1929 | 1930 | function block() { 1931 | let statements = []; 1932 | advance(); 1933 | const init = tok; 1934 | advance(); 1935 | let i = 0; 1936 | while (tok && tok.id !== "}" && i < 1e6) { 1937 | const st = statement(); 1938 | // Check for errors. 1939 | if (!findAndThrow(st)) { 1940 | statements.push(st); 1941 | } 1942 | advance(); 1943 | i += 1; 1944 | } 1945 | if (tok === undefined) { 1946 | tok = init; 1947 | const res = nqerror("Unclosed block. Block started"); 1948 | tok = undefined; 1949 | errors.push(res); 1950 | } 1951 | // Check for unclosed block. 1952 | if (i === 1e6) { 1953 | errors.push(error("Unclosed block.")); 1954 | } 1955 | return statements; 1956 | } 1957 | 1958 | function insertParams(node, params, cache = {}) { 1959 | switch (typeof node) { 1960 | case "string": 1961 | if (node.startsWith("_breadcrumb_")) { 1962 | const name = node.replace("_breadcrumb_", ""); 1963 | return params[name]; 1964 | } 1965 | if (node.startsWith("_id_")) { 1966 | const name = node.replace("_id_", ""); 1967 | if (cache[name] === undefined) { 1968 | cache[name] = 1969 | name + 1970 | "_" + 1971 | Math.random().toString().slice(2, 5) + 1972 | Math.random().toString().slice(2, 5); 1973 | } 1974 | return cache[name]; 1975 | } 1976 | break; 1977 | case "object": 1978 | if (node.type === "spread" && node.wunth[0].type === "function") { 1979 | const { rest } = params; 1980 | const results = []; 1981 | const template = node.wunth[0].wunth; 1982 | rest.forEach((paramList) => { 1983 | paramList = { ...paramList, ...params }; 1984 | const cache = {}; 1985 | let use = copy(template); 1986 | use = use.map((node) => insertParams(node, paramList, cache)); 1987 | results.push(...use); 1988 | }); 1989 | results.spreadOut = true; 1990 | return results; 1991 | } 1992 | if (node.zeroth) { 1993 | node.zeroth = insertParams(node.zeroth, params, cache); 1994 | } 1995 | if (node.wunth) { 1996 | node.wunth = insertParams(node.wunth, params, cache); 1997 | } 1998 | if (node.twoth) { 1999 | node.twoth = insertParams(node.twoth, params, cache); 2000 | } 2001 | if (Array.isArray(node.zeroth)) { 2002 | node.zeroth = node.zeroth.map((node) => 2003 | insertParams(node, params, cache) 2004 | ); 2005 | } 2006 | if (Array.isArray(node.wunth)) { 2007 | node.wunth = node.wunth.map((node) => 2008 | insertParams(node, params, cache) 2009 | ); 2010 | } 2011 | if (Array.isArray(node.twoth)) { 2012 | node.twoth = node.twoth.map((node) => 2013 | insertParams(node, params, cache) 2014 | ); 2015 | } 2016 | break; 2017 | } 2018 | return node; 2019 | } 2020 | 2021 | function paramMacro() { 2022 | const macroName = tok.id; 2023 | let macroParams = macros[macroName].params; 2024 | if (macros[macroName].divergencePoint !== undefined) { 2025 | macroParams = macros[macroName].paths[0].params; 2026 | } 2027 | const params = {}; 2028 | const ids = []; 2029 | let correctPath; 2030 | let err; 2031 | macroParams.every(function handle(param, p = params) { 2032 | const { name, captureType } = param; 2033 | if (p === macros[macroName].divergencePoint) { 2034 | const ops = macros[macroName].options; 2035 | advance(); 2036 | correctPath = ops.indexOf(tok.id); 2037 | if (correctPath === -1) { 2038 | err = error( 2039 | `Macro "${macroName}" is only overloaded for the following identifiers: ${ops 2040 | .map((op) => `"${op}"`) 2041 | .join(", ")}. There is no overload available for "${tok.id}".` 2042 | ); 2043 | return false; 2044 | } 2045 | params[name] = name.startsWith('"') ? name : '"' + name + '"'; 2046 | macros[macroName].paths[correctPath].params 2047 | .slice(macros[macroName].divergencePoint + 1) 2048 | .forEach((param, p) => { 2049 | handle(param, p + macros[macroName].divergencePoint + 1); 2050 | }); 2051 | return false; 2052 | } 2053 | if (typeof p === "number") { 2054 | p = params; 2055 | } 2056 | switch (captureType) { 2057 | case "expr": 2058 | advance(); 2059 | p[name] = expr(); 2060 | break; 2061 | case "block": 2062 | p[name] = block(); 2063 | p[name].spreadOut = true; 2064 | break; 2065 | case "id": 2066 | advance(); 2067 | ids.push(name); 2068 | p[name] = tok.string ? tok.string : tok.id; 2069 | break; 2070 | case "skip": 2071 | advance(); 2072 | isTok(name.replace(/^\"|"$/g, "")); 2073 | p[name] = name.startsWith('"') ? name : '"' + name + '"'; 2074 | break; 2075 | case "rest": { 2076 | advance(); 2077 | isTok(param.start); 2078 | const capturedExprs = []; 2079 | while (nextTok && nextTok.id !== param.end) { 2080 | let results = {}; 2081 | param.captureGroups.forEach((param) => handle(param, results)); 2082 | capturedExprs.push(results); 2083 | if (nextTok && nextTok.id !== param.end && param.comma) { 2084 | advance(); 2085 | } 2086 | } 2087 | p.rest = capturedExprs; 2088 | advance(); 2089 | break; 2090 | } 2091 | } 2092 | return true; 2093 | }); 2094 | if (err) { 2095 | return [err]; 2096 | } 2097 | let macroString; 2098 | if (correctPath !== undefined) { 2099 | macroString = macros[macroName].paths[correctPath].macro( 2100 | ...Object.values(params) 2101 | ); 2102 | } else { 2103 | macroString = macros[macroName].macro(...Object.values(params)); 2104 | } 2105 | macroString = macroString.replace(/{{(.+?)}}/g, (_, match) => { 2106 | if (match.startsWith("~")) { 2107 | const type = match.slice(1); 2108 | if (ids.includes(type)) { 2109 | return params[type]; 2110 | } 2111 | return "_breadcrumb_" + match.slice(1); 2112 | } else { 2113 | return "_id_" + match; 2114 | } 2115 | }); 2116 | let ast = parseMacro(tokenize(macroString), false); 2117 | const cache = {}; 2118 | ast = ast.map((node) => insertParams(node, params, cache)); 2119 | advance(); 2120 | return ast; 2121 | } 2122 | 2123 | function pureMacro(macroName, params) { 2124 | const ids = []; 2125 | let macroString = macros[macroName].macro(...Object.values(params)); 2126 | macroString = macroString.replace(/{{(.+?)}}/g, (_, match) => { 2127 | if (match.startsWith("~")) { 2128 | const type = match.slice(1); 2129 | if (ids.includes(type)) { 2130 | return params[type]; 2131 | } 2132 | return "_breadcrumb_" + match.slice(1); 2133 | } else { 2134 | return "_id_" + match; 2135 | } 2136 | }); 2137 | let ast = parseMacro(tokenize(macroString), false); 2138 | const cache = {}; 2139 | ast = ast.map((node) => insertParams(node, params, cache)); 2140 | return ast; 2141 | } 2142 | 2143 | // Gather all the statements together. 2144 | function statements() { 2145 | let statements = []; 2146 | let nextStatement; 2147 | // eslint freaks out about this, not sure why 2148 | // eslint-ignore-next-line 2149 | while (true) { 2150 | if (tok && tok.id === "$" && nextTok.id !== "Z") { 2151 | advance("$"); 2152 | if (macros[tok.id] === undefined) { 2153 | statements.push(error(`Undefined macro ${tok.id}.`)); 2154 | advance(); 2155 | continue; 2156 | } 2157 | statements.push(...paramMacro()); 2158 | continue; 2159 | } 2160 | if (tok && tok.id === "(keyword)" && tok.string === "include") { 2161 | advance(); 2162 | const file = fs.readFileSync(tok.string); 2163 | statements.push(...parseMacro(tokenize(file.toString()), false)); 2164 | advance(); 2165 | continue; 2166 | } 2167 | if (tok && tok.id === "(keyword)" && tok.string === "includestd") { 2168 | advance(); 2169 | const currpath = process.cwd(); 2170 | while (!fs.existsSync("node_modules")) { 2171 | process.chdir(".."); 2172 | } 2173 | const file = fs.readFileSync( 2174 | `node_modules/@zlanguage/zstdlib/src/macros/${tok.string}.zlang` 2175 | ); 2176 | statements.push(...parseMacro(tokenize(file.toString()), false)); 2177 | advance(); 2178 | process.chdir(currpath); 2179 | continue; 2180 | } 2181 | nextStatement = statement(); 2182 | // Are there no statements left? 2183 | if (nextStatement === undefined && nextTok === undefined) { 2184 | break; 2185 | } 2186 | if (nextStatement === undefined) { 2187 | break; 2188 | } 2189 | if ( 2190 | nextStatement.type === undefined && 2191 | nextTok === undefined && 2192 | nextStatement.id !== "(error)" 2193 | ) { 2194 | if (!isMacro) { 2195 | break; 2196 | } 2197 | } 2198 | // Handle suffix if: "something if cond" 2199 | if ( 2200 | nextTok && 2201 | nextTok.id === "(keyword)" && 2202 | nextTok.string === "if" && 2203 | nextTok.lineNumber === tok.lineNumber 2204 | ) { 2205 | advance(); 2206 | advance("(keyword)"); 2207 | nextStatement = { 2208 | type: "if", 2209 | zeroth: expr(), 2210 | wunth: [nextStatement], 2211 | }; 2212 | } 2213 | // Otherwise, continue 2214 | statements.push(nextStatement); 2215 | // Start a new list of errors. 2216 | errors.next(); 2217 | advance(); 2218 | } 2219 | // Go back to the beginning of the list of errors. 2220 | errors.restart(); 2221 | return statements; 2222 | } 2223 | 2224 | function arrayWrap(arr) { 2225 | if (!Array.isArray(arr)) { 2226 | return [arr]; 2227 | } 2228 | return arr; 2229 | } 2230 | 2231 | // Look for more errors and add them to the list. 2232 | // Return true if errors were found. 2233 | function findAndThrow(ast, topLevel = true) { 2234 | let errorFound = false; 2235 | arrayWrap(ast).forEach((part) => { 2236 | if (part && part.id === "(error)") { 2237 | errors.push(part); 2238 | errorFound = true; 2239 | } else if (part && part.type) { 2240 | if (part && part.zeroth) { 2241 | errorFound = findAndThrow(part.zeroth, false); 2242 | } 2243 | if (part && part.wunth) { 2244 | errorFound = findAndThrow(part.wunth, false); 2245 | } 2246 | if (part && part.twoth) { 2247 | (errorFound = findAndThrow(part.twoth)), false; 2248 | } 2249 | } 2250 | if (topLevel) { 2251 | errors.next(); 2252 | } 2253 | }); 2254 | if (errors.length > 0) { 2255 | return true; 2256 | } 2257 | return errorFound; 2258 | } 2259 | 2260 | function parseMacro(tokGen, exp = true) { 2261 | isMacro = true; 2262 | const oldTokList = tokList; 2263 | const oldPrevTok = prevTok; 2264 | const oldTok = tok; 2265 | const oldNextTok = nextTok; 2266 | const oldIndex = index; 2267 | tokList = (() => { 2268 | let res = []; 2269 | for (let i; (i = tokGen()) !== undefined; ) { 2270 | res.push(i); 2271 | } 2272 | return res; 2273 | })(); 2274 | [tok, nextTok] = [tokList[0], tokList[1]]; 2275 | index = 0; 2276 | const statementz = exp ? [expr()] : statements(); 2277 | tokList = oldTokList; 2278 | [prevTok, tok, nextTok] = [oldPrevTok, oldTok, oldNextTok]; 2279 | index = oldIndex; 2280 | isMacro = false; 2281 | return exp ? statementz[0] : statementz; 2282 | } 2283 | 2284 | /** 2285 | * @param {AstNode | any[]} ast 2286 | * @returns {AstNode | any[]} 2287 | */ 2288 | function resolveOpMacros(ast) { 2289 | if (Array.isArray(ast)) { 2290 | ast.forEach((statement, index) => { 2291 | if ( 2292 | statement.type === "invocation" && 2293 | Object.keys(macros).includes(statement.zeroth) 2294 | ) { 2295 | const macroName = statement.zeroth; 2296 | const paramNames = macros[macroName].params.map((param) => param.name); 2297 | const params = {}; 2298 | params[paramNames[0]] = resolveOpMacros([statement.wunth[0]])[0]; 2299 | params[paramNames[1]] = resolveOpMacros([statement.wunth[1]])[0]; 2300 | ast[index] = pureMacro(macroName, params)[0]; 2301 | } 2302 | if ( 2303 | statement.zeroth && 2304 | statement.zeroth.type === "invocation" && 2305 | Object.keys(macros).includes(statement.zeroth.zeroth) 2306 | ) { 2307 | statement.zeroth = resolveOpMacros([statement.zeroth])[0]; 2308 | } 2309 | if ( 2310 | statement.wunth && 2311 | statement.wunth.type === "invocation" && 2312 | Object.keys(macros).includes(statement.wunth.zeroth) 2313 | ) { 2314 | statement.wunth = resolveOpMacros([statement.wunth])[0]; 2315 | } 2316 | if ( 2317 | statement.twoth && 2318 | statement.twoth.type === "invocation" && 2319 | Object.keys(macros).includes(statement.twoth.zeroth) 2320 | ) { 2321 | statement.twoth = resolveOpMacros([statement.twoth])[0]; 2322 | } 2323 | if (Array.isArray(statement.zeroth)) { 2324 | statement.zeroth = resolveOpMacros(statement.zeroth); 2325 | } 2326 | if (Array.isArray(statement.wunth)) { 2327 | statement.wunth = resolveOpMacros(statement.wunth); 2328 | } 2329 | if (Array.isArray(statement.twoth)) { 2330 | statement.wunth = resolveOpMacros(statement.wunth); 2331 | } 2332 | }); 2333 | } 2334 | return ast; 2335 | } 2336 | 2337 | /** 2338 | * Parse a list of tokens from a generator. 2339 | * 2340 | * @param {() => any} tokGen The token generator. 2341 | * @param {boolean} debug If debugging should be enabled. 2342 | */ 2343 | function parse(tokGen, debug = true) { 2344 | // Generate a list of tokens 2345 | tokList = (() => { 2346 | let res = []; 2347 | for (let i; (i = tokGen()) !== undefined; ) { 2348 | res.push(i); 2349 | } 2350 | return res; 2351 | })(); 2352 | // Re-init all the variables 2353 | index = 0; 2354 | metadata = {}; 2355 | macros = {}; 2356 | errors.empty(); 2357 | warnings = []; 2358 | [tok, nextTok] = [tokList[0], tokList[1]]; 2359 | // Gather statements 2360 | const statementz = resolveOpMacros(statements()); 2361 | // Display warnings 2362 | if (warnings.length > 0 && debug) { 2363 | console.log( 2364 | `${warnings.length} warning${warnings.length === 1 ? "" : "s"} generated:` 2365 | ); 2366 | warnings.forEach((warning) => { 2367 | console.log(warning); 2368 | }); 2369 | } 2370 | if (!findAndThrow(statementz)) { 2371 | // Resolve top-level get. 2372 | if (isGoroutine(statementz)) { 2373 | if (statementz.some((statement) => statement.type === "export")) { 2374 | throw error( 2375 | "Export and top-level get do not work well together. Until top-level await is supported, another solution is needed. Try exporting a promise instead." 2376 | ).zeroth; 2377 | } 2378 | return [ 2379 | new AstNode({ 2380 | type: "def", 2381 | zeroth: [ 2382 | new AstNode({ 2383 | type: "assignment", 2384 | zeroth: "$main", 2385 | wunth: new AstNode({ 2386 | type: "goroutine", 2387 | zeroth: [], 2388 | wunth: statementz, 2389 | }), 2390 | }), 2391 | ], 2392 | }), 2393 | new AstNode({ 2394 | type: "invocation", 2395 | zeroth: "$main", 2396 | wunth: [], 2397 | }), 2398 | ]; 2399 | } 2400 | return statementz; 2401 | } 2402 | // If debug mode is on, print out a list of errors. 2403 | if (debug) { 2404 | console.log( 2405 | `${errors.length} error${errors.length === 1 ? "" : "s"} generated:` 2406 | ); 2407 | errors.forEach((error) => { 2408 | console.log(error.zeroth); 2409 | }); 2410 | } else { 2411 | // Otherwise, just throw the first one. 2412 | if (errors.first() != null) { 2413 | throw errors.first().zeroth; 2414 | } 2415 | } 2416 | return []; 2417 | } 2418 | 2419 | // Tie everything together with one function. 2420 | module.exports = Object.freeze(parse); 2421 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/tokenize.js: -------------------------------------------------------------------------------- 1 | export const symbolMap = { 2 | "+": "$plus", 3 | "-": "$minus", 4 | "*": "$star", 5 | "/": "$slash", 6 | "^": "$carot", 7 | "?": "$question", 8 | "=": "$eq", 9 | "<": "$lt", 10 | ">": "$gt", 11 | "\\": "$backslash", 12 | "&": "$and", 13 | "|": "$or", 14 | "%": "$percent", 15 | "'": "$quote", 16 | "!": "$exclam", 17 | }; 18 | 19 | export const symbolRegExps = Object.keys(symbolMap).map((x) => { 20 | const res = new RegExp(`\\${x}`, "g"); 21 | res.str = symbolMap[x]; 22 | return res; 23 | }); 24 | 25 | export const unicodeEscapementRegExp = /\\u\{([0-9A-F]{4,6})\}/g; 26 | export const newLineRegExp = /\n|\r\n?/; 27 | 28 | /** 29 | * See docs/dev-notes.md 30 | */ 31 | export const tokenRegExp = /(\u0020+|\t+)|(#.*)|("(?:[^"\\]|\\(?:[nrt"\\]|u\{[0-9A-F]{4,6}\}))*")|\b(let|loop|if|else|func|break|import|export|match|return|def|try|on|settle|raise|importstd|meta|enter|exit|operator|hoist|go|get|enum|where|derives|static|macro|include|includestd)\b|([A-Za-z_+\-/*%&|?^=<>'!][A-Za-z_0-9+\-/*%&|?^<>='!]*)|((?:0[box])?-?\d[\d_]*(?:\.[\d_]+)?(?:e\-?[\d_]+)?[a-z]*)|(\.{2,3}|~{|}~|[~@$(),.{}\[\]:])|(`.+?`[gimsuy]*)/y; 32 | 33 | /** 34 | * Create a token generator for a specific source. 35 | * @param {string | Array} source If string is provided, string is split along newlines. If array is provided, array is used as the array of lines. 36 | * @param {boolean} comments Should comments be tokenized and returned from the generator? 37 | * @returns {() => void | {id: string, lineNumber: number, columnNumber: number, string?: string, columnTo?: number, readonly?: boolean, alphanumeric?: boolean, number?: number, source?: string}} A function that generates the next token. 38 | */ 39 | export default function tokenize(source, comments = false) { 40 | // Handle multiline comments 41 | source = source.replace( 42 | new RegExp("/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/", "g"), 43 | "" 44 | ); 45 | // Configure source 46 | const lines = Array.isArray(source) ? source : source.split(newLineRegExp); 47 | let lineNumber = 0; 48 | // Refinements are not mangled 49 | let dotLast = false; 50 | let insideTemplate = false; 51 | let template = []; 52 | let linesDone = []; 53 | let line = lines[0]; 54 | // Keep track of where the token is on the line 55 | tokenRegExp.lastIndex = 0; 56 | return function tokenGenerator() { 57 | // Are we done yet? 58 | if (line === undefined) { 59 | return; 60 | } 61 | let columnNumber = tokenRegExp.lastIndex; 62 | // Do we need to go to the next line? 63 | if (columnNumber >= line.length) { 64 | tokenRegExp.lastIndex = 0; 65 | lineNumber += 1; 66 | line = lines[lineNumber]; 67 | // Get the next token. 68 | return line === undefined ? undefined : tokenGenerator(); 69 | } 70 | // Figure out the current token. 71 | let captives = tokenRegExp.exec(line); 72 | if ( 73 | insideTemplate && 74 | captives[7] !== "}~" && 75 | !linesDone.includes(lineNumber) 76 | ) { 77 | template.push(line); 78 | linesDone.push(lineNumber); 79 | return tokenGenerator(); 80 | } 81 | if (insideTemplate && captives[7] !== "}~") { 82 | return tokenGenerator(); 83 | } 84 | // If no tokens were matched, it's an error. 85 | if (!captives) { 86 | let res = { 87 | id: "(error)", 88 | lineNumber, 89 | columnNumber, 90 | columnTo: columnNumber, 91 | string: line.slice(columnNumber), 92 | }; 93 | line = undefined; 94 | return res; 95 | } 96 | // Whitespace matched 97 | if (captives[1]) { 98 | dotLast = false; 99 | // Ignore whitespace - Z has flexible whitespace rules. 100 | return tokenGenerator(); 101 | } 102 | const columnTo = tokenRegExp.lastIndex; 103 | // Comment matched 104 | if (captives[2]) { 105 | dotLast = false; 106 | // Ignore empty comments 107 | return comments 108 | ? { 109 | id: "(comment)", 110 | comment: captives[2], 111 | lineNumber, 112 | columnNumber, 113 | columnTo, 114 | } 115 | : tokenGenerator(); 116 | } 117 | // String Matched 118 | if (captives[3]) { 119 | dotLast = false; 120 | return { 121 | id: "(string)", 122 | readonly: true, 123 | string: JSON.parse( 124 | captives[3].replace( 125 | // Handle unicode escapement. 126 | unicodeEscapementRegExp, 127 | function (_ignore, code) { 128 | // @ts-ignore 129 | return String.fromCodePoint(parseInt(code, 16)); 130 | } 131 | ) 132 | ), 133 | lineNumber, 134 | columnNumber, 135 | columnTo, 136 | }; 137 | } 138 | // Keyword Matched 139 | if (captives[4]) { 140 | dotLast = false; 141 | return { 142 | id: "(keyword)", 143 | string: captives[4], 144 | lineNumber, 145 | columnNumber, 146 | columnTo, 147 | }; 148 | } 149 | // Name Matched 150 | if (captives[5]) { 151 | let res = captives[5]; 152 | if (!dotLast) { 153 | // If a . wasn't last (the identifier isn't part of a refinement/property access) 154 | // Mangle it 155 | symbolRegExps.forEach((regexp) => { 156 | res = res.replace(regexp, regexp.str); 157 | }); 158 | } 159 | // Allow negative numbers. 160 | if (/\$minus\d+/.test(res)) { 161 | res = res.replace("$minus", "-"); 162 | return { 163 | id: "(number)", 164 | readonly: true, 165 | number: Number( 166 | res 167 | .replace(/(?:[^\d])([^0])[a-z]+/g, "$1") 168 | .replace(/[a-z]$/, "") 169 | .replace(/_/g, "") 170 | ), 171 | string: res, 172 | lineNumber, 173 | columnNumber, 174 | columnTo, 175 | }; 176 | } 177 | dotLast = false; 178 | return { 179 | id: res, 180 | source: captives[5], 181 | alphanumeric: true, 182 | lineNumber, 183 | columnNumber, 184 | columnTo, 185 | }; 186 | } 187 | // Number Matched 188 | if (captives[6]) { 189 | dotLast = false; 190 | return { 191 | id: "(number)", 192 | readonly: true, 193 | number: Number( 194 | captives[6] 195 | .replace(/(?:[^\d])([^0])[a-z]+/g, "$1") 196 | .replace(/[a-z]$/, "") 197 | .replace(/_/g, "") 198 | ), // Make the number JS-Compatable for parsing 199 | string: captives[6], 200 | lineNumber, 201 | columnNumber, 202 | columnTo, 203 | }; 204 | } 205 | // Punctuator Matched 206 | if (captives[7]) { 207 | if (captives[7] === ".") { 208 | dotLast = true; // This means a refinement is coming. The next alphanumeric identifier will not be mangled. 209 | } else { 210 | dotLast = false; 211 | } 212 | if (captives[7] === "~{") { 213 | insideTemplate = true; 214 | return tokenGenerator(); 215 | } 216 | if (captives[7] === "}~") { 217 | insideTemplate = false; 218 | const temp = template; 219 | template = []; 220 | linesDone = []; 221 | return { 222 | id: "(template)", 223 | string: temp 224 | .slice(0, -1) 225 | .map((x) => x.trim()) 226 | .join("\n"), 227 | lineNumber, 228 | columnNumber, 229 | columnTo, 230 | }; 231 | } 232 | return { 233 | id: captives[7], 234 | lineNumber, 235 | columnNumber, 236 | columnTo, 237 | }; 238 | } 239 | if (captives[8]) { 240 | return { 241 | id: "(regexp)", 242 | string: captives[8].replace(/^`|`(.+)$/g, "").replace(/`$/, ""), 243 | flags: captives[8].split("`")[2], 244 | lineNumber, 245 | columnNumber, 246 | columnTo, 247 | }; 248 | } 249 | }; 250 | } 251 | -------------------------------------------------------------------------------- /packages/zcomp/src/compiler/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Valid options for the "type" field. 3 | */ 4 | export const validTypes = [ 5 | "subscript", 6 | "refinement", 7 | "condrefinement", 8 | "invocation", 9 | "assignment", 10 | "goroutine", 11 | "function", 12 | "spread", 13 | "match", 14 | "range", 15 | "loopexpr", 16 | "ifexpr", 17 | "if", 18 | ]; 19 | 20 | /** 21 | * An AST node. 22 | */ 23 | export class AstNode { 24 | /** 25 | * The node type. 26 | * 27 | * @type {"subscript" | "refinement" | "condrefinement" | "invocation" | "assignment" | "goroutine" | "function" | "spread" | "match" | "range" | "loopexpr" | "ifexpr" | "if"} 28 | */ 29 | type; 30 | 31 | /** 32 | * The zeroth. 33 | * 34 | * @type {AstNode | any[]} 35 | */ 36 | zeroth; 37 | 38 | /** 39 | * The wunth. 40 | * 41 | * @type {AstNode | any[]} 42 | */ 43 | wunth; 44 | 45 | /** 46 | * The twoth. 47 | * 48 | * @type {AstNode | any[]} 49 | */ 50 | twoth; 51 | 52 | /** 53 | * The species. 54 | */ 55 | species; 56 | 57 | /** 58 | * The ID. 59 | * 60 | * @type {string?} 61 | */ 62 | id; 63 | 64 | /** 65 | * Used in determining operator precedence. 66 | * 67 | * @type {boolean?} 68 | */ 69 | leftToRight; 70 | 71 | /** 72 | * @type {any[]} 73 | */ 74 | predicates; 75 | 76 | /** 77 | * A constructor for an AST node. 78 | * 79 | * @param {{ species?: any, type?: any, zeroth?: AstNode | any[], wunth?: AstNode | any[], twoth?: AstNode | any[], id?: string, data?: string, leftToRight?: boolean, predicates: any[] }} properties The node's properties 80 | */ 81 | constructor(properties) { 82 | for (let property of [ 83 | "species", 84 | "type", 85 | "zeroth", 86 | "wunth", 87 | "twoth", 88 | "id", 89 | "leftToRight", 90 | "predicates", 91 | ]) { 92 | if (properties.hasOwnProperty(property)) { 93 | this[property] = properties[property]; 94 | } 95 | } 96 | } 97 | } 98 | --------------------------------------------------------------------------------