├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.BSD ├── README.md ├── debug-test262.js ├── debug.js ├── helpers.js ├── mod.js ├── package-lock.json ├── package.json ├── printAst.js ├── profile.js ├── src ├── ast-dispatcher.js ├── binding-map.js ├── browser-sweet.js ├── codegen.js ├── compiler.js ├── enforester.js ├── env.js ├── errors.js ├── hygiene-utils.js ├── load-syntax.js ├── macro-context.js ├── module-visitor.js ├── multimap.js ├── node-loader.js ├── node-module-loader.js ├── node-module-resolver.js ├── operators.js ├── reader │ ├── default-readtable.js │ ├── read-comment.js │ ├── read-dispatch.js │ ├── read-identifier.js │ ├── read-numeric.js │ ├── read-regexp.js │ ├── read-string.js │ ├── read-template.js │ ├── token-reader.js │ └── utils.js ├── scope-reducer.js ├── scope.js ├── store-loader.js ├── store.js ├── sweet-loader.js ├── sweet-module.js ├── sweet-spec-utils.js ├── sweet-to-shift-reducer.js ├── sweet.js ├── symbol.js ├── syntax.js ├── template-processor.js ├── term-expander.js ├── terms.js ├── token-expander.js ├── tokens.js └── transforms.js ├── test.js └── test ├── assertions.js ├── modules ├── _helpers.js └── test-expanding-cjs.js ├── parser ├── __snapshots__ │ └── test-ast.js.snap ├── extra-parser-tests │ └── pass │ │ └── 1.js ├── test-ast.js ├── test-compile-external-libs.js └── test-run-test262.js └── unit ├── test-asi.js ├── test-default-readtable.js ├── test-helpers.js ├── test-hygiene.js ├── test-macro-context.js ├── test-macro-expansion.js ├── test-modules.js ├── test-node-loader.js ├── test-operators.js ├── test-parse.js ├── test-reader-extensions.js ├── test-reader.js ├── test-sweet-loader.js ├── test-symbols.js └── test-syntax.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["node7"], 3 | "plugins": ["transform-flow-strip-types"], 4 | "sourceMaps": "inline" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/parser/test262-parser-tests 2 | test/parser/extra-parser-tests 3 | debug.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "flowtype", 5 | "prettier" 6 | ], 7 | "env": { 8 | "es6": true, 9 | "node": true 10 | }, 11 | "ecmaVersion": 6, 12 | "parserOptions": { 13 | "sourceType": "module" 14 | }, 15 | "extends": [ 16 | "eslint:recommended", 17 | "plugin:flowtype/recommended", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "prettier/prettier": ["error", { 22 | "singleQuote": true, 23 | "trailingComma": "all" 24 | }], 25 | "no-constant-condition": ["error", { "checkLoops": false }], 26 | "no-console": ["warn"], 27 | "no-unused-vars": ["error", { "args": "none"}], 28 | "flowtype/no-types-missing-file-annotation": "warn" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | build/* 3 | dist/* 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | unsafe.enable_getters_and_setters=true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | 17 | build/ 18 | browser/scripts/src 19 | .DS_Store 20 | 21 | *.sublime-* 22 | 23 | *.map 24 | 25 | _site 26 | 27 | .tern-port 28 | .projectile 29 | .nyc_output 30 | coverage 31 | dist/ 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/test262-parser-tests"] 2 | path = test/parser/test262-parser-tests 3 | url = https://github.com/tc39/test262-parser-tests.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | script: npm run test:ci 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for contributing! So happy you are here! 4 | 5 | ## Pull Request Process 6 | 7 | 1. If you are adding new syntax or making changes to the API include appropriate 8 | updates to the [documentation](https://github.com/sweet-js/sweet.js/tree/master/doc/1.0). 9 | The documentation is written using [AsciiDoc](http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/) 10 | and generated via `npm run docs`. 11 | Only include changes to the `.adoc` files in the PR, not the generated `.html` files. 12 | 1. Do not include built source files generated via `npm run dist` in the PR. 13 | 1. Make sure you code passes linting (`npm run lint`). 14 | 15 | ## Contributor Covenant Code of Conduct 16 | 17 | ### Our Pledge 18 | 19 | In the interest of fostering an open and welcoming environment, we as 20 | contributors and maintainers pledge to making participation in our project and 21 | our community a harassment-free experience for everyone, regardless of age, body 22 | size, disability, ethnicity, gender identity and expression, level of experience, 23 | nationality, personal appearance, race, religion, or sexual identity and 24 | orientation. 25 | 26 | ### Our Standards 27 | 28 | Examples of behavior that contributes to creating a positive environment 29 | include: 30 | 31 | * Using welcoming and inclusive language 32 | * Being respectful of differing viewpoints and experiences 33 | * Gracefully accepting constructive criticism 34 | * Focusing on what is best for the community 35 | * Showing empathy towards other community members 36 | 37 | Examples of unacceptable behavior by participants include: 38 | 39 | * The use of sexualized language or imagery and unwelcome sexual attention or 40 | advances 41 | * Trolling, insulting/derogatory comments, and personal or political attacks 42 | * Public or private harassment 43 | * Publishing others' private information, such as a physical or electronic 44 | address, without explicit permission 45 | * Other conduct which could reasonably be considered inappropriate in a 46 | professional setting 47 | 48 | ### Our Responsibilities 49 | 50 | Project maintainers are responsible for clarifying the standards of acceptable 51 | behavior and are expected to take appropriate and fair corrective action in 52 | response to any instances of unacceptable behavior. 53 | 54 | Project maintainers have the right and responsibility to remove, edit, or 55 | reject comments, commits, code, wiki edits, issues, and other contributions 56 | that are not aligned to this Code of Conduct, or to ban temporarily or 57 | permanently any contributor for other behaviors that they deem inappropriate, 58 | threatening, offensive, or harmful. 59 | 60 | ### Scope 61 | 62 | This Code of Conduct applies both within project spaces and in public spaces 63 | when an individual is representing the project or its community. Examples of 64 | representing a project or community include using an official project e-mail 65 | address, posting via an official social media account, or acting as an appointed 66 | representative at an online or offline event. Representation of a project may be 67 | further defined and clarified by project maintainers. 68 | 69 | 70 | ### Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at [http://contributor-covenant.org/version/1/4][version] 74 | 75 | [homepage]: http://contributor-covenant.org 76 | [version]: http://contributor-covenant.org/version/1/4/ 77 | -------------------------------------------------------------------------------- /LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are met: 3 | 4 | * Redistributions of source code must retain the above copyright 5 | notice, this list of conditions and the following disclaimer. 6 | * Redistributions in binary form must reproduce the above copyright 7 | notice, this list of conditions and the following disclaimer in the 8 | documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 11 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 12 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 13 | ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 14 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 15 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 16 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 17 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 18 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 19 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sweet-js/sweet.js.svg)](https://travis-ci.org/sweet-js/sweet.js) 2 | 3 | [![Join the chat at https://gitter.im/mozilla/sweet.js](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mozilla/sweet.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Hygienic Macros for JavaScript! 6 | 7 | Macros allow *you* to build the language of your dreams. Sweeten JavaScript by defining new syntax for your code. 8 | 9 | Currently, Sweet should be considered experimental and under heavy development ([re-development](https://medium.com/@disnet/announcing-sweet-js-1-0-e7f4f3e15594#.fo9kyqu48) more like). As such, the API will be undergoing a bit of churn until probably the end of the year. So, probably best not to try Sweet in production systems just yet. If you're interested in helping out though we'd love to have you! 10 | 11 | # Getting started 12 | 13 | Install the command line app with npm: 14 | 15 | ```sh 16 | $ npm install -g @sweet-js/cli 17 | ``` 18 | 19 | Write your sweet code: 20 | 21 | ```js 22 | syntax hi = function (ctx) { 23 | return #`console.log('hello, world!')`; 24 | } 25 | hi 26 | ``` 27 | 28 | And compile: 29 | 30 | ```sh 31 | $ sjs my_sweet_code.js 32 | console.log('hello, world!') 33 | ``` 34 | 35 | # Learning More 36 | 37 | * Read the [tutorial](http://sweetjs.org/doc/1.0/tutorial.html). 38 | * Read the [reference documentation](http://sweetjs.org/doc/1.0/reference.html). 39 | * Play with the [editor](http://sweetjs.org/browser/editor.html). 40 | * Discuss on [Google Groups](https://groups.google.com/forum/#!forum/sweetjs). 41 | * Hang out on IRC: #sweet.js at irc.mozilla.org and on [Gitter](https://gitter.im/sweet-js/sweet.js). 42 | -------------------------------------------------------------------------------- /debug-test262.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | This file makes debugging sweet.js easier. Uses the built version of sweet.js 4 | to compile 'test.js'. You can use node-inspector to step through the expansion 5 | process: 6 | 7 | npm install -g node-inspector 8 | node-debug debug.js 9 | */ 10 | 11 | require('babel-register'); 12 | var compile = require('./src/sweet-loader.js').default; 13 | let fs = require('fs'); 14 | 15 | debugger; 16 | 17 | const PARSER_TEST_DIR = './test/test262-parser-tests'; 18 | 19 | let pass = fs.readdirSync(`${PARSER_TEST_DIR}/pass`); 20 | let fail = fs.readdirSync(`${PARSER_TEST_DIR}/fail`); 21 | 22 | // TODO: make these pass 23 | const passExcluded = [ 24 | '1012.script.js', 25 | '1051.module.js', 26 | '1052.module.js', 27 | '1053.module.js', 28 | '1054.module.js', 29 | '1055.module.js', 30 | '1056.module.js', 31 | '1057.module.js', 32 | '1058.module.js', 33 | '1059.module.js', 34 | '106.script.js', 35 | '1060.module.js', 36 | '1061.module.js', 37 | '1062.module.js', 38 | '1063.module.js', 39 | '1064.module.js', 40 | '1065.module.js', 41 | '1066.module.js', 42 | '1067.module.js', 43 | '1068.module.js', 44 | '1069.module.js', 45 | '1070.module.js', 46 | '1073.script.js', 47 | '1074.script.js', 48 | '1077.script.js', 49 | '1116.module.js', 50 | '1117.module.js', 51 | '1118.module.js', 52 | '1119.module.js', 53 | '1120.module.js', 54 | '1121.module.js', 55 | '1122.module.js', 56 | '1123.module.js', 57 | '1124.module.js', 58 | '1125.module.js', 59 | '1126.module.js', 60 | '1127.module.js', 61 | '1128.script.js', 62 | '1129.script.js', 63 | '1130.script.js', 64 | '1131.script.js', 65 | '1138.script.js', 66 | '1166.script.js', 67 | '117.script.js', 68 | '1202.script.js', 69 | '1239.script.js', 70 | '1240.script.js', 71 | '1245.script.js', 72 | '1246.script.js', 73 | '1247.script.js', 74 | '1248.script.js', 75 | '128.script.js', 76 | '1307.script.js', 77 | '1319.script.js', 78 | '1334.script.js', 79 | '1335.script.js', 80 | '1364.script.js', 81 | '1370.script.js', 82 | '140.script.js', 83 | '1427.script.js', 84 | '1428.script.js', 85 | '1429.script.js', 86 | '1430.script.js', 87 | '1431.script.js', 88 | '1432.script.js', 89 | '1434.script.js', 90 | '1467.script.js', 91 | '1623.script.js', 92 | '1638.script.js', 93 | '1686.module.js', 94 | '1687.module.js', 95 | '1688.module.js', 96 | '1689.module.js', 97 | '1690.module.js', 98 | '1691.module.js', 99 | '1692.module.js', 100 | '1693.module.js', 101 | '1694.module.js', 102 | '1695.module.js', 103 | '1698.module.js', 104 | '1699.module.js', 105 | '1700.module.js', 106 | '1701.module.js', 107 | '1736.script.js', 108 | '1739.script.js', 109 | '1745.script.js', 110 | '1779.script.js', 111 | '1789.script.js', 112 | '1844.script.js', 113 | '1954.script.js', 114 | '285.script.js', 115 | '290.script.js', 116 | '295.script.js', 117 | '296.script.js', 118 | '297.script.js', 119 | '301.script.js', 120 | '350.script.js', 121 | '37.script.js', 122 | '389.script.js', 123 | '391.script.js', 124 | '393.script.js', 125 | '397.module.js', 126 | '398.module.js', 127 | '400.module.js', 128 | '401.module.js', 129 | '402.module.js', 130 | '403.module.js', 131 | '404.module.js', 132 | '405.module.js', 133 | '406.module.js', 134 | '407.module.js', 135 | '408.module.js', 136 | '409.module.js', 137 | '411.module.js', 138 | '412.module.js', 139 | '413.module.js', 140 | '414.module.js', 141 | '415.module.js', 142 | '416.module.js', 143 | '417.module.js', 144 | '418.module.js', 145 | '419.module.js', 146 | '420.module.js', 147 | '516.script.js', 148 | '523.module.js', 149 | '533.script.js', 150 | '538.script.js', 151 | '546.module.js', 152 | '551.module.js', 153 | '572.script.js', 154 | '583.script.js', 155 | '608.script.js', 156 | '679.script.js', 157 | '680.script.js', 158 | '681.script.js', 159 | '84.script.js', 160 | '95.script.js', 161 | '993.script.js', 162 | '995.script.js', 163 | ] 164 | 165 | function mkTester(subdir) { 166 | function f(fname) { 167 | let result = compile(`${PARSER_TEST_DIR}/${subdir}/${fname}`).codegen() 168 | if (result == null) { 169 | throw new Error('un expected null result'); 170 | } 171 | } 172 | return f; 173 | } 174 | 175 | let passTest = mkTester('pass') 176 | 177 | pass.filter(f => !passExcluded.includes(f)).forEach(f => { 178 | console.log(f); 179 | passTest(f); 180 | }); 181 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | This file makes debugging sweet.js easier. Uses the built version of sweet.js 4 | to compile 'test.js'. You can use node-inspector to step through the expansion 5 | process: 6 | 7 | npm install -g node-inspector 8 | node-debug debug.js 9 | */ 10 | 11 | require('babel-register'); 12 | var compile = require('./src/sweet.js').compile; 13 | var NodeLoader = require('./src/node-loader').default; 14 | 15 | 16 | debugger; 17 | 18 | let result = compile('./test.js', new NodeLoader(__dirname)); 19 | console.log(result.code); 20 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | 'lang sweet.js'; 2 | 3 | var TypeCodes = { 4 | Identifier: 0, 5 | Keyword: 1, 6 | Punctuator: 2, 7 | NumericLiteral: 3, 8 | StringLiteral: 4, 9 | TemplateElement: 5, 10 | Template: 6, 11 | RegExp: 7, 12 | }; 13 | 14 | function check(obj, type) { 15 | return obj && obj.type === 'RawSyntax' && obj.value.token.typeCode === type; 16 | } 17 | 18 | export function unwrap(obj) { 19 | var hasTok = obj && obj.value && obj.value.token; 20 | if (hasTok && obj.value.token.typeCode === TypeCodes.StringLiteral) { 21 | return { 22 | value: obj.value.token.str, 23 | }; 24 | } else if (hasTok && obj.value.token.typeCode !== TypeCodes.Template) { 25 | return { 26 | value: obj.value.token.value, 27 | }; 28 | } else if (hasTok && obj.value.token.typeCode === TypeCodes.Template) { 29 | return { 30 | value: obj.value.token.items, 31 | }; 32 | } else if (obj && obj.type === 'RawDelimiter') { 33 | return { 34 | value: obj.inner, 35 | }; 36 | } 37 | return {}; 38 | } 39 | 40 | export function isIdentifier(obj) { 41 | return check(obj, TypeCodes.Identifier); 42 | } 43 | 44 | export function fromIdentifier(obj, x) { 45 | return obj.value.fromIdentifier(x); 46 | } 47 | 48 | export function isKeyword(obj) { 49 | return check(obj, TypeCodes.Keyword); 50 | } 51 | 52 | export function fromKeyword(obj, x) { 53 | return obj.value.fromKeyword(x); 54 | } 55 | 56 | export function isPunctuator(obj) { 57 | return check(obj, TypeCodes.Punctuator); 58 | } 59 | 60 | export function fromPunctuator(obj, x) { 61 | return obj.value.fromPunctuator(x); 62 | } 63 | 64 | export function isNumericLiteral(obj) { 65 | return check(obj, TypeCodes.NumericLiteral); 66 | } 67 | 68 | export function fromNumericLiteral(obj, x) { 69 | return obj.value.fromNumber(x); 70 | } 71 | 72 | export function isStringLiteral(obj) { 73 | return check(obj, TypeCodes.StringLiteral); 74 | } 75 | 76 | export function fromStringLiteral(obj, x) { 77 | return obj.value.fromString(x); 78 | } 79 | 80 | export function isTemplateElement(obj) { 81 | return check(obj, TypeCodes.TemplateElement); 82 | } 83 | 84 | export function isTemplate(obj) { 85 | return check(obj, TypeCodes.Template); 86 | } 87 | 88 | export function isRegExp(obj) { 89 | return check(obj, TypeCodes.RegExp); 90 | } 91 | 92 | export function isParens(obj) { 93 | return obj && obj.type === 'RawDelimiter' && obj.kind === 'parens'; 94 | } 95 | 96 | export function fromParens(obj, x) { 97 | return obj.value.from('parens', x); 98 | } 99 | 100 | export function isBrackets(obj) { 101 | return obj && obj.type === 'RawDelimiter' && obj.kind === 'brackets'; 102 | } 103 | 104 | export function fromBrackets(obj, x) { 105 | return obj.value.from('brackets', x); 106 | } 107 | 108 | export function isBraces(obj) { 109 | return obj && obj.type === 'RawDelimiter' && obj.kind === 'braces'; 110 | } 111 | 112 | export function fromBraces(obj, x) { 113 | return obj.value.from('braces', x); 114 | } 115 | 116 | export function isSyntaxTemplate(obj) { 117 | return obj && obj.type === 'RawDelimiter' && obj.kind === 'syntaxTemplate'; 118 | } 119 | -------------------------------------------------------------------------------- /mod.js: -------------------------------------------------------------------------------- 1 | #lang 'base'; 2 | export function id(x) { 3 | return x; 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sweet-js/core", 3 | "description": "Hygienic Macros for JavaScript", 4 | "main": "dist/sweet.js", 5 | "version": "3.0.13", 6 | "engines": { 7 | "node": ">=7.0.0" 8 | }, 9 | "author": "Tim Disney", 10 | "licenses": [ 11 | { 12 | "type": "BSD", 13 | "url": "http://github.com/sweet-js/sweet.js/master/LICENSE.BSD" 14 | } 15 | ], 16 | "scripts": { 17 | "clean": "rm -rf dist", 18 | "lint": "eslint src test && flow", 19 | "format": "eslint src test --fix", 20 | "prebuild": "mkdir -p dist/", 21 | "build:src": "babel --out-dir dist/ src --plugins transform-es2015-modules-commonjs", 22 | "build": "npm run build:src", 23 | "preprofile": "npm run build", 24 | "profile": "node --prof profile.js && node --prof-process *v8.log > v8-processed.log && rm *v8.log", 25 | "pretest": "npm run lint", 26 | "test:262": "ava test/parser/test-run-test262.js", 27 | "test:ci": "npm run pretest && ava && ava test/parser/test-*.js", 28 | "test": "ava", 29 | "report": "nyc ava && nyc report--reporter=html", 30 | "prepublish": "npm run build" 31 | }, 32 | "files": [ 33 | "dist", 34 | "helpers.js" 35 | ], 36 | "directories": { 37 | "test": "test" 38 | }, 39 | "dependencies": { 40 | "babel-core": "^6.18.0", 41 | "immutable": "^3.8.1", 42 | "ramda": "^0.22.0", 43 | "ramda-fantasy": "^0.6.0", 44 | "readtable": "^0.0.1", 45 | "resolve": "^1.1.7", 46 | "semver": "^5.3.0", 47 | "shift-codegen": "^5.0.4", 48 | "shift-reducer": "^4.0.1", 49 | "sweet-spec": "4.0.0", 50 | "transit-js": "^0.8.846", 51 | "utils-dirname": "^1.0.0" 52 | }, 53 | "devDependencies": { 54 | "angular": "1.6.0", 55 | "ava": "^0.19.1", 56 | "babel-cli": "^6.18.0", 57 | "prettier-eslint": "^5.1.0", 58 | "babel-eslint": "^7.0.0", 59 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", 60 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 61 | "babel-preset-node7": "1.4.0", 62 | "babel-register": "6.18.0", 63 | "eslint": "^3.7.1", 64 | "eslint-config-prettier": "^2.3.0", 65 | "eslint-plugin-flowtype": "^2.35.0", 66 | "eslint-plugin-prettier": "^2.1.2", 67 | "event-stream": "^3.3.2", 68 | "expect.js": "0.3.x", 69 | "flow-bin": "^0.45.0", 70 | "jquery": "3.1.1", 71 | "nyc": "^6.0.0", 72 | "prettier": "^1.5.3", 73 | "rimraf": "^2.6.1", 74 | "source-map": "~0.5.3", 75 | "source-map-support": "^0.4.0", 76 | "webpack": "^1.13.1" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "git://github.com/sweet-js/sweet.js.git" 81 | }, 82 | "keywords": [ 83 | "macros", 84 | "javascript" 85 | ], 86 | "license": "BSD-2-Clause", 87 | "ava": { 88 | "failWithoutAssertions": false, 89 | "babel": "inherit", 90 | "files": [ 91 | "test/unit/*.js", 92 | "test/parser/test-ast.js" 93 | ], 94 | "require": [ 95 | "babel-register" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /printAst.js: -------------------------------------------------------------------------------- 1 | var parse = require("./build/src/sweet.js").parse; 2 | var readFile = require("fs").readFileSync; 3 | var moduleResolver = require('./build/src/node-module-resolver').default; 4 | var moduleLoader = require('./build/src/node-module-loader').default; 5 | var transform = require('babel-core').transform; 6 | 7 | console.log(JSON.stringify(parse(readFile("test.js", "utf8"), { 8 | cwd: __dirname, 9 | transform: transform, 10 | filename: './test.js', 11 | moduleResolver: moduleResolver, 12 | moduleLoader: moduleLoader 13 | }), null, 2)); 14 | -------------------------------------------------------------------------------- /profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Shape Security, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | var fs = require('fs'); 20 | var path = require('path'); 21 | var parse = require('./build/src/sweet').parse; 22 | var NodeLoader = require('./build/src/node-loader').default; 23 | 24 | function benchmarkParsing(fileName) { 25 | var loader = new NodeLoader(path.dirname(fs.realpathSync(__filename))); 26 | var start = Date.now(), N = 100; 27 | for (var i = 0; i < N; i++) { 28 | parse(fileName, loader); 29 | } 30 | var time = Date.now() - start; 31 | console.log((time / N).toFixed(2) + "ms"); 32 | } 33 | 34 | benchmarkParsing('./node_modules/angular/angular.js'); 35 | -------------------------------------------------------------------------------- /src/ast-dispatcher.js: -------------------------------------------------------------------------------- 1 | export default class ASTDispatcher { 2 | constructor(prefix, errorIfMissing) { 3 | this.errorIfMissing = errorIfMissing; 4 | this.prefix = prefix; 5 | } 6 | 7 | dispatch(term) { 8 | let field = this.prefix + term.type; 9 | if (typeof this[field] === 'function') { 10 | return this[field](term); 11 | } else if (!this.errorIfMissing) { 12 | return term; 13 | } 14 | throw new Error(`Missing implementation for: ${field}`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/binding-map.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { List } from 'immutable'; 3 | import { assert } from './errors'; 4 | import { Maybe } from 'ramda-fantasy'; 5 | import type { SymbolClass } from './symbol'; 6 | import Syntax from './syntax'; 7 | 8 | type Scopeset = any; 9 | 10 | type ScopesetBinding = { 11 | scopes: Scopeset, 12 | binding: SymbolClass, 13 | alias: Maybe, 14 | }; 15 | 16 | export default class BindingMap { 17 | _map: Map>; 18 | 19 | constructor() { 20 | this._map = new Map(); 21 | } 22 | 23 | // given a syntax object and a binding, 24 | // add the binding to the map associating the binding with the syntax object's 25 | // scope set 26 | add( 27 | stx: Syntax, 28 | { 29 | binding, 30 | phase, 31 | skipDup = false, 32 | }: { binding: SymbolClass, phase: number | {}, skipDup: boolean }, 33 | ) { 34 | let stxName = stx.val(); 35 | let allScopeset = stx.scopesets.all; 36 | let scopeset = stx.scopesets.phase.has(phase) 37 | ? stx.scopesets.phase.get(phase) 38 | : List(); 39 | scopeset = allScopeset.concat(scopeset); 40 | assert(phase != null, 'must provide a phase for binding add'); 41 | 42 | let scopesetBindingList = this._map.get(stxName); 43 | if (scopesetBindingList) { 44 | if (skipDup && scopesetBindingList.some(s => s.scopes.equals(scopeset))) { 45 | return; 46 | } 47 | this._map.set( 48 | stxName, 49 | scopesetBindingList.push({ 50 | scopes: scopeset, 51 | binding: binding, 52 | alias: Maybe.Nothing(), 53 | }), 54 | ); 55 | } else { 56 | this._map.set( 57 | stxName, 58 | List.of({ 59 | scopes: scopeset, 60 | binding: binding, 61 | alias: Maybe.Nothing(), 62 | }), 63 | ); 64 | } 65 | } 66 | 67 | addForward( 68 | stx: Syntax, 69 | forwardStx: Syntax, 70 | binding: SymbolClass, 71 | phase: number | {}, 72 | ) { 73 | let stxName = stx.token.value; 74 | let allScopeset = stx.scopesets.all; 75 | let scopeset = stx.scopesets.phase.has(phase) 76 | ? stx.scopesets.phase.get(phase) 77 | : List(); 78 | scopeset = allScopeset.concat(scopeset); 79 | assert(phase != null, 'must provide a phase for binding add'); 80 | 81 | let scopesetBindingList = this._map.get(stxName); 82 | if (scopesetBindingList) { 83 | this._map.set( 84 | stxName, 85 | scopesetBindingList.push({ 86 | scopes: scopeset, 87 | binding: binding, 88 | alias: Maybe.of(forwardStx), 89 | }), 90 | ); 91 | } else { 92 | this._map.set( 93 | stxName, 94 | List.of({ 95 | scopes: scopeset, 96 | binding: binding, 97 | alias: Maybe.of(forwardStx), 98 | }), 99 | ); 100 | } 101 | } 102 | 103 | get(stx: Syntax) { 104 | return this._map.get(stx.token.value); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/browser-sweet.js: -------------------------------------------------------------------------------- 1 | import { compile as sweetCompile } from './sweet'; 2 | import StoreLoader from './store-loader'; 3 | import Store from './store'; 4 | 5 | class BrowserStoreLoader extends StoreLoader { 6 | store: Map; 7 | 8 | constructor(baseDir: string, store: Map) { 9 | super(baseDir, store, true); 10 | } 11 | 12 | fetch({ name, address }: { name: string, address: any }) { 13 | if (this.store.has(address.path)) { 14 | return this.store.get(address.path); 15 | } 16 | throw new Error( 17 | `The module ${name} is not in the debug store: addr.path is ${address.path}`, 18 | ); 19 | } 20 | 21 | freshStore() { 22 | return new Store({}); 23 | } 24 | 25 | eval(source: string, store: Store) { 26 | return (0, eval)(source); 27 | } 28 | } 29 | 30 | export function compile(source, helpers) { 31 | let s = new Map(); 32 | s.set('main.js', source); 33 | s.set('sweet.js/helpers.js', helpers); 34 | s.set('sweet.js/helpers', helpers); 35 | let loader = new BrowserStoreLoader('.', s); 36 | return sweetCompile('main.js', loader); 37 | } 38 | -------------------------------------------------------------------------------- /src/codegen.js: -------------------------------------------------------------------------------- 1 | import shiftCodegen, { FormattedCodeGen } from 'shift-codegen'; 2 | 3 | export default function codegen(modTerm) { 4 | return { 5 | code: shiftCodegen(modTerm, new FormattedCodeGen()), 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/compiler.js: -------------------------------------------------------------------------------- 1 | import TermExpander from './term-expander.js'; 2 | import TokenExpander from './token-expander'; 3 | import * as _ from 'ramda'; 4 | import Multimap from './multimap'; 5 | 6 | export default class Compiler { 7 | constructor(phase, env, store, context) { 8 | this.phase = phase; 9 | this.env = env; 10 | this.store = store; 11 | this.invokedRegistry = new Multimap(); 12 | this.context = context; 13 | } 14 | 15 | compile(stxl) { 16 | let tokenExpander = new TokenExpander( 17 | _.merge(this.context, { 18 | phase: this.phase, 19 | env: this.env, 20 | store: this.store, 21 | invokedRegistry: this.invokedRegistry, 22 | }), 23 | ); 24 | let termExpander = new TermExpander( 25 | _.merge(this.context, { 26 | phase: this.phase, 27 | env: this.env, 28 | store: this.store, 29 | invokedRegistry: this.invokedRegistry, 30 | }), 31 | ); 32 | 33 | return tokenExpander.expand(stxl).map(t => termExpander.expand(t)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | import { 2 | FunctionDeclTransform, 3 | VariableDeclTransform, 4 | LetDeclTransform, 5 | ConstDeclTransform, 6 | SyntaxDeclTransform, 7 | SyntaxrecDeclTransform, 8 | OperatorDeclTransform, 9 | ReturnStatementTransform, 10 | IfTransform, 11 | ForTransform, 12 | SwitchTransform, 13 | BreakTransform, 14 | ContinueTransform, 15 | DoTransform, 16 | DebuggerTransform, 17 | WithTransform, 18 | ImportTransform, 19 | ExportTransform, 20 | SuperTransform, 21 | ThisTransform, 22 | YieldTransform, 23 | ClassTransform, 24 | DefaultTransform, 25 | TryTransform, 26 | ThrowTransform, 27 | NewTransform, 28 | WhileTransform, 29 | AsyncTransform, 30 | AwaitTransform, 31 | } from './transforms'; 32 | 33 | export default class Env { 34 | constructor() { 35 | this.map = new Map(); 36 | this.map.set('function', FunctionDeclTransform); 37 | this.map.set('var', VariableDeclTransform); 38 | this.map.set('let', LetDeclTransform); 39 | this.map.set('const', ConstDeclTransform); 40 | this.map.set('syntaxrec', SyntaxrecDeclTransform); 41 | this.map.set('syntax', SyntaxDeclTransform); 42 | this.map.set('operator', OperatorDeclTransform); 43 | this.map.set('return', ReturnStatementTransform); 44 | this.map.set('while', WhileTransform); 45 | this.map.set('if', IfTransform); 46 | this.map.set('for', ForTransform); 47 | this.map.set('switch', SwitchTransform); 48 | this.map.set('break', BreakTransform); 49 | this.map.set('continue', ContinueTransform); 50 | this.map.set('do', DoTransform); 51 | this.map.set('debugger', DebuggerTransform); 52 | this.map.set('with', WithTransform); 53 | this.map.set('import', ImportTransform); 54 | this.map.set('export', ExportTransform); 55 | this.map.set('super', SuperTransform); 56 | this.map.set('this', ThisTransform); 57 | this.map.set('class', ClassTransform); 58 | this.map.set('default', DefaultTransform); 59 | this.map.set('try', TryTransform); 60 | this.map.set('yield', YieldTransform); 61 | this.map.set('throw', ThrowTransform); 62 | this.map.set('new', NewTransform); 63 | this.map.set('async', AsyncTransform); 64 | this.map.set('await', AwaitTransform); 65 | } 66 | 67 | has(key) { 68 | return this.map.has(key); 69 | } 70 | 71 | get(key) { 72 | return this.map.get(key); 73 | } 74 | 75 | set(key, val) { 76 | return this.map.set(key, val); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export function expect(cond, message, offendingSyntax, rest) { 2 | if (!cond) { 3 | let ctx = ''; 4 | if (rest) { 5 | ctx = rest 6 | .slice(0, 20) 7 | .map(s => { 8 | let val = s.isDelimiter() ? '( ... )' : s.val(); 9 | if (s === offendingSyntax) { 10 | return '__' + val + '__'; 11 | } 12 | return val; 13 | }) 14 | .join(' '); 15 | } 16 | throw new Error('[error]: ' + message + '\n' + ctx); 17 | } 18 | } 19 | 20 | export function assert(cond, message) { 21 | if (!cond) { 22 | throw new Error('[assertion error]: ' + message); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/hygiene-utils.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | import ASTDispatcher from './ast-dispatcher'; 4 | 5 | export class CollectBindingSyntax extends ASTDispatcher { 6 | constructor() { 7 | super('collect', true); 8 | this.names = List(); 9 | } 10 | 11 | // registerSyntax(stx) { 12 | // let newBinding = gensym(stx.val()); 13 | // this.context.bindings.add(stx, { 14 | // binding: newBinding, 15 | // phase: this.context.phase, 16 | // // skip dup because js allows variable redeclarations 17 | // // (technically only for `var` but we can let later stages of the pipeline 18 | // // handle incorrect redeclarations of `const` and `let`) 19 | // skipDup: true 20 | // }); 21 | // return stx; 22 | // } 23 | 24 | collect(term) { 25 | return this.dispatch(term); 26 | } 27 | 28 | collectBindingIdentifier(term) { 29 | return this.names.concat(term.name); 30 | } 31 | 32 | collectBindingPropertyIdentifier(term) { 33 | return this.collect(term.binding); 34 | } 35 | 36 | collectBindingPropertyProperty(term) { 37 | return this.collect(term.binding); 38 | } 39 | 40 | collectArrayBinding(term) { 41 | let rest = null; 42 | if (term.rest != null) { 43 | rest = this.collect(term.rest); 44 | } 45 | return this.names 46 | .concat(rest) 47 | .concat( 48 | term.elements.filter(el => el != null).flatMap(el => this.collect(el)), 49 | ); 50 | } 51 | 52 | collectObjectBinding() { 53 | // return term.properties.flatMap(prop => this.collect(prop)); 54 | return List(); 55 | } 56 | 57 | // registerVariableDeclaration(term) { 58 | // let declarators = term.declarators.map(decl => { 59 | // return decl.extend({ 60 | // binding: this.register(decl.binding) 61 | // }); 62 | // }); 63 | // return term.extend({ declarators }); 64 | // } 65 | // 66 | // registerFunctionDeclaration(term) { 67 | // return term.extend({ 68 | // name: this.register(term.name) 69 | // }); 70 | // } 71 | // 72 | // registerExport(term) { 73 | // return term.extend({ 74 | // declaration: this.register(term.declaration) 75 | // }); 76 | // } 77 | } 78 | 79 | export function collectBindings(term) { 80 | return new CollectBindingSyntax().collect(term); 81 | } 82 | -------------------------------------------------------------------------------- /src/load-syntax.js: -------------------------------------------------------------------------------- 1 | import * as S from 'sweet-spec'; 2 | import * as _ from 'ramda'; 3 | import { List } from 'immutable'; 4 | import Syntax from './syntax'; 5 | import codegen, { FormattedCodeGen } from 'shift-codegen'; 6 | import SweetToShiftReducer from './sweet-to-shift-reducer'; 7 | import TermExpander from './term-expander'; 8 | import Env from './env'; 9 | 10 | import { replaceTemplate } from './template-processor'; 11 | 12 | export function expandCompiletime(term, context) { 13 | // each compiletime value needs to be expanded with a fresh 14 | // environment and in the next higher phase 15 | let syntaxExpander = new TermExpander( 16 | _.merge(context, { 17 | phase: context.phase + 1, 18 | env: new Env(), 19 | store: context.store, 20 | }), 21 | ); 22 | 23 | return syntaxExpander.expand(term); 24 | } 25 | 26 | export function sanitizeReplacementValues(values) { 27 | if (Array.isArray(values)) { 28 | return sanitizeReplacementValues(List(values)); 29 | } else if (List.isList(values)) { 30 | return values.map(sanitizeReplacementValues); 31 | } else if (values == null) { 32 | throw new Error( 33 | 'replacement values for syntax template must not be null or undefined', 34 | ); 35 | } else if (typeof values.next === 'function') { 36 | return sanitizeReplacementValues(List(values)); 37 | } 38 | return values; 39 | } 40 | 41 | // (Expression, Context) -> [function] 42 | export function evalCompiletimeValue(expr: S.Expression, context: any) { 43 | let sandbox = { 44 | syntaxTemplate: function(ident, ...values) { 45 | return replaceTemplate( 46 | context.templateMap.get(ident), 47 | sanitizeReplacementValues(values), 48 | ); 49 | }, 50 | }; 51 | 52 | let sandboxKeys = List(Object.keys(sandbox)); 53 | let sandboxVals = sandboxKeys.map(k => sandbox[k]).toArray(); 54 | 55 | let parsed = new S.Module({ 56 | directives: List(), 57 | items: List.of( 58 | new S.ExpressionStatement({ 59 | expression: new S.FunctionExpression({ 60 | isAsync: false, 61 | isGenerator: false, 62 | name: null, 63 | params: new S.FormalParameters({ 64 | items: sandboxKeys.map(param => { 65 | return new S.BindingIdentifier({ 66 | name: Syntax.from('identifier', param), 67 | }); 68 | }), 69 | rest: null, 70 | }), 71 | body: new S.FunctionBody({ 72 | directives: List.of( 73 | new S.Directive({ 74 | rawValue: 'use strict', 75 | }), 76 | ), 77 | statements: List.of( 78 | new S.ReturnStatement({ 79 | expression: expr, 80 | }), 81 | ), 82 | }), 83 | }), 84 | }), 85 | ), 86 | }).reduce(new SweetToShiftReducer(context.phase)); 87 | 88 | let gen = codegen(parsed, new FormattedCodeGen()); 89 | let result = context.transform(gen); 90 | 91 | let val = context.loader.eval(result.code, context.store); 92 | return val.apply(undefined, sandboxVals); 93 | } 94 | -------------------------------------------------------------------------------- /src/macro-context.js: -------------------------------------------------------------------------------- 1 | import { expect } from './errors'; 2 | import { List } from 'immutable'; 3 | import { Enforester } from './enforester'; 4 | import { ALL_PHASES } from './syntax'; 5 | import * as _ from 'ramda'; 6 | import ScopeReducer from './scope-reducer'; 7 | import * as T from 'sweet-spec'; 8 | import Term, * as S from 'sweet-spec'; 9 | import Syntax from './syntax'; 10 | import { isTemplate, isDelimiter, getKind } from './tokens'; 11 | import type { TokenTree } from './tokens'; 12 | 13 | export function wrapInTerms(stx: List): List { 14 | return stx.map(s => { 15 | if (isTemplate(s)) { 16 | if (s.items) { 17 | s.items = wrapInTerms(s.items); 18 | return new T.RawSyntax({ 19 | value: new Syntax(s), 20 | }); 21 | } 22 | return new T.RawSyntax({ 23 | value: new Syntax(s), 24 | }); 25 | } else if (isDelimiter(s)) { 26 | return new S.RawDelimiter({ 27 | kind: getKind(s), 28 | inner: wrapInTerms(s), 29 | }); 30 | } 31 | return new S.RawSyntax({ 32 | value: new Syntax(s), 33 | }); 34 | }); 35 | } 36 | 37 | const privateData = new WeakMap(); 38 | 39 | function cloneEnforester(enf) { 40 | const { rest, prev, context } = enf; 41 | return new Enforester(rest, prev, context); 42 | } 43 | 44 | function Marker() {} 45 | 46 | /* 47 | ctx :: { 48 | of: (Syntax) -> ctx 49 | next: (String) -> Syntax or Term 50 | } 51 | */ 52 | export default class MacroContext { 53 | constructor(enf, name, context, useScope, introducedScope) { 54 | const startMarker = new Marker(); 55 | const startEnf = cloneEnforester(enf); 56 | const priv = { 57 | name, 58 | context, 59 | enf: startEnf, 60 | startMarker, 61 | markers: new Map([[startMarker, enf]]), 62 | }; 63 | 64 | if (useScope && introducedScope) { 65 | priv.noScopes = false; 66 | priv.useScope = useScope; 67 | priv.introducedScope = introducedScope; 68 | } else { 69 | priv.noScopes = true; 70 | } 71 | privateData.set(this, priv); 72 | this.reset(); // set current enforester 73 | 74 | this[Symbol.iterator] = () => this; 75 | } 76 | 77 | name() { 78 | const { name } = privateData.get(this); 79 | return new T.RawSyntax({ value: name }); 80 | } 81 | 82 | contextify(delim: any) { 83 | if (!(delim instanceof T.RawDelimiter)) { 84 | throw new Error(`Can only contextify a delimiter but got ${delim}`); 85 | } 86 | const { context } = privateData.get(this); 87 | 88 | let enf = new Enforester( 89 | delim.inner.slice(1, delim.inner.size - 1), 90 | List(), 91 | context, 92 | ); 93 | return new MacroContext(enf, 'inner', context); 94 | } 95 | 96 | expand(type) { 97 | const { enf } = privateData.get(this); 98 | if (enf.rest.size === 0) { 99 | return { 100 | done: true, 101 | value: null, 102 | }; 103 | } 104 | enf.expandMacro(); 105 | let originalRest = enf.rest; 106 | let value; 107 | switch (type) { 108 | case 'AssignmentExpression': 109 | case 'expr': 110 | value = enf.enforestExpressionLoop(); 111 | break; 112 | case 'Expression': 113 | value = enf.enforestExpression(); 114 | break; 115 | case 'Statement': 116 | case 'stmt': 117 | value = enf.enforestStatement(); 118 | break; 119 | case 'BlockStatement': 120 | case 'WhileStatement': 121 | case 'IfStatement': 122 | case 'ForStatement': 123 | case 'SwitchStatement': 124 | case 'BreakStatement': 125 | case 'ContinueStatement': 126 | case 'DebuggerStatement': 127 | case 'WithStatement': 128 | case 'TryStatement': 129 | case 'ThrowStatement': 130 | case 'ClassDeclaration': 131 | case 'FunctionDeclaration': 132 | case 'LabeledStatement': 133 | case 'VariableDeclarationStatement': 134 | case 'ReturnStatement': 135 | case 'ExpressionStatement': 136 | value = enf.enforestStatement(); 137 | expect( 138 | _.whereEq({ type }, value), 139 | `Expecting a ${type}`, 140 | value, 141 | originalRest, 142 | ); 143 | break; 144 | case 'YieldExpression': 145 | value = enf.enforestYieldExpression(); 146 | break; 147 | case 'ClassExpression': 148 | value = enf.enforestClass({ isExpr: true }); 149 | break; 150 | case 'ArrowExpression': 151 | value = enf.enforestArrowExpression(); 152 | break; 153 | case 'NewExpression': 154 | value = enf.enforestNewExpression(); 155 | break; 156 | case 'ThisExpression': 157 | case 'FunctionExpression': 158 | case 'IdentifierExpression': 159 | case 'LiteralNumericExpression': 160 | case 'LiteralInfinityExpression': 161 | case 'LiteralStringExpression': 162 | case 'TemplateExpression': 163 | case 'LiteralBooleanExpression': 164 | case 'LiteralNullExpression': 165 | case 'LiteralRegExpExpression': 166 | case 'ObjectExpression': 167 | case 'ArrayExpression': 168 | value = enf.enforestPrimaryExpression(); 169 | break; 170 | case 'UnaryExpression': 171 | case 'UpdateExpression': 172 | case 'BinaryExpression': 173 | case 'StaticMemberExpression': 174 | case 'ComputedMemberExpression': 175 | case 'CompoundAssignmentExpression': 176 | case 'ConditionalExpression': 177 | value = enf.enforestExpressionLoop(); 178 | expect( 179 | _.whereEq({ type }, value), 180 | `Expecting a ${type}`, 181 | value, 182 | originalRest, 183 | ); 184 | break; 185 | default: 186 | throw new Error('Unknown term type: ' + type); 187 | } 188 | return { 189 | done: false, 190 | value: value, 191 | }; 192 | } 193 | 194 | _rest(enf) { 195 | const priv = privateData.get(this); 196 | if (priv.markers.get(priv.startMarker) === enf) { 197 | return priv.enf.rest; 198 | } 199 | throw Error('Unauthorized access!'); 200 | } 201 | 202 | reset(marker) { 203 | const priv = privateData.get(this); 204 | let enf; 205 | if (marker == null) { 206 | // go to the beginning 207 | enf = priv.markers.get(priv.startMarker); 208 | } else if (marker && marker instanceof Marker) { 209 | // marker could be from another context 210 | if (priv.markers.has(marker)) { 211 | enf = priv.markers.get(marker); 212 | } else { 213 | throw new Error('marker must originate from this context'); 214 | } 215 | } else { 216 | throw new Error('marker must be an instance of Marker'); 217 | } 218 | priv.enf = cloneEnforester(enf); 219 | } 220 | 221 | mark() { 222 | const priv = privateData.get(this); 223 | let marker; 224 | 225 | // the idea here is that marking at the beginning shouldn't happen more than once. 226 | // We can reuse startMarker. 227 | if (priv.enf.rest === priv.markers.get(priv.startMarker).rest) { 228 | marker = priv.startMarker; 229 | } else if (priv.enf.rest.isEmpty()) { 230 | // same reason as above 231 | if (!priv.endMarker) priv.endMarker = new Marker(); 232 | marker = priv.endMarker; 233 | } else { 234 | //TODO(optimization/dubious): check that there isn't already a marker for this index? 235 | marker = new Marker(); 236 | } 237 | if (!priv.markers.has(marker)) { 238 | priv.markers.set(marker, cloneEnforester(priv.enf)); 239 | } 240 | return marker; 241 | } 242 | 243 | next() { 244 | const { 245 | enf, 246 | noScopes, 247 | useScope, 248 | introducedScope, 249 | context, 250 | } = privateData.get(this); 251 | if (enf.rest.size === 0) { 252 | return { 253 | done: true, 254 | value: null, 255 | }; 256 | } 257 | let value = enf.advance(); 258 | if (!noScopes) { 259 | value = value.reduce( 260 | new ScopeReducer( 261 | [ 262 | { scope: useScope, phase: ALL_PHASES, flip: false }, 263 | { scope: introducedScope, phase: ALL_PHASES, flip: true }, 264 | ], 265 | context.bindings, 266 | ), 267 | ); 268 | } 269 | return { 270 | done: false, 271 | value: value, 272 | }; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/module-visitor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { evalCompiletimeValue } from './load-syntax'; 3 | import * as _ from 'ramda'; 4 | import * as T from 'sweet-spec'; 5 | import * as S from './sweet-spec-utils'; 6 | import { gensym } from './symbol'; 7 | import { ModuleNamespaceTransform, CompiletimeTransform } from './transforms'; 8 | import { collectBindings } from './hygiene-utils'; 9 | import SweetModule from './sweet-module'; 10 | import { List } from 'immutable'; 11 | import SweetToShiftReducer from './sweet-to-shift-reducer'; 12 | import codegen, { FormattedCodeGen } from 'shift-codegen'; 13 | import Syntax from './syntax'; 14 | 15 | import type { Context } from './sweet-loader'; 16 | 17 | export function isBoundToCompiletime(name: Syntax, store: Map<*, *>) { 18 | let resolvedName = name.resolve(0); 19 | if (store.has(resolvedName)) { 20 | return store.get(resolvedName) instanceof CompiletimeTransform; 21 | } 22 | return false; 23 | } 24 | 25 | export function bindImports( 26 | impTerm: any, 27 | exModule: SweetModule, 28 | phase: any, 29 | context: Context, 30 | isEntrypoint: boolean, 31 | ) { 32 | let names = []; 33 | let phaseToBind = impTerm.forSyntax ? phase + 1 : phase; 34 | if (impTerm.defaultBinding != null && impTerm instanceof T.Import) { 35 | let name = impTerm.defaultBinding.name; 36 | let exportName = exModule.exportedNames.find( 37 | exName => exName.exportedName.val() === '_default', 38 | ); 39 | if (exportName != null) { 40 | let newBinding = gensym('_default'); 41 | let toForward = exportName.exportedName; 42 | 43 | if ( 44 | !isEntrypoint || 45 | isBoundToCompiletime(toForward, context.store) || 46 | impTerm.forSyntax 47 | ) { 48 | context.bindings.addForward(name, toForward, newBinding, phaseToBind); 49 | } 50 | names.push(name); 51 | } 52 | } 53 | if (impTerm.namedImports) { 54 | impTerm.namedImports.forEach(specifier => { 55 | let name = specifier.binding.name; 56 | let exportName = exModule.exportedNames.find(exName => { 57 | if (exName.exportedName != null) { 58 | return exName.exportedName.val() === name.val(); 59 | } 60 | return exName.name && exName.name.val() === name.val(); 61 | }); 62 | if (exportName != null) { 63 | let newBinding = gensym(name.val()); 64 | let toForward = exportName.name 65 | ? exportName.name 66 | : exportName.exportedName; 67 | if ( 68 | !isEntrypoint || 69 | isBoundToCompiletime(toForward, context.store) || 70 | impTerm.forSyntax 71 | ) { 72 | context.bindings.addForward(name, toForward, newBinding, phaseToBind); 73 | } 74 | names.push(name); 75 | } 76 | }); 77 | } 78 | if (impTerm.namespaceBinding) { 79 | let name = impTerm.namespaceBinding.name; 80 | let newBinding = gensym(name.val()); 81 | context.store.set( 82 | newBinding.toString(), 83 | new ModuleNamespaceTransform(name, exModule), 84 | ); 85 | context.bindings.add(name, { 86 | binding: newBinding, 87 | phase: phaseToBind, 88 | skipDup: false, 89 | }); 90 | 91 | names.push(name); 92 | } 93 | return List(names); 94 | } 95 | 96 | export default class { 97 | context: Context; 98 | 99 | constructor(context: Context) { 100 | this.context = context; 101 | } 102 | 103 | visit(mod: SweetModule, phase: any, store: any, cwd: string) { 104 | mod.imports.forEach(imp => { 105 | if (imp.forSyntax) { 106 | let mod = this.context.loader.get( 107 | imp.moduleSpecifier.val(), 108 | phase + 1, 109 | cwd, 110 | ); 111 | this.visit(mod, phase + 1, store, mod.path); 112 | this.invoke(mod, phase + 1, store, mod.path); 113 | } else { 114 | let mod = this.context.loader.get( 115 | imp.moduleSpecifier.val(), 116 | phase, 117 | cwd, 118 | ); 119 | this.visit(mod, phase, store, mod.path); 120 | } 121 | bindImports(imp, mod, phase, this.context, false); 122 | }); 123 | for (let term of mod.compiletimeItems()) { 124 | if (S.isSyntaxDeclarationStatement(term)) { 125 | this.registerSyntaxDeclaration((term: any).declaration, phase, store); 126 | } 127 | } 128 | return store; 129 | } 130 | 131 | invoke(mod: any, phase: any, store: any, cwd: string) { 132 | if (this.context.invokedRegistry.containsAt(mod.path, phase)) { 133 | return store; 134 | } 135 | mod.imports.forEach(imp => { 136 | if (!imp.forSyntax) { 137 | let mod = this.context.loader.get( 138 | imp.moduleSpecifier.val(), 139 | phase, 140 | cwd, 141 | ); 142 | this.invoke(mod, phase, store, mod.path); 143 | bindImports(imp, mod, phase, this.context, false); 144 | } 145 | }); 146 | let items = mod.runtimeItems(); 147 | for (let term of items) { 148 | if (S.isVariableDeclarationStatement(term)) { 149 | this.registerVariableDeclaration(term.declaration, phase, store); 150 | } else if (S.isFunctionDeclaration(term)) { 151 | this.registerFunctionOrClass(term, phase, store); 152 | } 153 | } 154 | let parsed = new T.Module({ 155 | directives: List(), 156 | items, 157 | // $FlowFixMe: flow doesn't know about reduce yet 158 | }).reduce(new SweetToShiftReducer(phase)); 159 | 160 | let gen = codegen(parsed, new FormattedCodeGen()); 161 | let result = this.context.transform(gen); 162 | 163 | this.context.loader.eval(result.code, store); 164 | this.context.invokedRegistry.add(mod.path, phase); 165 | return store; 166 | } 167 | 168 | registerSyntaxDeclaration(term: any, phase: any, store: any) { 169 | term.declarators.forEach(decl => { 170 | let val = evalCompiletimeValue( 171 | decl.init, 172 | _.merge(this.context, { 173 | phase: phase + 1, 174 | store, 175 | }), 176 | ); 177 | 178 | collectBindings(decl.binding).forEach(stx => { 179 | if (phase !== 0) { 180 | // phase 0 bindings extend the binding map during compilation 181 | let newBinding = gensym(stx.val()); 182 | this.context.bindings.add(stx, { 183 | binding: newBinding, 184 | phase: phase, 185 | skipDup: false, 186 | }); 187 | } 188 | let resolvedName = stx.resolve(phase); 189 | let compiletimeType = term.kind === 'operator' ? 'operator' : 'syntax'; 190 | store.set( 191 | resolvedName, 192 | new CompiletimeTransform({ 193 | type: compiletimeType, 194 | prec: decl.prec == null ? void 0 : decl.prec.val(), 195 | assoc: decl.assoc == null ? void 0 : decl.assoc.val(), 196 | f: val, 197 | }), 198 | ); 199 | }); 200 | }); 201 | } 202 | 203 | registerVariableDeclaration(term: any, phase: any, store: any) { 204 | term.declarators.forEach(decl => { 205 | collectBindings(decl.binding).forEach(stx => { 206 | if (phase !== 0) { 207 | // phase 0 bindings extend the binding map during compilation 208 | let newBinding = gensym(stx.val()); 209 | this.context.bindings.add(stx, { 210 | binding: newBinding, 211 | phase: phase, 212 | skipDup: term.kind === 'var', 213 | }); 214 | } 215 | }); 216 | }); 217 | } 218 | 219 | registerFunctionOrClass(term: any, phase: any, store: any) { 220 | collectBindings(term.name).forEach(stx => { 221 | if (phase !== 0) { 222 | let newBinding = gensym(stx.val()); 223 | this.context.bindings.add(stx, { 224 | binding: newBinding, 225 | phase: phase, 226 | skipDup: false, 227 | }); 228 | } 229 | }); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/multimap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { List } from 'immutable'; 3 | 4 | export default class Multimap extends Map> { 5 | add(key: K, value: V) { 6 | let bucket = this.get(key); 7 | if (bucket != null) { 8 | this.set(key, bucket.push(value)); 9 | } else { 10 | this.set(key, List.of(value)); 11 | } 12 | } 13 | 14 | containsAt(key: K, value: V) { 15 | let bucket = this.get(key); 16 | if (bucket != null) { 17 | return bucket.contains(value); 18 | } 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/node-loader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import SweetLoader, { phaseInModulePathRegexp } from './sweet-loader'; 3 | import { readFileSync } from 'fs'; 4 | import { dirname } from 'path'; 5 | import resolve from 'resolve'; 6 | import vm from 'vm'; 7 | import Store from './store'; 8 | import type { LoaderOptions } from './sweet-loader'; 9 | 10 | type NodeLoaderOptions = LoaderOptions & { extensions?: Array }; 11 | 12 | export default class NodeLoader extends SweetLoader { 13 | extensions: Array; 14 | 15 | constructor(baseDir: string, options: NodeLoaderOptions = {}) { 16 | super(baseDir, options); 17 | this.extensions = options.extensions || ['.js', '.mjs']; 18 | } 19 | 20 | normalize(name: string, refererName?: string, refererAddress?: string) { 21 | let normName = super.normalize(name, refererName, refererAddress); 22 | let match = normName.match(phaseInModulePathRegexp); 23 | if (match && match.length >= 3) { 24 | let resolvedName = match[1]; 25 | try { 26 | resolvedName = resolve.sync(match[1], { 27 | basedir: refererName ? dirname(refererName) : this.baseDir, 28 | extensions: this.extensions, 29 | }); 30 | } catch (e) { 31 | // ignored 32 | } 33 | return `${resolvedName}:${match[2]}`; 34 | } 35 | throw new Error(`Module ${name} is missing phase information`); 36 | } 37 | 38 | fetch({ 39 | name, 40 | address, 41 | metadata, 42 | }: { 43 | name: string, 44 | address: { path: string, phase: number }, 45 | metadata: {}, 46 | }) { 47 | let src = this.sourceCache.get(address.path); 48 | if (src != null) { 49 | return src; 50 | } else { 51 | try { 52 | src = readFileSync(address.path, 'utf8'); 53 | } catch (e) { 54 | src = ''; 55 | } 56 | this.sourceCache.set(address.path, src); 57 | return src; 58 | } 59 | } 60 | 61 | freshStore() { 62 | let sandbox = { 63 | process: global.process, 64 | console: global.console, 65 | }; 66 | return new Store(vm.createContext(sandbox)); 67 | } 68 | 69 | eval(source: string, store: Store) { 70 | return vm.runInContext(source, store.getBackingObject()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/node-module-loader.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | 3 | export default function moduleLoader(path) { 4 | try { 5 | return readFileSync(path, 'utf8'); 6 | } catch (e) { 7 | return ''; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/node-module-resolver.js: -------------------------------------------------------------------------------- 1 | import resolve from 'resolve'; 2 | 3 | export default function resolveModule(path, cwd) { 4 | return resolve.sync(path, { basedir: cwd }); 5 | } 6 | -------------------------------------------------------------------------------- /src/operators.js: -------------------------------------------------------------------------------- 1 | const unaryOperators = { 2 | '+': true, 3 | '-': true, 4 | '!': true, 5 | '~': true, 6 | '++': true, 7 | '--': true, 8 | typeof: true, 9 | void: true, 10 | delete: true, 11 | await: true, 12 | }; 13 | const binaryOperatorPrecedence = { 14 | '*': 14, 15 | '/': 14, 16 | '%': 14, 17 | '+': 13, 18 | '-': 13, 19 | '>>': 12, 20 | '<<': 12, 21 | '>>>': 12, 22 | '<': 11, 23 | '<=': 11, 24 | '>': 11, 25 | '>=': 11, 26 | in: 11, 27 | instanceof: 11, 28 | '==': 10, 29 | '!=': 10, 30 | '===': 10, 31 | '!==': 10, 32 | '&': 9, 33 | '^': 8, 34 | '|': 7, 35 | '&&': 6, 36 | '||': 5, 37 | }; 38 | 39 | var operatorAssoc = { 40 | '*': 'left', 41 | '/': 'left', 42 | '%': 'left', 43 | '+': 'left', 44 | '-': 'left', 45 | '>>': 'left', 46 | '<<': 'left', 47 | '>>>': 'left', 48 | '<': 'left', 49 | '<=': 'left', 50 | '>': 'left', 51 | '>=': 'left', 52 | in: 'left', 53 | instanceof: 'left', 54 | '==': 'left', 55 | '!=': 'left', 56 | '===': 'left', 57 | '!==': 'left', 58 | '&': 'left', 59 | '^': 'left', 60 | '|': 'left', 61 | '&&': 'left', 62 | '||': 'left', 63 | }; 64 | 65 | export function operatorLt(left, right, assoc) { 66 | if (assoc === 'left') { 67 | return left < right; 68 | } else { 69 | return left <= right; 70 | } 71 | } 72 | 73 | export function getOperatorPrec(op) { 74 | return binaryOperatorPrecedence[op]; 75 | } 76 | export function getOperatorAssoc(op) { 77 | return operatorAssoc[op]; 78 | } 79 | 80 | export function isUnaryOperator(op) { 81 | return ( 82 | (op.match('punctuator') || op.match('identifier') || op.match('keyword')) && 83 | unaryOperators.hasOwnProperty(op.val()) 84 | ); 85 | } 86 | 87 | export function isOperator(op) { 88 | if (op.match('punctuator') || op.match('identifier') || op.match('keyword')) { 89 | return ( 90 | binaryOperatorPrecedence.hasOwnProperty(op) || 91 | unaryOperators.hasOwnProperty(op.val()) 92 | ); 93 | } 94 | return false; 95 | } 96 | -------------------------------------------------------------------------------- /src/reader/default-readtable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { List } from 'immutable'; 4 | import { isEOS, getCurrentReadtable, setCurrentReadtable } from 'readtable'; 5 | import readIdentifier from './read-identifier'; 6 | import readNumericLiteral from './read-numeric'; 7 | import readStringLiteral from './read-string'; 8 | import readTemplateLiteral from './read-template'; 9 | import readRegExp from './read-regexp.js'; 10 | import readComment from './read-comment'; 11 | import { readSyntaxTemplate } from './read-dispatch'; 12 | import { 13 | punctuatorTable as punctuatorMapping, 14 | keywordTable as keywordMapping, 15 | KeywordToken, 16 | PunctuatorToken, 17 | EmptyToken, 18 | IdentifierToken, 19 | } from '../tokens'; 20 | import { 21 | insertSequence, 22 | retrieveSequenceLength, 23 | isExprPrefix, 24 | isRegexPrefix, 25 | isIdentifierPart, 26 | isWhiteSpace, 27 | isLineTerminator, 28 | isDecimalDigit, 29 | } from './utils'; 30 | 31 | import type { CharStream } from 'readtable'; 32 | 33 | // use https://github.com/mathiasbynens/regenerate to generate the Unicode code points when implementing modes 34 | 35 | function eatWhitespace(stream: CharStream) { 36 | stream.readString(); 37 | return EmptyToken; 38 | } 39 | 40 | const punctuatorTable = Object.keys(punctuatorMapping).reduce( 41 | insertSequence, 42 | {}, 43 | ); 44 | 45 | function readPunctuator(stream) { 46 | const len = retrieveSequenceLength(punctuatorTable, stream, 0); 47 | if (len > 0) { 48 | return new PunctuatorToken({ 49 | value: stream.readString(len), 50 | }); 51 | } 52 | throw Error('Unknown punctuator'); 53 | } 54 | 55 | const punctuatorEntries = Object.keys(punctuatorTable).map(p => ({ 56 | key: p, 57 | mode: 'terminating', 58 | action: readPunctuator, 59 | })); 60 | 61 | const whiteSpaceTable = [ 62 | 0x20, 63 | 0x09, 64 | 0x0b, 65 | 0x0c, 66 | 0xa0, 67 | 0x1680, 68 | 0x2000, 69 | 0x2001, 70 | 0x2002, 71 | 0x2003, 72 | 0x2004, 73 | 0x2005, 74 | 0x2006, 75 | 0x2007, 76 | 0x2008, 77 | 0x2009, 78 | 0x200a, 79 | 0x202f, 80 | 0x205f, 81 | 0x3000, 82 | 0xfeff, 83 | ]; 84 | 85 | const whiteSpaceEntries = whiteSpaceTable.map(w => ({ 86 | key: w, 87 | mode: 'terminating', 88 | action: eatWhitespace, 89 | })); 90 | 91 | const lineTerminatorTable = [0x0a, 0x0d, 0x2028, 0x2029]; 92 | 93 | const lineTerminatorEntries = lineTerminatorTable.map(lt => ({ 94 | key: lt, 95 | mode: 'terminating', 96 | action: function readLineTerminator(stream) { 97 | this.incrementLine(); 98 | return eatWhitespace(stream); 99 | }, 100 | })); 101 | 102 | const digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 103 | 104 | const numericEntries = digits.map(d => ({ 105 | key: d, 106 | mode: 'non-terminating', 107 | action: readNumericLiteral, 108 | })); 109 | 110 | const quotes = ["'", '"']; 111 | 112 | const stringEntries = quotes.map(q => ({ 113 | key: q, 114 | mode: 'terminating', 115 | action: readStringLiteral, 116 | })); 117 | 118 | const identifierEntry = { 119 | mode: 'non-terminating', 120 | action: readIdentifier, 121 | }; 122 | 123 | const templateEntry = { 124 | key: '`', 125 | mode: 'terminating', 126 | action: readTemplateLiteral, 127 | }; 128 | 129 | const primitiveReadtable = getCurrentReadtable().extend( 130 | ...[ 131 | identifierEntry, 132 | ...whiteSpaceEntries, 133 | templateEntry, 134 | ...punctuatorEntries, 135 | ...lineTerminatorEntries, 136 | ...numericEntries, 137 | ...stringEntries, 138 | ], 139 | ); 140 | 141 | const dotEntry = { 142 | key: '.', 143 | mode: 'terminating', 144 | action: function readDot(stream, ...rest) { 145 | const nxt = stream.peek(1).charCodeAt(0); 146 | if (isDecimalDigit(nxt)) { 147 | return readNumericLiteral(stream, ...rest); 148 | } 149 | return readPunctuator.call(this, stream); 150 | }, 151 | }; 152 | 153 | const keywordTable = Object.keys(keywordMapping).reduce(insertSequence, {}); 154 | 155 | const keywordEntries = Object.keys(keywordTable).map(k => ({ 156 | key: k, 157 | mode: 'non-terminating', 158 | action: function readKeyword(stream) { 159 | const len = retrieveSequenceLength(keywordTable, stream, 0); 160 | if (len > 0 && !isIdentifierPart(stream.peek(len).charCodeAt(0))) { 161 | return new KeywordToken({ 162 | value: stream.readString(len), 163 | }); 164 | } 165 | return readIdentifier.call(this, stream); 166 | }, 167 | })); 168 | 169 | const delimiterPairs = [['[', ']'], ['(', ')']]; 170 | 171 | function readDelimiters(closing, stream, prefix, b) { 172 | const currentReadtable = getCurrentReadtable(); 173 | setCurrentReadtable(primitiveReadtable); 174 | 175 | let results = List.of(this.readToken(stream, List(), b)); 176 | 177 | setCurrentReadtable(currentReadtable); 178 | return this.readUntil(closing, stream, results, b); 179 | } 180 | 181 | const delimiterEntries = delimiterPairs.map(p => ({ 182 | key: p[0], 183 | mode: 'terminating', 184 | action: function readDefaultDelimiters(stream, prefix, b) { 185 | return readDelimiters.call(this, p[1], stream, prefix, true); 186 | }, 187 | })); 188 | 189 | const bracesEntry = { 190 | key: '{', 191 | mode: 'terminating', 192 | action: function readBraces(stream, prefix, b) { 193 | const line = this.locationInfo.line; 194 | const innerB = isExprPrefix(line, b, prefix); 195 | return readDelimiters.call(this, '}', stream, prefix, innerB); 196 | }, 197 | }; 198 | 199 | function readClosingDelimiter(opening, closing, stream, prefix, b) { 200 | if (prefix.first().value !== opening) { 201 | throw Error('Unmatched delimiter:', closing); 202 | } 203 | return readPunctuator.call(this, stream); 204 | } 205 | 206 | const unmatchedDelimiterEntries = [ 207 | ['{', '}'], 208 | ['[', ']'], 209 | ['(', ')'], 210 | ].map(p => ({ 211 | key: p[1], 212 | mode: 'terminating', 213 | action: function readClosingDelimiters(stream, prefix, b) { 214 | return readClosingDelimiter.call(this, ...p, stream, prefix, b); 215 | }, 216 | })); 217 | 218 | const divEntry = { 219 | key: '/', 220 | mode: 'terminating', 221 | action: function readDiv(stream, prefix, b) { 222 | let nxt = stream.peek(1); 223 | if (nxt === '/' || nxt === '*') { 224 | const result = readComment.call(this, stream); 225 | return result; 226 | } 227 | if (isRegexPrefix(b, prefix)) { 228 | return readRegExp.call(this, stream, prefix, b); 229 | } 230 | return readPunctuator.call(this, stream); 231 | }, 232 | }; 233 | 234 | const dispatchBacktickEntry = { 235 | key: '`', 236 | mode: 'dispatch', 237 | action: readSyntaxTemplate, 238 | }; 239 | 240 | const defaultDispatchEntry = { 241 | mode: 'dispatch', 242 | action: function readDefaultDispatch(...args) { 243 | this.readToken(...args); 244 | return EmptyToken; 245 | }, 246 | }; 247 | 248 | const dispatchWhiteSpaceEntries = whiteSpaceTable 249 | .concat(lineTerminatorTable) 250 | .map(w => ({ 251 | key: w, 252 | mode: 'dispatch', 253 | action: function readDispatchWhitespace( 254 | stream, 255 | prefix, 256 | allowExprs, 257 | dispatchKey, 258 | ) { 259 | this.readToken(stream, prefix, allowExprs); 260 | return new IdentifierToken({ value: dispatchKey }); 261 | }, 262 | })); 263 | 264 | const atEntry = { 265 | key: '@', 266 | mode: 'terminating', 267 | action: function readAt(stream, prefix) { 268 | const nxt = stream.peek(1), 269 | nxtCode = nxt.charCodeAt(0); 270 | if (isEOS(nxt) || isWhiteSpace(nxtCode) || isLineTerminator(nxtCode)) { 271 | return new IdentifierToken({ value: stream.readString() }); 272 | } 273 | throw new SyntaxError('Invalid or unexpected token'); 274 | }, 275 | }; 276 | 277 | const defaultReadtable = primitiveReadtable.extend( 278 | ...[ 279 | dotEntry, 280 | ...delimiterEntries, 281 | ...unmatchedDelimiterEntries, 282 | bracesEntry, 283 | divEntry, 284 | ...keywordEntries, 285 | defaultDispatchEntry, 286 | dispatchBacktickEntry, 287 | ...dispatchWhiteSpaceEntries, 288 | atEntry, 289 | ], 290 | ); 291 | 292 | export default defaultReadtable; 293 | -------------------------------------------------------------------------------- /src/reader/read-comment.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { CharStream } from 'readtable'; 3 | 4 | import { isEOS } from 'readtable'; 5 | import { skipSingleLineComment } from './utils'; 6 | import { EmptyToken } from '../tokens'; 7 | 8 | export default function readComment(stream: CharStream): typeof EmptyToken { 9 | let char = stream.peek(); 10 | 11 | while (!isEOS(char)) { 12 | let chCode = char.charCodeAt(0); 13 | if (chCode === 47) { 14 | /* "/" */ const nxt = stream.peek(1); 15 | if (isEOS(nxt)) { 16 | break; 17 | } 18 | chCode = nxt.charCodeAt(0); 19 | if (chCode === 47) { 20 | /* "/" */ skipSingleLineComment.call(this, stream); 21 | } else if (chCode === 42) { 22 | /* "*" */ skipMultiLineComment.call(this, stream); 23 | } else { 24 | break; 25 | } 26 | } else { 27 | break; 28 | } 29 | char = stream.peek(); 30 | } 31 | 32 | return EmptyToken; 33 | } 34 | 35 | function skipMultiLineComment(stream: CharStream): void { 36 | let idx = 2; 37 | let char = stream.peek(idx); 38 | const { position: startPosition } = stream.sourceInfo; 39 | let lineStart; 40 | while (!isEOS(char)) { 41 | let chCode = char.charCodeAt(0); 42 | if (chCode < 0x80) { 43 | switch (chCode) { 44 | case 42: // "*" 45 | // Block comment ends with "*/". 46 | if (stream.peek(idx + 1).charAt(0) === '/') { 47 | stream.readString(idx + 2); 48 | if (lineStart) 49 | this.locationInfo.column = stream.sourceInfo.position - lineStart; 50 | return; 51 | } 52 | ++idx; 53 | break; 54 | case 10: // "\n" 55 | this.incrementLine(); 56 | lineStart = startPosition + idx; 57 | ++idx; 58 | break; 59 | case 13: { 60 | // "\r": 61 | let startIdx = idx; 62 | if (stream.peek(idx + 1).charAt(0) === '\n') { 63 | ++idx; 64 | } 65 | ++idx; 66 | this.incrementLine(); 67 | lineStart = startPosition + startIdx; 68 | break; 69 | } 70 | default: 71 | ++idx; 72 | } 73 | } else if (chCode === 0x2028 || chCode === 0x2029) { 74 | this.incrementLine(); 75 | lineStart = startPosition + idx; 76 | ++idx; 77 | } else { 78 | ++idx; 79 | } 80 | char = stream.peek(idx); 81 | } 82 | throw this.createILLEGAL(char); 83 | } 84 | -------------------------------------------------------------------------------- /src/reader/read-dispatch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { getCurrentReadtable, setCurrentReadtable } from 'readtable'; 4 | import { List } from 'immutable'; 5 | import { TokenType as TT } from '../tokens'; 6 | 7 | import type { CharStream } from 'readtable'; 8 | 9 | const backtickEntry = { 10 | key: '`', 11 | mode: 'terminating', 12 | action: function readBacktick( 13 | stream: CharStream, 14 | prefix: List, 15 | e: boolean, 16 | ) { 17 | if (prefix.isEmpty()) { 18 | return { 19 | type: TT.LSYNTAX, 20 | value: stream.readString(), 21 | }; 22 | } 23 | 24 | return { 25 | type: TT.RSYNTAX, 26 | value: stream.readString(), 27 | }; 28 | }, 29 | }; 30 | 31 | export function readSyntaxTemplate( 32 | stream: CharStream, 33 | prefix: List, 34 | exprAllowed: boolean, 35 | dispatchChar: string, 36 | ): List | { type: typeof TT.RSYNTAX, value: string } { 37 | // return read('syntaxTemplate').first().token; 38 | // TODO: Can we simply tack 'syntaxTemplate' on the front and process it as a 39 | // syntax macro? 40 | const prevTable = getCurrentReadtable(); 41 | setCurrentReadtable(prevTable.extend(backtickEntry)); 42 | 43 | const result = this.readUntil( 44 | '`', 45 | stream, 46 | List.of( 47 | updateSyntax(dispatchChar, this.readToken(stream, List(), exprAllowed)), 48 | ), 49 | exprAllowed, 50 | ); 51 | 52 | setCurrentReadtable(prevTable); 53 | return result; 54 | } 55 | 56 | function updateSyntax(prefix, token) { 57 | token.value = prefix + token.value; 58 | token.slice.text = prefix + token.slice.text; 59 | token.slice.start -= 1; 60 | token.slice.startLocation.position -= 1; 61 | return token; 62 | } 63 | -------------------------------------------------------------------------------- /src/reader/read-identifier.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { scanUnicode } from './utils'; 4 | 5 | import { isEOS, getCurrentReadtable } from 'readtable'; 6 | import type { CharStream } from 'readtable'; 7 | 8 | import { IdentifierToken } from '../tokens'; 9 | 10 | import { isTerminating, isIdentifierPart, isIdentifierStart } from './utils'; 11 | 12 | let terminates; 13 | 14 | const startsEscape = code => { 15 | if (code === 0x005c /* backslash */) return true; 16 | return 0xd800 <= code && code <= 0xdbff; 17 | }; 18 | 19 | export default function readIdentifier(stream: CharStream) { 20 | terminates = isTerminating(getCurrentReadtable()); 21 | let char = stream.peek(); 22 | let code = char.charCodeAt(0); 23 | let check = isIdentifierStart; 24 | 25 | // If the first char is invalid 26 | if (!check(code) && !startsEscape(code)) { 27 | throw this.createError('Invalid or unexpected token'); 28 | } 29 | 30 | let idx = 0; 31 | while (!terminates(char) && !isEOS(char)) { 32 | if (startsEscape(code)) { 33 | return new IdentifierToken({ 34 | value: getEscapedIdentifier.call(this, stream), 35 | }); 36 | } 37 | if (!check(code)) { 38 | return new IdentifierToken({ 39 | value: stream.readString(idx), 40 | }); 41 | } 42 | char = stream.peek(++idx); 43 | code = char.charCodeAt(0); 44 | check = isIdentifierPart; 45 | } 46 | return new IdentifierToken({ 47 | value: stream.readString(idx), 48 | }); 49 | } 50 | 51 | function getEscapedIdentifier(stream) { 52 | const sPeek = stream.peek.bind(stream); 53 | let id = ''; 54 | let check = isIdentifierStart; 55 | let char = sPeek(); 56 | let code = char.charCodeAt(0); 57 | while (!terminates(char) && !isEOS(char)) { 58 | let streamRead = false; 59 | if (char === '\\') { 60 | let nxt = sPeek(1); 61 | if (isEOS(nxt)) { 62 | throw this.createILLEGAL(char); 63 | } 64 | if (nxt !== 'u') { 65 | throw this.createILLEGAL(char); 66 | } 67 | code = scanUnicode(stream, 2); 68 | streamRead = true; 69 | if (code < 0) { 70 | throw this.createILLEGAL(char); 71 | } 72 | } else if (0xd800 <= code && code <= 0xdbff) { 73 | if (isEOS(char)) { 74 | throw this.createILLEGAL(char); 75 | } 76 | let lowSurrogateCode = sPeek(1).charCodeAt(0); 77 | if (0xdc00 > lowSurrogateCode || lowSurrogateCode > 0xdfff) { 78 | throw this.createILLEGAL(char); 79 | } 80 | stream.readString(2); 81 | code = decodeUtf16(code, lowSurrogateCode); 82 | streamRead = true; 83 | } 84 | if (!check(code)) { 85 | if (id.length < 1) { 86 | throw this.createILLEGAL(char); 87 | } 88 | return id; 89 | } 90 | 91 | if (!streamRead) stream.readString(); 92 | 93 | id += String.fromCodePoint(code); 94 | char = sPeek(); 95 | code = char.charCodeAt(0); 96 | check = isIdentifierPart; 97 | } 98 | return id; 99 | } 100 | 101 | function decodeUtf16(lead, trail) { 102 | return (lead - 0xd800) * 0x400 + (trail - 0xdc00) + 0x10000; 103 | } 104 | -------------------------------------------------------------------------------- /src/reader/read-numeric.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { isEOS, getCurrentReadtable } from 'readtable'; 4 | import { code } from 'esutils'; 5 | import { isTerminating, getHexValue } from './utils'; 6 | import { NumericToken } from '../tokens'; 7 | 8 | import type { CharStream } from 'readtable'; 9 | 10 | const { 11 | isIdentifierPartES6: isIdentifierPart, 12 | isIdentifierStartES6: isIdentifierStart, 13 | } = code; 14 | 15 | let terminates; 16 | 17 | export default function readNumericLiteral(stream: CharStream) { 18 | terminates = isTerminating(getCurrentReadtable()); 19 | let idx = 0, 20 | char = stream.peek(); 21 | 22 | if (char === '0') { 23 | char = stream.peek(++idx); 24 | if (!isEOS(char)) { 25 | char = char.toLowerCase(); 26 | switch (char) { 27 | case 'x': 28 | return readHexLiteral.call(this, stream); 29 | case 'b': 30 | return readBinaryLiteral.call(this, stream); 31 | case 'o': 32 | return readOctalLiteral.call(this, stream); 33 | default: 34 | if (isDecimalChar(char)) { 35 | return readLegacyOctalLiteral.call(this, stream); // reads legacy octal and decimal 36 | } 37 | } 38 | } else { 39 | return new NumericToken({ 40 | value: +stream.readString(), 41 | }); 42 | } 43 | } else if (char !== '.') { 44 | while (!terminates(char) && isDecimalChar(char)) { 45 | char = stream.peek(++idx); 46 | } 47 | if (isEOS(char)) { 48 | return new NumericToken({ 49 | value: +stream.readString(idx), 50 | }); 51 | } 52 | } 53 | 54 | idx = addDecimalLiteralSuffixLength.call(this, stream, idx); 55 | 56 | char = stream.peek(idx); 57 | if (!isEOS(char) && !terminates(char) && isIdentifierStart(char)) { 58 | throw this.createILLEGAL(char); 59 | } 60 | 61 | return new NumericToken({ 62 | value: +stream.readString(idx), 63 | }); 64 | } 65 | 66 | function addDecimalLiteralSuffixLength(stream, idx) { 67 | let char = stream.peek(idx); 68 | if (char === '.') { 69 | char = stream.peek(++idx); 70 | if (isEOS(char)) return idx; 71 | 72 | while (isDecimalChar(char)) { 73 | char = stream.peek(++idx); 74 | if (terminates(char) || isEOS(char)) return idx; 75 | } 76 | } 77 | 78 | if (char.toLowerCase() === 'e') { 79 | char = stream.peek(++idx); 80 | if (isEOS(char)) throw this.createILLEGAL(char); 81 | 82 | if (char === '+' || char === '-') { 83 | char = stream.peek(++idx); 84 | if (isEOS(char)) throw this.createILLEGAL(char); 85 | } 86 | 87 | while (isDecimalChar(char)) { 88 | char = stream.peek(++idx); 89 | if (terminates(char) || isEOS(char)) break; 90 | } 91 | } 92 | return idx; 93 | } 94 | 95 | function readLegacyOctalLiteral(stream) { 96 | let idx = 0, 97 | isOctal = true, 98 | char = stream.peek(); 99 | 100 | while (!terminates(char) && !isEOS(char)) { 101 | if ('0' <= char && char <= '7') { 102 | idx++; 103 | } else if (char === '8' || char === '9') { 104 | isOctal = false; 105 | idx++; 106 | } else if (isIdentifierPart(char.charCodeAt(0))) { 107 | throw this.createILLEGAL(char); 108 | } else { 109 | break; 110 | } 111 | 112 | char = stream.peek(idx); 113 | } 114 | 115 | if (!isOctal) 116 | return new NumericToken({ 117 | value: parseNumeric(stream, idx, 10), 118 | octal: true, 119 | noctal: !isOctal, 120 | }); 121 | 122 | return new NumericToken({ 123 | value: parseNumeric(stream, idx, 8), 124 | octal: true, 125 | noctal: !isOctal, 126 | }); 127 | } 128 | 129 | function readOctalLiteral(stream) { 130 | let start, 131 | idx = (start = 2), 132 | char = stream.peek(idx); 133 | while (!terminates(char) && !isEOS(char)) { 134 | if ('0' <= char && char <= '7') { 135 | char = stream.peek(++idx); 136 | } else if (isIdentifierPart(char.charCodeAt(0))) { 137 | throw this.createILLEGAL(char); 138 | } else { 139 | break; 140 | } 141 | } 142 | 143 | if (idx === start) { 144 | throw this.createILLEGAL(char); 145 | } 146 | 147 | return new NumericToken({ 148 | value: parseNumeric(stream, idx, 8, start), 149 | }); 150 | } 151 | 152 | function readBinaryLiteral(stream) { 153 | let start, 154 | idx = (start = 2); 155 | let char = stream.peek(idx); 156 | 157 | while (!terminates(char) && !isEOS(char)) { 158 | if (char !== '0' && char !== '1') { 159 | break; 160 | } 161 | char = stream.peek(idx); 162 | idx++; 163 | } 164 | 165 | if (idx === start) { 166 | throw this.createILLEGAL(char); 167 | } 168 | 169 | if ( 170 | !isEOS(char) && 171 | !terminates(char) && 172 | (isIdentifierStart(char) || isDecimalChar(char)) 173 | ) { 174 | throw this.createILLEGAL(char); 175 | } 176 | 177 | return new NumericToken({ 178 | value: parseNumeric(stream, idx, 2, start), 179 | }); 180 | } 181 | 182 | function readHexLiteral(stream) { 183 | let start, 184 | idx = (start = 2), 185 | char = stream.peek(idx); 186 | while (!terminates(char)) { 187 | let hex = getHexValue(char); 188 | if (hex === -1) { 189 | break; 190 | } 191 | char = stream.peek(++idx); 192 | } 193 | 194 | if (idx === start) { 195 | throw this.createILLEGAL(char); 196 | } 197 | 198 | if (!isEOS(char) && !terminates(char) && isIdentifierStart(char)) { 199 | throw this.createILLEGAL(char); 200 | } 201 | 202 | return new NumericToken({ 203 | value: parseNumeric(stream, idx, 16, start), 204 | }); 205 | } 206 | 207 | function parseNumeric(stream, len, radix, start = 0) { 208 | stream.readString(start); 209 | return parseInt(stream.readString(len - start), radix); 210 | } 211 | 212 | function isDecimalChar(char) { 213 | return '0' <= char && char <= '9'; 214 | } 215 | -------------------------------------------------------------------------------- /src/reader/read-regexp.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { CharStream } from 'readtable'; 3 | 4 | import { isEOS } from 'readtable'; 5 | import { RegExpToken } from '../tokens'; 6 | import { isLineTerminator, isIdentifierPart } from './utils'; 7 | 8 | export default function readRegExp(stream: CharStream) { 9 | let value = stream.readString(), 10 | char = stream.peek(), 11 | idx = 0, 12 | classMarker = false, 13 | terminated = false; 14 | 15 | const UNTERMINATED_REGEXP_MSG = 'Invalid regular expression: missing /'; 16 | 17 | while (!isEOS(char)) { 18 | if (char === '\\') { 19 | value += char; 20 | ++idx; 21 | char = stream.peek(idx); 22 | 23 | if (isLineTerminator(char.charCodeAt(0))) { 24 | throw this.createError(UNTERMINATED_REGEXP_MSG); 25 | } 26 | value += char; 27 | ++idx; 28 | } else if (isLineTerminator(char.charCodeAt(0))) { 29 | throw this.createError(UNTERMINATED_REGEXP_MSG); 30 | } else { 31 | if (classMarker) { 32 | if (char === ']') { 33 | classMarker = false; 34 | } 35 | } else { 36 | if (char === '/') { 37 | terminated = true; 38 | value += char; 39 | ++idx; 40 | char = stream.peek(idx); 41 | break; 42 | } else if (char === '[') { 43 | classMarker = true; 44 | } 45 | } 46 | value += char; 47 | ++idx; 48 | } 49 | char = stream.peek(idx); 50 | } 51 | 52 | if (!terminated) { 53 | throw this.createError(UNTERMINATED_REGEXP_MSG); 54 | } 55 | 56 | while (!isEOS(char)) { 57 | if (char === '\\') { 58 | throw this.createError('Invalid regular expression flags'); 59 | } 60 | if (!isIdentifierPart(char.charCodeAt(0))) { 61 | break; 62 | } 63 | value += char; 64 | ++idx; 65 | char = stream.peek(idx); 66 | } 67 | 68 | stream.readString(idx); 69 | 70 | return new RegExpToken({ 71 | value, 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/reader/read-string.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { CharStream } from 'readtable'; 3 | 4 | import { readStringEscape, isLineTerminator } from './utils'; 5 | import { isEOS } from 'readtable'; 6 | import { StringToken } from '../tokens'; 7 | 8 | export default function readStringLiteral(stream: CharStream): StringToken { 9 | let str = '', 10 | octal = null, 11 | idx: number = 0, 12 | quote = stream.readString(), 13 | char = stream.peek(), 14 | lineStart; 15 | 16 | while (!isEOS(char)) { 17 | if (char === quote) { 18 | stream.readString(++idx); 19 | if (lineStart != null) this.locationInfo.column += idx - lineStart; 20 | return new StringToken({ str, octal }); 21 | } else if (char === '\\') { 22 | [str, idx, octal, lineStart] = readStringEscape.call( 23 | this, 24 | str, 25 | stream, 26 | idx, 27 | octal, 28 | ); 29 | } else if (isLineTerminator(char.charCodeAt(0))) { 30 | throw this.createILLEGAL(char); 31 | } else { 32 | ++idx; 33 | str += char; 34 | } 35 | char = stream.peek(idx); 36 | } 37 | throw this.createILLEGAL(char); 38 | } 39 | -------------------------------------------------------------------------------- /src/reader/read-template.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { List } from 'immutable'; 3 | 4 | import type { CharStream } from 'readtable'; 5 | import { isEOS } from 'readtable'; 6 | 7 | import { readStringEscape } from './utils'; 8 | import { getSlice } from './token-reader'; 9 | import { TemplateToken, TemplateElementToken } from '../tokens'; 10 | 11 | export default function readTemplateLiteral( 12 | stream: CharStream, 13 | prefix: List, 14 | ): TemplateToken { 15 | let element, 16 | items = []; 17 | stream.readString(); 18 | 19 | do { 20 | element = readTemplateElement.call(this, stream); 21 | items.push(element); 22 | if (element.interp) { 23 | element = this.readToken(stream, List(), false); 24 | items.push(element); 25 | } 26 | } while (!element.tail); 27 | 28 | return new TemplateToken({ 29 | items: List(items), 30 | }); 31 | } 32 | 33 | function readTemplateElement(stream: CharStream): TemplateElementToken { 34 | let char = stream.peek(), 35 | idx = 0, 36 | value = '', 37 | octal = null; 38 | const startLocation = Object.assign({}, this.locationInfo, stream.sourceInfo); 39 | while (!isEOS(char)) { 40 | switch (char) { 41 | case '`': { 42 | stream.readString(idx); 43 | const slice = getSlice(stream, startLocation); 44 | stream.readString(); 45 | return new TemplateElementToken({ 46 | tail: true, 47 | interp: false, 48 | value, 49 | slice, 50 | }); 51 | } 52 | case '$': { 53 | if (stream.peek(idx + 1) === '{') { 54 | stream.readString(idx); 55 | const slice = getSlice(stream, startLocation); 56 | stream.readString(); 57 | 58 | return new TemplateElementToken({ 59 | tail: false, 60 | interp: true, 61 | value, 62 | slice, 63 | }); 64 | } 65 | break; 66 | } 67 | case '\\': { 68 | let newVal; 69 | [newVal, idx, octal] = readStringEscape.call( 70 | this, 71 | '', 72 | stream, 73 | idx, 74 | octal, 75 | ); 76 | if (octal != null) throw this.createILLEGAL(octal); 77 | value += newVal; 78 | --idx; 79 | break; 80 | } 81 | default: { 82 | value += char; 83 | } 84 | } 85 | char = stream.peek(++idx); 86 | } 87 | throw this.createILLEGAL(char); 88 | } 89 | -------------------------------------------------------------------------------- /src/reader/token-reader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CharStream, isEOS, Reader, setCurrentReadtable } from 'readtable'; 4 | import defaultReadtable from './default-readtable'; 5 | import { List } from 'immutable'; 6 | import { EmptyToken } from '../tokens'; 7 | 8 | import type { StartLocation, Slice } from '../tokens'; 9 | 10 | setCurrentReadtable(defaultReadtable); 11 | 12 | export type LocationInfo = { 13 | line: number, 14 | column: number, 15 | }; 16 | 17 | type Context = { 18 | bindings: any, 19 | scopesets: any, 20 | }; 21 | 22 | export function getSlice( 23 | stream: CharStream, 24 | startLocation: StartLocation, 25 | ): Slice { 26 | return { 27 | text: stream.getSlice(startLocation.position), 28 | start: startLocation.position, 29 | startLocation, 30 | end: stream.sourceInfo.position, 31 | }; 32 | } 33 | 34 | const streams = new WeakMap(); 35 | 36 | class ReadError extends Error { 37 | index: number; 38 | line: number; 39 | column: number; 40 | message: string; 41 | constructor({ 42 | index, 43 | line, 44 | column, 45 | message, 46 | }: { 47 | index: number, 48 | line: number, 49 | column: number, 50 | message: string, 51 | }) { 52 | super(message); 53 | this.index = index; 54 | this.line = line; 55 | this.column = column; 56 | this.message = `[${line}:${column}] ${message}`; 57 | } 58 | } 59 | 60 | class TokenReader extends Reader { 61 | locationInfo: LocationInfo; 62 | context: ?Context; 63 | constructor(stream: CharStream, context?: Context) { 64 | super(); 65 | this.context = context; 66 | streams.set(this, stream); 67 | this.locationInfo = { 68 | line: 1, 69 | column: 1, 70 | }; 71 | } 72 | 73 | createError(msg: string): ReadError { 74 | let message = msg.replace(/\{(\d+)\}/g, (_, n) => 75 | JSON.stringify(arguments[+n + 1]), 76 | ); 77 | return new ReadError({ 78 | message, 79 | // $FlowFixMe: decide on how to handle possible nullability 80 | index: streams.get(this).sourceInfo.position, 81 | line: this.locationInfo.line, 82 | column: this.locationInfo.column, 83 | }); 84 | } 85 | 86 | createILLEGAL(char) { 87 | return !isEOS(char) 88 | ? this.createError('Unexpected {0}', char) 89 | : this.createError('Unexpected end of input'); 90 | } 91 | 92 | readToken(stream: CharStream, ...rest: Array) { 93 | const startLocation = Object.assign( 94 | {}, 95 | this.locationInfo, 96 | stream.sourceInfo, 97 | ); 98 | const result = super.read(stream, ...rest); 99 | 100 | if ( 101 | startLocation.column === this.locationInfo.column && 102 | startLocation.line === this.locationInfo.line 103 | ) { 104 | this.locationInfo.column += 105 | stream.sourceInfo.position - startLocation.position; 106 | } 107 | 108 | if (result === EmptyToken) return result; 109 | 110 | if (!List.isList(result)) result.slice = getSlice(stream, startLocation); 111 | 112 | return result; 113 | } 114 | 115 | readUntil( 116 | close: ?Function | ?string, 117 | stream: CharStream, 118 | prefix: List, 119 | exprAllowed: boolean, 120 | ): List { 121 | let result, 122 | results = prefix, 123 | done = false; 124 | do { 125 | if (isEOS(stream.peek())) break; 126 | done = typeof close === 'function' ? close() : stream.peek() === close; 127 | result = this.readToken(stream, results, exprAllowed); 128 | 129 | if (result !== EmptyToken) { 130 | results = results.push(result); 131 | } 132 | } while (!done); 133 | return results; 134 | } 135 | 136 | incrementLine(): void { 137 | this.locationInfo.line += 1; 138 | this.locationInfo.column = 1; 139 | } 140 | } 141 | 142 | export default function read( 143 | source: string | CharStream, 144 | context?: Context, 145 | ): List { 146 | const stream = typeof source === 'string' ? new CharStream(source) : source; 147 | if (isEOS(stream.peek())) return List(); 148 | return new TokenReader(stream, context).readUntil( 149 | null, 150 | stream, 151 | List(), 152 | false, 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /src/scope-reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Term, * as S from 'sweet-spec'; 3 | import type Syntax from './syntax'; 4 | import type { SymbolClass } from './symbol'; 5 | import type BindingMap from './binding-map'; 6 | 7 | // $FlowFixMe: flow doesn't know about the CloneReducer yet 8 | export default class extends Term.CloneReducer { 9 | scopes: Array<{ scope: SymbolClass, phase: number | {}, flip: boolean }>; 10 | bindings: BindingMap; 11 | 12 | constructor( 13 | scopes: Array<{ scope: SymbolClass, phase: number | {}, flip: boolean }>, 14 | bindings: BindingMap, 15 | ) { 16 | super(); 17 | this.scopes = scopes; 18 | this.bindings = bindings; 19 | } 20 | 21 | applyScopes(s: Syntax) { 22 | return this.scopes.reduce((acc, sc) => { 23 | return acc.addScope(sc.scope, this.bindings, sc.phase, { 24 | flip: sc.flip, 25 | }); 26 | }, s); 27 | } 28 | 29 | reduceBindingIdentifier(t: Term, s: { name: Syntax }) { 30 | return new S.BindingIdentifier({ 31 | name: this.applyScopes(s.name), 32 | }); 33 | } 34 | 35 | reduceIdentifierExpression(t: Term, s: { name: Syntax }) { 36 | return new S.IdentifierExpression({ 37 | name: this.applyScopes(s.name), 38 | }); 39 | } 40 | 41 | reduceRawSyntax(t: Term, s: { value: Syntax }) { 42 | // TODO: fix this once reading tokens is reasonable 43 | if (s.value.isTemplate() && s.value.items) { 44 | s.value.token.items = s.value.token.items.map(t => { 45 | if (t instanceof Term) { 46 | return t.reduce(this); 47 | } 48 | return t; 49 | }); 50 | } 51 | return new S.RawSyntax({ 52 | value: this.applyScopes(s.value), 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/scope.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Symbol } from './symbol'; 3 | 4 | let scopeIndex = 0; 5 | 6 | export function freshScope(name: string = 'scope') { 7 | scopeIndex++; 8 | return Symbol(name + '_' + scopeIndex); 9 | } 10 | 11 | export function Scope(name: string) { 12 | return Symbol(name); 13 | } 14 | -------------------------------------------------------------------------------- /src/store-loader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import SweetLoader from './sweet-loader'; 3 | import vm from 'vm'; 4 | import Store from './store'; 5 | 6 | export default class extends SweetLoader { 7 | store: Map; 8 | 9 | constructor( 10 | baseDir: string, 11 | store: Map, 12 | noBabel: boolean = false, 13 | ) { 14 | super(baseDir, { noBabel }); 15 | this.store = store; 16 | } 17 | 18 | fetch({ name, address }: { name: string, address: any }) { 19 | if (this.store.has(address.path)) { 20 | return this.store.get(address.path); 21 | } 22 | throw new Error( 23 | `The module ${name} is not in the debug store: addr.path is ${address.path}`, 24 | ); 25 | } 26 | 27 | freshStore() { 28 | return new Store(vm.createContext()); 29 | } 30 | 31 | eval(source: string, store: Store) { 32 | return vm.runInContext(source, store.getBackingObject()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | export default class Store extends Map { 2 | constructor(backingObject) { 3 | super(); 4 | this.backingObject = backingObject; 5 | } 6 | 7 | set(key, val) { 8 | super.set(key, val); 9 | this.backingObject[key] = val; 10 | } 11 | 12 | getBackingObject() { 13 | return this.backingObject; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/sweet-loader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import read from './reader/token-reader'; 3 | import { freshScope } from './scope'; 4 | import Env from './env'; 5 | import { List } from 'immutable'; 6 | import Compiler from './compiler'; 7 | import { ALL_PHASES } from './syntax'; 8 | import BindingMap from './binding-map.js'; 9 | import Term from 'sweet-spec'; 10 | import SweetModule from './sweet-module'; 11 | import * as _ from 'ramda'; 12 | import ScopeReducer from './scope-reducer'; 13 | import { wrapInTerms } from './macro-context'; 14 | import { transform as babel } from 'babel-core'; 15 | import Store from './store'; 16 | import Multimap from './multimap'; 17 | 18 | export const phaseInModulePathRegexp = /(.*):(\d+)\s*$/; 19 | 20 | export type Context = { 21 | bindings: any, 22 | templateMap: any, 23 | getTemplateIdentifier: any, 24 | loader: any, 25 | transform: any, 26 | phase: number, 27 | store: Store, 28 | invokedRegistry: Multimap, 29 | }; 30 | 31 | export type LoaderOptions = { 32 | noBabel?: boolean, 33 | logging?: boolean, 34 | }; 35 | 36 | export default class SweetLoader { 37 | sourceCache: Map; 38 | compiledCache: Map; 39 | context: any; 40 | baseDir: string; 41 | logging: boolean; 42 | 43 | constructor(baseDir: string, options?: LoaderOptions = {}) { 44 | this.sourceCache = new Map(); 45 | this.compiledCache = new Map(); 46 | this.baseDir = baseDir; 47 | this.logging = options.logging || false; 48 | 49 | let bindings = new BindingMap(); 50 | let templateMap = new Map(); 51 | let tempIdent = 0; 52 | this.context = { 53 | phase: 0, 54 | bindings, 55 | templateMap, 56 | getTemplateIdentifier: () => ++tempIdent, 57 | loader: this, 58 | invokedRegistry: new Multimap(), 59 | transform: c => { 60 | if (options.noBabel) { 61 | return { 62 | code: c, 63 | }; 64 | } 65 | return babel(c, { 66 | babelrc: true, 67 | }); 68 | }, 69 | }; 70 | } 71 | 72 | normalize(name: string, refererName?: string, refererAddress?: string) { 73 | // takes `..path/to/source.js:` 74 | // gives `/abs/path/to/source.js:` 75 | // missing phases are turned into 0 76 | if (!phaseInModulePathRegexp.test(name)) { 77 | return `${name}:0`; 78 | } 79 | return name; 80 | } 81 | 82 | locate({ name, metadata }: { name: string, metadata: {} }) { 83 | // takes `/abs/path/to/source.js:` 84 | // gives { path: '/abs/path/to/source.js', phase: } 85 | let match = name.match(phaseInModulePathRegexp); 86 | if (match && match.length >= 3) { 87 | return { 88 | path: match[1], 89 | phase: parseInt(match[2], 10), 90 | }; 91 | } 92 | throw new Error(`Module ${name} is missing phase information`); 93 | } 94 | 95 | fetch({ 96 | name, 97 | address, 98 | metadata, 99 | }: { 100 | name: string, 101 | address: { path: string, phase: number }, 102 | metadata: {}, 103 | }) { 104 | throw new Error('No default fetch defined'); 105 | } 106 | 107 | translate({ 108 | name, 109 | address, 110 | source, 111 | metadata, 112 | }: { 113 | name: string, 114 | address: { path: string, phase: number }, 115 | source: string, 116 | metadata: {}, 117 | }) { 118 | let src = this.compiledCache.get(address.path); 119 | if (src != null) { 120 | return src; 121 | } 122 | let compiledModule = this.compileSource(source, address.path, metadata); 123 | this.compiledCache.set(address.path, compiledModule); 124 | return compiledModule; 125 | } 126 | 127 | instantiate({ 128 | name, 129 | address, 130 | source, 131 | metadata, 132 | }: { 133 | name: string, 134 | address: { path: string, phase: number }, 135 | source: SweetModule, 136 | metadata: {}, 137 | }) { 138 | throw new Error('Not implemented yet'); 139 | } 140 | 141 | eval(source: string) { 142 | return (0, eval)(source); 143 | } 144 | 145 | load(entryPath: string) { 146 | let metadata = {}; 147 | let name = this.normalize(entryPath); 148 | let address = this.locate({ name, metadata }); 149 | let source = this.fetch({ name, address, metadata }); 150 | source = this.translate({ name, address, source, metadata }); 151 | return this.instantiate({ name, address, source, metadata }); 152 | } 153 | 154 | // skip instantiate 155 | compile( 156 | entryPath: string, 157 | { 158 | refererName, 159 | enforceLangPragma, 160 | isEntrypoint, 161 | }: { 162 | refererName?: string, 163 | enforceLangPragma: boolean, 164 | isEntrypoint: boolean, 165 | }, 166 | ) { 167 | let metadata = { 168 | isEntrypoint, 169 | enforceLangPragma, 170 | entryPath, 171 | }; 172 | let name = this.normalize(entryPath, refererName); 173 | let address = this.locate({ name, metadata }); 174 | let source = this.fetch({ name, address, metadata }); 175 | return this.translate({ name, address, source, metadata }); 176 | } 177 | 178 | get(entryPath: string, entryPhase: number, refererName?: string) { 179 | return this.compile(`${entryPath}:${entryPhase}`, { 180 | refererName, 181 | enforceLangPragma: true, 182 | isEntrypoint: false, 183 | }); 184 | } 185 | 186 | read(source: string): List { 187 | return wrapInTerms(read(source)); 188 | } 189 | 190 | freshStore() { 191 | return new Store({}); 192 | } 193 | 194 | compileSource(source: string, path: string, metadata: any) { 195 | let directive = getLangDirective(source); 196 | if (directive == null && metadata.enforceLangPragma) { 197 | // eslint-disable-next-line no-console 198 | if (this.logging) console.log(`skipping module ${metadata.entryPath}`); 199 | return new SweetModule(path, List.of()); 200 | } 201 | let stxl = this.read(source); 202 | let outScope = freshScope('outsideEdge'); 203 | let inScope = freshScope('insideEdge0'); 204 | // the compiler starts at phase 0, with an empty environment and store 205 | let compiler = new Compiler( 206 | 0, 207 | new Env(), 208 | this.freshStore(), 209 | _.merge(this.context, { 210 | currentScope: [outScope, inScope], 211 | cwd: path, 212 | isEntrypoint: metadata.isEntrypoint, 213 | }), 214 | ); 215 | return new SweetModule( 216 | path, 217 | compiler.compile( 218 | stxl.map(s => 219 | // $FlowFixMe: flow doesn't know about reduce yet 220 | s.reduce( 221 | new ScopeReducer( 222 | [ 223 | { scope: outScope, phase: ALL_PHASES, flip: false }, 224 | { scope: inScope, phase: 0, flip: false }, 225 | ], 226 | this.context.bindings, 227 | ), 228 | ), 229 | ), 230 | ), 231 | ); 232 | } 233 | } 234 | 235 | const langDirectiveRegexp = /\s*('lang .*')/; 236 | function getLangDirective(source: string) { 237 | let match = source.match(langDirectiveRegexp); 238 | if (match) { 239 | return match[1]; 240 | } 241 | return null; 242 | } 243 | -------------------------------------------------------------------------------- /src/sweet-module.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Term, * as T from 'sweet-spec'; 3 | import * as _ from 'ramda'; 4 | import * as S from './sweet-spec-utils'; 5 | import codegen from './codegen'; 6 | import { List } from 'immutable'; 7 | import SweetToShiftReducer from './sweet-to-shift-reducer.js'; 8 | import Syntax from './syntax'; 9 | 10 | const extractDeclaration = _.cond([ 11 | [S.isExport, _.prop('declaration')], 12 | [S.isExportDefault, _.prop('body')], 13 | [ 14 | _.T, 15 | term => { 16 | throw new Error(`Expecting an Export or ExportDefault but got ${term}`); 17 | }, 18 | ], 19 | ]); 20 | 21 | const ExpSpec = x => ({ 22 | exportedName: x, 23 | }); 24 | 25 | const extractDeclarationNames = _.cond([ 26 | [S.isVariableDeclarator, ({ binding }) => List.of(binding.name)], 27 | [ 28 | S.isVariableDeclaration, 29 | ({ declarators }) => declarators.flatMap(extractDeclarationNames), 30 | ], 31 | [S.isFunctionDeclaration, ({ name }) => List.of(name.name)], 32 | [S.isClassDeclaration, ({ name }) => List.of(name.name)], 33 | ]); 34 | 35 | const extractDeclarationSpecifiers = _.cond([ 36 | [S.isVariableDeclarator, ({ binding }) => List.of(ExpSpec(binding.name))], 37 | [ 38 | S.isVariableDeclaration, 39 | ({ declarators }) => declarators.flatMap(extractDeclarationSpecifiers), 40 | ], 41 | [S.isFunctionDeclaration, ({ name }) => List.of(ExpSpec(name.name))], 42 | [S.isClassDeclaration, ({ name }) => List.of(ExpSpec(name.name))], 43 | ]); 44 | 45 | type ExportSpecifier = { 46 | name?: Syntax, 47 | exportedName: Syntax, 48 | }; 49 | 50 | function extractSpecifiers(term: any): List { 51 | if (S.isExport(term)) { 52 | return extractDeclarationSpecifiers(term.declaration); 53 | } else if (S.isExportDefault(term)) { 54 | return List(); 55 | } else if (S.isExportFrom(term)) { 56 | return term.namedExports; 57 | } else if (S.isExportLocals(term)) { 58 | return term.namedExports.map(({ name, exportedName }) => ({ 59 | name: name == null ? null : name.name, 60 | exportedName: exportedName, 61 | })); 62 | } 63 | throw new Error(`Unknown export type`); 64 | } 65 | 66 | function wrapStatement(declaration: Term) { 67 | if (S.isVariableDeclaration(declaration)) { 68 | return new T.VariableDeclarationStatement({ 69 | declaration, 70 | }); 71 | } 72 | return declaration; 73 | } 74 | 75 | const memoSym = Symbol('memo'); 76 | 77 | function makeVarDeclStmt(name: T.BindingIdentifier, expr: T.Expression) { 78 | return new T.VariableDeclarationStatement({ 79 | declaration: new T.VariableDeclaration({ 80 | kind: 'var', 81 | declarators: List.of( 82 | new T.VariableDeclarator({ 83 | binding: name, 84 | init: expr, 85 | }), 86 | ), 87 | }), 88 | }); 89 | } 90 | 91 | export default class SweetModule { 92 | path: string; 93 | items: List; 94 | directives: List; 95 | imports: List; 96 | exports: List; 97 | exportedNames: List; 98 | defaultExport: any; 99 | 100 | runtime: List; 101 | compiletime: List; 102 | 103 | constructor(path: string, items: List) { 104 | let moreDirectives = true; 105 | let directives = []; 106 | let body = []; 107 | let imports = []; 108 | let exports = []; 109 | this.path = path; 110 | this.exportedNames = List(); 111 | for (let item of items) { 112 | if ( 113 | moreDirectives && 114 | item instanceof T.ExpressionStatement && 115 | item.expression instanceof T.LiteralStringExpression 116 | ) { 117 | directives.push(item.expression.value); 118 | continue; 119 | } else { 120 | moreDirectives = false; 121 | } 122 | 123 | if (item instanceof T.ImportDeclaration) { 124 | imports.push(item); 125 | } else if (item instanceof T.ExportDeclaration) { 126 | if (S.isExport(item)) { 127 | let decl = extractDeclaration(item); 128 | let stmt = wrapStatement(decl); 129 | let names = extractDeclarationNames(decl); 130 | body.push(stmt); 131 | // TODO: support ExportFrom 132 | let exp = new T.ExportLocals({ 133 | moduleSpecifier: null, 134 | namedExports: names.map( 135 | name => 136 | new T.ExportLocalSpecifier({ 137 | name: new T.IdentifierExpression({ 138 | name, 139 | }), 140 | exportedName: name, 141 | }), 142 | ), 143 | }); 144 | body.push(exp); 145 | exports.push(exp); 146 | this.exportedNames = this.exportedNames.concat( 147 | extractSpecifiers(exp), 148 | ); 149 | } else if (item instanceof T.ExportLocals) { 150 | let exp = new T.ExportLocals({ 151 | namedExports: item.namedExports.map(({ name, exportedName }) => { 152 | if (name == null) { 153 | return new T.ExportLocalSpecifier({ 154 | name: new T.IdentifierExpression({ 155 | name: exportedName, 156 | }), 157 | exportedName, 158 | }); 159 | } 160 | return new T.ExportLocalSpecifier({ 161 | name, 162 | exportedName, 163 | }); 164 | }), 165 | }); 166 | body.push(exp); 167 | exports.push(exp); 168 | this.exportedNames = this.exportedNames.concat( 169 | extractSpecifiers(exp), 170 | ); 171 | } else { 172 | exports.push(item); 173 | body.push(item); 174 | this.exportedNames = this.exportedNames.concat( 175 | extractSpecifiers(item), 176 | ); 177 | if (S.isExportDefault(item)) { 178 | this.defaultExport = Syntax.fromIdentifier('_default'); 179 | this.exportedNames = this.exportedNames.push( 180 | ExpSpec(this.defaultExport), 181 | ); 182 | } 183 | } 184 | } else { 185 | body.push(item); 186 | } 187 | } 188 | this.items = List(body); 189 | this.imports = List(imports); 190 | this.exports = List(exports); 191 | this.directives = List(directives); 192 | } 193 | 194 | // $FlowFixMe: flow doesn't support computed property keys yet 195 | [memoSym]() { 196 | let runtime = [], 197 | compiletime = []; 198 | for (let item of this.items) { 199 | if (S.isExportDeclaration(item)) { 200 | if (S.isExportDefault(item)) { 201 | let decl = extractDeclaration(item); 202 | let def = new T.BindingIdentifier({ 203 | name: this.defaultExport, 204 | }); 205 | if (S.isFunctionDeclaration(decl) || S.isClassDeclaration(decl)) { 206 | runtime.push(decl); 207 | // extract name and bind it to _default 208 | runtime.push( 209 | makeVarDeclStmt( 210 | def, 211 | new T.IdentifierExpression({ 212 | name: decl.name.name, 213 | }), 214 | ), 215 | ); 216 | } else { 217 | // expression so bind it to _default 218 | let stmt = makeVarDeclStmt(def, decl); 219 | if (S.isCompiletimeStatement(stmt)) { 220 | compiletime.push(stmt); 221 | } else { 222 | runtime.push(stmt); 223 | } 224 | } 225 | } 226 | } else { 227 | if (S.isCompiletimeStatement(item)) { 228 | compiletime.push(item); 229 | } else { 230 | runtime.push(item); 231 | } 232 | } 233 | } 234 | this.runtime = List(runtime); 235 | this.compiletime = List(compiletime); 236 | } 237 | 238 | runtimeItems() { 239 | if (this.runtime == null) { 240 | // $FlowFixMe: flow doesn't support computed property keys yet 241 | this[memoSym](); 242 | } 243 | return this.runtime; 244 | } 245 | 246 | compiletimeItems() { 247 | if (this.compiletime == null) { 248 | // $FlowFixMe: flow doesn't support computed property keys yet 249 | this[memoSym](); 250 | } 251 | return this.compiletime; 252 | } 253 | 254 | parse() { 255 | return new T.Module({ 256 | items: (this.imports: any).concat(this.items), 257 | directives: this.directives, 258 | // $FlowFixMe: flow doesn't know about reduce yet 259 | }).reduce(new SweetToShiftReducer(0)); 260 | } 261 | 262 | codegen() { 263 | return codegen(this.parse()).code; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/sweet-spec-utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as T from 'sweet-spec'; 4 | import * as _ from 'ramda'; 5 | 6 | export const isImportDeclaration = _.is(T.ImportDeclaration); 7 | 8 | export const isExportDeclaration = _.is(T.ExportDeclaration); 9 | export const isExport = _.is(T.Export); 10 | export const isExportDefault = _.is(T.ExportDefault); 11 | export const isExportFrom = _.is(T.ExportFrom); 12 | export const isExportLocals = _.is(T.ExportLocals); 13 | 14 | export const isVariableDeclaration = _.is(T.VariableDeclaration); 15 | export const isVariableDeclarator = _.is(T.VariableDeclarator); 16 | export const isSyntaxVariableDeclartion = _.both( 17 | isVariableDeclaration, 18 | _.either(_.propEq('kind', 'syntax'), _.propEq('kind', 'syntaxrec')), 19 | ); 20 | 21 | export const isVariableDeclarationStatement = _.is( 22 | T.VariableDeclarationStatement, 23 | ); 24 | export const isSyntaxDeclarationStatement = (term: any) => { 25 | // syntax m = ... 26 | // syntaxrec m = ... 27 | return ( 28 | isVariableDeclarationStatement(term) && 29 | term.declaration.type === 'VariableDeclaration' && 30 | (term.declaration.kind === 'syntax' || 31 | term.declaration.kind === 'syntaxrec' || 32 | term.declaration.kind === 'operator') 33 | ); 34 | }; 35 | 36 | export const isCompiletimeStatement = isSyntaxDeclarationStatement; 37 | 38 | export const isFunctionDeclaration = _.is(T.FunctionDeclaration); 39 | export const isClassDeclaration = _.is(T.ClassDeclaration); 40 | -------------------------------------------------------------------------------- /src/sweet-to-shift-reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Term, * as S from 'sweet-spec'; 3 | import { complement } from 'ramda'; 4 | import { List } from 'immutable'; 5 | 6 | import { isEmptyStatement } from './terms'; 7 | 8 | import type Syntax from './syntax.js'; 9 | 10 | const notEmptyStatement = complement(isEmptyStatement); 11 | 12 | // $FlowFixMe: flow doesn't know about CloneReducer yet 13 | export default class extends Term.CloneReducer { 14 | phase: number; 15 | 16 | constructor(phase: number) { 17 | super(); 18 | this.phase = phase; 19 | } 20 | 21 | reduceModule(t: Term, s: { directives: List, items: List }) { 22 | return new S.Module({ 23 | directives: s.directives 24 | .filter(d => !d.startsWith('lang')) 25 | .map(d => ({ type: 'Directive', rawValue: d })) 26 | .toArray(), 27 | items: s.items.toArray().filter(notEmptyStatement), 28 | }); 29 | } 30 | 31 | reduceIdentifierExpression(t: Term, s: { name: Syntax }) { 32 | return new S.IdentifierExpression({ 33 | name: s.name.resolve(this.phase), 34 | }); 35 | } 36 | 37 | reduceStaticPropertyName(t: Term, s: { value: Syntax }) { 38 | return new S.StaticPropertyName({ 39 | value: s.value.val().toString(), 40 | }); 41 | } 42 | 43 | reduceBindingIdentifier(t: Term, s: { name: Syntax }) { 44 | return new S.BindingIdentifier({ 45 | name: s.name.resolve(this.phase), 46 | }); 47 | } 48 | 49 | reduceAssignmentTargetIdentifier(t: Term, s: { name: Syntax }) { 50 | return new S.AssignmentTargetIdentifier({ 51 | name: s.name.resolve(this.phase), 52 | }); 53 | } 54 | 55 | reduceStaticMemberExpression(t: Term, s: { object: any, property: Syntax }) { 56 | return new S.StaticMemberExpression({ 57 | object: s.object, 58 | property: s.property.val(), 59 | }); 60 | } 61 | 62 | reduceStaticMemberAssignmentTarget( 63 | t: Term, 64 | s: { object: any, property: Syntax }, 65 | ) { 66 | return new S.StaticMemberAssignmentTarget({ 67 | object: s.object, 68 | property: s.property.val(), 69 | }); 70 | } 71 | 72 | reduceFunctionBody( 73 | t: Term, 74 | s: { statements: List, directives: List }, 75 | ) { 76 | return new S.FunctionBody({ 77 | directives: s.directives.toArray(), 78 | statements: s.statements.toArray().filter(notEmptyStatement), 79 | }); 80 | } 81 | 82 | reduceVariableDeclarationStatement(t: any, s: { declaration: any }) { 83 | if ( 84 | t.declaration.kind === 'syntax' || 85 | t.declaration.kind === 'syntaxrec' || 86 | t.declaration.kind === 'operator' 87 | ) { 88 | return new S.EmptyStatement(); 89 | } 90 | return new S.VariableDeclarationStatement({ 91 | declaration: s.declaration, 92 | }); 93 | } 94 | 95 | reduceVariableDeclaration(t: Term, s: { kind: any, declarators: List }) { 96 | return new S.VariableDeclaration({ 97 | kind: s.kind, 98 | declarators: s.declarators.toArray(), 99 | }); 100 | } 101 | 102 | reduceCallExpression(t: Term, s: { callee: any, arguments: List }) { 103 | return new S.CallExpression({ 104 | callee: s.callee, 105 | arguments: s.arguments.toArray(), 106 | }); 107 | } 108 | 109 | reduceArrayExpression(t: Term, s: { elements: List }) { 110 | return new S.ArrayExpression({ 111 | elements: s.elements.toArray(), 112 | }); 113 | } 114 | 115 | reduceImportNamespace( 116 | t: Term, 117 | s: { 118 | defaultBinding: S.BindingIdentifier, 119 | moduleSpecifier: Syntax, 120 | namespaceBinding: S.BindingIdentifier, 121 | }, 122 | ) { 123 | if (s.forSyntax) { 124 | return new S.EmptyStatement(); 125 | } 126 | return t; 127 | } 128 | 129 | reduceImport( 130 | t: Term, 131 | s: { 132 | defaultBinding: S.BindingIdentifier, 133 | moduleSpecifier: Syntax, 134 | namedImports: List, 135 | }, 136 | ) { 137 | if (s.forSyntax) { 138 | return new S.EmptyStatement(); 139 | } 140 | return new S.Import({ 141 | forSyntax: false, 142 | defaultBinding: s.defaultBinding, 143 | moduleSpecifier: s.moduleSpecifier.val(), 144 | namedImports: s.namedImports.toArray(), 145 | }); 146 | } 147 | 148 | reduceBlock(t: Term, s: { statements: List }) { 149 | return new S.Block({ 150 | statements: s.statements.toArray().filter(notEmptyStatement), 151 | }); 152 | } 153 | 154 | reduceExportFromSpecifier(t: Term, s: { name: any, exportedName?: Syntax }) { 155 | return new S.ExportFromSpecifier({ 156 | name: s.name.resolve(0), 157 | exportedName: s.exportedName == null ? null : s.exportedName.val(), 158 | }); 159 | } 160 | 161 | reduceExportLocalSpecifier(t: Term, s: { name: any, exportedName?: Syntax }) { 162 | return new S.ExportLocalSpecifier({ 163 | name: s.name, 164 | exportedName: s.exportedName == null ? null : s.exportedName.val(), 165 | }); 166 | } 167 | 168 | reduceExportFrom( 169 | t: Term, 170 | s: { moduleSpecifier?: Syntax, namedExports: List }, 171 | ) { 172 | return new S.ExportFrom({ 173 | moduleSpecifier: 174 | s.moduleSpecifier != null ? s.moduleSpecifier.val() : null, 175 | namedExports: s.namedExports.toArray(), 176 | }); 177 | } 178 | 179 | reduceExportLocals( 180 | t: Term, 181 | s: { moduleSpecifier?: Syntax, namedExports: List }, 182 | ) { 183 | return new S.ExportLocals({ 184 | moduleSpecifier: 185 | s.moduleSpecifier != null ? s.moduleSpecifier.val() : null, 186 | namedExports: s.namedExports.toArray(), 187 | }); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/sweet.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type SweetLoader from './sweet-loader'; 3 | import { transform as babel } from 'babel-core'; 4 | 5 | type CompileOptions = { 6 | refererName?: string, 7 | debugStore?: Map, 8 | noBabel?: boolean, 9 | loader: SweetLoader, 10 | }; 11 | 12 | function compileModule( 13 | entryPath: string, 14 | loader: SweetLoader, 15 | refererName?: string, 16 | ) { 17 | return loader.compile(entryPath, { 18 | refererName, 19 | enforceLangPragma: false, 20 | isEntrypoint: true, 21 | }); 22 | } 23 | 24 | export function parse( 25 | entryPath: string, 26 | loader: SweetLoader, 27 | options?: CompileOptions, 28 | ) { 29 | let refererName; 30 | if (options != null) { 31 | refererName = options.refererName; 32 | } 33 | return compileModule(entryPath, loader, refererName).parse(); 34 | } 35 | 36 | export function compile( 37 | entryPath: string, 38 | loader: SweetLoader, 39 | options?: CompileOptions, 40 | ) { 41 | let refererName, 42 | noBabel = true; 43 | if (options != null) { 44 | refererName = options.refererName; 45 | noBabel = options.noBabel; 46 | } 47 | let code = compileModule(entryPath, loader, refererName).codegen(); 48 | if (noBabel) { 49 | return { 50 | code, 51 | }; 52 | } 53 | return babel(code, { 54 | babelrc: true, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/symbol.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | let internedMap: Map = new Map(); 3 | 4 | let counter = 0; 5 | 6 | export function gensym(name: string) { 7 | let prefix = name == null ? 's_' : name + '_'; 8 | let sym = new Symbol(prefix + counter); 9 | counter++; 10 | return sym; 11 | } 12 | 13 | class Symbol { 14 | name: string; 15 | 16 | constructor(name: string) { 17 | this.name = name; 18 | } 19 | toString() { 20 | return this.name; 21 | } 22 | } 23 | 24 | function makeSymbol(name: string): Symbol { 25 | let s = internedMap.get(name); 26 | if (s) { 27 | return s; 28 | } else { 29 | let sym = new Symbol(name); 30 | internedMap.set(name, sym); 31 | return sym; 32 | } 33 | } 34 | 35 | export { makeSymbol as Symbol, Symbol as SymbolClass }; 36 | -------------------------------------------------------------------------------- /src/template-processor.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import _ from 'ramda'; 3 | import { assert } from './errors'; 4 | import * as T from 'sweet-spec'; 5 | import Syntax from './syntax'; 6 | 7 | /* 8 | Given a syntax list like: 9 | 10 | [foo, bar, $, { 42, +, 24 }, baz] 11 | 12 | convert it to: 13 | 14 | [foo, bar, $, { 0 }, baz] 15 | 16 | and return another list with the interpolated values at the corresponding 17 | positions. 18 | 19 | Requires either lookahead/lookbehind of one (to see the $). 20 | */ 21 | 22 | const isDolar = (s: T.SyntaxTerm) => 23 | s instanceof T.RawSyntax && 24 | typeof s.value.match === 'function' && 25 | s.value.match('identifier') && 26 | s.value.val() === '$'; 27 | const isDelimiter = (s: T.SyntaxTerm) => s instanceof T.RawDelimiter; 28 | const isBraces = (s: T.SyntaxTerm) => 29 | s instanceof T.RawDelimiter && s.kind === 'braces'; 30 | const isParens = (s: T.SyntaxTerm) => 31 | s instanceof T.RawDelimiter && s.kind === 'parens'; 32 | const isBrackets = (s: T.SyntaxTerm) => 33 | s instanceof T.RawDelimiter && s.kind === 'brackets'; 34 | 35 | type DelimKind = 'braces' | 'parens' | 'brackets'; 36 | 37 | const mkDelimiter = ( 38 | kind: DelimKind, 39 | inner: List, 40 | from: T.RawDelimiter, 41 | ) => { 42 | return new T.RawDelimiter({ 43 | kind, 44 | // $FlowFixMe: flow doesn't know arrays are actually lists 45 | inner: List.of(from.inner.first()).concat(inner).concat(from.inner.last()), 46 | }); 47 | }; 48 | 49 | const insertIntoDelimiter = _.cond([ 50 | [isBraces, (s, r) => mkDelimiter('braces', r, s)], 51 | [isParens, (s, r) => mkDelimiter('parens', r, s)], 52 | [isBrackets, (s, r) => mkDelimiter('brackets', r, s)], 53 | ]); 54 | 55 | const process = ( 56 | acc: { template: List, interp: List> }, 57 | s: T.SyntaxTerm, 58 | ) => { 59 | if (isBraces(s) && isDolar(acc.template.last())) { 60 | let idx = Syntax.fromNumber(acc.interp.size, s.inner.first().value); 61 | return { 62 | template: acc.template.push( 63 | mkDelimiter( 64 | 'braces', 65 | List.of( 66 | new T.RawSyntax({ 67 | value: idx, 68 | }), 69 | ), 70 | s, 71 | ), 72 | ), 73 | interp: acc.interp.push(s.inner.slice(1, s.inner.size - 1)), 74 | }; 75 | } else if (isDelimiter(s)) { 76 | let innerResult = processTemplate( 77 | s.inner.slice(1, s.inner.size - 1), 78 | acc.interp, 79 | ); 80 | return { 81 | template: acc.template.push(insertIntoDelimiter(s, innerResult.template)), 82 | interp: innerResult.interp, 83 | }; 84 | } else { 85 | return { 86 | template: acc.template.push(s), 87 | interp: acc.interp, 88 | }; 89 | } 90 | }; 91 | 92 | function getLineNumber(t: T.SyntaxTerm) { 93 | if (t instanceof T.RawSyntax) { 94 | return t.value.lineNumber(); 95 | } 96 | return t.inner.first().value.lineNumber(); 97 | } 98 | 99 | function setLineNumber(t: T.Term | List, lineNumber: number) { 100 | if (t instanceof T.RawSyntax) { 101 | return t.extend({ 102 | value: t.value.setLineNumber(lineNumber), 103 | }); 104 | } else if (t instanceof T.RawDelimiter) { 105 | return t.extend({ 106 | inner: t.inner.map(tt => setLineNumber(tt, lineNumber)), 107 | }); 108 | } else if (List.isList(t)) { 109 | return t.map(tt => setLineNumber(tt, lineNumber)); 110 | } 111 | // TODO: need to handle line numbers for all AST nodes 112 | return t; 113 | } 114 | 115 | function cloneLineNumber(to: T.Term, from: T.SyntaxTerm) { 116 | if (from && to) { 117 | return setLineNumber(to, getLineNumber(from)); 118 | } 119 | return to; 120 | } 121 | 122 | const replace = ( 123 | acc: { template: List, rep: List> }, 124 | s: T.SyntaxTerm, 125 | ) => { 126 | let last = acc.template.get(-1); 127 | let beforeLast = acc.template.get(-2); 128 | if (isBraces(s) && isDolar(last)) { 129 | let index = s.inner.get(1).value.val(); 130 | assert(acc.rep.size > index, 'unknown replacement value'); 131 | // TODO: figure out holistic solution to line nubmers and ASI 132 | let replacement = cloneLineNumber(acc.rep.get(index), beforeLast); 133 | // let replacement = acc.rep.get(index); 134 | return { 135 | template: acc.template.pop().concat(replacement), 136 | rep: acc.rep, 137 | }; 138 | } else if (isDelimiter(s)) { 139 | let innerResult = replaceTemplate( 140 | s.inner.slice(1, s.inner.size - 1), 141 | acc.rep, 142 | ); 143 | return { 144 | template: acc.template.push(insertIntoDelimiter(s, innerResult)), 145 | rep: acc.rep, 146 | }; 147 | } else { 148 | return { 149 | template: acc.template.push(s), 150 | rep: acc.rep, 151 | }; 152 | } 153 | }; 154 | 155 | export function processTemplate( 156 | temp: List, 157 | interp: List = List(), 158 | ) { 159 | return temp.reduce(process, { template: List(), interp }); 160 | } 161 | 162 | export function replaceTemplate(temp: List, rep: any) { 163 | return temp.reduce(replace, { template: List(), rep }).template; 164 | } 165 | -------------------------------------------------------------------------------- /src/terms.js: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | import Term from 'sweet-spec'; 3 | 4 | // bindings 5 | export const isBindingWithDefault = R.whereEq({ type: 'BindingWithDefault' }); 6 | export const isBindingIdentifier = R.whereEq({ type: 'BindingIdentifier' }); 7 | export const isArrayBinding = R.whereEq({ type: 'ArrayBinding' }); 8 | export const isObjectBinding = R.whereEq({ type: 'ObjectBinding' }); 9 | export const isBindingPropertyIdentifier = R.whereEq({ 10 | type: 'BindingPropertyIdentifier', 11 | }); 12 | export const isBindingPropertyProperty = R.whereEq({ 13 | type: 'BindingPropertyIdentifier', 14 | }); 15 | 16 | // class 17 | export const isClassExpression = R.whereEq({ type: 'ClassExpression' }); 18 | export const isClassDeclaration = R.whereEq({ type: 'ClassDeclaration' }); 19 | export const isClassElement = R.whereEq({ type: 'ClassElement' }); 20 | 21 | // modules 22 | export const isModule = R.whereEq({ type: 'Module' }); 23 | export const isImport = R.whereEq({ type: 'Import' }); 24 | export const isImportNamespace = R.whereEq({ type: 'ImportNamespace' }); 25 | export const isImportSpecifier = R.whereEq({ type: 'ImportSpecifier' }); 26 | export const isExportAllFrom = R.whereEq({ type: 'ExportAllFrom' }); 27 | export const isExportFrom = R.whereEq({ type: 'ExportFrom' }); 28 | export const isExport = R.whereEq({ type: 'Export' }); 29 | export const isExportDefault = R.whereEq({ type: 'ExportDefault' }); 30 | export const isExportFromSpecifier = R.whereEq({ type: 'ExportFromSpecifier' }); 31 | export const isExportLocalSpecifier = R.whereEq({ 32 | type: 'ExportLocalSpecifier', 33 | }); 34 | 35 | // property definition 36 | export const isMethod = R.whereEq({ type: 'Method' }); 37 | export const isGetter = R.whereEq({ type: 'Getter' }); 38 | export const isSetter = R.whereEq({ type: 'Setter' }); 39 | export const isDataProperty = R.whereEq({ type: 'DataProperty' }); 40 | export const isShorthandProperty = R.whereEq({ type: 'ShorthandProperty' }); 41 | export const isComputedPropertyName = R.whereEq({ 42 | type: 'ComputedPropertyName', 43 | }); 44 | export const isStaticPropertyName = R.whereEq({ type: 'StaticPropertyName' }); 45 | 46 | // literals 47 | export const isLiteralBooleanExpression = R.whereEq({ 48 | type: 'LiteralBooleanExpression', 49 | }); 50 | export const isLiteralInfinityExpression = R.whereEq({ 51 | type: 'LiteralInfinityExpression', 52 | }); 53 | export const isLiteralNullExpression = R.whereEq({ 54 | type: 'LiteralNullExpression', 55 | }); 56 | export const isLiteralNumericExpression = R.whereEq({ 57 | type: 'LiteralNumericExpression', 58 | }); 59 | export const isLiteralRegExpExpression = R.whereEq({ 60 | type: 'LiteralRegExpExpression', 61 | }); 62 | export const isLiteralStringExpression = R.whereEq({ 63 | type: 'LiteralStringExpression', 64 | }); 65 | 66 | // expressions 67 | export const isArrayExpression = R.whereEq({ type: 'ArrayExpression' }); 68 | export const isArrowExpression = R.whereEq({ type: 'ArrowExpression' }); 69 | export const isAssignmentExpression = R.whereEq({ 70 | type: 'AssignmentExpression', 71 | }); 72 | export const isBinaryExpression = R.whereEq({ type: 'BinaryExpression' }); 73 | export const isCallExpression = R.whereEq({ type: 'CallExpression' }); 74 | export const isComputedAssignmentExpression = R.whereEq({ 75 | type: 'ComputedAssignmentExpression', 76 | }); 77 | export const isComputedMemberExpression = R.whereEq({ 78 | type: 'ComputedMemberExpression', 79 | }); 80 | export const isConditionalExpression = R.whereEq({ 81 | type: 'ConditionalExpression', 82 | }); 83 | export const isFunctionExpression = R.whereEq({ type: 'FunctionExpression' }); 84 | export const isIdentifierExpression = R.whereEq({ 85 | type: 'IdentifierExpression', 86 | }); 87 | export const isNewExpression = R.whereEq({ type: 'NewExpression' }); 88 | export const isNewTargetExpression = R.whereEq({ type: 'NewTargetExpression' }); 89 | export const isObjectExpression = R.whereEq({ type: 'ObjectExpression' }); 90 | export const isUnaryExpression = R.whereEq({ type: 'UnaryExpression' }); 91 | export const isStaticMemberExpression = R.whereEq({ 92 | type: 'StaticMemberExpression', 93 | }); 94 | export const isTemplateExpression = R.whereEq({ type: 'TemplateExpression' }); 95 | export const isThisExpression = R.whereEq({ type: 'ThisExpression' }); 96 | export const isUpdateExpression = R.whereEq({ type: 'UpdateExpression' }); 97 | export const isYieldExpression = R.whereEq({ type: 'YieldExpression' }); 98 | export const isYieldGeneratorExpression = R.whereEq({ 99 | type: 'YieldGeneratorExpression', 100 | }); 101 | 102 | // statements 103 | export const isBlockStatement = R.whereEq({ type: 'BlockStatement' }); 104 | export const isBreakStatement = R.whereEq({ type: 'BreakStatement' }); 105 | export const isContinueStatement = R.whereEq({ type: 'ContinueStatement' }); 106 | export const isCompoundAssignmentExpression = R.whereEq({ 107 | type: 'CompoundAssignmentExpression', 108 | }); 109 | export const isDebuggerStatement = R.whereEq({ type: 'DebuggerStatement' }); 110 | export const isDoWhileStatement = R.whereEq({ type: 'DoWhileStatement' }); 111 | export const isEmptyStatement = R.whereEq({ type: 'EmptyStatement' }); 112 | export const isExpressionStatement = R.whereEq({ type: 'ExpressionStatement' }); 113 | export const isForInStatement = R.whereEq({ type: 'ForInStatement' }); 114 | export const isForOfStatement = R.whereEq({ type: 'ForOfStatement' }); 115 | export const isForStatement = R.whereEq({ type: 'ForStatement' }); 116 | export const isIfStatement = R.whereEq({ type: 'IfStatement' }); 117 | export const isLabeledStatement = R.whereEq({ type: 'LabeledStatement' }); 118 | export const isReturnStatement = R.whereEq({ type: 'ReturnStatement' }); 119 | export const isSwitchStatement = R.whereEq({ type: 'SwitchStatement' }); 120 | export const isSwitchStatementWithDefault = R.whereEq({ 121 | type: 'SwitchStatementWithDefault', 122 | }); 123 | export const isThrowStatement = R.whereEq({ type: 'ThrowStatement' }); 124 | export const isTryCatchStatement = R.whereEq({ type: 'TryCatchStatement' }); 125 | export const isTryFinallyStatement = R.whereEq({ type: 'TryFinallyStatement' }); 126 | export const isVariableDeclarationStatement = R.whereEq({ 127 | type: 'VariableDeclarationStatement', 128 | }); 129 | export const isWhileStatement = R.whereEq({ type: 'WhileStatement' }); 130 | export const isWithStatement = R.whereEq({ type: 'WithStatement' }); 131 | 132 | // other 133 | export const isBlock = R.whereEq({ type: 'Block' }); 134 | export const isCatchClause = R.whereEq({ type: 'CatchClause' }); 135 | export const isDirective = R.whereEq({ type: 'Directive' }); 136 | export const isFormalParameters = R.whereEq({ type: 'FormalParameters' }); 137 | export const isFunctionBody = R.whereEq({ type: 'FunctionBody' }); 138 | export const isFunctionDeclaration = R.whereEq({ type: 'FunctionDeclaration' }); 139 | export const isScript = R.whereEq({ type: 'Script' }); 140 | export const isSpreadElement = R.whereEq({ type: 'SpreadElement' }); 141 | export const isSuper = R.whereEq({ type: 'Super' }); 142 | export const isSwitchCase = R.whereEq({ type: 'SwitchCase' }); 143 | export const isSwitchDefault = R.whereEq({ type: 'SwitchDefault' }); 144 | export const isTemplateElement = R.whereEq({ type: 'TemplateElement' }); 145 | export const isSyntaxTemplate = R.whereEq({ type: 'SyntaxTemplate' }); 146 | export const isVariableDeclaration = R.whereEq({ type: 'VariableDeclaration' }); 147 | export const isVariableDeclarator = R.whereEq({ type: 'VariableDeclarator' }); 148 | export const isEOF = R.whereEq({ type: 'EOF' }); 149 | export const isSyntaxDeclaration = R.both( 150 | isVariableDeclaration, 151 | R.whereEq({ kind: 'syntax' }), 152 | ); 153 | export const isSyntaxrecDeclaration = R.both( 154 | isVariableDeclaration, 155 | R.whereEq({ kind: 'syntaxrec' }), 156 | ); 157 | export const isFunctionTerm = R.either( 158 | isFunctionDeclaration, 159 | isFunctionExpression, 160 | ); 161 | export const isFunctionWithName = R.and( 162 | isFunctionTerm, 163 | R.complement(R.where({ name: R.isNil })), 164 | ); 165 | export const isParenthesizedExpression = R.whereEq({ 166 | type: 'ParenthesizedExpression', 167 | }); 168 | export const isExportSyntax = R.both(isExport, exp => 169 | R.or( 170 | isSyntaxDeclaration(exp.declaration), 171 | isSyntaxrecDeclaration(exp.declaration), 172 | ), 173 | ); 174 | export const isSyntaxDeclarationStatement = R.both( 175 | isVariableDeclarationStatement, 176 | decl => isCompiletimeDeclaration(decl.declaration), 177 | ); 178 | 179 | export const isCompiletimeDeclaration = R.either( 180 | isSyntaxDeclaration, 181 | isSyntaxrecDeclaration, 182 | ); 183 | export const isCompiletimeStatement = term => { 184 | return ( 185 | term instanceof Term && 186 | isVariableDeclarationStatement(term) && 187 | isCompiletimeDeclaration(term.declaration) 188 | ); 189 | }; 190 | export const isImportDeclaration = R.either(isImport, isImportNamespace); 191 | export const isExportDeclaration = R.either( 192 | isExport, 193 | isExportDefault, 194 | isExportFrom, 195 | isExportAllFrom, 196 | ); 197 | -------------------------------------------------------------------------------- /src/token-expander.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Term, * as S from 'sweet-spec'; 3 | import { List } from 'immutable'; 4 | import { Enforester } from './enforester'; 5 | import TermExpander from './term-expander.js'; 6 | import Env from './env'; 7 | import * as _ from 'ramda'; 8 | import * as T from './terms'; 9 | import { gensym } from './symbol'; 10 | import { VarBindingTransform, CompiletimeTransform } from './transforms'; 11 | import { evalCompiletimeValue } from './load-syntax'; 12 | import { freshScope } from './scope'; 13 | import { ALL_PHASES } from './syntax'; 14 | import ASTDispatcher from './ast-dispatcher'; 15 | import Syntax from './syntax.js'; 16 | import ScopeReducer from './scope-reducer'; 17 | import ModuleVisitor, { 18 | bindImports, 19 | isBoundToCompiletime, 20 | } from './module-visitor'; 21 | 22 | // $FlowFixMe: flow doesn't know about the CloneReducer yet 23 | class RegisterBindingsReducer extends Term.CloneReducer { 24 | useScope: any; 25 | phase: number; 26 | bindings: any; 27 | skipDup: boolean; 28 | env: Env; 29 | 30 | constructor( 31 | useScope: any, 32 | phase: number, 33 | skipDup: boolean, 34 | bindings: any, 35 | env: Env, 36 | ) { 37 | super(); 38 | this.useScope = useScope; 39 | this.phase = phase; 40 | this.bindings = bindings; 41 | this.skipDup = skipDup; 42 | this.env = env; 43 | } 44 | 45 | reduceBindingIdentifier(t: Term, s: { name: Syntax }) { 46 | let newName = s.name.removeScope(this.useScope, this.phase); 47 | let newBinding = gensym(newName.val()); 48 | this.bindings.add(newName, { 49 | binding: newBinding, 50 | phase: this.phase, 51 | skipDup: this.skipDup, 52 | }); 53 | this.env.set(newBinding.toString(), new VarBindingTransform(newName)); 54 | // $FlowFixMe: flow doesn't know about extend 55 | return t.extend({ 56 | name: newName, 57 | }); 58 | } 59 | } 60 | 61 | // $FlowFixMe: flow doesn't know about the CloneReducer yet 62 | class RegisterSyntaxBindingsReducer extends Term.CloneReducer { 63 | useScope: any; 64 | phase: number; 65 | bindings: any; 66 | env: Env; 67 | val: any; 68 | 69 | constructor(useScope: any, phase: number, bindings: any, env: Env, val: any) { 70 | super(); 71 | this.useScope = useScope; 72 | this.phase = phase; 73 | this.bindings = bindings; 74 | this.env = env; 75 | this.val = val; 76 | } 77 | 78 | reduceBindingIdentifier(t: Term, s: { name: Syntax }) { 79 | let newName = s.name.removeScope(this.useScope, this.phase); 80 | let newBinding = gensym(newName.val()); 81 | this.bindings.add(newName, { 82 | binding: newBinding, 83 | phase: this.phase, 84 | skipDup: false, 85 | }); 86 | let resolvedName = newName.resolve(this.phase); 87 | this.env.set(resolvedName, new CompiletimeTransform(this.val)); 88 | // $FlowFixMe: flow doesn't know about extend 89 | return t.extend({ 90 | name: newName, 91 | }); 92 | } 93 | } 94 | 95 | export default class TokenExpander extends ASTDispatcher { 96 | constructor(context: any) { 97 | super('expand', false); 98 | this.context = context; 99 | } 100 | 101 | expand(stxl: List) { 102 | let result = []; 103 | if (stxl.size === 0) { 104 | return List(result); 105 | } 106 | let prev = List(); 107 | let enf = new Enforester(stxl, prev, this.context); 108 | 109 | while (!enf.done) { 110 | result.push(this.dispatch(enf.enforest())); 111 | } 112 | 113 | return List(result); 114 | } 115 | 116 | expandVariableDeclarationStatement(term: S.VariableDeclarationStatement) { 117 | // $FlowFixMe: flow doesn't know about extend 118 | return term.extend({ 119 | declaration: this.registerVariableDeclaration(term.declaration), 120 | }); 121 | } 122 | 123 | expandFunctionDeclaration(term: Term) { 124 | return this.registerFunctionOrClass(term); 125 | } 126 | 127 | // TODO: think about function expressions 128 | 129 | registerImport(term: S.Import | S.ImportNamespace) { 130 | let path = term.moduleSpecifier.val(); 131 | let mod; 132 | let visitor = new ModuleVisitor(this.context); 133 | if (term.forSyntax) { 134 | mod = this.context.loader.get( 135 | path, 136 | this.context.phase + 1, 137 | this.context.cwd, 138 | ); 139 | this.context.store = visitor.visit( 140 | mod, 141 | this.context.phase + 1, 142 | this.context.store, 143 | mod.path, 144 | ); 145 | this.context.store = visitor.invoke( 146 | mod, 147 | this.context.phase + 1, 148 | this.context.store, 149 | mod.path, 150 | ); 151 | } else { 152 | mod = this.context.loader.get(path, this.context.phase, this.context.cwd); 153 | this.context.store = visitor.visit( 154 | mod, 155 | this.context.phase, 156 | this.context.store, 157 | mod.path, 158 | ); 159 | } 160 | bindImports( 161 | term, 162 | mod, 163 | this.context.phase, 164 | this.context, 165 | this.context.isEntrypoint, 166 | ); 167 | let defaultBinding = null; 168 | let namedImports = List(); 169 | if (term.defaultBinding != null) { 170 | if (!isBoundToCompiletime(term.defaultBinding.name, this.context.store)) { 171 | defaultBinding = term.defaultBinding; 172 | } 173 | } 174 | if (term instanceof S.Import) { 175 | namedImports = term.namedImports.filter( 176 | specifier => 177 | !isBoundToCompiletime(specifier.binding.name, this.context.store), 178 | ); 179 | if (defaultBinding == null && namedImports.size === 0) { 180 | return new S.EmptyStatement(); 181 | } 182 | return new S.Import({ 183 | forSyntax: term.forSyntax, 184 | moduleSpecifier: term.moduleSpecifier, 185 | defaultBinding, 186 | namedImports, 187 | }); 188 | } else if (term instanceof S.ImportNamespace) { 189 | return new S.ImportNamespace({ 190 | forSyntax: term.forSyntax, 191 | moduleSpecifier: term.moduleSpecifier, 192 | defaultBinding, 193 | namespaceBinding: term.namespaceBinding, 194 | }); 195 | } 196 | // return a new import filtered to just the runtime imports 197 | return term; 198 | } 199 | 200 | expandImport(term: S.Import) { 201 | return this.registerImport(term); 202 | } 203 | 204 | expandImportNamespace(term: S.ImportNamespace) { 205 | return this.registerImport(term); 206 | } 207 | 208 | expandExport(term: any) { 209 | if ( 210 | T.isFunctionDeclaration(term.declaration) || 211 | T.isClassDeclaration(term.declaration) 212 | ) { 213 | return term.extend({ 214 | declaration: this.registerFunctionOrClass(term.declaration), 215 | }); 216 | } else if (T.isVariableDeclaration(term.declaration)) { 217 | return term.extend({ 218 | declaration: this.registerVariableDeclaration(term.declaration), 219 | }); 220 | } 221 | return term; 222 | } 223 | 224 | registerFunctionOrClass(term: any) { 225 | let red = new RegisterBindingsReducer( 226 | this.context.useScope, 227 | this.context.phase, 228 | false, 229 | this.context.bindings, 230 | this.context.env, 231 | ); 232 | return term.extend({ 233 | name: term.name.reduce(red), 234 | }); 235 | } 236 | 237 | registerVariableDeclaration(term: any) { 238 | if ( 239 | term.kind === 'syntax' || 240 | term.kind === 'syntaxrec' || 241 | term.kind === 'operator' 242 | ) { 243 | return this.registerSyntaxDeclaration(term); 244 | } 245 | let red = new RegisterBindingsReducer( 246 | this.context.useScope, 247 | this.context.phase, 248 | term.kind === 'var', 249 | this.context.bindings, 250 | this.context.env, 251 | ); 252 | return term.extend({ 253 | declarators: term.declarators.map(decl => { 254 | return decl.extend({ 255 | binding: decl.binding.reduce(red), 256 | }); 257 | }), 258 | }); 259 | } 260 | 261 | registerSyntaxDeclaration(term: any) { 262 | if (term.kind === 'syntax' || term.kind === 'operator') { 263 | // syntax id^{a, b} = ^{a, b} 264 | // -> 265 | // syntaxrec id^{a,b,c} = function() { return <> } 266 | // syntaxrec id^{a,b} = ^{a,b,c} 267 | let scope = freshScope('nonrec'); 268 | let scopeReducer = new ScopeReducer( 269 | [{ scope: scope, phase: ALL_PHASES, flip: false }], 270 | this.context.bindings, 271 | ); 272 | term = term.extend({ 273 | declarators: term.declarators.map(decl => { 274 | let name = decl.binding.name; 275 | let nameAdded = name.addScope( 276 | scope, 277 | this.context.bindings, 278 | ALL_PHASES, 279 | ); 280 | let nameRemoved = name.removeScope( 281 | this.context.currentScope[this.context.currentScope.length - 1], 282 | this.context.phase, 283 | ); 284 | let newBinding = gensym(name.val()); 285 | this.context.bindings.addForward( 286 | nameAdded, 287 | nameRemoved, 288 | newBinding, 289 | this.context.phase, 290 | ); 291 | return decl.extend({ 292 | init: decl.init.reduce(scopeReducer), 293 | }); 294 | }), 295 | }); 296 | } 297 | // for syntax declarations we need to load the compiletime value 298 | // into the environment 299 | let compiletimeType = term.kind === 'operator' ? 'operator' : 'syntax'; 300 | return term.extend({ 301 | declarators: term.declarators.map(decl => { 302 | // each compiletime value needs to be expanded with a fresh 303 | // environment and in the next higher phase 304 | let syntaxExpander = new TermExpander( 305 | _.merge(this.context, { 306 | phase: this.context.phase + 1, 307 | env: new Env(), 308 | store: this.context.store, 309 | }), 310 | ); 311 | 312 | let init = syntaxExpander.expand(decl.init); 313 | let val = evalCompiletimeValue( 314 | init, 315 | _.merge(this.context, { 316 | phase: this.context.phase + 1, 317 | }), 318 | ); 319 | let red = new RegisterSyntaxBindingsReducer( 320 | this.context.useScope, 321 | this.context.phase, 322 | this.context.bindings, 323 | this.context.env, 324 | { 325 | type: compiletimeType, 326 | prec: decl.prec == null ? void 0 : decl.prec.val(), 327 | assoc: decl.assoc == null ? void 0 : decl.assoc.val(), 328 | f: val, 329 | }, 330 | ); 331 | return decl.extend({ binding: decl.binding.reduce(red), init }); 332 | }), 333 | }); 334 | } 335 | 336 | // registerSyntaxDeclarator(term) { 337 | // 338 | // } 339 | } 340 | -------------------------------------------------------------------------------- /src/transforms.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import SweetModule from './sweet-module'; 3 | import Syntax from './syntax'; 4 | 5 | export class FunctionDeclTransform {} 6 | export class VariableDeclTransform {} 7 | export class NewTransform {} 8 | export class ThrowTransform {} 9 | export class LetDeclTransform {} 10 | export class ConstDeclTransform {} 11 | export class TryTransform {} 12 | export class WhileTransform {} 13 | export class IfTransform {} 14 | export class ForTransform {} 15 | export class SwitchTransform {} 16 | export class BreakTransform {} 17 | export class ContinueTransform {} 18 | export class DoTransform {} 19 | export class WithTransform {} 20 | export class ImportTransform {} 21 | export class ExportTransform {} 22 | export class SuperTransform {} 23 | export class YieldTransform {} 24 | export class ThisTransform {} 25 | export class ClassTransform {} 26 | export class DefaultTransform {} 27 | export class DebuggerTransform {} 28 | export class SyntaxrecDeclTransform {} 29 | export class SyntaxDeclTransform {} 30 | export class OperatorDeclTransform {} 31 | export class ReturnStatementTransform {} 32 | export class AsyncTransform {} 33 | export class AwaitTransform {} 34 | export class ModuleNamespaceTransform { 35 | namespace: Syntax; 36 | mod: SweetModule; 37 | 38 | constructor(namespace: Syntax, mod: SweetModule) { 39 | this.namespace = namespace; 40 | this.mod = mod; 41 | } 42 | } 43 | export class VarBindingTransform { 44 | id: Syntax; 45 | 46 | constructor(id: Syntax) { 47 | this.id = id; 48 | } 49 | } 50 | export class CompiletimeTransform { 51 | value: any; 52 | 53 | constructor(value: any) { 54 | this.value = value; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { RawDelimiter } from 'sweet-spec'; 3 | 4 | let t = new RawDelimiter(); 5 | -------------------------------------------------------------------------------- /test/assertions.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | import read from '../src/reader/token-reader'; 4 | import { compile, parse } from '../src/sweet'; 5 | import { Enforester } from '../src/enforester'; 6 | import StoreLoader from '../src/store-loader'; 7 | 8 | export const stmt = x => x.items[0]; 9 | export const expr = x => stmt(x).expression; 10 | export const items = x => x.items; 11 | 12 | export function makeEnforester(code) { 13 | let stxl = read(code); 14 | return new Enforester(stxl, List(), {}); 15 | } 16 | 17 | export function getAst(code) { 18 | const store = new Map(); 19 | store.set('main.js', code); 20 | 21 | const loader = new StoreLoader(__dirname, store); 22 | return parse('main.js', loader); 23 | } 24 | 25 | function testParseWithOpts(t, acc, code, expectedAst) { 26 | try { 27 | t.deepEqual(expectedAst, acc(getAst(code))); 28 | } catch (e) { 29 | throw new Error(e.message); 30 | } 31 | } 32 | 33 | export function testParse(t, acc, code, expectedAst) { 34 | return testParseWithOpts(t, acc, code, expectedAst); 35 | } 36 | 37 | export function testParseComparison(t, acc, codeA, codeB) { 38 | testParse(t, acc, codeA, acc(getAst(codeB))); 39 | } 40 | 41 | export function testParseFailure() { 42 | // TODO 43 | } 44 | 45 | export function testEval(store, cb) { 46 | let loader = new StoreLoader(__dirname, store); 47 | let result = compile('main.js', loader).code; 48 | 49 | var output; 50 | try { 51 | eval(result); 52 | } catch (e) { 53 | throw new Error( 54 | `Syntax error: ${e.message} 55 | 56 | ${result}`, 57 | ); 58 | } 59 | return cb(output); 60 | } 61 | 62 | export function evalWithStore(t, inputStore, expected) { 63 | let store = new Map(); 64 | for (let key of Object.keys(inputStore)) { 65 | store.set(key, inputStore[key]); 66 | } 67 | testEval(store, output => t.is(output, expected)); 68 | } 69 | evalWithStore.title = (title, inputStore, expected) => 70 | `${title} 71 | ${Array.from(Object.entries(inputStore)) 72 | .map(([modName, modSrc]) => `${modName}\n----\n${modSrc}\n----`) 73 | .join('\n')} 74 | > ${expected}`; 75 | 76 | export function evalWithOutput(t, input, expected) { 77 | let store = new Map(); 78 | store.set('main.js', input); 79 | testEval(store, output => t.is(output, expected)); 80 | } 81 | evalWithOutput.title = (title, input, expected) => 82 | `${title} 83 | ${input} 84 | > ${expected}`; 85 | 86 | export function evalThrows(t, input) { 87 | let store = new Map(); 88 | store.set('main.js', input); 89 | t.throws(() => testEval(store, () => {})); 90 | } 91 | evalThrows.title = (title, input) => 92 | `${title} 93 | ${input} 94 | > should have thrown`; 95 | 96 | export function testThrow(source) { 97 | // expect(() => compile(source, { cwd: '.', transform})).to.throwError(); 98 | } 99 | -------------------------------------------------------------------------------- /test/modules/_helpers.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, mkdtempSync } from 'fs'; 2 | import { execSync } from 'child_process'; 3 | import { compile } from '../../src/sweet'; 4 | import NodeLoader from '../../src/node-loader'; 5 | import { sync as rimraf } from 'rimraf'; 6 | 7 | // write each module to a temp dir, compile them with sweet, 8 | // compile with babel to get commonjs modules, then assert on 9 | // the console log in main.js 10 | export function compileToCjs(t, inputModules, expected) { 11 | let tmp = mkdtempSync(`./`); 12 | let loader = new NodeLoader(tmp); 13 | for (let fname of Object.keys(inputModules)) { 14 | let path = `${tmp}/${fname}`; 15 | writeFileSync(path, inputModules[fname], 'utf8'); 16 | let outfile = compile(path, loader); 17 | writeFileSync(`${tmp}/${fname}.esm`, outfile.code, 'utf8'); 18 | execSync( 19 | `babel --out-file ${tmp}/${fname} ${tmp}/${fname}.esm --no-babelrc --plugins=transform-es2015-modules-commonjs`, 20 | ); 21 | } 22 | let result = execSync(`node ${tmp}/main.js`).toString().trim(); 23 | t.is(result, expected); 24 | rimraf(tmp); 25 | } 26 | compileToCjs.title = (title, inputStore, expected) => 27 | `${title} 28 | ${Array.from(Object.entries(inputStore)) 29 | .map(([modName, modSrc]) => `${modName}\n----\n${modSrc}\n----`) 30 | .join('\n')} 31 | > ${expected}`; 32 | -------------------------------------------------------------------------------- /test/modules/test-expanding-cjs.js: -------------------------------------------------------------------------------- 1 | import { compileToCjs } from './_helpers'; 2 | import test from 'ava'; 3 | 4 | test( 5 | 'exporting and importing a variable works', 6 | compileToCjs, 7 | { 8 | 'mod.js': ` 9 | 'lang sweet.js'; 10 | export var x = 'foo'; 11 | `, 12 | 13 | 'main.js': ` 14 | 'lang sweet.js'; 15 | import { x } from './mod.js'; 16 | console.log(x); 17 | `, 18 | }, 19 | 'foo', 20 | ); 21 | 22 | test( 23 | 'exporting and importing a name passing through macro expansion works', 24 | compileToCjs, 25 | { 26 | 'mod.js': ` 27 | 'lang sweet.js'; 28 | syntax m = ctx => { 29 | let first = ctx.next().value; 30 | let second = ctx.next().value; 31 | return #\`var \${first} = \${second}\`; 32 | } 33 | export m x 'foo' 34 | `, 35 | 36 | 'main.js': ` 37 | 'lang sweet.js'; 38 | import { x } from './mod.js'; 39 | console.log(x); 40 | `, 41 | }, 42 | 'foo', 43 | ); 44 | -------------------------------------------------------------------------------- /test/parser/extra-parser-tests/pass/1.js: -------------------------------------------------------------------------------- 1 | (a => a.b('c')); 2 | -------------------------------------------------------------------------------- /test/parser/test-ast.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { getAst } from '../assertions'; 3 | 4 | test('does not include the lang directive in the AST', t => { 5 | t.snapshot( 6 | getAst(` 7 | 'lang sweet.js'; 8 | `), 9 | ); 10 | }); 11 | 12 | test('does include the use strict directive in the AST', t => { 13 | t.snapshot( 14 | getAst(` 15 | 'use strict'; 16 | `), 17 | ); 18 | }); 19 | 20 | test('includes export in AST', t => { 21 | t.snapshot( 22 | getAst(` 23 | export { b } 24 | `), 25 | ); 26 | }); 27 | 28 | test('includes export with renaming in AST', t => { 29 | t.snapshot( 30 | getAst(` 31 | export { b as c} 32 | `), 33 | ); 34 | }); 35 | 36 | test('includes export declaration in AST', t => { 37 | t.snapshot( 38 | getAst(` 39 | export var x = 1; 40 | `), 41 | ); 42 | }); 43 | 44 | test('includes support for async function declarations', t => { 45 | t.snapshot( 46 | getAst(` 47 | async function f() {} 48 | `), 49 | ); 50 | }); 51 | 52 | test('includes support for async function expressions', t => { 53 | t.snapshot( 54 | getAst(` 55 | let f = async function f() {} 56 | `), 57 | ); 58 | }); 59 | 60 | test('includes support for exporting async functions', t => { 61 | t.snapshot( 62 | getAst(` 63 | export async function f() {} 64 | `), 65 | ); 66 | }); 67 | 68 | test('includes support for exporting default async functions', t => { 69 | t.snapshot( 70 | getAst(` 71 | export default async function f() {} 72 | `), 73 | ); 74 | }); 75 | 76 | test('includes no-line-terminator requirement for async functions', t => { 77 | t.snapshot( 78 | getAst(` 79 | async 80 | function f() {} 81 | `), 82 | ); 83 | }); 84 | 85 | test('includes no-line-terminator requirement for async function expressions', t => { 86 | t.snapshot( 87 | getAst(` 88 | let f = async 89 | function f() {} 90 | `), 91 | ); 92 | }); 93 | 94 | test('includes support for async arrow functions', t => { 95 | t.snapshot( 96 | getAst(` 97 | let f = async () => {} 98 | `), 99 | ); 100 | }); 101 | 102 | test('includes support for async method definitions', t => { 103 | t.snapshot( 104 | getAst(` 105 | class C { 106 | async f() {} 107 | } 108 | `), 109 | ); 110 | }); 111 | 112 | test('handles properties named async', t => { 113 | t.snapshot( 114 | getAst(` 115 | let o = { 116 | async: true 117 | } 118 | `), 119 | ); 120 | }); 121 | 122 | test('includes support for await', t => { 123 | t.snapshot( 124 | getAst(` 125 | async function f () { 126 | await g(); 127 | } 128 | `), 129 | ); 130 | }); 131 | -------------------------------------------------------------------------------- /test/parser/test-compile-external-libs.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { compile } from '../../src/sweet.js'; 3 | import NodeLoader from '../../src/node-loader'; 4 | 5 | function compileLib(t, input) { 6 | // just want to assert no errors were thrown and we got back *something* 7 | let result = compile(input, new NodeLoader(__dirname)); 8 | t.not(result.code, ''); 9 | } 10 | compileLib.title = (title, input) => `Compiling: ${input}`; 11 | 12 | test(compileLib, '../../node_modules/jquery/dist/jquery.js'); 13 | test(compileLib, '../../node_modules/angular/angular.js'); 14 | -------------------------------------------------------------------------------- /test/unit/test-asi.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { evalWithOutput } from '../assertions'; 3 | 4 | test( 5 | 'should handle interpolations for normal tokens', 6 | evalWithOutput, 7 | ` 8 | syntax m = ctx => #\`return \${ctx.next().value}\`; 9 | output = function f() { 10 | m 1; 11 | }()`, 12 | 1, 13 | ); 14 | 15 | test( 16 | evalWithOutput, 17 | ` 18 | syntax m = ctx => #\`return \${ctx.next().value}\`; 19 | output = function f() { 20 | m 'a'; 21 | }()`, 22 | 'a', 23 | ); 24 | 25 | test( 26 | evalWithOutput, 27 | ` 28 | syntax m = ctx => #\`return \${ctx.next().value}\`; 29 | output = function f() { 30 | m false; 31 | }()`, 32 | false, 33 | ); 34 | 35 | test( 36 | 'should handle interpolations for delimiter tokens', 37 | evalWithOutput, 38 | ` 39 | syntax m = ctx => #\`return \${ctx.next().value}\`; 40 | output = function f() { 41 | m (1); 42 | }()`, 43 | 1, 44 | ); 45 | 46 | test( 47 | evalWithOutput, 48 | ` 49 | syntax m = ctx => #\`return \${ctx.next().value}\`; 50 | output = function f() { 51 | m [ 52 | 1 53 | ]; 54 | }()[0]`, 55 | 1, 56 | ); 57 | 58 | test( 59 | evalWithOutput, 60 | ` 61 | syntax m = ctx => #\`return \${ctx.next().value}\`; 62 | output = function f() { 63 | m { 64 | a: 1 65 | }; 66 | }().a`, 67 | 1, 68 | ); 69 | 70 | test( 71 | evalWithOutput, 72 | ` 73 | syntax m = ctx => #\`return \${ctx.contextify(ctx.next().value)}\`; 74 | output = function f () { 75 | m { 1 } 76 | }()`, 77 | 1, 78 | ); 79 | 80 | test( 81 | 'should handle return and template literals', 82 | evalWithOutput, 83 | ` 84 | function f() { 85 | return \`foo\` 86 | } 87 | output = f(); 88 | `, 89 | 'foo', 90 | ); 91 | 92 | // // test('should handle interpolations for terms', t => { 93 | // // return testEval(`syntax m = ctx => #\`return \${ctx.next('expr').value}\`; 94 | // // output = function f() { 95 | // // m 1 96 | // // }()`, output => t.is(output, 1)); 97 | // // }); 98 | -------------------------------------------------------------------------------- /test/unit/test-default-readtable.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import expect from 'expect.js'; 3 | import { List } from 'immutable'; 4 | 5 | import { getCurrentReadtable } from 'readtable'; 6 | import read from '../../src/reader/token-reader'; 7 | import { 8 | TokenType as TT, 9 | TokenClass as TC, 10 | EmptyToken, 11 | } from '../../src/tokens'; 12 | 13 | function testParse(source, tst) { 14 | const prevTable = getCurrentReadtable(); 15 | const [result] = read(source); 16 | expect(getCurrentReadtable() === prevTable).to.be.true; 17 | 18 | if (result == null) return; 19 | 20 | tst(result); 21 | } 22 | 23 | function testParseResults(source, tst) { 24 | const prevTable = getCurrentReadtable(); 25 | const result = read(source); 26 | expect(getCurrentReadtable() === prevTable).to.be.true; 27 | tst(result); 28 | } 29 | 30 | test('should parse Unicode identifiers', t => { 31 | function testParseIdentifier(source, id) { 32 | testParse(source, result => { 33 | t.is(result.value, id); 34 | t.is(result.type, TT.IDENTIFIER); 35 | t.deepEqual(result.slice.startLocation, { 36 | filename: '', 37 | line: 1, 38 | column: 1, 39 | position: 0, 40 | }); 41 | }); 42 | } 43 | 44 | testParseIdentifier('abcd xyz', 'abcd'); 45 | testParseIdentifier('awaits ', 'awaits'); 46 | testParseIdentifier('日本語 ', '日本語'); 47 | testParseIdentifier('\u2163\u2161 ', '\u2163\u2161'); 48 | testParseIdentifier('\\u2163\\u2161 ', '\u2163\u2161'); 49 | testParseIdentifier('\u{102A7} ', '\u{102A7}'); 50 | testParseIdentifier('\\u{102A7} ', '\u{102A7}'); 51 | testParseIdentifier('a\u{102A7}a ', 'a\u{102A7}a'); 52 | testParseIdentifier('a\\u{102A7}a ', 'a\u{102A7}a'); 53 | testParseIdentifier('\uD800\uDC00 ', '\uD800\uDC00'); 54 | testParseIdentifier('\u2163\u2161\u200A', '\u2163\u2161'); 55 | testParseIdentifier('a\u2163\u2161a\u200A', 'a\u2163\u2161a'); 56 | testParseIdentifier('a\\u0061', 'aa'); 57 | testParseIdentifier('a\\u0061a', 'aaa'); 58 | testParseIdentifier('\\u0061a', 'aa'); 59 | }); 60 | 61 | test('should throw given invalid characters', t => { 62 | const error = t.throws(() => read('∇')); 63 | 64 | t.true(error.message.includes('Invalid or unexpected token')); 65 | }); 66 | 67 | test('should parse keywords', t => { 68 | function testParseKeyword(source, id) { 69 | testParse(source, result => { 70 | t.is(result.value, id); 71 | t.is(result.type.klass, TC.Keyword); 72 | t.deepEqual(result.slice.startLocation, { 73 | filename: '', 74 | line: 1, 75 | column: 1, 76 | position: 0, 77 | }); 78 | }); 79 | } 80 | 81 | // testParseKeyword('await ', 'await'); TODO: uncomment when await is a keyword 82 | testParseKeyword('break ', 'break'); 83 | testParseKeyword('case ', 'case'); 84 | testParseKeyword('catch ', 'catch'); 85 | testParseKeyword('class ', 'class'); 86 | }); 87 | 88 | test('should parse punctuators', t => { 89 | function testParsePunctuator(source, p) { 90 | testParse(source, result => { 91 | t.is(result.value, p); 92 | t.is(result.type.klass, TC.Punctuator); 93 | t.deepEqual(result.slice.startLocation, { 94 | filename: '', 95 | line: 1, 96 | column: 1, 97 | position: 0, 98 | }); 99 | }); 100 | } 101 | 102 | testParsePunctuator('+ ', '+'); 103 | testParsePunctuator('+= ', '+='); 104 | testParsePunctuator('; ', ';'); 105 | testParsePunctuator('>>> ', '>>>'); 106 | testParsePunctuator('+42', '+'); 107 | }); 108 | 109 | test('should parse whitespace', t => { 110 | function testParseWhiteSpace(source) { 111 | testParse(source, result => t.is(result, EmptyToken)); 112 | } 113 | testParseWhiteSpace(' '); 114 | testParseWhiteSpace('\t'); 115 | testParseWhiteSpace('\uFEFF'); 116 | }); 117 | 118 | test('should parse line terminators', t => { 119 | function testParseLineTerminators(source) { 120 | testParse(source, result => { 121 | t.is(result, EmptyToken); 122 | }); 123 | } 124 | 125 | testParseLineTerminators('\n'); 126 | testParseLineTerminators('\r\n'); 127 | testParseLineTerminators('\u2029'); 128 | }); 129 | 130 | test('should parse numeric literals', t => { 131 | function testParseNumericLiterals(source, value) { 132 | testParse(source, result => t.is(result.value, value)); 133 | } 134 | testParseNumericLiterals('0', 0); 135 | testParseNumericLiterals('1', 1); 136 | testParseNumericLiterals('2', 2); 137 | testParseNumericLiterals('3', 3); 138 | testParseNumericLiterals('4', 4); 139 | testParseNumericLiterals('5', 5); 140 | testParseNumericLiterals('6', 6); 141 | testParseNumericLiterals('7', 7); 142 | testParseNumericLiterals('8', 8); 143 | testParseNumericLiterals('9', 9); 144 | testParseNumericLiterals('0xFFFF ', 0xffff); 145 | testParseNumericLiterals('0xFF ', 0xff); 146 | testParseNumericLiterals('0o0756 ', 0o0756); 147 | testParseNumericLiterals('0o76 ', 0o76); 148 | testParseNumericLiterals('0b1010 ', 0b1010); 149 | testParseNumericLiterals('0b10 ', 0b10); 150 | testParseNumericLiterals('042 ', 0o042); 151 | testParseNumericLiterals('42 ', 42); 152 | testParseNumericLiterals('2e308', 1 / 0); 153 | testParseNumericLiterals('1.5', 1.5); 154 | }); 155 | 156 | test('should parse string literals', t => { 157 | function testParseStringLiteral(source, value) { 158 | testParse(source, result => { 159 | expect(result.type).to.eql(TT.STRING); 160 | expect(result.str).to.eql(value); 161 | }); 162 | } 163 | 164 | testParseStringLiteral('""', ''); 165 | testParseStringLiteral("'x'", 'x'); 166 | testParseStringLiteral('"x"', 'x'); 167 | testParseStringLiteral("'\\\\\\''", "\\'"); 168 | testParseStringLiteral('"\\\\\\""', '\\"'); 169 | testParseStringLiteral("'\\\r'", ''); 170 | testParseStringLiteral('"\\\r\n"', ''); 171 | testParseStringLiteral('"\\\n"', ''); 172 | testParseStringLiteral('"\\\u2028"', ''); 173 | testParseStringLiteral('"\\\u2029"', ''); 174 | testParseStringLiteral('"\\u202a"', '\u202a'); 175 | testParseStringLiteral('"\\0"', '\0'); 176 | testParseStringLiteral('"\\0x"', '\0x'); 177 | testParseStringLiteral('"\\01"', '\x01'); 178 | testParseStringLiteral('"\\1"', '\x01'); 179 | testParseStringLiteral('"\\11"', '\t'); 180 | testParseStringLiteral('"\\111"', 'I'); 181 | testParseStringLiteral('"\\1111"', 'I1'); 182 | testParseStringLiteral('"\\2111"', '\x891'); 183 | testParseStringLiteral('"\\5111"', ')11'); 184 | testParseStringLiteral('"\\5a"', '\x05a'); 185 | testParseStringLiteral('"\\7a"', '\x07a'); 186 | testParseStringLiteral('"a"', 'a'); 187 | testParseStringLiteral('"\\u{00F8}"', '\xF8'); 188 | testParseStringLiteral('"\\u{0}"', '\0'); 189 | testParseStringLiteral('"\\u{10FFFF}"', '\uDBFF\uDFFF'); 190 | testParseStringLiteral('"\\u{0000000000F8}"', '\xF8'); 191 | }); 192 | 193 | test('should parse template literals', t => { 194 | function testParseTemplateLiteral(source, value, isTail, isInterp) { 195 | testParse(source, result => { 196 | t.is(result.type, TT.TEMPLATE); 197 | const elt = result.items.first(); 198 | t.is(elt.type, TT.TEMPLATE); 199 | t.is(elt.value, value); 200 | t.is(elt.tail, isTail); 201 | t.is(elt.interp, isInterp); 202 | }); 203 | } 204 | 205 | testParseTemplateLiteral('`foo`', 'foo', true, false); 206 | testParseTemplateLiteral('`"foo"`', '"foo"', true, false); 207 | // should test that this throws in strict mode and passes in sloppy 208 | // testParseTemplateLiteral('`\\111`', 'I', true, false); 209 | testParseTemplateLiteral('`foo${bar}`', 'foo', false, true); 210 | testParse('`foo${bar}baz`', result => { 211 | t.is(result.type, TT.TEMPLATE); 212 | const [x, y, z] = result.items; 213 | 214 | t.is(x.type, TT.TEMPLATE); 215 | t.is(x.value, 'foo'); 216 | t.false(x.tail); 217 | t.true(x.interp); 218 | 219 | t.true(List.isList(y)); 220 | t.is(y.get(1).type, TT.IDENTIFIER); 221 | t.is(y.get(1).value, 'bar'); 222 | 223 | t.is(z.type, TT.TEMPLATE); 224 | t.is(z.value, 'baz'); 225 | t.true(z.tail); 226 | t.false(z.interp); 227 | }); 228 | }); 229 | 230 | test('should parse delimiters', t => { 231 | function testParseDelimiter(source, value) { 232 | testParse(source, results => { 233 | t.true(List.isList(results)); 234 | results.forEach((r, i) => t.true(source.includes(r.value))); 235 | }); 236 | } 237 | 238 | testParseDelimiter('{a}', 'a'); 239 | 240 | testParse('{ x + z }', result => { 241 | t.true(List.isList(result)); 242 | 243 | const [v, w, x, y, z] = result; 244 | 245 | t.is(v.type, TT.LBRACE); 246 | 247 | t.is(w.type, TT.IDENTIFIER); 248 | t.is(w.value, 'x'); 249 | 250 | t.is(x.type, TT.ADD); 251 | 252 | t.is(y.type, TT.IDENTIFIER); 253 | t.is(y.value, 'z'); 254 | 255 | t.is(z.type, TT.RBRACE); 256 | }); 257 | 258 | testParse('[ x , z ]', result => { 259 | t.true(List.isList(result)); 260 | 261 | const [v, w, x, y, z] = result; 262 | 263 | t.is(v.type, TT.LBRACK); 264 | 265 | t.is(w.type, TT.IDENTIFIER); 266 | t.is(w.value, 'x'); 267 | 268 | t.is(x.type, TT.COMMA); 269 | 270 | t.is(y.type, TT.IDENTIFIER); 271 | t.is(y.value, 'z'); 272 | 273 | t.is(z.type, TT.RBRACK); 274 | }); 275 | 276 | testParse('[{x : 3}, z]', result => { 277 | t.true(List.isList(result)); 278 | 279 | const [v, w, x, y, z] = result; 280 | 281 | t.is(v.type, TT.LBRACK); 282 | 283 | t.true(List.isList(w)); 284 | 285 | const [a, b, c, d, e] = w; 286 | 287 | t.is(a.type, TT.LBRACE); 288 | 289 | t.is(b.type, TT.IDENTIFIER); 290 | t.is(b.value, 'x'); 291 | 292 | t.is(c.type, TT.COLON); 293 | 294 | t.is(d.type, TT.NUMBER); 295 | t.is(d.value, 3); 296 | 297 | t.is(e.type, TT.RBRACE); 298 | 299 | t.is(x.type, TT.COMMA); 300 | 301 | t.is(y.type, TT.IDENTIFIER); 302 | t.is(y.value, 'z'); 303 | 304 | t.is(z.type, TT.RBRACK); 305 | }); 306 | 307 | testParseResults(`foo('bar')`, ([foo, bar]) => { 308 | t.is(foo.type, TT.IDENTIFIER); 309 | t.is(foo.value, 'foo'); 310 | 311 | const [x, y, z] = bar; 312 | 313 | t.is(x.type, TT.LPAREN); 314 | 315 | t.is(y.type, TT.STRING); 316 | t.is(y.str, 'bar'); 317 | t.is(y.slice.text, "'bar'"); 318 | 319 | t.is(z.type, TT.RPAREN); 320 | }); 321 | }); 322 | 323 | test('should parse regexp literals', t => { 324 | function testParseRegExpLiteral(source, value) { 325 | testParse(source, result => { 326 | t.is(result.type, TT.REGEXP); 327 | t.is(result.value, value); 328 | }); 329 | } 330 | 331 | testParseRegExpLiteral('/foo/g ', '/foo/g'); 332 | testParseRegExpLiteral('/=foo/g ', '/=foo/g'); 333 | 334 | testParseResults('if (x) /a/', ([x, y, z]) => { 335 | t.is(x.type, TT.IF); 336 | t.true(List.isList(y)); 337 | 338 | t.is(z.type, TT.REGEXP); 339 | t.is(z.value, '/a/'); 340 | }); 341 | }); 342 | 343 | test('should parse division expressions', t => { 344 | testParseResults('a/4/3', ([v, w, x, y, z]) => { 345 | t.is(v.type, TT.IDENTIFIER); 346 | t.is(v.value, 'a'); 347 | 348 | t.is(w.type, TT.DIV); 349 | 350 | t.is(x.type, TT.NUMBER); 351 | t.is(x.value, 4); 352 | 353 | t.is(y.type, TT.DIV); 354 | 355 | t.is(z.type, TT.NUMBER); 356 | t.is(z.value, 3); 357 | }); 358 | }); 359 | 360 | test('should parse syntax templates', t => { 361 | testParseResults('#`a 1 ${}`', ([result]) => { 362 | const [u, v, w, x, y, z] = result; 363 | t.is(u.type, TT.LSYNTAX); 364 | 365 | t.is(v.type, TT.IDENTIFIER); 366 | t.is(v.value, 'a'); 367 | 368 | t.is(w.type, TT.NUMBER); 369 | t.is(w.value, 1); 370 | 371 | t.is(x.type, TT.IDENTIFIER); 372 | t.is(x.value, '$'); 373 | 374 | t.true(List.isList(y)); 375 | t.is(y.first().type, TT.LBRACE); 376 | t.is(y.get(1).type, TT.RBRACE); 377 | 378 | t.is(z.type, TT.RSYNTAX); 379 | }); 380 | }); 381 | 382 | test('should erase #lang pragmas', t => { 383 | const results = read(`#lang "sweet.js"`); 384 | t.true(List.isList(results)); 385 | t.true(results.size === 1); 386 | }); 387 | 388 | test('should return an identifier for a lone #', t => { 389 | const results = read(`const # = 3`); 390 | t.true(List.isList(results)); 391 | t.is(results.get(1).type, TT.IDENTIFIER); 392 | }); 393 | 394 | test('should parse comments', t => { 395 | function testParseComment(source) { 396 | const result = read(source); 397 | t.true(result.isEmpty()); 398 | } 399 | 400 | testParseComment("// this is a single line comment\n // here's another"); 401 | testParseComment('/* this is a block line comment */'); 402 | testParseComment( 403 | `/* 404 | * this 405 | * is 406 | * a 407 | * multi 408 | * line 409 | * comment 410 | */`, 411 | ); 412 | }); 413 | 414 | test('should properly update location information', t => { 415 | function testLocationInfo( 416 | source, 417 | { idx, size, line: expectedLine, column: expectedColumn }, 418 | ) { 419 | let result = read(source); 420 | let { line, column } = result.get(idx).slice.startLocation; 421 | t.is(result.size, size); 422 | t.is(line, expectedLine); 423 | t.is(column, expectedColumn); 424 | } 425 | testLocationInfo('1 2 3 []\na b c', { idx: 6, size: 7, line: 2, column: 5 }); 426 | testLocationInfo('1 2 3 []\na b c', { idx: 2, size: 7, line: 1, column: 5 }); 427 | testLocationInfo(' /*3456789*/a', { idx: 0, size: 1, line: 1, column: 13 }); 428 | testLocationInfo( 429 | `a/* 430 | * this 431 | * is 432 | * a 433 | * multi 434 | * line 435 | * comment 436 | */b c`, 437 | { idx: 2, size: 3, line: 8, column: 7 }, 438 | ); 439 | testLocationInfo('"a\\\nb c\\\n d f g" a', { 440 | idx: 1, 441 | size: 2, 442 | line: 3, 443 | column: 9, 444 | }); 445 | }); 446 | -------------------------------------------------------------------------------- /test/unit/test-helpers.js: -------------------------------------------------------------------------------- 1 | import read from '../../src/reader/token-reader'; 2 | import test from 'ava'; 3 | import * as H from '../../helpers'; 4 | import { wrapInTerms } from '../../src/macro-context.js'; 5 | 6 | function wrappingRead(src) { 7 | let [r] = wrapInTerms(read(src)); 8 | return r; 9 | } 10 | 11 | test('should help with an identifier', t => { 12 | let r = wrappingRead('foo'); 13 | t.true(H.isIdentifier(r)); 14 | t.is(H.unwrap(r).value, 'foo'); 15 | 16 | let x = H.fromIdentifier(r, 'foo'); 17 | t.true(H.isIdentifier(x)); 18 | t.is(H.unwrap(x).value, 'foo'); 19 | }); 20 | 21 | test('should help with a keyword', t => { 22 | let r = wrappingRead('if'); 23 | t.true(H.isKeyword(r)); 24 | t.is(H.unwrap(r).value, 'if'); 25 | 26 | let x = H.fromKeyword(r, 'if'); 27 | t.true(H.isKeyword(x)); 28 | t.is(H.unwrap(x).value, 'if'); 29 | }); 30 | 31 | test('should help with a punctuator', t => { 32 | let r = wrappingRead(';'); 33 | t.true(H.isPunctuator(r)); 34 | t.is(H.unwrap(r).value, ';'); 35 | 36 | let x = H.fromPunctuator(r, ';'); 37 | t.true(H.isPunctuator(x)); 38 | t.is(H.unwrap(x).value, ';'); 39 | }); 40 | 41 | test('should help with a numeric', t => { 42 | let r = wrappingRead('42'); 43 | t.true(H.isNumericLiteral(r)); 44 | t.is(H.unwrap(r).value, 42); 45 | 46 | let x = H.fromNumericLiteral(r, 42); 47 | t.true(H.isNumericLiteral(x)); 48 | t.is(H.unwrap(x).value, 42); 49 | }); 50 | 51 | test('should help with a string literal', t => { 52 | let r = wrappingRead('"foo"'); 53 | t.true(H.isStringLiteral(r)); 54 | t.is(H.unwrap(r).value, 'foo'); 55 | 56 | let x = H.fromStringLiteral(r, 'foo'); 57 | t.true(H.isStringLiteral(x)); 58 | t.is(H.unwrap(x).value, 'foo'); 59 | }); 60 | 61 | test('should help with a template', t => { 62 | let r = wrappingRead('`foo`'); 63 | let [el] = H.unwrap(r).value; 64 | t.true(H.isTemplate(r)); 65 | t.true(H.isTemplateElement(el)); 66 | t.is(H.unwrap(el).value, 'foo'); 67 | }); 68 | 69 | test('should help with a syntax template', t => { 70 | let r = wrappingRead('#`foo`'); 71 | t.true(H.isSyntaxTemplate(r)); 72 | }); 73 | 74 | test('should help with a paren', t => { 75 | let r = wrappingRead('(foo)'); 76 | t.true(H.isParens(r)); 77 | 78 | let here = wrappingRead('here'); 79 | let x = H.fromParens(here, [wrappingRead('foo')]); 80 | t.true(H.isParens(x)); 81 | }); 82 | 83 | test('should help with a bracket', t => { 84 | let r = wrappingRead('[foo]'); 85 | t.true(H.isBrackets(r)); 86 | 87 | let here = wrappingRead('here'); 88 | let x = H.fromBrackets(here, [wrappingRead('foo')]); 89 | t.true(H.isBrackets(x)); 90 | }); 91 | 92 | test('should help with a brace', t => { 93 | let r = wrappingRead('{foo}'); 94 | t.true(H.isBraces(r)); 95 | 96 | let here = wrappingRead('here'); 97 | let x = H.fromBraces(here, [wrappingRead('foo')]); 98 | t.true(H.isBraces(x)); 99 | }); 100 | -------------------------------------------------------------------------------- /test/unit/test-hygiene.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { evalWithOutput, evalThrows } from '../assertions'; 4 | 5 | test( 6 | 'should work with references to function expression parameters', 7 | evalWithOutput, 8 | ` 9 | output = function foo(x) { 10 | syntaxrec m = function (ctx) { 11 | return #\`x\` 12 | } 13 | return function (x) { 14 | return m; 15 | }(2); 16 | }(1);`, 17 | 1, 18 | ); 19 | 20 | test( 21 | 'should work with references to function declaration parameters', 22 | evalWithOutput, 23 | ` 24 | function foo(x) { 25 | syntaxrec m = function (ctx) { 26 | return #\`x\` 27 | } 28 | function bar(x) { 29 | return m; 30 | } 31 | return bar(2); 32 | }; 33 | output = foo(1)`, 34 | 1, 35 | ); 36 | 37 | test( 38 | 'should work with introduced var declarations', 39 | evalWithOutput, 40 | ` 41 | syntaxrec m = function (ctx) { 42 | return #\`var x = 42;\` 43 | } 44 | output = function foo() { 45 | var x = 100; 46 | m; 47 | return x; 48 | }()`, 49 | 100, 50 | ); 51 | 52 | test( 53 | 'should allow duplicate var declarations', 54 | evalWithOutput, 55 | ` 56 | var x = 100; 57 | var x = 200; 58 | output = x;`, 59 | 200, 60 | ); 61 | 62 | test( 63 | 'should throw exception for duplicate let declarations', 64 | evalThrows, 65 | ` 66 | let x = 100; 67 | let x = 200`, 68 | ); 69 | 70 | test( 71 | 'should handle shorthand destructuring correctly', 72 | evalWithOutput, 73 | ` 74 | var { x } = { x: 1 }; 75 | output = x; 76 | `, 77 | 1, 78 | ); 79 | 80 | test( 81 | 'should handle shorthand destructuring with default values correctly', 82 | evalWithOutput, 83 | ` 84 | var { x = 1 } = { }; 85 | output = x; 86 | `, 87 | 1, 88 | ); 89 | -------------------------------------------------------------------------------- /test/unit/test-macro-context.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import MacroContext from '../../src/macro-context'; 3 | import Syntax from '../../src/syntax'; 4 | import { makeEnforester } from '../assertions'; 5 | import { List } from 'immutable'; 6 | 7 | test('a macro context should have a name', t => { 8 | let enf = makeEnforester('a'); 9 | let ctx = new MacroContext(enf, Syntax.fromIdentifier('foo'), {}); 10 | t.true(ctx.name().value.val() === 'foo'); 11 | }); 12 | 13 | test('a macro context should be resettable', t => { 14 | let enf = makeEnforester('a b c'); 15 | let ctx = new MacroContext(enf, 'foo', enf.context); 16 | 17 | let [a1, b1, c1] = [...ctx]; 18 | t.true(ctx.next().done); 19 | 20 | ctx.reset(); 21 | 22 | let nxt = ctx.next(); 23 | t.false(nxt.done); 24 | 25 | let [a2, b2, c2] = [nxt.value, ...ctx]; 26 | t.true(a1 === a2); 27 | t.true(b1 === b2); 28 | t.true(c1 === c2); 29 | }); 30 | 31 | test('a macro context should be able to create a reset point', t => { 32 | let enf = makeEnforester('a b c'); 33 | let ctx = new MacroContext(enf, Syntax.fromIdentifier('foo'), {}); 34 | 35 | let a1 = ctx.next(); // a 36 | 37 | const bMarker = ctx.mark(); 38 | 39 | const b1 = ctx.next(); // b 40 | 41 | const cMarker = ctx.mark(); 42 | 43 | const c1 = ctx.next(); // c 44 | 45 | t.true(ctx.next().done); 46 | 47 | ctx.reset(bMarker); 48 | 49 | const [b2, c2] = [...ctx]; 50 | 51 | ctx.reset(cMarker); 52 | 53 | const c3 = ctx.next(); 54 | 55 | ctx.reset(); 56 | 57 | let [a2, b3, c4] = [...ctx]; 58 | 59 | ctx.reset(cMarker); 60 | 61 | let c5 = ctx.next(); 62 | 63 | t.true(a1.value === a2); 64 | t.true(b1.value === b2 && b2 === b3); 65 | t.true(c1.value === c2 && c2 === c3.value && c2 === c4 && c4 === c5.value); 66 | }); 67 | 68 | test("an enforester should be able to access a macro context's syntax list", t => { 69 | let enf = makeEnforester('a'); 70 | let ctx = new MacroContext(enf, Syntax.fromIdentifier('foo'), {}); 71 | t.true(ctx._rest(enf) instanceof List); 72 | }); 73 | -------------------------------------------------------------------------------- /test/unit/test-macro-expansion.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { evalWithOutput, evalThrows } from '../assertions'; 4 | 5 | test( 6 | 'should handle basic expansion at a statement expression position', 7 | evalWithOutput, 8 | ` 9 | syntaxrec m = function(ctx) { 10 | return #\`200\`; 11 | } 12 | output = m`, 13 | 200, 14 | ); 15 | 16 | test( 17 | 'should handle basic expansion with an arrow transformer', 18 | evalWithOutput, 19 | ` 20 | syntaxrec m = ctx => #\`200\` 21 | output = m`, 22 | 200, 23 | ); 24 | 25 | test( 26 | 'should handle basic expansion at an expression position', 27 | evalWithOutput, 28 | ` 29 | syntaxrec m = function (ctx) { 30 | return #\`200\`; 31 | } 32 | let v = m; 33 | output = v;`, 34 | 200, 35 | ); 36 | 37 | test( 38 | 'should handle expansion where an argument is eaten', 39 | evalWithOutput, 40 | ` 41 | syntaxrec m = function(ctx) { 42 | ctx.next(); 43 | return #\`200\` 44 | } 45 | output = m 42`, 46 | 200, 47 | ); 48 | 49 | test( 50 | 'should handle expansion that eats an expression', 51 | evalWithOutput, 52 | ` 53 | syntaxrec m = function(ctx) { 54 | let term = ctx.expand('expr') 55 | return #\`200\` 56 | } 57 | output = m 100 + 200`, 58 | 200, 59 | ); 60 | 61 | test( 62 | 'should handle expansion that takes an argument', 63 | evalWithOutput, 64 | ` 65 | syntaxrec m = function(ctx) { 66 | var x = ctx.next().value; 67 | return #\`40 + \${x}\`; 68 | } 69 | output = m 2;`, 70 | 42, 71 | ); 72 | 73 | test( 74 | 'should handle expansion that matches an expression argument', 75 | evalWithOutput, 76 | ` 77 | syntaxrec m = function(ctx) { 78 | let x = ctx.expand('expr').value; 79 | return #\`40 + \${x}\`; 80 | } 81 | output = m 2;`, 82 | 42, 83 | ); 84 | 85 | test( 86 | 'should handle the macro returning an array', 87 | evalWithOutput, 88 | ` 89 | syntax m = function (ctx) { 90 | let x = ctx.next().value; 91 | return [x]; 92 | } 93 | output = m 42;`, 94 | 42, 95 | ); 96 | 97 | test( 98 | 'should handle the full macro context api', 99 | evalWithOutput, 100 | ` 101 | syntaxrec def = function(ctx) { 102 | let id = ctx.next().value; 103 | ctx.reset(); 104 | id = ctx.next().value; 105 | 106 | const paramsMark = ctx.mark(); 107 | let parens = ctx.next().value; 108 | ctx.reset(paramsMark); 109 | parens = ctx.next().value; 110 | let body = ctx.next().value; 111 | 112 | let parenCtx = ctx.contextify(parens); 113 | let paren_id = parenCtx.next().value; 114 | parenCtx.next() // = 115 | let paren_init = parenCtx.expand('expr').value; 116 | 117 | let bodyCtx = ctx.contextify(body); 118 | let b = []; 119 | for (let s of bodyCtx) { 120 | b.push(s); 121 | } 122 | 123 | return #\`function \${id} (\${paren_id}) { 124 | \${paren_id} = \${paren_id} || \${paren_init}; 125 | \${b} 126 | }\`; 127 | } 128 | 129 | def foo (x = 10 + 100) { return x; } 130 | output = foo(); 131 | `, 132 | 110, 133 | ); 134 | 135 | test( 136 | 'should handle iterators inside a syntax template', 137 | evalWithOutput, 138 | ` 139 | syntax let = function (ctx) { 140 | let ident = ctx.next().value; 141 | ctx.next(); 142 | let init = ctx.expand('expr').value; 143 | return #\` 144 | (function (\${ident}) { 145 | \${ctx} 146 | }(\${init})) 147 | \` 148 | } 149 | let x = 42; 150 | output = x; 151 | `, 152 | 42, 153 | ); 154 | 155 | test( 156 | 'should allow macros to be defined with @', 157 | evalWithOutput, 158 | ` 159 | syntax @ = function (ctx) { 160 | return #\`42\`; 161 | } 162 | output = @ 163 | `, 164 | 42, 165 | ); 166 | 167 | test( 168 | 'should allow macros to be defined with #', 169 | evalWithOutput, 170 | ` 171 | syntax # = function (ctx) { 172 | return #\`42\`; 173 | } 174 | output = # 175 | `, 176 | 42, 177 | ); 178 | 179 | test( 180 | 'should allow macros to be defined with *', 181 | evalWithOutput, 182 | ` 183 | syntax * = function (ctx) { 184 | return #\`42\`; 185 | } 186 | output = * 187 | `, 188 | 42, 189 | ); 190 | 191 | test( 192 | 'should allow the macro context to be reset', 193 | evalWithOutput, 194 | ` 195 | syntax m = ctx => { 196 | ctx.expand('expr'); // 42 + 66 197 | // oops, just wanted one token 198 | ctx.reset(); 199 | let value = ctx.next().value; // 42 200 | ctx.next(); 201 | ctx.next(); 202 | return #\`\${value}\`; 203 | } 204 | 205 | output = m 42 + 66 206 | `, 207 | 42, 208 | ); 209 | 210 | test( 211 | 'should allow the macro context to create a reset point', 212 | evalWithOutput, 213 | ` 214 | syntax m = ctx => { 215 | ctx.next(); // 30 216 | ctx.next(); // + 217 | // lets play it safe 218 | const marker42 = ctx.mark(); 219 | ctx.expand('expr'); // 42 + 66 220 | // oops, just wanted one token 221 | ctx.reset(marker42); 222 | let value = ctx.next().value; // 42 223 | ctx.next(); 224 | ctx.next(); 225 | return #\`\${value}\`; 226 | } 227 | 228 | output = m 30 + 42 + 66 229 | `, 230 | 42, 231 | ); 232 | 233 | test( 234 | 'should throw if marker is from a different macro context', 235 | evalThrows, 236 | ` 237 | syntax m = ctx => { 238 | const result = ctx.next().value; // 1 239 | const marker = ctx.mark(); 240 | ctx.next() // , 241 | const innerCtx = ctx.next().value.inner(); 242 | innerCtx.reset(marker); 243 | return #\`\${result}\`; 244 | } 245 | output = m 1, [1];`, 246 | ); 247 | 248 | test( 249 | 'should allow the macro context to match on a identifier expression', 250 | evalWithOutput, 251 | ` 252 | syntax m = ctx => { 253 | let expr = ctx.expand('IdentifierExpression').value; 254 | return #\`\${expr}\`; 255 | } 256 | var foo = 1; 257 | output = m foo 258 | `, 259 | 1, 260 | ); 261 | 262 | test( 263 | evalWithOutput, 264 | ` 265 | syntax m = ctx => { 266 | let expr = ctx.expand('IdentifierExpression').value; 267 | return #\`1\`; 268 | } 269 | var foo = 1; 270 | output = m foo + 1 271 | `, 272 | 2, 273 | ); 274 | 275 | test( 276 | 'should allow the macro context to match on a binary expression', 277 | evalWithOutput, 278 | ` 279 | syntax m = ctx => { 280 | let expr = ctx.expand('BinaryExpression').value; 281 | return #\`\${expr}\`; 282 | } 283 | output = m 1 + 1 - 1 284 | `, 285 | 1, 286 | ); 287 | 288 | test( 289 | 'should throw an error if the match fails for MacroContext::expand', 290 | evalThrows, 291 | ` 292 | syntax m = ctx => { 293 | let expr = ctx.expand('BinaryExpression').value; 294 | return #\`\${expr}\`; 295 | } 296 | output = m foo`, 297 | ); 298 | 299 | test( 300 | 'should construct syntax from existing syntax', 301 | evalWithOutput, 302 | ` 303 | syntax m = ctx => { 304 | let arg = ctx.next().value; 305 | let dummy = #\`here\`.get(0); 306 | return #\`\${dummy.fromString(arg.value.val())}\` 307 | } 308 | output = m foo 309 | `, 310 | 'foo', 311 | ); 312 | 313 | test( 314 | 'should construct a delimiter from existing syntax', 315 | evalWithOutput, 316 | ` 317 | syntax m = ctx => { 318 | let arg = ctx.next().value; 319 | let dummy = #\`here\`.get(0); 320 | return #\`(\${dummy.fromNumber(arg.value.val())})\`; 321 | } 322 | output = m 1`, 323 | 1, 324 | ); 325 | 326 | test( 327 | 'should handle macros in blocks', 328 | evalWithOutput, 329 | ` 330 | { 331 | syntax m = ctx => #\`1\`; 332 | output = m 333 | } 334 | `, 335 | 1, 336 | ); 337 | 338 | test( 339 | 'should construct syntax from arguments', 340 | evalWithOutput, 341 | ` 342 | syntax m = ctx => { 343 | let arg = ctx.next().value; 344 | let stx = arg.fromNumber(1); 345 | return #\`\${stx}\`; 346 | } 347 | output = m 1`, 348 | 1, 349 | ); 350 | 351 | test( 352 | 'should construct delimiters', 353 | evalWithOutput, 354 | ` 355 | syntax m = ctx => { 356 | let dummy = #\`dummy\`.get(0); 357 | let expr = #\`5 * 5\`; 358 | return #\`1 + \${dummy.fromParens(expr)}\`; 359 | } 360 | output = m 361 | `, 362 | 26, 363 | ); 364 | 365 | test( 366 | 'should allow binding macros to import keyword', 367 | evalWithOutput, 368 | ` 369 | syntax import = ctx => { 370 | return #\`1\`; 371 | } 372 | output = import 373 | `, 374 | 1, 375 | ); 376 | 377 | test( 378 | 'should allow binding macros to export keyword', 379 | evalWithOutput, 380 | ` 381 | syntax export = ctx => { 382 | return #\`1\`; 383 | } 384 | output = export 385 | `, 386 | 1, 387 | ); 388 | 389 | test( 390 | 'should allow binding macros to super keyword', 391 | evalWithOutput, 392 | ` 393 | syntax super = ctx => { 394 | return #\`Number(1)\`; 395 | } 396 | output = new super instanceof Number 397 | `, 398 | true, 399 | ); 400 | -------------------------------------------------------------------------------- /test/unit/test-modules.js: -------------------------------------------------------------------------------- 1 | import { evalWithStore } from '../assertions'; 2 | import test from 'ava'; 3 | import { readFileSync } from 'fs'; 4 | 5 | test( 6 | 'should load a simple syntax transformer', 7 | evalWithStore, 8 | { 9 | './m.js': ` 10 | 'lang sweet.js'; 11 | export syntax m = function (ctx) { 12 | return #\`1\`; 13 | }`, 14 | 15 | 'main.js': ` 16 | import { m } from "./m.js"; 17 | output = m`, 18 | }, 19 | 1, 20 | ); 21 | 22 | test( 23 | 'should export a simple operator', 24 | evalWithStore, 25 | { 26 | './m.js': ` 27 | 'lang sweet.js'; 28 | export operator sub left 2 = (left, right) => { 29 | return #\`\${left} - \${right}\`; 30 | }`, 31 | 32 | 'main.js': ` 33 | import { sub } from "./m.js"; 34 | output = 2 sub 2`, 35 | }, 36 | 0, 37 | ); 38 | 39 | test( 40 | 'should export a simple punctuator operator', 41 | evalWithStore, 42 | { 43 | './m.js': ` 44 | 'lang sweet.js'; 45 | export operator - left 2 = (left, right) => { 46 | return #\`\${left} + \${right}\`; 47 | }`, 48 | 49 | 'main.js': ` 50 | import { - } from "./m.js"; 51 | output = 2 - 2`, 52 | }, 53 | 4, 54 | ); 55 | 56 | test( 57 | 'importing for syntax with a single number exported', 58 | evalWithStore, 59 | { 60 | './num.js': ` 61 | 'lang sweet.js'; 62 | export var n = 1;`, 63 | 64 | 'main.js': ` 65 | import { n } from './num.js' for syntax; 66 | 67 | syntax m = function (ctx) { 68 | if (n === 1) { 69 | return #\`true\`; 70 | } 71 | return #\`false\`; 72 | } 73 | output = m;`, 74 | }, 75 | true, 76 | ); 77 | 78 | test( 79 | 'import for syntax; export var; function', 80 | evalWithStore, 81 | { 82 | './id.js': ` 83 | 'lang sweet.js'; 84 | export var id = function (x) { 85 | return x; 86 | } 87 | `, 88 | 'main.js': ` 89 | import { id } from './id.js' for syntax; 90 | 91 | syntax m = ctx => { 92 | return id(#\`1\`); 93 | } 94 | output = m; 95 | `, 96 | }, 97 | 1, 98 | ); 99 | 100 | test( 101 | 'import for syntax; export declaration; function', 102 | evalWithStore, 103 | { 104 | './id.js': ` 105 | 'lang sweet.js'; 106 | export function id(x) { 107 | return x; 108 | } 109 | `, 110 | 'main.js': ` 111 | import { id } from './id.js' for syntax; 112 | 113 | syntax m = ctx => { 114 | return id(#\`1\`); 115 | } 116 | output = m; 117 | `, 118 | }, 119 | 1, 120 | ); 121 | 122 | test( 123 | 'importing a macro for syntax', 124 | evalWithStore, 125 | { 126 | './id.js': ` 127 | 'lang sweet.js'; 128 | export syntax m = function (ctx) { 129 | return #\`1\`; 130 | } 131 | `, 132 | 'main.js': ` 133 | import { m } from './id.js' for syntax; 134 | 135 | syntax m = ctx => { 136 | let x = m; 137 | return #\`1\`; 138 | } 139 | output = m; 140 | `, 141 | }, 142 | 1, 143 | ); 144 | 145 | test( 146 | 'importing a macro for syntax only binds what is named', 147 | evalWithStore, 148 | { 149 | './id.js': ` 150 | 'lang sweet.js'; 151 | syntax n = ctx => #\`2\`; 152 | 153 | export syntax m = function (ctx) { 154 | return #\`1\`; 155 | } 156 | 157 | `, 158 | 'main.js': ` 159 | import { m } from './id.js' for syntax; 160 | 161 | syntax test = ctx => { 162 | if (typeof n !== 'undefined' && n === 2) { 163 | throw new Error('un-exported and un-imported syntax should not be bound'); 164 | } 165 | return #\`1\`; 166 | } 167 | output = test; 168 | `, 169 | }, 170 | 1, 171 | ); 172 | 173 | test( 174 | 'exporting names for syntax', 175 | evalWithStore, 176 | { 177 | './mod.js': ` 178 | 'lang sweet.js'; 179 | function id(x) { return x; } 180 | export { id } 181 | `, 182 | 'main.js': ` 183 | import { id } from './mod.js' for syntax; 184 | syntax m = ctx => { 185 | return id(#\`1\`); 186 | } 187 | output = m 188 | `, 189 | }, 190 | 1, 191 | ); 192 | 193 | test( 194 | 'exporting names with renaming for syntax', 195 | evalWithStore, 196 | { 197 | './mod.js': ` 198 | 'lang sweet.js'; 199 | function id(x) { return x; } 200 | export { id as di } 201 | `, 202 | 'main.js': ` 203 | import { di } from './mod.js' for syntax; 204 | syntax m = ctx => { 205 | return di(#\`1\`); 206 | } 207 | output = m 208 | `, 209 | }, 210 | 1, 211 | ); 212 | 213 | test( 214 | 'exporting default names for syntax', 215 | evalWithStore, 216 | { 217 | './mod.js': ` 218 | 'lang sweet.js'; 219 | export default function id(x) { return x; } 220 | `, 221 | 'main.js': ` 222 | import id from './mod.js' for syntax; 223 | syntax m = ctx => { 224 | return id(#\`1\`); 225 | } 226 | output = m 227 | `, 228 | }, 229 | 1, 230 | ); 231 | 232 | test( 233 | 'importing a namespace for syntax', 234 | evalWithStore, 235 | { 236 | './mod.js': ` 237 | 'lang sweet.js'; 238 | export function id(x) { return x; }`, 239 | 'main.js': ` 240 | import * as M from './mod.js' for syntax; 241 | syntax m = ctx => { 242 | return M.id(#\`1\`); 243 | } 244 | output = m`, 245 | }, 246 | 1, 247 | ); 248 | 249 | test( 250 | 'importing a function through multiple modules for syntax', 251 | evalWithStore, 252 | { 253 | './a.js': ` 254 | 'lang sweet.js'; 255 | export function f(x) { return x; } 256 | `, 257 | './mod.js': ` 258 | 'lang sweet.js'; 259 | import { f } from './a.js'; 260 | export function id(x) { return f(x); }`, 261 | 'main.js': ` 262 | import * as M from './mod.js' for syntax; 263 | syntax m = ctx => { 264 | return M.id(#\`1\`); 265 | } 266 | output = m`, 267 | }, 268 | 1, 269 | ); 270 | 271 | let helperSrc = readFileSync('./helpers.js', 'utf8'); 272 | 273 | test( 274 | 'using helpers works', 275 | evalWithStore, 276 | { 277 | './helpers.js': helperSrc, 278 | 'main.js': ` 279 | import { isKeyword } from './helpers.js' for syntax; 280 | syntax m = ctx => { 281 | let n = ctx.next().value; 282 | if (isKeyword(n)) { 283 | return #\`true\`; 284 | } 285 | return #\`false\`; 286 | } 287 | output = m if 288 | `, 289 | }, 290 | true, 291 | ); 292 | 293 | test( 294 | 'using helpers in a chain works', 295 | evalWithStore, 296 | { 297 | './helpers.js': helperSrc, 298 | a: ` 299 | 'lang sweet.js'; 300 | import { isKeyword } from './helpers.js' for syntax; 301 | export syntax m = ctx => { 302 | let n = ctx.next().value; 303 | if (isKeyword(n)) { 304 | return #\`true\`; 305 | } 306 | return #\`false\`; 307 | } 308 | `, 309 | 'main.js': ` 310 | 'lang sweet.js'; 311 | import { m } from 'a'; 312 | output = m foo; 313 | `, 314 | }, 315 | false, 316 | ); 317 | 318 | test( 319 | 'only invokes a module once per-phase', 320 | evalWithStore, 321 | { 322 | lib: ` 323 | 'lang sweet.js'; 324 | export const x = 1; 325 | `, 326 | 327 | m: ` 328 | 'lang sweet.js'; 329 | import { x } from 'lib' for syntax; 330 | export syntax m = ctx => #\`1\`;`, 331 | 332 | 'main.js': ` 333 | 'lang sweet.js'; 334 | import { x } from 'lib' for syntax; 335 | import { m } from 'm'; 336 | output = true`, 337 | }, 338 | true, 339 | ); 340 | 341 | // test('importing a chain for syntax works', evalWithStore, { 342 | // 'b': `#lang 'sweet.js'; 343 | // export function b(x) { return x; } 344 | // `, 345 | // 'a': `#lang 'sweet.js'; 346 | // import { b } from 'b' for syntax; 347 | // 348 | // export function a() { 349 | // return b(1); 350 | // } 351 | // `, 352 | // 'main.js': `#lang 'sweet.js'; 353 | // import { a } from 'a' for syntax; 354 | // syntax m = ctx => { 355 | // if (a() !== 1) { 356 | // throw new Error('un expected something or rather'); 357 | // } 358 | // return #\`1\`; 359 | // } 360 | // output = m 361 | // ` 362 | // }, 1); 363 | -------------------------------------------------------------------------------- /test/unit/test-node-loader.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import NodeLoader from '../../src/node-loader'; 3 | 4 | test('a node loader should normalize a path that is missing the extension', t => { 5 | let loader = new NodeLoader(__dirname); 6 | t.regex(loader.normalize('./test-node-loader'), /.*test-node-loader\.js/); 7 | }); 8 | -------------------------------------------------------------------------------- /test/unit/test-operators.js: -------------------------------------------------------------------------------- 1 | import { evalWithOutput } from '../assertions'; 2 | import test from 'ava'; 3 | 4 | test( 5 | 'should handle basic prefix custom operators', 6 | evalWithOutput, 7 | ` 8 | operator neg prefix 1 = (right) => { 9 | return #\`-\${right}\`; 10 | } 11 | output = neg 1`, 12 | -1, 13 | ); 14 | 15 | test( 16 | 'should handle basic postfix custom operators', 17 | evalWithOutput, 18 | ` 19 | operator neg postfix 1 = (left) => { 20 | return #\`-\${left}\`; 21 | } 22 | output = 1 neg`, 23 | -1, 24 | ); 25 | 26 | test( 27 | 'should not recursively define the unary operator', 28 | evalWithOutput, 29 | ` 30 | operator - prefix 1 = (right) => { 31 | return #\`-\${right}\`; 32 | } 33 | output = - 1`, 34 | -1, 35 | ); 36 | 37 | test( 38 | 'should handle basic binary custom operators', 39 | evalWithOutput, 40 | ` 41 | operator add left 1 = (left, right) => { 42 | return #\`\${left} + \${right}\`; 43 | } 44 | output = 1 add 1`, 45 | 2, 46 | ); 47 | 48 | test( 49 | 'should handle basic binary custom operator precedence following existing operators', 50 | evalWithOutput, 51 | ` 52 | operator add left 1 = (left, right) => { 53 | return #\`\${left} + \${right}\`; 54 | } 55 | operator mul left 2 = (left, right) => { 56 | return #\`\${left} * \${right}\`; 57 | } 58 | output = 1 add 2 mul 3`, 59 | 7, 60 | ); 61 | 62 | test( 63 | 'should handle basic binary custom operator precedence subverting existing operators', 64 | evalWithOutput, 65 | ` 66 | operator add left 2 = (left, right) => { 67 | return #\`\${left} + \${right}\`; 68 | } 69 | operator mul left 1 = (left, right) => { 70 | return #\`\${left} * \${right}\`; 71 | } 72 | output = 1 add 2 mul 3`, 73 | 9, 74 | ); 75 | 76 | test( 77 | 'should handle a contrived slightly complex example of binary custom operators', 78 | evalWithOutput, 79 | ` 80 | operator >>= left 3 = (left, right) => { 81 | return #\`\${left}.chain(\${right})\`; 82 | } 83 | class IdMonad { 84 | constructor(value) { 85 | this.value = value; 86 | } 87 | chain(f) { 88 | return f(this.value); 89 | } 90 | } 91 | 92 | function Id(value) { 93 | return new IdMonad(value); 94 | } 95 | 96 | let result = Id(1) >>= v => Id(v + 1) 97 | >>= v => Id(v * 10); 98 | output = result.value; 99 | `, 100 | 20, 101 | ); 102 | 103 | test( 104 | 'should not recursively define the binary operator', 105 | evalWithOutput, 106 | ` 107 | operator + left 2 = (left, right) => { 108 | return #\`\${left} + \${right}\`; 109 | } 110 | output = 1 + 2`, 111 | 3, 112 | ); 113 | 114 | test( 115 | 'should handle converting a postfix to a binary', 116 | evalWithOutput, 117 | ` 118 | operator ++ left 1 = (left, right) => #\`\${left} + \${right}\` 119 | output = 1 ++ 2;`, 120 | 3, 121 | ); 122 | 123 | test( 124 | 'should handle converting a prefix to a binary', 125 | evalWithOutput, 126 | ` 127 | operator ~ left 1 = (left, right) => #\`\${left} + \${right}\` 128 | output = 1 ~ 2;`, 129 | 3, 130 | ); 131 | -------------------------------------------------------------------------------- /test/unit/test-parse.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { items, testParseComparison } from '../assertions'; 4 | 5 | test( 6 | 'CallExpression followed by identifier', 7 | testParseComparison, 8 | items, 9 | ` 10 | x 11 | foo.bar(1,2) 12 | x`, 13 | ` 14 | x; 15 | foo.bar(1,2); 16 | x;`, 17 | ); 18 | 19 | test( 20 | 'NewExpression followed by identifier', 21 | testParseComparison, 22 | items, 23 | ` 24 | x 25 | new Foo(1,2) 26 | x`, 27 | ` 28 | x; 29 | new Foo(1,2); 30 | x;`, 31 | ); 32 | 33 | test( 34 | 'NewExpression followed by identifier', 35 | testParseComparison, 36 | items, 37 | ` 38 | syntax m = ctx => #\`1\` 39 | m 40 | `, 41 | `1;`, 42 | ); 43 | 44 | test( 45 | 'Expand to nothing inside an array', 46 | testParseComparison, 47 | items, 48 | ` 49 | syntax m = ctx => ctx; 50 | [[m], [m, 1], [1, m], [1, m, 2]]; 51 | `, 52 | '[[], [, 1], [1], [1,, 2]]', 53 | ); 54 | 55 | // test doesn't throw 56 | test( 57 | 'LeftHandSideExpression after extends', 58 | testParseComparison, 59 | items, 60 | ` 61 | class foo extends bar.baz {} 62 | `, 63 | ` 64 | class foo extends bar.baz {} 65 | `, 66 | ); 67 | -------------------------------------------------------------------------------- /test/unit/test-reader-extensions.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { List } from 'immutable'; 3 | 4 | import read from '../../src/reader/token-reader'; 5 | import { getCurrentReadtable, setCurrentReadtable } from 'readtable'; 6 | import { 7 | keywordTable, 8 | IdentifierToken, 9 | EmptyToken, 10 | isKeyword, 11 | isIdentifier, 12 | } from '../../src/tokens'; 13 | 14 | test('terminating macros should delimit identifiers and numbers', t => { 15 | const prevTable = getCurrentReadtable(); 16 | const newTable = prevTable.extend( 17 | { 18 | key: 'z', 19 | mode: 'terminating', 20 | action: function readZ(stream) { 21 | stream.readString(); 22 | return EmptyToken; 23 | }, 24 | }, 25 | { 26 | key: '0', 27 | mode: 'terminating', 28 | action: function readZero(stream) { 29 | stream.readString(); 30 | return EmptyToken; 31 | }, 32 | }, 33 | ); 34 | 35 | // reading with 'z' and '0' as 'non-terminating' 36 | let [result] = read('abczefgzhij\u{102A7}ba '); 37 | t.is(prevTable.getMapping('z').mode, 'non-terminating'); 38 | t.is(result.value, 'abczefgzhij𐊧ba'); 39 | 40 | [result] = read('12304560789'); 41 | t.is(prevTable.getMapping('0').mode, 'non-terminating'); 42 | t.is(result.value, 12304560789); 43 | 44 | setCurrentReadtable(newTable); 45 | 46 | // reading with 'z' and '0' as 'terminating' 47 | let [x, y, z] = read('abczefgzhij\u{102A7}ba ').map(s => s.value); 48 | t.is(x, 'abc'); 49 | t.is(y, 'efg'); 50 | t.is(z, 'hij𐊧ba'); 51 | [x, y, z] = read('12304560789').map(s => s.value); 52 | t.is(x, 123); 53 | t.is(y, 456); 54 | t.is(z, 789); 55 | setCurrentReadtable(prevTable); 56 | }); 57 | test('should create a dispatch macro', t => { 58 | const prevTable = getCurrentReadtable(); 59 | const newTable = prevTable.extend({ 60 | key: ':', 61 | mode: 'dispatch', 62 | action: function readColon(stream, prefix, allowExpr) { 63 | stream.readString(); 64 | setCurrentReadtable(defaultTable); 65 | return new IdentifierToken({ 66 | value: 'Keyword', 67 | }); 68 | }, 69 | }); 70 | function readDefault(stream, prefix, allowExpr) { 71 | const [[openParen, closeParen]] = read('()'); 72 | setCurrentReadtable(prevTable); 73 | const stx = this.readToken(stream, List(), false); 74 | let result = List.of(openParen).push(stx); 75 | setCurrentReadtable(newTable); 76 | return result.push(closeParen); 77 | } 78 | const kwLetters = Array.from( 79 | new Set(Object.keys(keywordTable).map(w => w[0])), 80 | ); 81 | const keywordEntries = kwLetters.map(key => ({ 82 | key, 83 | mode: 'non-terminating', 84 | action: readDefault, 85 | })); 86 | const defaultTable = newTable.extend(...keywordEntries, { 87 | mode: 'non-terminating', 88 | action: readDefault, 89 | }); 90 | setCurrentReadtable(newTable); 91 | // eslint-disable-next-line no-unused-vars 92 | const [one, [open, kw, close], iff, els, [open2, elkw]] = read( 93 | '#:for if #:else', 94 | ); 95 | t.true(isIdentifier(one, 'Keyword')); 96 | t.true(isKeyword(kw, 'for')); 97 | t.true(isKeyword(iff, 'if')); 98 | t.true(isIdentifier(els, 'Keyword')); 99 | t.true(isKeyword(elkw, 'else')); 100 | setCurrentReadtable(prevTable); 101 | }); 102 | test('should allow replacing the dispatch character', t => { 103 | const prevTable = getCurrentReadtable(); 104 | const newTable = prevTable.extend( 105 | { 106 | key: '@', 107 | mode: 'terminating', 108 | action: prevTable.getMapping('#').action, 109 | }, 110 | { 111 | key: '#', 112 | mode: 'terminating', 113 | action: prevTable.getMapping('@').action, 114 | }, 115 | ); 116 | const result = read('#``'); 117 | const error = t.throws(() => read('@``')); 118 | setCurrentReadtable(newTable); 119 | const result2 = read('@``'); 120 | const error2 = t.throws(() => read('#``')); 121 | t.is(error.message, error2.message); 122 | t.deepEqual(result, result2); 123 | setCurrentReadtable(prevTable); 124 | }); 125 | -------------------------------------------------------------------------------- /test/unit/test-reader.js: -------------------------------------------------------------------------------- 1 | import read from '../../src/reader/token-reader'; 2 | import test from 'ava'; 3 | import * as T from '../../src/tokens'; 4 | 5 | /* eslint-disable no-unused-vars */ 6 | 7 | test('should read a numeric', t => { 8 | let [r] = read('42'); 9 | t.true(T.isNumeric(r, 42)); 10 | }); 11 | 12 | test('should read an identifier', t => { 13 | let [r] = read('foo'); 14 | t.true(T.isIdentifier(r, 'foo')); 15 | }); 16 | 17 | test('should read a true keyword', t => { 18 | let [r] = read('true'); 19 | 20 | t.true(T.isKeyword(r, 'true')); 21 | }); 22 | 23 | test('should read a null keyword', t => { 24 | let [r] = read('null'); 25 | 26 | t.true(T.isKeyword(r, 'null')); 27 | }); 28 | 29 | test('should read a string literal', t => { 30 | let [r] = read('"foo"'); 31 | 32 | t.true(T.isString(r, 'foo')); 33 | }); 34 | 35 | test('should read a punctuator', t => { 36 | let [r] = read('+'); 37 | 38 | t.true(T.isPunctuator(r, '+')); 39 | }); 40 | 41 | test('should read an empty delimiter', t => { 42 | let [r] = read('()'); 43 | 44 | t.true(T.isParens(r)); 45 | }); 46 | 47 | test('should read a () delimiter with one element', t => { 48 | let [r] = read('(42)'); 49 | t.true(T.isParens(r)); 50 | let [open, inner, close] = r; 51 | t.true(T.isNumeric(inner, 42)); 52 | }); 53 | 54 | test('should read a [] delimiter with one element', t => { 55 | let [r] = read('[42]'); 56 | t.true(T.isBrackets(r)); 57 | let [open, inner, close] = r; 58 | t.true(T.isNumeric(inner, 42)); 59 | }); 60 | 61 | test('should read a {} delimiter with one element', t => { 62 | let [r] = read('{42}'); 63 | t.true(T.isBraces(r)); 64 | let [open, inner, close] = r; 65 | t.true(T.isNumeric(inner, 42)); 66 | }); 67 | 68 | test('should read a `x` as a simple template', t => { 69 | let [r] = read('`x`'); 70 | t.true(T.isTemplate(r)); 71 | let [el] = r.items; 72 | t.true(T.isTemplateElement(el, 'x')); 73 | }); 74 | 75 | test('should read a `x${1}` as a template', t => { 76 | let [r] = read('`x${1}`'); 77 | 78 | t.true(T.isTemplate(r)); 79 | 80 | let [first, middle, end] = r.items; 81 | t.true(T.isTemplateElement(first, 'x')); 82 | t.true(T.isBraces(middle)); 83 | t.true(T.isTemplateElement(end, '')); 84 | 85 | let [open, num, close] = middle; 86 | t.true(T.isNumeric(num, 1)); 87 | }); 88 | 89 | test('should read a `x${1}y` as a template', t => { 90 | let [r] = read('`x${1}y`'); 91 | 92 | t.true(T.isTemplate(r)); 93 | let [first, middle, end] = r.items; 94 | t.true(T.isTemplateElement(first, 'x')); 95 | t.true(T.isBraces(middle)); 96 | t.true(T.isTemplateElement(end, 'y')); 97 | let [open, inner, close] = middle; 98 | t.true(T.isNumeric(inner, 1)); 99 | }); 100 | 101 | test('should handle syntax templates', t => { 102 | let [r] = read('#`foo`'); 103 | t.true(T.isSyntaxTemplate(r)); 104 | 105 | let [open, inner, close] = r; 106 | t.true(T.isIdentifier(inner, 'foo')); 107 | }); 108 | 109 | test('should handle nested syntax templates', t => { 110 | let [r] = read('#`#`bar``'); 111 | t.true(T.isSyntaxTemplate(r)); 112 | let [open, inner, close] = r; 113 | t.true(T.isSyntaxTemplate(inner)); 114 | let [iopen, iinner, iclose] = inner; 115 | t.true(T.isIdentifier(iinner, 'bar')); 116 | }); 117 | 118 | test.skip( 119 | 'should handle escaped string templates literals inside a syntax literal', 120 | t => { 121 | let [r] = read('#`x = \\`foo\\``'); 122 | t.true(T.isSyntaxTemplate(r)); 123 | let [open, x, eq, str, close] = r; 124 | t.true(T.isIdentifier(x, 'x')); 125 | t.true(T.isPunctuator(eq, '=')); 126 | t.true(T.isTemplate(str, 'foo')); 127 | }, 128 | ); 129 | 130 | test('should read a regex when it begins the source', t => { 131 | let [r] = read('/42/i'); 132 | 133 | t.true(T.isRegExp(r, '/42/i')); 134 | }); 135 | 136 | test('should read a regex when it follows a addition', t => { 137 | let [num, plus, re] = read('4 + /42/i'); 138 | t.true(T.isRegExp(re, '/42/i')); 139 | }); 140 | 141 | test('should read a regex when it follows a keyword', t => { 142 | let [ret, re] = read('return /42/i'); 143 | t.true(T.isRegExp(re, '/42/i')); 144 | }); 145 | 146 | test('should read a regex when it follows an assign', t => { 147 | let [id, eq, re] = read('x = /42/i'); 148 | t.true(T.isRegExp(re, '/42/i')); 149 | }); 150 | 151 | test('should read a regex when it follows an if statement', t => { 152 | let [iff, paren, re] = read('if () /42/i'); 153 | t.true(T.isRegExp(re, '/42/i')); 154 | }); 155 | 156 | test('should read a regex when it follows a while statement', t => { 157 | let [wh, paren, re] = read('while () /42/i'); 158 | t.true(T.isRegExp(re, '/42/i')); 159 | }); 160 | 161 | test('should read a regex when it follows a for statement', t => { 162 | let [forr, paren, re] = read('for () /42/i'); 163 | t.true(T.isRegExp(re, '/42/i')); 164 | }); 165 | 166 | test('should read a regex when it follows a function declaration', t => { 167 | let [fn, fnname, paren, curly, re] = read('function foo () { } /42/i'); 168 | t.true(T.isRegExp(re, '/42/i')); 169 | }); 170 | 171 | test('should read a regex when it follows a block statement', t => { 172 | let [block, re] = read('{x: 42} /42/i'); 173 | t.true(T.isRegExp(re, '/42/i')); 174 | }); 175 | 176 | test('should read a regex when it follows a function declaration following a return', t => { 177 | let [ret, fn, fnname, paren, curly, re] = read( 178 | 'return\nfunction foo() {} /42/i', 179 | ); 180 | t.true(T.isRegExp(re, '/42/i')); 181 | }); 182 | 183 | test('should read a regex when it follows a labeled statement inside a labeled statement', t => { 184 | let [[open, lab, colon, block, re]] = read('{x: {x: 42}/42/i}'); 185 | t.true(T.isRegExp(re, '/42/i')); 186 | }); 187 | 188 | test('should read as regex {x=4}/42/i', t => { 189 | let [block, re] = read('{x=4}/42/i'); 190 | t.true(T.isRegExp(re, '/42/i')); 191 | }); 192 | 193 | test('should read as regex {x:4}/b/i', t => { 194 | let [block, re] = read('{x:4}/b/i'); 195 | t.true(T.isRegExp(re, '/b/i')); 196 | }); 197 | 198 | test('should read as regex {y:5}{x:4}/b/i', t => { 199 | let [block, block2, re] = read('{y:5}{x:4}/b/i'); 200 | t.true(T.isRegExp(re, '/b/i')); 201 | }); 202 | 203 | test('should read as regex {y:{x:4}/b/i}', t => { 204 | let [[open, lab, colon, block, re]] = read('{y:{x:4}/b/i}'); 205 | t.true(T.isRegExp(re, '/b/i')); 206 | }); 207 | 208 | test('should read as regex foo\n{} /b/i', t => { 209 | let [id, curly, re] = read('foo\n{} /b/i'); 210 | t.true(T.isRegExp(re, '/b/i')); 211 | }); 212 | 213 | test('should read as regex foo = 2\n{} /b/i', t => { 214 | let [id, eq, num, curly, re] = read('foo = 2\n{} /b/i'); 215 | t.true(T.isRegExp(re, '/b/i')); 216 | }); 217 | 218 | test('should read as regex {a:function foo() {}/b/i}', t => { 219 | let [[open, lab, colon, fn, fnname, paren, curly, re]] = read( 220 | '{a:function foo() {}/b/i}', 221 | ); 222 | t.true(T.isRegExp(re, '/b/i')); 223 | }); 224 | 225 | test('should read as regex for( ; {a:/b/i} ; ){}', t => { 226 | let [forkw, [open, semi, [op, lab, colon, re]]] = read( 227 | 'for( ; {a:/b/i} ; ){}', 228 | ); 229 | t.true(T.isRegExp(re, '/b/i')); 230 | }); 231 | 232 | test('should read as regex function foo() {} /asdf/', t => { 233 | let [fn, fnname, parens, curly, re] = read('function foo() {} /asdf/'); 234 | t.true(T.isRegExp(re, '/asdf/')); 235 | }); 236 | 237 | test('should read as regex {false}function foo() {} /42/', t => { 238 | let [curly, fn, fnname, parens, curly2, re] = read( 239 | '{false} function foo() {} /42/i', 240 | ); 241 | t.true(T.isRegExp(re, '/42/i')); 242 | }); 243 | 244 | test('should read as regex if (false) false\nfunction foo() {} /42/i', t => { 245 | let [ifkw, parens, fls, fn, fnname, parens2, curly, re] = read( 246 | 'if (false) false\nfunction foo() {} /42/i', 247 | ); 248 | t.true(T.isRegExp(re, '/42/i')); 249 | }); 250 | 251 | test('should read as regex i = 0;function foo() {} /42/i', t => { 252 | let [id, eq, num, semi, fn, fnname, parens, curly, re] = read( 253 | 'i = 0;function foo() {} /42/i', 254 | ); 255 | t.true(T.isRegExp(re, '/42/i')); 256 | }); 257 | 258 | test('should read as regex if (false) {} function foo() {} /42/i', t => { 259 | let [ifkw, cond, body, fn, fnname, parens, curly, re] = read( 260 | 'if (false) {} function foo() {} /42/i', 261 | ); 262 | t.true(T.isRegExp(re, '/42/i')); 263 | }); 264 | 265 | // expect(read("function foo() {} function foo() {} /42/i")[8].literal) 266 | // .to.equal("/42/i"); 267 | // 268 | // expect(read("if (false) function foo() {} /42/i")[6].literal) 269 | // .to.equal("/42/i"); 270 | // 271 | // expect(read("{function foo() {} /42/i}")[0].inner[4].literal) 272 | // .to.equal("/42/i"); 273 | // 274 | // expect(read("foo\nfunction foo() {} /42/i")[5].literal) 275 | // .to.equal("/42/i"); 276 | // 277 | // expect(read("42\nfunction foo() {} /42/i")[5].literal) 278 | // .to.equal("/42/i"); 279 | // 280 | // expect(read("[2,3]\nfunction foo() {} /42/i")[5].literal) 281 | // .to.equal("/42/i"); 282 | // 283 | // expect(read("{a: 2}\nfunction foo() {} /42/i")[5].literal) 284 | // .to.equal("/42/i"); 285 | // 286 | // expect(read("\"foo\"\nfunction foo() {} /42/i")[5].literal) 287 | // .to.equal("/42/i"); 288 | // 289 | // expect(read("/42/i\nfunction foo() {} /42/i")[5].literal) 290 | // .to.equal("/42/i"); 291 | // 292 | // 293 | test('should read a div when it follows a numeric literal', t => { 294 | let [n1, div, n2] = read('2 / 2'); 295 | t.true(T.isPunctuator(div, '/')); 296 | }); 297 | 298 | test('should read a div when it follows an identifier', t => { 299 | let [a, div, b] = read('foo / 2'); 300 | t.true(T.isPunctuator(div, '/')); 301 | }); 302 | 303 | test('should read a div when it follows a call', t => { 304 | let [id, paren, div, num] = read('foo () / 2'); 305 | t.true(T.isPunctuator(div, '/')); 306 | }); 307 | 308 | test('should read a div when it follows a keyword with a dot in front of it', t => { 309 | let [obj, dot, ret, div] = read('o.return /42/i'); 310 | t.true(T.isPunctuator(div, '/')); 311 | }); 312 | 313 | test('should read a div when it follows an if+parens with a dot in front of it', t => { 314 | let [obj, dot, ifkw, parens, div] = read('o.if () /42/i'); 315 | t.true(T.isPunctuator(div, '/')); 316 | }); 317 | 318 | test('should read a div when it follows a this keyword', t => { 319 | let [thiskw, div] = read('this /42/i'); 320 | t.true(T.isPunctuator(div, '/')); 321 | }); 322 | 323 | test('should read a div when it follows a null keyword', t => { 324 | let [a, div] = read('null /42/i'); 325 | t.true(T.isPunctuator(div, '/')); 326 | }); 327 | 328 | test('should read a div when it follows a true keyword', t => { 329 | let [a, div] = read('true /42/i'); 330 | t.true(T.isPunctuator(div, '/')); 331 | }); 332 | 333 | test('should read a div when it follows a false keyword', t => { 334 | let [a, div] = read('false /42/i'); 335 | t.true(T.isPunctuator(div, '/')); 336 | }); 337 | 338 | test('should read a div when it follows a false keyword and parens', t => { 339 | let [a, b, div] = read('false() /42/i'); 340 | t.true(T.isPunctuator(div, '/')); 341 | }); 342 | 343 | test('should read a div when it follows a true keyword and parens', t => { 344 | let [a, b, div] = read('true() /42/i'); 345 | t.true(T.isPunctuator(div, '/')); 346 | }); 347 | 348 | test('should read a div when it follows a null keyword and parens', t => { 349 | let [a, b, div] = read('null() /42/i'); 350 | t.true(T.isPunctuator(div, '/')); 351 | }); 352 | 353 | test('should read a div when it follows a this keyword and parens', t => { 354 | let [a, b, div] = read('this() /42/i'); 355 | t.true(T.isPunctuator(div, '/')); 356 | }); 357 | 358 | test('should read a div when it follows a function expression', t => { 359 | let [id, eq, fn, fnname, paren, curly, div] = read( 360 | 'f = function foo () {} /42/i', 361 | ); 362 | t.true(T.isPunctuator(div, '/')); 363 | }); 364 | 365 | test('should read a div when it follows an anonymous function expression', t => { 366 | let [id, eq, fn, paren, curly, div] = read('f = function () {} /42/i'); 367 | t.true(T.isPunctuator(div, '/')); 368 | }); 369 | 370 | test('should read a div when it follows a function expression', t => { 371 | let [id, div1, fn, fnname, paren, curly, div2] = read( 372 | 'f / function foo () {} /42/i', 373 | ); 374 | t.true(T.isPunctuator(div1, '/')); 375 | t.true(T.isPunctuator(div2, '/')); 376 | }); 377 | 378 | test('should read a div when it follows a function expression following a return', t => { 379 | let [ret, fn, fnname, paren, curly, div] = read( 380 | 'return function foo () {} /42/i', 381 | ); 382 | t.true(T.isPunctuator(div, '/')); 383 | }); 384 | 385 | test('should read a div when it follows a object literal following a return', t => { 386 | let [ret, curly, div] = read('return {x: 42} /42/i'); 387 | t.true(T.isPunctuator(div, '/')); 388 | }); 389 | 390 | test('should read a div when it follows a object literal inside an object literal', t => { 391 | let [id, eq, [open, lab, colon, curly, div]] = read('o = {x: {x: 42}/42/i}'); 392 | t.true(T.isPunctuator(div, '/')); 393 | }); 394 | 395 | test('should read as div a={x:4}/b/i', t => { 396 | let [id, eq, curly, div] = read('a={x:4}/b/i'); 397 | t.true(T.isPunctuator(div, '/')); 398 | }); 399 | 400 | test('should read as div foo({x:4}/b/i);', t => { 401 | let [id, [open, curly, div]] = read('foo({x:4}/b/i);'); 402 | t.true(T.isPunctuator(div, '/')); 403 | }); 404 | 405 | test('should read as div a={y:{x:4}/b/i};', t => { 406 | let [id, eq, [open, lab, colon, curly, div]] = read('a={y:{x:4}/b/i};'); 407 | t.true(T.isPunctuator(div, '/')); 408 | }); 409 | 410 | test('should read as div return{x:4}/b/i;', t => { 411 | let [ret, curly, div] = read('return{x:4}/b/i;'); 412 | t.true(T.isPunctuator(div, '/')); 413 | }); 414 | 415 | test('should read as div throw{x:4}/b/i;', t => { 416 | let [kw, curly, div] = read('throw{x:4}/b/i;'); 417 | t.true(T.isPunctuator(div, '/')); 418 | }); 419 | 420 | test('should read as div for( ; {a:2}/a/g ; ){}', t => { 421 | let [kw, [open, semi, curly, div]] = read('for( ; {a:2}/a/g ; ){}'); 422 | t.true(T.isPunctuator(div, '/')); 423 | }); 424 | 425 | test('should read as div for( ; function(){ /a/g; } /a/g; ){}', t => { 426 | let [kw, [open, semi, fn, paren, curly, div]] = read( 427 | 'for( ; function(){ /a/g; } /a/g; ){}', 428 | ); 429 | t.true(T.isPunctuator(div, '/')); 430 | }); 431 | 432 | test('should read as div o.if() / 42 /i', t => { 433 | let [obj, dot, id, paren, div] = read('o.if() / 42 /i'); 434 | t.true(T.isPunctuator(div, '/')); 435 | }); 436 | 437 | test('should fail with mismatched closing delimiters', t => { 438 | t.throws(_ => read('42 }')); 439 | }); 440 | 441 | test.skip('should fail with mismatched opening delimiters', t => { 442 | t.throws(_ => read('{ 42 ')); 443 | }); 444 | 445 | test('should fail with mismatched nested closing delimiters', t => { 446 | t.throws(_ => read('{ 42 } }')); 447 | }); 448 | 449 | test.skip('should fail with mismatched nested opening delimiters', t => { 450 | t.throws(_ => read('{ { 42 }')); 451 | }); 452 | -------------------------------------------------------------------------------- /test/unit/test-sweet-loader.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import SweetLoader from '../../src/sweet-loader'; 4 | import { evalWithOutput } from '../assertions'; 5 | 6 | // SweetLoader unit tests 7 | 8 | test('locate pulls out the phase from the path', t => { 9 | let l = new SweetLoader(); 10 | 11 | let addr = l.locate({ name: '/foo/bar:0' }); 12 | t.is(addr.path, '/foo/bar'); 13 | t.is(addr.phase, 0); 14 | 15 | addr = l.locate({ name: '/foo/bar:0 ' }); 16 | t.is(addr.path, '/foo/bar'); 17 | t.is(addr.phase, 0); 18 | }); 19 | 20 | test('locate throws an error if phase is missing', t => { 21 | let l = new SweetLoader(); 22 | 23 | t.throws(() => l.locate({ name: '/foo/bar' })); 24 | }); 25 | 26 | // High-level API 27 | 28 | test( 29 | 'nested syntax definitions', 30 | evalWithOutput, 31 | ` 32 | function f() { 33 | syntax n = ctx => #\`1\`; 34 | return n; 35 | if (true) { 36 | syntax o = ctx => #\`1\`; 37 | return o; 38 | } 39 | } 40 | syntax m = ctx => #\`1\`; 41 | output = m;`, 42 | 1, 43 | ); 44 | -------------------------------------------------------------------------------- /test/unit/test-symbols.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import test from 'ava'; 3 | 4 | import { Symbol, gensym } from '../../src/symbol'; 5 | 6 | test('with the same names should be ===', () => { 7 | let s1 = Symbol('foo'); 8 | let s2 = Symbol('foo'); 9 | 10 | expect(s1).to.be(s2); 11 | }); 12 | 13 | test('with two different names should not be ===', () => { 14 | let s1 = Symbol('foo'); 15 | let s2 = Symbol('bar'); 16 | 17 | expect(s1).to.not.be(s2); 18 | }); 19 | 20 | test('two gensyms should not be ===', () => { 21 | let s1 = gensym(); 22 | let s2 = gensym(); 23 | 24 | expect(s1).to.not.be(s2); 25 | }); 26 | 27 | test('two symbols recreated from the names of two gensyms should not be ===', () => { 28 | let s1 = gensym(); 29 | let s2 = gensym(); 30 | 31 | expect(Symbol(s1.name)).to.not.be(Symbol(s2.name)); 32 | }); 33 | 34 | test('a symbol recreated from the name of a gensym should not be === with the original gensym', () => { 35 | let s1 = gensym(); 36 | 37 | expect(Symbol(s1.name)).to.not.be(s1); 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/test-syntax.js: -------------------------------------------------------------------------------- 1 | import Syntax from '../../src/syntax'; 2 | import expect from 'expect.js'; 3 | import { freshScope } from '../../src/scope'; 4 | import BindingMap from '../../src/binding-map'; 5 | 6 | import { gensym } from '../../src/symbol'; 7 | import test from 'ava'; 8 | 9 | test('that have no bindings or scopes should resolve to their original name ', () => { 10 | let foo = Syntax.fromIdentifier('foo'); 11 | expect(foo.resolve(0)).to.be('foo'); 12 | }); 13 | 14 | test('where one identifier has a scope and associated binding and the other does not will resolve to different names', () => { 15 | let bindings = new BindingMap(); 16 | let scope1 = freshScope('1'); 17 | 18 | let foo = Syntax.fromIdentifier('foo'); 19 | let foo_1 = Syntax.fromIdentifier('foo'); 20 | 21 | foo_1 = foo_1.addScope(scope1, bindings, 0); 22 | 23 | bindings.add(foo_1, { binding: gensym('foo'), phase: 0 }); 24 | 25 | expect(foo.resolve(0)).to.not.be(foo_1.resolve(0)); 26 | }); 27 | 28 | test('resolve to different bindings when both identifiers have a binding on a different scope', () => { 29 | let bindings = new BindingMap(); 30 | let scope1 = freshScope('1'); 31 | let scope2 = freshScope('2'); 32 | 33 | let foo_1 = Syntax.fromIdentifier('foo'); 34 | let foo_2 = Syntax.fromIdentifier('foo'); 35 | 36 | foo_1 = foo_1.addScope(scope1, bindings, 0); 37 | foo_2 = foo_2.addScope(scope2, bindings, 0); 38 | 39 | bindings.add(foo_1, { binding: gensym('foo'), phase: 0 }); 40 | bindings.add(foo_2, { binding: gensym('foo'), phase: 0 }); 41 | 42 | expect(foo_1.resolve(0)).to.not.be(foo_2.resolve(0)); 43 | }); 44 | 45 | test('should resolve when syntax object has a scopeset that is a superset of the binding', () => { 46 | let bindings = new BindingMap(); 47 | let scope1 = freshScope('1'); 48 | let scope2 = freshScope('2'); 49 | let scope3 = freshScope('3'); 50 | 51 | let foo_1 = Syntax.fromIdentifier('foo'); 52 | let foo_123 = Syntax.fromIdentifier('foo'); 53 | 54 | foo_1 = foo_1.addScope(scope1, bindings, 0); 55 | 56 | foo_123 = foo_123 57 | .addScope(scope1, bindings, 0) 58 | .addScope(scope2, bindings, 0) 59 | .addScope(scope3, bindings, 0); 60 | 61 | bindings.add(foo_1, { binding: gensym('foo'), phase: 0 }); 62 | 63 | expect(foo_1.resolve(0)).to.be(foo_123.resolve(0)); 64 | }); 65 | 66 | test.skip('should throw an error for ambiguous scops sets', () => { 67 | let bindings = new BindingMap(); 68 | let scope1 = freshScope('1'); 69 | let scope2 = freshScope('2'); 70 | let scope3 = freshScope('3'); 71 | 72 | let foo_13 = Syntax.fromIdentifier('foo'); 73 | let foo_12 = Syntax.fromIdentifier('foo'); 74 | let foo_123 = Syntax.fromIdentifier('foo'); 75 | 76 | foo_13 = foo_13.addScope(scope1, bindings, 0).addScope(scope3, bindings, 0); 77 | 78 | foo_12 = foo_12.addScope(scope1, bindings, 0).addScope(scope2, bindings, 0); 79 | 80 | foo_123 = foo_123 81 | .addScope(scope1, bindings, 0) 82 | .addScope(scope2, bindings, 0) 83 | .addScope(scope3, bindings, 0); 84 | 85 | bindings.add(foo_13, { binding: gensym('foo'), phase: 0 }); 86 | bindings.add(foo_12, { binding: gensym('foo'), phase: 0 }); 87 | 88 | expect(() => foo_123.resolve(0)).to.throwError(); 89 | }); 90 | 91 | test('should make a number syntax object', () => { 92 | let s = Syntax.fromNumber(42); 93 | 94 | expect(s.val()).to.be(42); 95 | }); 96 | 97 | test('should make an identifier syntax object', () => { 98 | let s = Syntax.fromIdentifier('foo'); 99 | 100 | expect(s.val()).to.be('foo'); 101 | expect(s.resolve(0)).to.be('foo'); 102 | }); 103 | 104 | test('should make an identifier syntax object with another identifier as the context', () => { 105 | let bindings = new BindingMap(); 106 | let scope1 = freshScope('1'); 107 | 108 | let foo = Syntax.fromIdentifier('foo').addScope(scope1, bindings, 0); 109 | bindings.add(foo, { binding: gensym('foo'), phase: 0 }); 110 | 111 | let foo_1 = Syntax.fromIdentifier('foo', foo); 112 | 113 | expect(foo.resolve(0)).to.be(foo_1.resolve(0)); 114 | }); 115 | 116 | test('should make new syntax from instance methods', t => { 117 | let base = Syntax.fromIdentifier('foo'); 118 | let derived = base.from('identifier', 'bar'); 119 | 120 | t.is(derived.value.val(), 'bar'); 121 | }); 122 | --------------------------------------------------------------------------------