├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── assets ├── favicon.ico ├── favicon.png ├── logo.png └── logo.svg ├── docs ├── Makefile ├── background.rst ├── conf.py ├── design.rst ├── directives.rst ├── genindex.rst ├── index.rst ├── internals.rst ├── interop.rst ├── make.bat ├── requirements.txt └── setup.rst ├── index.development.js ├── index.html ├── index.js ├── lib ├── ast.js ├── backend.js ├── config.js ├── config.node.js ├── config.web.js ├── frontend.js ├── frontend.node.js ├── frontend.web.js ├── overrides.js ├── shen.js └── utils.js ├── package-lock.json ├── package.json ├── scripts ├── config.js ├── fetch.js ├── parser.js ├── render.js ├── repl.js └── utils.js ├── test ├── backend │ ├── test.async.js │ ├── test.eval.js │ ├── test.parser.js │ └── test.primitive.js ├── frontend │ ├── test.ast.js │ └── test.interop.js └── kernel │ └── test.kernel.js └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "globals": { 11 | "Atomics": true, 12 | "globalThis": true, 13 | "SharedArrayBuffer": true 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-unused-vars": [ 21 | "error", { 22 | "argsIgnorePattern": "^_", 23 | "caughtErrorsIgnorePattern": "^_", 24 | "varsIgnorePattern": "^_" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /kernel 2 | kernel.js 3 | dist 4 | node_modules 5 | out 6 | _build 7 | _static 8 | _templates 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | language: node_js 4 | node_js: 5 | - "lts/*" 6 | install: 7 | - npm install 8 | script: 9 | - npm run lint || true # informational only, ignore errors 10 | - npm run test-backend 11 | - npm run fetch-kernel 12 | - npm run render-kernel 13 | - npm run test-kernel dump 14 | - npm run test-frontend 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2021, Robert Koeninger 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Shen Version](https://img.shields.io/badge/shen-22.4-blue.svg)](https://github.com/Shen-Language) 2 | [![Build Status](https://travis-ci.org/rkoeninger/ShenScript.svg?branch=master)](https://travis-ci.org/rkoeninger/ShenScript) 3 | [![Docs Status](https://readthedocs.org/projects/shenscript/badge/?version=latest)](https://shenscript.readthedocs.io/en/latest/?badge=latest) 4 | [![npm](https://img.shields.io/npm/v/shen-script.svg)](https://www.npmjs.com/package/shen-script) 5 | 6 | # Shen for JavaScript 7 | 8 | 9 | 10 | An implementation of the [Shen Language](http://www.shenlanguage.org) by [Mark Tarver](http://marktarver.com/) for JavaScript. Full documentation can be viewed at [shenscript.readthedocs.io](https://shenscript.readthedocs.io/en/latest/). 11 | 12 | ## Features 13 | 14 | * Allows integration with arbitrary I/O. 15 | * Async operations are transparent to written Shen code. 16 | * Easy interop: JS can be called from Shen, Shen can be called from JS. 17 | * Fairly small production webpack bundle (\~370KB uncompressed, \~60KB gzip compressed). 18 | * Decent web startup time (\~50ms in Chromium, \~100ms in Firefox). 19 | 20 | ## Prerequisites 21 | 22 | Requires recent version (10+) of [Node.js and npm](https://nodejs.org/en/download/). 23 | 24 | Works in most modern browers (Chromium, Firefox, Safari and Edge). 25 | 26 | ## Building and Testing 27 | 28 | First, run `npm install` as you would with any other Node project. Then run the following scripts build and test the project. Steps need to be run in order - steps after `fetch-kernel` won't work if the kernel hasn't been fetched. 29 | 30 | | Script | Description | 31 | |:----------------|:------------------------------------------------------------------------------------------------------------------| 32 | | `test-backend` | Runs `mocha` tests for the basic environment and compiler. | 33 | | `fetch-kernel` | Downloads the kernel sources from [shen-sources](https://github.com/Shen-Language/shen-sources.git) to `kernel/`. | 34 | | `render-kernel` | Translates the kernel sources to JavaScript and stores under `kernel/js/`. | 35 | | `test-kernel` | Runs the test suite that comes with the Shen kernel. | 36 | | `test-frontend` | Runs `mocha` tests for helper and interop functions. | 37 | | `bundle-dev` | Applies babel transforms and webpack's into web-deployable bundle. | 38 | | `bundle` | Builds bundle in production mode. | 39 | | `bundle-min` | Builds minified production bundle. | 40 | | `bundles` | Generates all bundles. | 41 | | `lint` | If you make changes, run `lint` to check adherence to style and code quality. | 42 | 43 | ## Running 44 | 45 | ### Demo Page 46 | 47 | Run `npm start` to start webpack watch or `npm run bundle-dev` to do a one-time build. 48 | 49 | If you open `index.html` in your browser a basic webpage will load, and when ready, it will display the load time. (The production webpack bundle does not automatically create a Shen environment and does not log anything.) `index.html` should be viewable without hosting in a web server, but you will not be able to use the `load` function to load additional Shen code if opened from a relative `file://` path. `http-server` is adequate for hosting in a web server. 50 | 51 | If you open the JavaScript console in the developer tools, it is possible to access to the `$` global object and execute commands: 52 | 53 | ```javascript 54 | $.exec("(+ 1 1)").then(console.log); 55 | ``` 56 | 57 | Chaining the `then` call is necessary because `exec` will return a `Promise`. For more information refer to the [documentation](https://shenscript.readthedocs.io/en/latest/interop.html). 58 | 59 | ### REPL 60 | 61 | Run `npm run repl` to run a command-line REPL. It should have the same behavior as the `shen-cl` REPL. `node.` functions will be available. Run `(node.exit)` to exit the REPL. 62 | 63 | Neither command-line options nor the `launcher` kernel extension are implemented. ShenScript is not intended to take the form of a standalone executable. 64 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkoeninger/ShenScript/f8155dd1fc7a89b05394d35a4cb01dac5197552b/assets/favicon.ico -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkoeninger/ShenScript/f8155dd1fc7a89b05394d35a4cb01dac5197552b/assets/favicon.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkoeninger/ShenScript/f8155dd1fc7a89b05394d35a4cb01dac5197552b/assets/logo.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/background.rst: -------------------------------------------------------------------------------- 1 | .. include:: directives.rst 2 | 3 | Motivation 4 | ========== 5 | 6 | JavaScript is one of the most commonly used languages in the world, running in every browser and on almost every platform, as well as being the basis for the prolific Node.js platform. It has some built-in capabilities like dynamic expression evaluation, async/await syntax and highly optimised runtimes that make it a preferred target. It's not a perfect match, however and the details of how the gaps between JavaScript and Shen are bridged is detailed in this documentation. 7 | 8 | The purpose of building ShenScript is to bring the powerful functionality inherent in the Shen language to web development. And with the way ShenScript is implemented, asynchronous code is handled transparently so the Shen developer doesn't have to think about the distinction between a synchronous call and an async one. This is very helpful in the JavaScript ecosystem where asynchronous operations are everywhere. 9 | 10 | Prior Art 11 | ========= 12 | 13 | Before going into detail on ShenScript, I wanted to highlight and say thank you for pre-existing work on Shen and on the porting of Shen to JavaScript that both this port and I personally have benefitted from studying. 14 | 15 | shen-js 16 | ------- 17 | 18 | Shen has previously been brought to JavaScript by way of the `shen-js `_ project by `Ramil Farkhshatov `_. shen-js implements its own KLVM on top of JS, allowing it to handle deep recursion without stack overflow and make asynchronous I/O transparent to Shen code. I wanted to make a JavaScript port that is built more directly on JavaScript and makes use on newer features. ShenScript is also intended to produce a smaller deployable (\<1MB vs \~12MB for shen-cl) that is retrievable from npm. 19 | 20 | shen-cl 21 | ------- 22 | 23 | ShenScript also takes inspiration from the original Shen port, `shen-cl `_. Shen for Common Lisp offers a good demonstration of how to embed Shen's semantics in another dynamic language. shen-cl also has good native interop which this port has attempted to replicate. 24 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # For config options, refer to: http://www.sphinx-doc.org/en/master/config 2 | 3 | project = 'ShenScript' 4 | author = 'Robert Koeninger' 5 | copyright = '2019, ' + author 6 | extensions = ['sphinx_js'] 7 | templates_path = ['_templates'] 8 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 9 | html_theme = 'sphinx_rtd_theme' 10 | html_logo = '../assets/logo.svg' 11 | html_static_path = ['_static'] 12 | master_doc = 'index' 13 | js_source_path = '../lib' 14 | -------------------------------------------------------------------------------- /docs/design.rst: -------------------------------------------------------------------------------- 1 | .. include:: directives.rst 2 | 3 | Encoding of Semantics 4 | ===================== 5 | 6 | Many of Shen's semantics are translated directly to matching concepts in JavaScript. Where there are incongruities, they are described here. 7 | 8 | Data Types 9 | ---------- 10 | 11 | numbers, strings, exceptions, absvectors 12 | Shen numbers, strings, exceptions (Error) and absvectors (array) are just their related JavaScript types with no special handling. 13 | 14 | symbols 15 | Shen symbols are interned JavaScript symbols fetched with :js:`Symbol.for` so two Shen symbols with the same name will be equivalent. 16 | 17 | empty list 18 | JavaScript :js:`null` is used for the empty list. 19 | 20 | conses 21 | ShenScript declares a :js:`Cons` class specifically to represent conses and cons lists. 22 | 23 | functions, lambdas, continuations 24 | All function-like types are JavaScript functions that have been wrapped in logic to automatically perform partial and curried application. 25 | 26 | streams 27 | Streams are implemented in a custom way by the host. Typically, they are objects with a :js:`close` function and a :js:`readByte` or :js:`writeByte` function. 28 | 29 | Special Forms 30 | ------------- 31 | 32 | if, and, or, cond 33 | Conditional code translates directly into matching JavaScript conditionals. :shen:`cond` expressions are turned into :js:`if` - :js:`else` chains. 34 | 35 | simple-error, trap-error 36 | Error handling works just like JavaScript, using the :js:`throw` and :js:`try-catch` constructs. 37 | 38 | Both :js:`throw` and :js:`try` are statement syntax in JavaScript, so to make them expressions, there is the :js:`raise` function for throwing errors and :js:`try-catch` is wrapped in an `iife `_ so it can be embedded in expressions. That iife is async and its immediate invocation is awaited. 39 | 40 | defun, lambda, freeze 41 | All function forms build and return JavaScript functions. 42 | 43 | let 44 | Local variable bindings are translated into immediately-invoked lambda expressions. This is both for brevity and it because it's the most natural way to translate Lisp-style let-as-an-expression since variable declarations are statements in JavaScript. 45 | 46 | Consider that the form :shen:`(let X Y Z)` can be transformed into the behaviorally equivalent :shen:`((lambda X Z) Y)`. ShenScript does not do it precisely this way because of involved optimizations, but that's the idea. 47 | 48 | do 49 | The do form isn't officially a form in Shen, just a function that returns it's second argument, but it is handled as a form in ShenScript in order to take advantage of an additional opportunity for tail-call optimisation. 50 | 51 | Shen Booleans vs JavaScript Booleans 52 | ------------------------------------ 53 | 54 | Shen uses the symbols :shen:`true` and :shen:`false` for booleans and does not have truthy or falsy semantics like JavaScript or other Lisps. This can make things tricky since Shen's :shen:`true` and :shen:`false` will always be considered :js:`true` by JavaScript, and in JavaScript, anything not `falsy `_ will count as :js:`true`. 55 | 56 | The KLambda-to-JavaScript transpiler does not actually consider booleans to be their own datatype, it treats them as any other symbol. 57 | 58 | When doing interop between Shen and JavaScript, it will necessary to carefully convert between the two boolean representations as with so much JavaScript code's dependence on truthy/falsy semantics, there is no general way of doing so. 59 | 60 | Equality 61 | -------- 62 | 63 | Equality sematics are implemented by the :js:`equate` function in the :js:`backend` module. 64 | 65 | Values are considered equivalent in ShenScript if they are equal according to the JavaScript :js:`===` operator or in the following cases: 66 | 67 | * Both values are :js:`Cons` and both their :js:`head` and :js:`tail` are equal according to :js:`equate`. 68 | * Both values are JavaScript arrays of the same length and all of their values are equal according to :js:`equate`. 69 | * Both values are JavaScript objects with the same constructor, same set of keys and the values for each key are equal according to :js:`equate`. 70 | 71 | Partial Function Application 72 | ---------------------------- 73 | 74 | In Shen, functions have precise arities, and when a function is applied to fewer arguments than it takes, it returns a function that takes the remaining arguments. So since :shen:`+` takes two arguments, if it is applied to a single one, as in :shen:`(+ 1)`, the result is a function that takes one number and returns :shen:`1` plus that number. 75 | 76 | ShenScript also supports curried application, where there are more arguments than the function actually takes. The function is applied to the first :code:`N` arguments equal to the function's arity, the result is asserted to be a function, and then the resulting function is applied to the remaining arguments. Repeat this process until there are no remaining arguments or until a non-function is returned and an error is raised. 77 | 78 | .. warning:: Curried application is not a part of the Shen standard, is not supported by `shen-cl `_, and might be removed from ShenScript. 79 | 80 | This is implemented in ShenScript for primitives, kernel functions and user-defined functions by wrapping them in a function that takes a variable number of arguments, checks the number passed in, and then returns another function to take the remaining arguments, performs curried application, or simply returns the result. 81 | 82 | Tail-Call Optimization 83 | ---------------------- 84 | 85 | Tail-call optimization is required by the Shen standard and Shen code make prolific use of recursion making TCO a necessity. 86 | 87 | In ShenScript, tail calls are handled dynamically using `trampolines `_. When a function is built by the transpiler, the lexical position of expressions are tracked as being in head or tail position. Function calls in head position are a simple invocation and the result is settled; calls in tail position are bounced. 88 | 89 | bounce 90 | Bouncing a function call means making a trampoline from a reference to the function and the list of arguments and returning. The function will actually be invoked when the trampoline is settled at some point later. 91 | 92 | settle 93 | Settling is the process of taking a value that might be a :js:`Trampoline`, checking if it's a tramoline, and if it is, running it. The result of running the trampoline is checked if it's a trampoline, and if so, that is run and this process is repeated until the final result is a non-trampoline value, which is returned. 94 | 95 | Generation of Syntax 96 | ==================== 97 | 98 | Generating JavaScript 99 | --------------------- 100 | 101 | ShenScript code generation is built on rendering objects rendering a JavaScript abstract syntax tree conforming to the informal `ESTree `_ standard. These ASTs are then rendered to strings using the `astring `_ library and then evaluated with an immediately-applied `Function `_ constructor. 102 | 103 | The Function constructor acts as a kind of isolated-scope version of eval. When a Function is created this way, it does not capture local variables like eval does. This prevents odd behavior from cropping up where Shen code uses a variable or function name that matches some local variable. 104 | 105 | Hoisting Globals and Idle Symbols 106 | --------------------------------- 107 | 108 | Lookups of global functions, global symbol values (:shen:`set`/:shen:`value`) and idle symbols (:js:`Symbol.for`) don't take up much time, but when done repeatedly are wasteful. To prevent repeated lookups, references to the aforementioned are hoisted to the top of each expression tree evaluated by :shen:`eval-kl` and to the top of the pre-rendered kernel. This way, they only get fetched once and are enclosed over so each time a function is called that depends on one of these, it only needs to access a local variable in scope. 109 | 110 | Global functions and global symbol values with the same name are both attached to the same lookup Cells and those Cells get assigned to escaped local variables with :js:`$c` appended to the end. Idle symbols get assigned to escaped local variables with :js:`$s` appended to the end. 111 | 112 | Fabrications 113 | ------------ 114 | 115 | Just as a :js:`Context` carries information downward while building an AST, a :js:`Fabrication` carries resulting context back upward. A fabrication contains the subsection of the AST that was built, along with the results of decisions that were made down in that tree so it doesn't have to be scanned again. 116 | 117 | Fabrications are useful because they are easily composible. There is a composition function for fabrications called :js:`assemble` which takes a function to combine ASTs and a list of fabrications as arguments. The combining function gets us a single AST and the rest of the metadata being carried by the fabrications has a known means of combination. The result is a single AST and a single body of metadata out of which a single fabrication is made. 118 | 119 | At the moment, the only additional information fabrications carry is a substitution map of variable names and the expressions that they will need to be initialized with. The substitution map is used to "hoist" global references to the top of the scope being constructed. When an AST is constructed that depends on one of these substitutions, it refers to a variable by the name specified as a key in the map. 120 | 121 | Escaping Special Variable Names 122 | ------------------------------- 123 | 124 | There are still some variables that need to be accessible to generated code, but not to the source code - referenceable in JavaScript, but not Shen. The main example is the environment object, conventionally named :js:`$`. Dollar signs and other special characters get escaped by replacing them with a dollar sign followed by the two-digit hex code for that character. Since dollar signs are valid identifier characters in JavaScript, hidden environment variables can be named ending with dollar sign, because if a Shen variable ends with :shen:`$`, the escaped JavaScript name will have a trailing :js:`$24` instead. 125 | 126 | Dynamic Type-Checking 127 | --------------------- 128 | 129 | Many JavaScript operators, like the :js:`+` operator, are not limited to working with specific types like they are in Shen. In JavaScript, :js:`+` can do numberic addition, string concatenation and offers a variety of strange behaviors are argument types are mixed. In Shen, the :shen:`+` function only works on numbers and passing a non-number is an error. 130 | 131 | So in order to make sure these primitive functions are being applied to the correct types, there are a series of :js:`is` functions like :js:`isNumber`, :js:`isString` and :js:`isCons`, which determine if a value is of that type. There are also a series of :js:`as` functions for the same types which check if the argument is of that type and returns it if it is, but raises an error if it is not. 132 | 133 | This allows concise definition of primitive functions. The :shen:`+` primitve is defined like this: 134 | 135 | .. code-block:: js 136 | 137 | (x, y) => asNumber(x) + asNumber(y) 138 | 139 | and the :shen:`cn` primitive is defined like this: 140 | 141 | .. code-block:: js 142 | 143 | (x, y) => asString(x) + asString(y) 144 | 145 | Code Inlining and Optimisation 146 | ------------------------------ 147 | 148 | To reduce the volume of generated code, and to improve performance, most primitive operations are inlined when fully applied. Since the type checks described in the previous section are still necessary, they get inlined as well, but can be excluded on certain circumstances. For instance, when generating code for the expression :shen:`(+ 1 X)`, it is certain that the argument expression :shen:`1` is of a numeric type as it is a numeric constant. So instead of generating the code :js:`asNumber(1) + asNumber(X)`, we can just render :js:`1 + asNumber(X)`. 149 | 150 | The transpiler does this simple type inference following a few rules: 151 | 152 | * Literal numeric, string and idle symbol values are inferred to be of those types. 153 | 154 | * :shen:`123` is :code:`Number`. 155 | * :shen:`"hello"` is :code:`String`. 156 | * :shen:`thing` is :code:`Symbol`. 157 | 158 | * The value returned by a primitive function is inferred to be of that function's known return type, regardless of the type of the arguments. If the arguments are of an unexpected type, an error will be raised anyway. 159 | 160 | * :shen:`(+ X Y)` is :code:`Number`. 161 | * :shen:`(tlstr X)` is :code:`String`. 162 | * :shen:`(cons X Y)` is :code:`Cons`. 163 | 164 | * Local variables in :shen:`let` bindings are inferred to be of the type their bound value was inferred to be. 165 | 166 | * The :shen:`X` in :shen:`(+ X Y)` in :shen:`(let X 1 (+ X Y))` is :code:`Number`. :shen:`X` would not need an :js:`asNumber` cast in :shen:`(+ X Y)`. 167 | 168 | * The parameter to a lambda expression used as an error handler in a :shen:`trap-error` form is inferred to be :code:`Error`. 169 | 170 | * :shen:`(trap-error (whatever) (/. E (error-to-string E)))` does not generate an :js:`asError` check for :shen:`E`. 171 | 172 | More sophisticated analysis could be done, but with dimishing returns in the number of cases it actually catches. And consider that user-defined functions can be re-defined, either in a REPL session or in code loaded from a file, meaning assumptions made by optimised functions could be invalidated. When a function was re-defined, all dependent functions would have to be re-visited and potentially all functions dependent on those functions. That's why these return type assumptions are only made for primitives. 173 | 174 | Pervasive Asynchronocity 175 | ------------------------ 176 | 177 | I/O functions, including primitives like :shen:`read-byte` and :shen:`write-byte` are idiomatically synchronous in Shen. This poses a problem when porting Shen to a platform like JavaScript which pervasively uses asynchronous I/O. 178 | 179 | Functions like :shen:`read-byte` and :shen:`open` are especially problematic, because where :shen:`write-byte` and :shen:`close` can be fire-and-forget even if they do need to be async, Shen code will expect :shen:`read-byte` and :shen:`open` to be blocking and the kernel isn't designed to await a promise or even pass a continuation. 180 | 181 | In order to make the translation process fairly direct, generated JavaScript uses `async/await `_ syntax so that code structured like synchronous code can actually be asynchronous. This allows use of async functions to look just like the use of sync functions in Shen, but be realized however necessary in the host language. 182 | 183 | :js:`async` code generation is controlled by a flag on the :js:`Context` object that the compiler disables for functions that it is sure can be synchronous. 184 | 185 | Future Design Options 186 | ===================== 187 | 188 | Some designs are still up in the air. They either solve remaining problems like performance or provide additional capabilites. 189 | 190 | Polychronous Functions 191 | ---------------------- 192 | 193 | Currently, it is difficult to be sure an execution path will be entirely synchronous so the compiler plays it safe and only renders functions as sync when it is sure that function and its referents are sync. 194 | 195 | Instead, the compiler could render both sync and async versions of most functions and choose which to call on a case-by-case basis. And when a referent is re-defined from sync to async or vice-versa, the environment could quickly switch the referring function from preferring one mode or the other. 196 | 197 | For example, if we have Shen code like :shen:`(map F Xs)` and :shen:`F` is known to be sync, we can call the sync version of :shen:`map` which is tail-recursive or is a simple for-loop by way of a pinhole optimisation. This way, we won't have to evaluate the long chain of promises and trampolines the async version would result in for any list of decent length. 198 | 199 | The compiler would have to keep track of additional information like which functions are always sync, which are always async and which can be one or the other and based on what criteria. 200 | 201 | KLambda-Expression Interpreter 202 | ------------------------------ 203 | 204 | Some JavaScript environments will have a `Content Security Policy `_ enabled that forbids the use of :js:`eval` and :js:`Function`. This would completely break the current design of the ShenScript evaluator. The transpiler would continue to work, and could produce JavaScript ASTs, but they could not be evaluated. 205 | 206 | A scratch ESTree interpreter could be written, but as it might need to support most of the capabilities of JavaScript itself, it would easier to write an interpreter that acts on the incoming KLambda expression trees themselves and forgo the transpiler entirely. 207 | 208 | The obvious downside is that the interpreter would be much slower that generated JavaScript which would enjoy all the optimisations built into a modern runtime like V8. The interpreter would only be used when it was absolutely necessary. 209 | 210 | Expression Type Tracking 211 | ------------------------ 212 | 213 | Right now, the compiler only tracks the known types of local variables and nested expressions. It could also retain information like :shen:`(tl X)` is a cons and not just that :shen:`X` is a cons and then have to check again later when evaluating :shen:`(hd (tl X))`. This would remove plenty of :js:`asCons` calls that are not strictly necessary. 214 | 215 | Historical and Abandoned Design 216 | =============================== 217 | 218 | String Concatenation 219 | -------------------- 220 | 221 | In a much earlier version, code generation was done with JavaScript template strings and string concatenation. This was replaced with the use of the astring library since it is cleaner, more reliable and more flexible to have an AST that can undergo further manipulation as opposed to a final code string that can only be concatenated. 222 | 223 | Using Fabrications for Statement-oriented Syntax 224 | ------------------------------------------------ 225 | 226 | Shen's code style is very expression-oriented and is most easily translated to another expression-oriented language. Most of Shen's forms translate directly to expression syntax in JavaScript without a problem. All function calls are expressions in JavaScript, conditions can use the :js:`? :` ternary operator, etc. Two forms in particular pose a problem: :shen:`let` and :shen:`trap-error` would most naturally be represented with statements in JavaScript. 227 | 228 | We could just emit :code:`VariableDeclaration` and :code:`TryStatement` AST nodes for these forms, but that causes a compilication when either a statement or an expression might be emitted by the transpiler at any point in the code. And while it's easy to embed an expression in a statement construct - just by wrapping in an :code:`ExpressionStatement` - it's harder to embed a statement in an expression. 229 | 230 | An expression like :shen:`(+ 1 (trap-error (whatever) (/. _ 0)))` would normally be rendered like :js:`1 + asNumber(trap(() => whatever(), _ => 0))`. How would it be rendered if we wanted to inline a :js:`try-catch` instead of using the :js:`trap` function? 231 | 232 | The concept of a Fabrication (aka "fabr") was introduced to represent the composition of the two forms of syntax. Whenever a child form is built, it would return a fabr, consisting of a list of prerequiste statements and a resulting expression. Fabrs are typically composed by making a new fabr with all the statements from the first fabr, followed by the statements from the second fabr and then the result expressions are combined as they would be in the current design. 233 | 234 | Since every fabr needs a result expression, for statement syntax, an additional variable declaration is added for a result variable and the result of the fabr is just the identifier expression for that variable. 235 | 236 | An example like :shen:`(+ (let X 3 (* X 2)) (trap-error (whatever) (/. _ 0)))` would get rendered like this. The :shen:`(let X 3 (* X 2))` expression becomes: 237 | 238 | .. code-block:: none 239 | 240 | { 241 | "statements": [ 242 | << const X = 3; >> 243 | ], 244 | "result": << X * 2 >> 245 | } 246 | 247 | The :shen:`(trap-error (whatever) (/. _ 0))` becomes: 248 | 249 | .. code-block:: none 250 | 251 | { 252 | "statements": [ 253 | << let R123$; >>, 254 | << 255 | try { 256 | R123$ = settle(whatever()); 257 | } catch (e$) { 258 | R123$ = 0; 259 | } 260 | >> 261 | ], 262 | "result": << R123$ >> 263 | } 264 | 265 | And composed together, they are: 266 | 267 | .. code-block:: none 268 | 269 | { 270 | "statements": [ 271 | << const X = 3; >> 272 | << let R123$; >>, 273 | << 274 | try { 275 | R123$ = settle(whatever()); 276 | } catch (e$) { 277 | R123$ = 0; 278 | } 279 | >> 280 | ], 281 | "result": << (X * 2) + asNumber(R123$) >> 282 | } 283 | 284 | This whole approach was attempted on the premise that using more idiomatic JavaScript syntax would give the runtime more opportunities to identify optimisations vs using :js:`trap` and immediately-invoked lambdas. Turns out using fabrs produced about twice the code volume and benchmarks took 3-4 times as long to run. I guess V8 is really good at optimising IIFEs. So fabrs were reverted. The design is documented here for historical reasons. 285 | 286 | This approach could be brought back in order to better handle :shen:`trap-error` at the root of a lambda, avoiding an additional iife. 287 | 288 | Another possibility is this would allow the introduction of :shen:`js.while`, :shen:`js.for`, etc. types of special forms where Shen code could directly invoke imperative syntax. 289 | -------------------------------------------------------------------------------- /docs/directives.rst: -------------------------------------------------------------------------------- 1 | .. role:: shen(code) 2 | :language: lisp 3 | 4 | .. role:: js(code) 5 | :language: javascript 6 | -------------------------------------------------------------------------------- /docs/genindex.rst: -------------------------------------------------------------------------------- 1 | .. This file is a placeholder and will be replaced with generated content 2 | 3 | Index 4 | ===== 5 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ShenScript 2 | ========== 3 | 4 | An implementation of the `Shen Language `_ by `Mark Tarver `_ for JavaScript. Built for modern browsers and recent versions of Node, requiring the `latest features `_ of the ECMAScript standard. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Introduction 9 | 10 | background 11 | design 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Walkthrough and API Docs 16 | 17 | setup 18 | interop 19 | internals 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: Appendix 24 | 25 | genindex 26 | -------------------------------------------------------------------------------- /docs/internals.rst: -------------------------------------------------------------------------------- 1 | .. include:: directives.rst 2 | 3 | Accessing ShenScript Internals from JavaScript 4 | ============================================== 5 | 6 | .. note:: Some of these functions and classes are exported, but they are primarily referred to internally. They are exported so generated JavaScript code will have access to them. Or, in rare cases, they are exported in case your Shen code needs to work around something. 7 | 8 | Data 9 | ---- 10 | 11 | .. warning:: These objects are not meant to be tampered with by user or client code. Tinker with them if you must, but it will void the warranty. 12 | 13 | .. data:: globals 14 | 15 | A :js:`Map` of :js:`Cell` objects, indexed by string name. Used to hold references to both global functions and global symbol values. If a function and a global symbol have the same name, they will be referred to by the same entry in this map. 16 | 17 | Classes 18 | ------- 19 | 20 | .. class:: Cell(name) 21 | 22 | Contains mutable pointers to the function and/or the global symbol value by the given name. Should only be created by the :js:`lookup` function. 23 | 24 | :param string name: The name of the global. 25 | 26 | .. class:: Cons(head, tail) 27 | 28 | The classic Lisp cons cell. :js:`head` and :js:`tail` are akin to :js:`car` and :js:`cdr`. 29 | 30 | When a chain of :js:`Cons` is used to build a list, the last :js:`Cons` in the chain has a :js:`tail` of :js:`null`. 31 | 32 | :param any head: Named :js:`head` because it's typically the head of a list. 33 | :param any tail: Named :js:`tail` because it's typically the tail of a list. 34 | 35 | .. class:: Context(options) 36 | 37 | :js:`Context` objects are passed between calls to the :js:`build` function to track syntax context and rendering options. 38 | 39 | :param object options: Collection of code generation options. 40 | :param boolean options.async: :js:`true` if code should be generated in async mode. 41 | :param boolean options.head: :js:`true` if current expression is in head position. 42 | :param Map options.locals: Used as an immutable map of local variables and their known types. 43 | :param object options.inlines: Map containing code inlining rules. 44 | 45 | .. class:: Fabrication(ast, subs) 46 | 47 | Returned by the :js:`build` function, containing the resulting AST and a substitution map of hoisted references. 48 | 49 | :param ast ast: The AST result of building a KL expression. 50 | :param object subs: Has string keys and AST values. 51 | 52 | .. class:: Trampoline(f, args) 53 | 54 | A :js:`Trampoline` represents a deferred tail call. 55 | 56 | :param function f: A JavaScript function. 57 | :param array args: A JavaScript array of arguments that :code:`f` will get applied to. 58 | 59 | Functions 60 | --------- 61 | 62 | .. function:: as___(x) 63 | 64 | There are several functions following this naming pattern which first check if their argument passes the related :js:`is___` function and returns it if it does. If it does not pass the type check, an error is raised. 65 | 66 | :param any x: Whatever. 67 | :returns: The same value. 68 | :throws: If argument does not pass the type check. 69 | 70 | .. function:: assemble(f, ...xs) 71 | 72 | Composes a series of fabrications into a single fabrication. 73 | 74 | :param function f: A function that combines a sequence of ASTs into an AST (or fabrication). 75 | :param array xs: A sequence of fabrications/ASTs. 76 | :returns: The composed fabrication. 77 | 78 | .. function:: bounce(f, args) 79 | 80 | Creates a :js:`Trampoline`. 81 | 82 | Aliased as :js:`b` for brevity in generated code. 83 | 84 | :param function f: A JavaScript function. 85 | :param args: A variadic parameter containing any values. 86 | :returns: A :js:`Trampoline`. 87 | 88 | .. function:: compile(expr) 89 | 90 | Builds a KLambda expression tree in the root context. 91 | 92 | :param expr expr: Expression to build. 93 | :returns: Rendered JavaScript AST. 94 | 95 | .. function:: construct(expr) 96 | 97 | Like :js:`compile`, but returns a fabrication, not an AST. 98 | 99 | :param expr expr: Expression to build. 100 | :returns: Rendered fabrication. 101 | 102 | .. function:: lookup(name) 103 | 104 | Retrieves the :js:`Cell` for the given name, creating it and adding it to :js:`globals` if it did not already exist. 105 | 106 | Aliased as :js:`c` for brevity in generated code. 107 | 108 | :param string name: Name of a global function or symbol. 109 | :returns: A :js:`Cell` for that name. 110 | 111 | .. function:: fun(f) 112 | 113 | Takes a function that takes a precise number of arguments and returns a wrapper that automatically applies partial and curried application. 114 | 115 | Aliased as :js:`l` for brevity in generated code. 116 | 117 | :param function f: Function wrap with partial application logic. 118 | :returns: Wrapper function. 119 | 120 | .. function:: is___(x) 121 | 122 | There are several functions following this naming pattern which checks if the argument qualifies as the type it's named for. 123 | 124 | :param any x: Whatever. 125 | :returns: A JavaScript boolean. 126 | 127 | .. function:: nameOf(symbol) 128 | 129 | Returns string name of given symbol. Symbol does not have to have been declared. 130 | 131 | :param symbol symbol: A symbol. 132 | :returns: Symbol name. 133 | 134 | .. function:: raise(message) 135 | 136 | Throws an Error with the given message. 137 | 138 | :param string message: Error message. 139 | :returns: Doesn't. 140 | :throws: Error with the given message. 141 | 142 | .. function:: settle(x) 143 | 144 | If given a trampoline, runs trampoline and checks if result is a trampoline, in which case that is then run. Process repeats until result is not a trampoline. Never returns a trampoline. Potentially any function in :js:`functions` will need to be settled after being called to get a useful value. 145 | 146 | Handles async trampolines by branching off to unexported :js:`future` function when encountering a promise. :js:`future` will automatically thread trampoline settling through promise chains. 147 | 148 | Aliased as :js:`t` for brevity in generated code. 149 | 150 | :param any x: May be a :js:`Trampoline` (or a :js:`Promise`), which will be run, or any other value, which will be returned immediately. 151 | :returns: Final non-trampoline result. 152 | 153 | .. function:: symbolOf(name) 154 | 155 | Returns the interned symbol by the given name. 156 | 157 | Aliased as :js:`s` for brevity in generated code. 158 | 159 | :param string name: Symbol name. 160 | :returns: Symbol by that name. 161 | 162 | Accessing ShenScript Internals from Shen 163 | ======================================== 164 | 165 | Functions in the :shen:`shen-script` namespace are for directly accessing ShenScript internals. 166 | 167 | Functions 168 | --------- 169 | 170 | These functions are callable from Shen to give access to the implementation details of ShenScript. 171 | 172 | .. function:: (shen-script.$) 173 | 174 | Provides access to the ShenScript environment object, which when combined with :code:`js` interop functions, allows arbitrary manipulation of the port's implementation details from Shen. 175 | 176 | :returns: ShenScript environment object. 177 | 178 | .. function:: (shen-script.ast) 179 | 180 | Returns a JavaScript object with all the JavaScript AST constructor functions. 181 | 182 | :returns: The :js:`exports` of the :js:`ast` module. 183 | 184 | .. function:: (shen-script.lookup-function Name) 185 | 186 | Allows lookup of global function by name instead of building wrapper lambdas or the like. 187 | 188 | :param symbol Name: Name of function to lookup. 189 | :returns: Shen function by that name, or :shen:`[]` when function does not exist. 190 | 191 | .. function:: (shen-script.boolean.js->shen X) 192 | 193 | Converts a JavaScript boolean to a Shen boolean. Any truthy value counts as JavaScript :js:`true` and any falsy value counts as JavaScript :js:`false`. 194 | 195 | :param any X: Accepts any value as an argument. 196 | :returns: A Shen boolean. 197 | 198 | .. function:: (shen-script.boolean.shen->js X) 199 | 200 | Converts a Shen boolean to a JavaScript boolean. 201 | 202 | :param boolean X: A Shen boolean. 203 | :returns: A JavaScript boolean. 204 | :throws: Error if argument is not a Shen boolean. 205 | 206 | .. function:: (shen-script.array->list X) 207 | 208 | Converts a JavaScript array to a cons list. 209 | 210 | :param array X: A JavaScript array. 211 | :returns: A Shen list. 212 | 213 | .. function:: (shen-script.array->list+ X Tail) 214 | 215 | Converts a JavaScript array to a cons list with a specified value for the tail of the last cons. 216 | 217 | :param array X: A JavaScript array. 218 | :param any Tail: Specific final tail value. 219 | :returns: A Shen list. 220 | 221 | .. function:: (shen-script.array->list-tree X) 222 | 223 | Converts a tree of nested JavaScript arrays to a tree of nested cons lists. 224 | 225 | :param array X: A JavaScript array with elements that may be arrays. 226 | :returns: A Shen list with elements that will be lists. 227 | 228 | .. function:: (shen-script.list->array X) 229 | 230 | Converts cons list to JavaScript array. 231 | 232 | :param array X: A Shen list. 233 | :returns: A JavaScript array. 234 | 235 | .. function:: (shen-script.list->array-tree X) 236 | 237 | Converts tree of nested cons lists to tree of nested JavaScript arrays. 238 | 239 | :param array X: A Shen list with elements that may be lists. 240 | :returns: A JavaScript array with elements that will be arrays. 241 | 242 | AST Construction Functions 243 | -------------------------- 244 | 245 | Functions in the `js.ast` namespace are used to construct, emit and evaluate arbitrary JavaScript code. All of the AST builder functions return JavaScript objects conforming to the informal ESTree standard `ESTree `_. 246 | 247 | .. function:: (js.ast.arguments) 248 | 249 | Constructs a reference to the :js:`arguments` object. 250 | 251 | :returns: An :code:`Identifier` AST node. 252 | 253 | .. function:: (js.ast.array Values) 254 | 255 | Constructs array literal syntax. 256 | 257 | Example: :js:`[x, y, z]`. 258 | 259 | :param list Values: A Shen list of value AST's to initialise a JavaScript array with. 260 | :returns: An :code:`ArrayExpression` AST node. 261 | 262 | .. function:: (js.ast.arrow Parameters Body) 263 | 264 | Constructs a lambda expression. 265 | 266 | Example: :js:`x => ...` 267 | 268 | :param list Parameters: A Shen list of parameter identifiers. 269 | :param ast Body: A body expression. 270 | :returns: A :code:`ArrowFunctionExpression` AST Node. 271 | 272 | .. function:: (js.ast.assign Target Value) 273 | 274 | Constructs an assignment expression. 275 | 276 | Example :js:`x = y` 277 | 278 | :param ast Target: The variable to assign to. 279 | :param ast Value: The value to assign. 280 | :returns: An :code:`AssignmentExpression` AST Node. 281 | 282 | .. function:: (js.ast.async Ast) 283 | 284 | Makes function or class member async. 285 | 286 | Examples: :js:`async (x, y) => ...`, :js:`async function(x, y) { ... }`, :js:`async method(x, y) { ... }` 287 | 288 | :param ast Ast: Ast to make async. 289 | :returns: The same AST after setting the :js:`async` property to :js:`true`. 290 | 291 | .. function:: (js.ast.await Argument) 292 | 293 | Constructs an await expression for use in an async function. 294 | 295 | Example: :js:`await x` 296 | 297 | :param ast Argument: Expression to await. 298 | :returns: An :code:`AwaitExpression` AST Node. 299 | 300 | .. function:: (js.ast.binary Operator Left Right) 301 | 302 | Constructs a binary operator application. 303 | 304 | Examples: :js:`x && y`, :js:`x + y` 305 | 306 | :param string Operator: Name of operator to apply. 307 | :param ast Left: Expression on the left side. 308 | :param ast Right: Expression on the right side. 309 | :returns: A :code:`BinaryExpression` AST Node. 310 | 311 | .. function:: (js.ast.block Statements) 312 | 313 | Constructs a block that groups statements into a single statement and provides isolated scope for :js:`const` and :js:`let` bindings. 314 | 315 | Example: :js:`{ x; y; z; }` 316 | 317 | :param list Statements: A Shen list of statement AST's. 318 | :returns: A :code:`BlockStatement` AST Node. 319 | 320 | .. function:: (js.ast.call Function Args) 321 | 322 | Constructs a function call expression. 323 | 324 | Example: :js:`f(x, y)` 325 | 326 | :param ast Function: An expression AST that evaluates to a function. 327 | :param list Args: A Shen list of argument AST's. 328 | :returns: A :code:`CallExpression` AST Node. 329 | 330 | .. function:: (js.ast.catch Parameter Body) 331 | 332 | Constructs a catch clause. 333 | 334 | Example: :js:`catch (e) { ... }` 335 | 336 | :param ast Parameter: An identifer for the error that was caught. 337 | :param ast Body: A block of statements that get run when the preceeding try has failed. 338 | :returns: A :code:`CatchClause` AST Node. 339 | 340 | .. function:: (js.ast.class Name SuperClass Slots) 341 | 342 | Constructs ES6 class syntax. Members are constructed using :shen:`js.ast.constructor`, :shen:`js.ast.method`, :shen:`js.ast.getter`, :shen:`js.ast.setter` or the more general function :shen:`js.ast.slot`. 343 | 344 | Example: 345 | 346 | .. code-block:: js 347 | 348 | class Class extends SuperClass { 349 | constructor(...) { 350 | ... 351 | } 352 | method(...) { 353 | ... 354 | } 355 | } 356 | 357 | :param ast Name: Identifier naming the class. 358 | :param ast SuperClass: Identifier node of super class, can be undefined or null. 359 | :param list Slots: A Shen list of slot AST's. 360 | :returns: A :code:`ClassExpression` AST Node. 361 | 362 | .. function:: (js.ast.const Id Value) 363 | 364 | Constructs :js:`const` variable declaration. 365 | 366 | :param ast Id: Variable name. 367 | :param ast Value: Value to initialise variable with. 368 | :returns: A :code:`VariableDeclaration` AST node. 369 | 370 | .. function:: (js.ast.constructor Body) 371 | 372 | Specialisation of :shen:`js.ast.slot` for class constructors. 373 | 374 | Example: :js:`constructor(...) { ... }` 375 | 376 | .. function:: (js.ast.debugger) 377 | 378 | Constructs a :js:`debugger;` statement. 379 | 380 | :returns: A :code:`DebuggerStatement` AST node. 381 | 382 | .. function:: (js.ast.do-while Test Body) 383 | 384 | Constructs a do-while loop. 385 | 386 | Example: :js:`do { ... } while (condition);` 387 | 388 | :param ast Test: Conditional expression that determines if the loop will run again. 389 | :param ast Body: Block of statements to run each time the loop repeats or the first time. 390 | :returns: A :code:`DoWhileStatement` AST Node. 391 | 392 | .. function:: (js.ast.empty) 393 | 394 | Constructs an empty statement. 395 | 396 | Example: :js:`;` 397 | 398 | :returns: An :code:`EmptyStatement` AST Node. 399 | 400 | .. function:: (js.ast.for Init Test Update Body) 401 | 402 | Constructs a for loop. 403 | 404 | Example: :js:`for (let x = 0; x < i; ++x) { ... }` 405 | 406 | :param ast Init: Declarations and initial statements. This can be a sequence expression. 407 | :param ast Test: Conditional expression that determines if the loop will run again or for the first time. 408 | :param ast Update: Update expressions to evaluate at the end of each iteration. This can be a sequence expression. 409 | :param ast Body: Block of statements to run each time the loop repeats. 410 | :returns: A :code:`ForStatement` AST Node. 411 | 412 | .. function:: (js.ast.for-await-of Left Right Body) 413 | 414 | Constructs an asynchronous for-of loop. Each value iterated from :shen:`Right` gets awaited before being bound to the variable or pattern declared in :shen:`Left`. 415 | 416 | Example: :js:`for await (let x of xs) { ... }` 417 | 418 | :param ast Left: Declaration of local variable that each value from the iterable on the right side gets assigned to. 419 | :param ast Right: Expression that evaluates to an iterable value. 420 | :param ast Body: Block of statements to run each time the loop repeats. 421 | :returns: A :code:`ForOfStatement` AST Node. 422 | 423 | .. function:: (js.ast.for-in Left Right Body) 424 | 425 | Constructs a for-in loop. 426 | 427 | Example: :js:`for (let x in xs) { ... }` 428 | 429 | :param ast Left: Declaration of local variable that each key from the object on the right side gets assigned to. 430 | :param ast Right: Expression that evaluates to some object. 431 | :param ast Body: Block of statements to run each time the loop repeats. 432 | :returns: A :code:`ForInStatement` AST Node. 433 | 434 | .. function:: (js.ast.for-of Left Right Body) 435 | 436 | Constructs a for-of loop. 437 | 438 | Example: :js:`for (let x of xs) { ... }` 439 | 440 | :param ast Left: Declaration of local variable that each value from the iterable on the right side gets assigned to. 441 | :param ast Right: Expression that evaluates to an iterable value. 442 | :param ast Body: Block of statements to run each time the loop repeats. 443 | :returns: A :code:`ForOfStatement` AST Node. 444 | 445 | .. function:: (js.ast.function Name Parameters Body) 446 | 447 | Constructs a function expression. 448 | 449 | Example: :js:`function name(x, y) { ... }` 450 | 451 | :param ast Name: Optional identifier naming the function. 452 | :param list Parameters: A Shen list of parameter expression. 453 | :param ast Body: A block of statements that make of the body of the function. 454 | :returns: A :code:`FunctionExpression` AST Node. 455 | 456 | .. function:: (js.ast.function* Name Parameters Body) 457 | 458 | Constructs a generator function expression. 459 | 460 | Example: :js:`function* name(x, y) { ... }` 461 | 462 | :param ast Name: Optional identifier naming the function. 463 | :param list Parameters: A Shen list of parameter expression. 464 | :param ast Body: A block of statements that make of the body of the function. 465 | :returns: A :code:`FunctionExpression` AST Node. 466 | 467 | .. function:: (js.ast.getter Name Body) 468 | 469 | Specialisation of :shen:`js.ast.slot` for class getters. 470 | 471 | Example: :js:`get thing(...) { ... }` 472 | 473 | .. function:: (js.ast.id Name) 474 | 475 | Constructs an identifier - the name of a function or variable. Identifier is named exactly as the given argument. 476 | 477 | Example: :js:`x` 478 | 479 | :param string Name: Name of identifier. 480 | :returns: An :code:`Identifier` AST node. 481 | 482 | .. function:: (js.ast.if Condition Consequent Alternate) 483 | 484 | Constructs an if statement with optional else clause. 485 | 486 | Examples: 487 | 488 | .. code-block:: js 489 | 490 | if (condition) { 491 | ... 492 | } else { 493 | ... 494 | } 495 | 496 | .. code-block:: js 497 | 498 | if (condition) { 499 | ... 500 | } 501 | 502 | :param ast Condition: Conditional expression that determines which clause to step into. 503 | :param ast Consequent: The then clause. 504 | :param ast Alternate: Optional else clause. 505 | :returns: An :code:`IfStatement` AST Node. 506 | 507 | .. function:: (js.ast.let Id Value) 508 | 509 | Constructs :js:`let` variable declaration. 510 | 511 | :param ast Id: Variable name. 512 | :param ast Value: Value to initialise variable with. 513 | :returns: A :code:`VariableDeclaration` AST node. 514 | 515 | .. function:: (js.ast.literal Value) 516 | 517 | Constructs literal value syntax. 518 | 519 | :param Value: JavaScript value that can be a literal (number, string, boolean, null). 520 | :returns: A :code:`Literal` AST node. 521 | 522 | .. function:: (js.ast.member Object Member) 523 | 524 | Constructs a member access expression with the dot operator. 525 | 526 | Examples: :js:`x.y`, :js:`x[y]` 527 | 528 | :param ast Object: Expression AST to access member of. 529 | :param ast Member: Expression that computes member name to access. If non-string, will automatically be wrapped in square brackets. 530 | :returns: A :code:`MemberExpression` AST Node. 531 | 532 | .. function:: (js.ast.method Name Body) 533 | 534 | Specialisation of :shen:`js.ast.slot` for class methods. 535 | 536 | Example: :js:`method(...) { ... }` 537 | 538 | .. function:: (js.ast.new-target) 539 | 540 | Constructs a reference to the :js:`new.target` meta-property. 541 | 542 | :returns: A :code:`MetaProperty` AST node. 543 | 544 | .. function:: (js.ast.object Properties) 545 | 546 | Constructs object literal syntax. 547 | 548 | Example: :js:`{ a: b, c: d }` 549 | 550 | :param list Properties: A flat Shen list of property names and values. 551 | :returns: An :code:`ObjectExpression` AST Node. 552 | 553 | .. function:: (js.ast.return Argument) 554 | 555 | Constructs a return statement. 556 | 557 | Example: :js:`return x;` 558 | 559 | :param ast Argument: Expression to return. 560 | :returns: A :code:`ReturnStatement` AST Node. 561 | 562 | .. function:: (js.ast.safe-id Name) 563 | 564 | Constructs an identifier where the name is escaped to make it valid in JavaScript and to not collide with reserved names in ShenScript. 565 | 566 | :param string Name: Name of identifier. 567 | :returns: An :code:`Identifier` AST node. 568 | 569 | .. function:: (js.ast.setter Name Body) 570 | 571 | Specialisation of :shen:`js.ast.slot` for class setters. 572 | 573 | Example: :js:`set thing(...) { ... }` 574 | 575 | .. function:: (js.ast.sequence Expressions) 576 | 577 | Constructs a compound expression using the comma operator. 578 | 579 | Example: :js:`(x, y, z)` 580 | 581 | :param list Expressions: A Shen list of expression AST's. 582 | :returns: A :code:`SequenceExpresssion` AST Node. 583 | 584 | .. function:: (js.ast.slot Kind Name Body) 585 | 586 | Constructs a class property of the given kind. 587 | 588 | :param string Kind: "constructor", "method", "get" or "set". 589 | :param ast Name: Identifier naming the property. 590 | :param ast Body: Expression representing the function or value assigned to the property. 591 | :returns: A :code:`MethodDefinition` AST Node. 592 | 593 | .. function:: (js.ast.statement Expression) 594 | 595 | Constructs a wrapper that allows an expression to be a statement. 596 | 597 | :param ast Expression: The expression in question. 598 | :returns: An :code:`ExpressionStatement` AST Node. 599 | 600 | .. function:: (js.ast.static Ast) 601 | 602 | Makes class member static. 603 | 604 | Example: :js:`static method(x, y) { ... }` 605 | 606 | :param ast Ast: Ast to make static. 607 | :returns: The same AST after setting the :js:`static` property to :js:`true`. 608 | 609 | .. function:: (js.ast.spread Argument) 610 | 611 | Constructs spread operator/pattern syntax. 612 | 613 | Example: :js:`...x` 614 | 615 | :param ast Argument: Identifier or pattern that are gathered or spread. 616 | :returns: A :code:`SpreadElement` AST Node. 617 | 618 | .. function:: (js.ast.super Arguments) 619 | 620 | Constructs a call to the super (prototype) constructor. 621 | 622 | Example: :js:`super(x, y);` 623 | 624 | :param list Arguments: A Shen list of argument AST's. 625 | :returns: A :code:`Super` AST Node. 626 | 627 | .. function:: (js.ast.ternary Condition Consequent Alternate) 628 | 629 | Constructs an application of the ternary operator. 630 | 631 | Example :js:`x ? y : z` 632 | 633 | :param ast Condition: True/false expression on the left of the :js:`?`. 634 | :param ast Consequent: Expression that gets evaluated if the condition is true. 635 | :param ast Alternate: Expression that gets evaluated if the condition is false. 636 | :returns: A :code:`ConditionalExpression` AST Node. 637 | 638 | .. function:: (js.ast.this) 639 | 640 | Constructs a reference to the :js:`this` keyword. 641 | 642 | :returns: A :code:`ThisExpression` AST node. 643 | 644 | .. function:: (js.ast.try Body Handler) 645 | 646 | Constructs a try statement. 647 | 648 | Example: 649 | 650 | .. code-block:: js 651 | 652 | try { 653 | ... 654 | } catch (e) { 655 | ... 656 | } 657 | 658 | :param ast Body: A block of statements that get tried. 659 | :param ast Handler: A catch clause as constructed by :js:`js.ast.catch`. 660 | :returns: A :code:`TryStatement` AST Node. 661 | 662 | .. function:: (js.ast.unary Operator Argument) 663 | 664 | Construts a unary operator application. 665 | 666 | Examples: :js:`!x`, :js:`-x` 667 | 668 | :param string Operator: Name of operator to apply. 669 | :param ast Argument: Argument to apply operator to. 670 | :returns: A :code:`UnaryExpression` AST Node. 671 | 672 | .. function:: (js.ast.update Operator Target Value) 673 | 674 | Constructs an assignment expression with a specific operator. 675 | 676 | Examples :js:`x += y`, :js:`x *= y` 677 | 678 | :param string Operator: The update operator without the :code:`=`, so :code:`+`, :code:`-`, etc. 679 | :param ast Target: The variable to assign to. 680 | :param ast Value: The value to assign. 681 | :returns: An :code:`AssignmentExpression` AST Node. 682 | 683 | .. function:: (js.ast.var Id Value) 684 | 685 | Constructs :js:`var` variable declaration. 686 | 687 | :param ast Id: Variable name. 688 | :param ast Value: Value to initialise variable with. 689 | :returns: A :code:`VariableDeclaration` AST node. 690 | 691 | .. function:: (js.ast.while Test Body) 692 | 693 | Constructs a while loop. 694 | 695 | Example: :js:`while (condition) { ... }` 696 | 697 | :param ast Test: Conditional expression that determines if the loop will run again or for the first time. 698 | :param ast Body: Block of statements to run each time the loop repeats. 699 | :returns: A :code:`WhileStatement` AST Node. 700 | 701 | .. function:: (js.ast.yield Argument) 702 | 703 | Constructs a yield expression for use in a generator function. 704 | 705 | Example: :js:`yield x` 706 | 707 | :param ast Argument: Expression to yield. 708 | :returns: A :code:`YieldExpression` AST Node. 709 | 710 | .. function:: (js.ast.yield* Argument) 711 | 712 | Constructs a yield delegate expression for use in a generator function. 713 | 714 | Example: :js:`yield* x` 715 | 716 | :param ast Argument: Iterable or generator expression to yield. 717 | :returns: A :code:`YieldExpression` AST Node. 718 | 719 | AST Evaluation Functions 720 | ------------------------ 721 | 722 | .. function:: (js.ast.compile Expr) 723 | 724 | Builds a JavaScript AST from given KLambda expression. Any hoisted references will be enclosed per the backend :js:`hoist` function. 725 | 726 | :param expr Ast: KLambda expression. 727 | :returns: JavaScript AST. 728 | 729 | .. function:: (js.ast.eval Ast) 730 | 731 | Takes a JavaScript AST as built by the :code:`js.ast` functions, renders it to JavaScript and evaluates it in the current environment. 732 | 733 | :param ast Ast: JavaScript AST. 734 | :returns: Whatever the code the AST represents evaluates to. 735 | 736 | .. function:: (js.ast.render Ast) 737 | 738 | Renders the string source code representation of a JavaScript AST. 739 | 740 | :param ast Ast: Code to be rendered. 741 | :returns: String representation of that :shen:`Ast`. 742 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-js 2 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | .. include:: directives.rst 2 | 3 | Building an Environment 4 | ======================= 5 | 6 | **module** ``shen`` 7 | 8 | This is the top-level module. The :js:`exports` of this module is a function that constructs a full populated ShenScript environment object. 9 | 10 | .. function:: (options) => $ 11 | 12 | :param object options: Can have all of the same properties as the options object accepted by the :js:`backend` function. 13 | :returns: A complete ShenScript environment. 14 | 15 | The default configuration options for this environment are specified in the ``config`` module and environment-derived properties are in the ``config.node`` and ``config.web`` modules. Any of these can be overwritten by specifying them in the :js:`options` to the :js:`shen` function. 16 | 17 | The Kernel Sandwich 18 | =================== 19 | 20 | A full ShenScript environment is created by initialising a new backend with the options passed into the top-level, running that through the pre-rendered kernel and then applying the frontend decorator for whichever node or web environment is specified in the options. The composition looks like :js:`frontend(kernel(backend(options)))`. I call this "The Kernel Sandwich". 21 | 22 | The Backend 23 | ----------- 24 | 25 | **module** ``backend`` 26 | 27 | The backend module contains the KLambda-to-JavaScript transpiler, global function and symbol indexes and proto-primitives for conses, trampolines, equality and partial application. 28 | 29 | The :js:`exports` of this module is just a function that constructs a new ShenScript environment object, which is conventionally named :js:`$`. 30 | 31 | .. function:: (options = {}) => $ 32 | 33 | :param Object options: Environment config and overrides. 34 | :param function options.clock: Provides current time in fractional seconds from the Unix epoch. Defaults to :js:`() => Date.now`. 35 | :param string options.homeDirectory: Initial working directory in file system. Defaults to :js:`"/"`. 36 | :param string options.implementation: Name of JavaScript platform in use. Defaults to :js:`"Unknown"`. 37 | :param class options.InStream: Class used for input streams. Not required if :js:`isInStream` and :js:`openRead` are specified. 38 | :param class options.OutStream: Class used for output streams. Not required if :js:`isInStream` and :js:`openRead` are specified. 39 | :param function options.isInStream: Returns true if argument is an :js:`InStream`. Defaults to a function that returns false. 40 | :param function options.isOutStream: Returns true if argument is an :js:`OutStream`. Defaults to a function that returns false. 41 | :param function options.openRead: Opens an :js:`InStream` for the given file path. Defaults to a function that raises an error. 42 | :param function options.openWrite: Opens an :js:`OutStream` for the given file path. Defaults to a function that raises an error. 43 | :param string options.os: Name of operating system in use. Defaults to :js:`"Unknown"`. 44 | :param string options.port: Current version of ShenScript. Defaults to :js:`"Unknown"`. 45 | :param string options.porters: Author(s) of ShenScript. Defaults to :js:`"Unknown"`. 46 | :param string options.release: Current version of JavaScript platform in use. Defaults to :js:`"Unknown"`. 47 | :param string options.sterror: :js:`OutStream` for error messages. Defaults to :js:`stdoutput`. 48 | :param string options.stinput: :js:`InStream` for standard input. Defaults to an object that raises an error. 49 | :param string options.stoutput: :js:`OutStream` for standard output. Defaults to an object that raises an error. 50 | :returns: An object conforming to the :js:`Backend` class description. 51 | 52 | .. class:: Backend 53 | 54 | This class is a description of object returned by the :js:`backend` function and does not actually exist. It contains an initial ShenScript environment, without the Shen kernel loaded. 55 | 56 | :param function assemble: Composes a sequence of JavaScript ASTs and Fabrications into a single Fabrication. 57 | :param function assign: Initialize or set a global symbol. 58 | :param function bounce: Creates a trampoline from function and rest arguments. 59 | :param function compile: Turns KLambda expression array tree into JavaScript AST. 60 | :param function construct: Turns KLambda expression array tree into Fabrication. 61 | :param function cons: Creates a Cons from a head and tail. 62 | :param function defun: Adds function to the global function registry. 63 | :param function equate: Determines if two values are equal according to the semantics of Shen. 64 | :param function evalJs: Evalutes a JavaScript AST in isolated scope with access to :js:`$`. 65 | :param function evalKl: Builds and evaluates a KLambda expression tree in isolated scope with access to $. 66 | :param Map globals: Map of symbol names to lookup Cells. 67 | :param function inline: Registers an inlining rule. 68 | :param function lookup: Looks up Cell in :js:`globals`, adding one if it doesn't exist yet. 69 | :param function settle: If value is a Trampoline, runs Trampoline and repeats. 70 | :param function show: :js:`toString` function. Returns string representation of any value. 71 | :param function valueOf: Returns the value of the given global symbol. Raises an error if it is not defined. 72 | 73 | The Kernel 74 | ---------- 75 | 76 | **module** ``kernel`` 77 | 78 | The :js:`kernel` module contains a JavaScript rendering of the Shen kernel that can be installed into a ShenScript environment. 79 | 80 | The :js:`exports` of this module is just a function that augments an environment and returns it. 81 | 82 | .. function:: ($) => $ 83 | 84 | :param object $: A ShenScript environment to add functions to. 85 | :returns: Same :js:`$` that was passed in, conforming to the :js:`Kernel` class. 86 | 87 | .. class:: Kernel extends Backend 88 | 89 | This class is a description of object returned by the :js:`kernel` module and does not actually exist. It contains a primitive ShenScript environment along with the Shen kernel and it adequate to run standard Shen programs. 90 | 91 | The :js:`Kernel` virtual class adds no members, but does imply additional entries in the :js:`globals` map. 92 | 93 | The Frontend 94 | ------------ 95 | 96 | **module** ``frontend`` 97 | 98 | The frontend module augments a ShenScript environment with JavaScript- and ShenScript-specific functionality. 99 | 100 | Functionality provided includes: 101 | 102 | * :shen:`js` package functions that allow access to common JavaScript types, objects and functions. 103 | * :shen:`js.ast` package functions that allow generation, rendering and evaluation of JavaScript code. 104 | * :shen:`shen-script` package functions that allow access to ShenScript environment internals. 105 | 106 | The :js:`exports` of this module is just a function that augments an environment and returns it. 107 | 108 | .. function:: ($) => $ 109 | 110 | :param object $: A ShenScript environment to add functions to. 111 | :returns: Same :js:`$` that was passed in, conforming to the :js:`Frontend` class. 112 | 113 | .. class:: Frontend extends Kernel 114 | 115 | This class is a description of object returned by the :js:`frontend` function and does not actually exist. It contains a complete ShenScript environment. 116 | 117 | :param function caller: Returns a function that invokes the function by the given name, settling returned Trampolines. 118 | :param function define: Defines Shen function that defers to given JavaScript function. 119 | :param function defineTyped: Defines Shen function that defers to given JavaScript function and declares with the specified Shen type signature. 120 | :param function defmacro: Defines a Shen macro in terms of the given JavaScript function. 121 | :param function evalShen: Evaluates Shen expression tree in isolated environment. 122 | :param function exec: Parses string as Shen source, evaluates each expression and returns last result. 123 | :param function execEach: Parses string as Shen source, evaluates each expression and returns an array of the results. 124 | :param function load: Loads Shen code from the given file path. 125 | :param function parse: Returns parsed Shen source code as a cons tree. 126 | :param function pre: Registers a preprocessor function. 127 | :param function symbol: Declares a global symbol with the given value and a function by the same name that retrieves the value. 128 | :returns: Same :js:`$` that was passed in. 129 | 130 | The Node Frontend 131 | ~~~~~~~~~~~~~~~~~ 132 | 133 | **module** ``frontend.node`` 134 | 135 | Further adds :shen:`node` package helpers for interacting with the capabilites of the Node.js runtime. 136 | 137 | Functions are described `here `_. 138 | 139 | The Web Frontend 140 | ~~~~~~~~~~~~~~~~ 141 | 142 | **module** ``frontend.web`` 143 | 144 | Further adds :shen:`web` package helpers for interacting with the capabilites of a web browser or electron instance. 145 | 146 | Functions are described `here `_. 147 | -------------------------------------------------------------------------------- /index.development.js: -------------------------------------------------------------------------------- 1 | const { Shen } = require('./index.js'); 2 | const { onReady } = require('./lib/utils.js'); 3 | 4 | (async () => { 5 | try { 6 | const now = () => new Date().getTime(); 7 | const start = now(); 8 | console.log('creating shen environment...'); 9 | 10 | window.$ = await new Shen(); 11 | const message = () => `shen environment created in ${now() - start}ms.`; 12 | 13 | onReady(() => { 14 | const p = document.createElement('p'); 15 | p.classList.add('notification'); 16 | p.append(document.createTextNode(message())); 17 | p.setAttribute('title', 'Click to dismiss'); 18 | p.onclick = () => document.body.removeChild(p); 19 | document.body.prepend(p); 20 | }); 21 | console.log(message()); 22 | } catch (e) { 23 | console.error(e); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 25 | ShenScript 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ShenBase = require('./lib/shen.js'); 2 | const { StringInStream, fetchRead } = require('./lib/utils.js'); 3 | 4 | window.Shen = class extends ShenBase { 5 | constructor(options) { 6 | super({ openRead: fetchRead, InStream: StringInStream, ...options }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/ast.js: -------------------------------------------------------------------------------- 1 | const { generate } = require('astring'); 2 | 3 | const hex = ch => ('0' + ch.charCodeAt(0).toString(16)).slice(-2); 4 | const validCharacterRegex = /^[_A-Za-z0-9]$/; 5 | const escapeCharacter = ch => validCharacterRegex.test(ch) ? ch : `$${hex(ch)}`; 6 | const escapeName = name => name.split('').map(escapeCharacter).join(''); 7 | const validCharactersRegex = /^[_A-Za-z$][_A-Za-z0-9$]*$/; 8 | const validName = name => validCharactersRegex.test(name); 9 | 10 | const reserved = new Set([ 11 | 'abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 12 | 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 13 | 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final', 14 | 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 15 | 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 16 | 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 17 | 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 18 | 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with', 'yield' 19 | ]); 20 | const avoidReserved = x => x && reserved.has(x) ? '$_' + x : x; 21 | 22 | const statementTypes = new Set([ 23 | 'BlockStatement', 'DebuggerStatement', 'DoWhileStatement', 'EmptyStatement', 'ExpressionStatement', 24 | 'ForStatement', 'ForInStatement', 'ForOfStatement', 'IfStatement', 'ReturnStatement', 25 | 'TryStatement', 'VariableDeclaration', 'WhileStatement' 26 | ]); 27 | const isStatement = x => x && statementTypes.has(x.type); 28 | 29 | const Arrow = (params, body, async = false) => ({ type: 'ArrowFunctionExpression', async, params, body, }); 30 | const Assign = (left, right, operator = '=') => ({ type: 'AssignmentExpression', operator, left, right }); 31 | const Async = ast => ({ ...ast, async: true }); 32 | const Await = argument => ({ type: 'AwaitExpression', argument }); 33 | const Binary = (operator, left, right) => ({ type: 'BinaryExpression', operator, left, right }); 34 | const Block = (...body) => ({ type: 'BlockStatement', body: body.map(Statement) }); 35 | const Call = (callee, args, async = false) => { 36 | const ast = { type: 'CallExpression', callee, arguments: args }; 37 | return async ? Await(ast) : ast; 38 | }; 39 | const Catch = (param, body) => ({ type: 'CatchClause', param, body }); 40 | const Class = (id, superClass, body) => ({ type: 'ClassExpression', id, superClass, body: { type: 'ClassBody', body } }); 41 | const Conditional = (test, consequent, alternate) => ({ type: 'ConditionalExpression', test, consequent, alternate }); 42 | const Const = (id, init) => Local('const', id, init); 43 | const Debugger = () => ({ type: 'DebuggerStatement' }); 44 | const DoWhile = (test, body) => ({ type: 'DoWhileStatement', test, body }); 45 | const Empty = () => ({ type: 'EmptyStatement' }); 46 | const For = (init, test, update, body) => ({ type: 'ForStatement', init, test, update, body }); 47 | const ForIn = (left, right, body) => ({ type: 'ForInStatement', left, right, body }); 48 | const ForOf = (left, right, body, awaited = false) => ({ type: 'ForOfStatement', left, right, body, await: awaited }); 49 | const Generator = (id, params, body) => ({ type: 'FunctionExpression', generator: true, id, params, body }); 50 | const Id = name => ({ type: 'Identifier', name }); 51 | const If = (test, consequent, alternate) => ({ type: 'IfStatement', test, consequent, alternate }); 52 | const Iife = (params, args, body, async = false) => Call(Arrow(params, body, async), args, async); 53 | const Let = (id, init) => Local('let', id, init); 54 | const Literal = value => ({ type: 'Literal', value }); 55 | const Local = (kind, id, init) => ({ type: 'VariableDeclaration', kind, declarations: [{ type: 'VariableDeclarator', id, init }] }); 56 | const Member = (object, property) => 57 | property.type === 'Literal' && validName(property.value) 58 | ? { type: 'MemberExpression', object, property: Id(property.value) } 59 | : { type: 'MemberExpression', object, property, computed: property.type !== 'Identifier' }; 60 | const New = (callee, args) => ({ type: 'NewExpression', callee, arguments: args }); 61 | const NewObj = properties => ({ type: 'ObjectExpression', properties }); 62 | const NewTarget = () => ({ type: 'MetaProperty', meta: Id('new'), property: Id('target') }); 63 | const Procedure = (id, params, body) => ({ type: 'FunctionExpression', id, params, body }); 64 | const Program = body => ({ type: 'Program', body }); 65 | const Property = (key, value) => ({ type: 'Property', key, value, kind: 'init' }); 66 | const Return = argument => ({ type: 'ReturnStatement', argument }); 67 | const SafeId = (name, suffix = '') => Id(avoidReserved(escapeName(name) + suffix)); 68 | const Sequence = (...expressions) => ({ type: 'SequenceExpression', expressions }); 69 | const Slot = (kind, key, value) => ({ type: 'MethodDefinition', key, computed: key.type !== 'Identifier', kind, value }); 70 | const Spread = argument => ({ type: 'SpreadElement', argument }); 71 | const Statement = expression => isStatement(expression) ? expression : ({ type: 'ExpressionStatement', expression }); 72 | const Static = ast => ({ ...ast, static: true }); 73 | const Super = () => ({ type: 'Super' }); 74 | const TaggedTemplate = (tag, quasi) => ({ type: 'TaggedTemplateExpression', tag, quasi }); 75 | const TemplateElement = raw => ({ type: 'TemplateElement', value: { raw } }); 76 | const TemplateLiteral = (quasis, expressions) => ({ type: 'TemplateLiteral', quasis, expressions }); 77 | const Template = (tag, raw) => TaggedTemplate(tag, TemplateLiteral([TemplateElement(raw)], [])); 78 | const This = () => ({ type: 'ThisExpression' }); 79 | const Try = (block, handler) => ({ type: 'TryStatement', block, handler }); 80 | const Unary = (operator, argument, prefix = true) => ({ type: 'UnaryExpression', operator, argument, prefix }); 81 | const Var = (id, init) => Local('var', id, init); 82 | const Vector = elements => ({ type: 'ArrayExpression', elements }); 83 | const While = (test, body) => ({ type: 'WhileStatement', test, body }); 84 | const Yield = argument => ({ type: 'YieldExpression', argument }); 85 | const YieldMany = argument => ({ type: 'YieldExpression', delegate: true, argument }); 86 | 87 | module.exports = { 88 | Arrow, Assign, Async, Await, Binary, Block, Call, Catch, Class, Conditional, Const, Debugger, DoWhile, 89 | Empty, For, ForIn, ForOf, Generator, Id, If, Iife, Let, Literal, Local, Member, New, NewObj, NewTarget, Procedure, 90 | Program, Property, Return, SafeId, Sequence, Slot, Spread, Statement, Static, Super, TaggedTemplate, Template, 91 | TemplateElement, TemplateLiteral, This, Try, Unary, Var, Vector, While, Yield, YieldMany, 92 | generate, isStatement 93 | }; 94 | -------------------------------------------------------------------------------- /lib/backend.js: -------------------------------------------------------------------------------- 1 | const { 2 | Arrow, Binary, Block, Call, Catch, Conditional, Id, Iife, Literal, 3 | Member, Return, SafeId, Sequence, Template, Try, Unary, Vector, 4 | generate, isStatement 5 | } = require('./ast.js'); 6 | const { 7 | AsyncFunction, 8 | flatMap, last, most, produce, produceState, raise, s 9 | } = require('./utils.js'); 10 | 11 | class Cons { 12 | constructor(head, tail) { 13 | this.head = head; 14 | this.tail = tail; 15 | } 16 | } 17 | 18 | class Trampoline { 19 | constructor(f, args) { 20 | this.f = f; 21 | this.args = args; 22 | } 23 | } 24 | 25 | class Context { 26 | constructor(options) { 27 | Object.assign(this, options); 28 | } 29 | with(options) { return new Context({ ...this, ...options }) } 30 | sync() { return this.with({ async: false }); } 31 | now() { return this.with({ head: true }); } 32 | later() { return this.with({ head: false }); } 33 | add(...locals) { return this.with({ locals: new Map([...this.locals, ...locals]) }); } 34 | has(local) { return this.locals.has(local); } 35 | get(local) { return this.locals.get(local); } 36 | ann(local, dataType) { 37 | return this.with({ locals: new Map(this.locals).set(local, { ...(this.get(local) || {}), dataType }) }); 38 | } 39 | } 40 | 41 | class Cell { 42 | constructor(name) { 43 | this.name = name; 44 | this.f = () => raise(`function "${name}" is not defined`); 45 | this.value = undefined; 46 | this.valueExists = false; 47 | } 48 | set(x) { 49 | this.value = x; 50 | this.valueExists = true; 51 | return x; 52 | } 53 | get() { 54 | return this.valueExists ? this.value : raise(`global "${this.name}" is not defined`); 55 | } 56 | } 57 | 58 | // TODO: if fabrs compose statements again, can use inline js.for, js.while, js.return, etc 59 | // making it easier to write overrides in Shen/KL 60 | class Fabrication { 61 | constructor(ast, subs = {}) { 62 | this.ast = ast; 63 | this.subs = subs; 64 | } 65 | get keys() { return Object.keys(this.subs); } 66 | get values() { return Object.values(this.subs); } 67 | } 68 | 69 | const nameOf = Symbol.keyFor; 70 | const symbolOf = Symbol.for; 71 | const shenTrue = s`true`; 72 | const shenFalse = s`false`; 73 | const isObject = x => !Array.isArray(x) && typeof x === 'object' && x !== null; 74 | const isNumber = x => typeof x === 'number' && Number.isFinite(x); 75 | const isNzNumber = x => isNumber(x) && x !== 0; 76 | const isString = x => typeof x === 'string' || x instanceof String; 77 | const isNeString = x => isString(x) && x.length > 0; 78 | const isSymbol = x => typeof x === 'symbol'; 79 | const isFunction = x => typeof x === 'function'; 80 | const isArray = x => Array.isArray(x); 81 | const isEArray = x => isArray(x) && x.length === 0; 82 | const isNeArray = x => isArray(x) && x.length > 0; 83 | const isError = x => x instanceof Error; 84 | const isCons = x => x instanceof Cons; 85 | const isList = x => x === null || isCons(x); 86 | const asNumber = x => isNumber(x) ? x : raise('number expected'); 87 | const asNzNumber = x => isNzNumber(x) ? x : raise('non-zero number expected'); 88 | const asString = x => isString(x) ? x : raise('string expected'); 89 | const asNeString = x => isNeString(x) ? x : raise('non-empty string expected'); 90 | const asSymbol = x => isSymbol(x) ? x : raise('symbol expected'); 91 | const asFunction = x => isFunction(x) ? x : raise('function expected'); 92 | const asArray = x => isArray(x) ? x : raise('array expected'); 93 | const asCons = x => isCons(x) ? x : raise('cons expected'); 94 | const asList = x => isList(x) ? x : raise('list expected'); 95 | const asError = x => isError(x) ? x : raise('error expected'); 96 | const asIndex = (i, a) => 97 | !Number.isInteger(i) ? raise(`index ${i} is not valid`) : 98 | i < 0 || i >= a.length ? raise(`index ${i} is not with array bounds of [0, ${a.length})`) : 99 | i; 100 | const asShenBool = x => x ? shenTrue : shenFalse; 101 | const asJsBool = x => 102 | x === shenTrue ? true : 103 | x === shenFalse ? false : 104 | raise('Shen boolean expected'); 105 | 106 | const cons = (h, t) => new Cons(h, t); 107 | const toArray = x => isList(x) ? produce(isCons, c => c.head, c => c.tail, x) : x; 108 | const toArrayTree = x => isList(x) ? toArray(x).map(toArrayTree) : x; 109 | const toList = (x, tail = null) => isArray(x) ? x.reduceRight((t, h) => cons(h, t), tail) : x; 110 | const toListTree = x => isArray(x) ? toList(x.map(toListTree)) : x; 111 | 112 | const equateType = (x, y) => x.constructor === y.constructor && equate(Object.keys(x), Object.keys(y)); 113 | const equate = (x, y) => 114 | x === y 115 | || isCons(x) && isCons(y) && equate(x.head, y.head) && equate(x.tail, y.tail) 116 | || isArray(x) && isArray(y) && x.length === y.length && x.every((v, i) => equate(v, y[i])) 117 | || isObject(x) && isObject(y) && equateType(x, y) && Object.keys(x).every(k => equate(x[k], y[k])); 118 | 119 | const funSync = (f, arity) => 120 | (...args) => 121 | args.length === arity ? f(...args) : 122 | args.length > arity ? bounce(() => asFunction(settle(f(...args.slice(0, arity))))(...args.slice(arity))) : 123 | args.length === 0 ? funSync(f, arity) : 124 | Object.assign(funSync((...more) => f(...args, ...more), arity - args.length), { arity: f.arity - args.length }); 125 | const funAsync = (f, arity) => 126 | async (...args) => 127 | args.length === arity ? f(...args) : 128 | args.length > arity ? bounce(async () => asFunction(await settle(f(...args.slice(0, arity))))(...args.slice(arity))) : 129 | args.length === 0 ? funAsync(f, arity) : 130 | Object.assign(funAsync((...more) => f(...args, ...more), arity - args.length), { arity: f.arity - args.length }); 131 | const fun = (f, arity = f.arity || f.length) => 132 | Object.assign((f instanceof AsyncFunction ? funAsync : funSync)(f, arity), { arity }); 133 | 134 | const bounce = (f, ...args) => new Trampoline(f, args); 135 | const future = async x => { 136 | while (x = await x, x instanceof Trampoline) { 137 | x = x.f(...x.args); 138 | } 139 | return x; 140 | }; 141 | const settle = x => { 142 | for (;;) { 143 | if (x instanceof Trampoline) { 144 | x = x.f(...x.args); 145 | } else if (x instanceof Promise) { 146 | return future(x); 147 | } else { 148 | return x; 149 | } 150 | } 151 | }; 152 | 153 | const Member$ = name => Member(Id('$'), Id(name)); 154 | const Call$ = (name, args, async = false) => Call(Member$(name), args, async); 155 | const Call$f = (name, args, async = false) => inject(name, x => Call(Member(x, Id('f')), args, async)); 156 | const ann = (dataType, ast) => 157 | ast instanceof Fabrication ? assemble(x => ann(dataType, x), ast) : 158 | Object.assign(ast, { dataType }); 159 | const cast = (dataType, ast) => 160 | ast instanceof Fabrication ? assemble(x => cast(dataType, x), ast) : 161 | dataType !== ast.dataType ? Object.assign(Call$('as' + dataType, [ast]), { dataType }) : 162 | ast; 163 | const uncast = ast => 164 | ast instanceof Fabrication ? assemble(x => uncast(x), ast) : 165 | ast.dataType === 'JsBool' ? cast('ShenBool', ast) : 166 | ast; 167 | const isForm = (expr, length, lead) => 168 | isNeArray(expr) && expr[0] === symbolOf(lead) && (!length || expr.length === length); 169 | const canInline = (context, expr) => { 170 | if (isNeArray(expr) && isSymbol(expr[0])) { 171 | const inliner = context.inlines.get(nameOf(expr[0])); 172 | return inliner && inliner.arity === expr.length - 1; 173 | } 174 | return false; 175 | }; 176 | const calls = expr => 177 | isForm(expr, 4, 'if') ? flatMap(expr.slice(1), calls) : 178 | isForm(expr, 0, 'cond') ? flatMap(flatMap(expr.slice(1), x => x), calls) : 179 | isForm(expr, 4, 'let') ? flatMap(expr.slice(2), calls) : 180 | isForm(expr, 3, 'lambda') ? calls(expr[2]) : 181 | isForm(expr, 2, 'function') && isSymbol(expr[1]) ? [expr[1]] : 182 | isNeArray(expr) ? [expr[0], ...flatMap(expr.slice(1), calls)] : 183 | []; 184 | const primitives = [ 185 | s`+`, s`-`, s`*`, s`/`, s`=`, s`<`, s`>`, s`<=`, s`>=`, 186 | s`cn`, s`str`, s`tlstr`, s`pos`, s`n->string`, s`string->n`, s`cons`, s`hd`, s`tl`, 187 | s`cons?`, s`number?`, s`string?`, s`absvector?`, s`abvector`, s`<-address`, s`address->`, 188 | s`set`, s`value`, s`type`, s`simple-error`, s`error-to-string`, s`get-time`, 189 | s`and`, s`or`, s`if`, s`cond`, s`let`, s`do`, 190 | 191 | // overrides 192 | s`shen.pvar?`, s`@p`, s`shen.byte->digit`, s`shen.numbyte?`, s`integer?`, s`symbol?`, 193 | s`variable?`, s`shen.fillvector`, s`shen.initialise_arity_table`, s`put`, 194 | s`shen.dict`, s`shen.dict?`, s`shen.dict-count`, s`shen.dict->`, 195 | s`shen.<-dict`, s`shen.dict-rm`, s`shen.dict-keys`, s`shen.dict-values` 196 | ]; 197 | const hasExternalCalls = (f, body) => { 198 | const externals = new Set(calls(body)); 199 | externals.delete(f); 200 | for (const primitive of primitives) { 201 | externals.delete(primitive); 202 | } 203 | return externals.size > 0; 204 | }; 205 | const fabricate = (x, subs) => 206 | !(x instanceof Fabrication) ? new Fabrication(x, subs) : 207 | subs ? new Fabrication(x.ast, Object.assign({}, x.subs, subs)) : 208 | x; 209 | const assemble = (f, ...xs) => { 210 | const fabrs = xs.map(x => fabricate(x)); 211 | return fabricate(f(...fabrs.map(x => x.ast)), Object.assign({}, ...fabrs.map(x => x.subs))); 212 | }; 213 | const inject = (name, f) => { 214 | const placeholder = SafeId(name, '$c'); 215 | return fabricate(f(placeholder), { [placeholder.name]: Call(Member$('c'), [Literal(name)]) }); 216 | }; 217 | const variable = (context, symbol) => ann(context.get(symbol).dataType, SafeId(nameOf(symbol))); 218 | const idle = symbol => { 219 | const name = nameOf(symbol); 220 | const placeholder = ann('Symbol', SafeId(name, '$s')); 221 | return fabricate(placeholder, { [placeholder.name]: Template(Member$('s'), name) }); 222 | }; 223 | const inlineName = (context, symbol) => 224 | isSymbol(symbol) && !context.has(symbol) 225 | ? fabricate(Literal(nameOf(symbol))) 226 | : assemble( 227 | x => Call$('nameOf', [cast('Symbol', x)]), 228 | build(context.now(), symbol)); 229 | const strictlyEqualTypes = new Set(['Number', 'String', 'Symbol', 'Stream', 'Null', 'ShenBool']); 230 | const referenceEquatable = (x, y) => strictlyEqualTypes.has(x.dataType) || strictlyEqualTypes.has(y.dataType); 231 | const recognisors = new Map([ 232 | ['absvector?', 'Array'], 233 | ['boolean?', 'ShenBool'], 234 | ['cons?', 'Cons'], 235 | ['number?', 'Number'], 236 | ['string?', 'String'], 237 | ['symbol?', 'Symbol'] 238 | ]); 239 | const recognise = (context, expr) => { 240 | if (isArray(expr) && expr.length === 2 && isSymbol(expr[0]) && context.has(expr[1])) { 241 | const type = recognisors.get(nameOf(expr[0])); 242 | return type ? context.ann(expr[1], type) : context; 243 | } 244 | return context; 245 | }; 246 | const buildAnd = (context, [_and, left, right]) => 247 | assemble( 248 | (x, y) => ann('JsBool', Binary('&&', cast('JsBool', x), cast('JsBool', y))), 249 | build(context.now(), left), 250 | build(recognise(context.now(), left), right)); 251 | const buildIf = (context, [_if, condition, ifTrue, ifFalse]) => 252 | condition === shenTrue 253 | ? uncast(build(context, ifTrue)) 254 | : assemble( 255 | (x, y, z) => Conditional(cast('JsBool', x), y, z), 256 | build(context.now(), condition), 257 | uncast(build(recognise(context, condition), ifTrue)), 258 | uncast(build(context, ifFalse))); 259 | const buildCond = (context, [_cond, ...clauses]) => 260 | build(context, clauses.reduceRight( 261 | (alternate, [test, consequent]) => [s`if`, test, consequent, alternate], 262 | [s`simple-error`, 'no condition was true'])); 263 | const buildDo = (context, [_do, ...exprs]) => 264 | assemble( 265 | Sequence, 266 | ...most(exprs).map(x => build(context.now(), x)), 267 | uncast(build(context, last(exprs)))); 268 | const buildLet = (context, [_let, symbol, binding, body]) => 269 | assemble( 270 | (y, z) => Call(Arrow([SafeId(nameOf(symbol))], z, context.async), [y], context.async), 271 | uncast(build(context.now(), binding)), 272 | uncast(build(context.add([asSymbol(symbol), {}]), body))); 273 | const buildTrap = (context, [_trap, body, handler]) => 274 | isForm(handler, 3, 'lambda') 275 | ? assemble( 276 | (x, y) => 277 | Iife([], [], Block(Try( 278 | Block(Return(x)), 279 | Catch(SafeId(nameOf(handler[1])), Block(Return(y))))), context.async), 280 | uncast(build(context.now(), body)), 281 | uncast(build(context.add([asSymbol(handler[1]), { dataType: 'Error' }]), handler[2]))) 282 | : assemble( 283 | (x, y) => 284 | Iife([], [], Block(Try( 285 | Block(Return(x)), 286 | Catch(Id('e$'), Block(Return(Call(y, [Id('e$')])))))), context.async), 287 | uncast(build(context.now(), body)), 288 | uncast(build(context, handler))); 289 | const buildFunction = (context, params, body) => 290 | assemble( 291 | b => Call$('l', [Arrow(params.map(x => isSymbol(x) ? SafeId(nameOf(x)) : x), b, context.async)]), 292 | uncast(build(context.later().add(...params.filter(isSymbol).map(x => [x, {}])), body))); 293 | const buildNestedLambda = (context, params, body) => 294 | isForm(body, 3, 'lambda') 295 | ? buildNestedLambda(context, [...params.map((p, i) => p === body[1] ? Id(`$${i}$`) : p), body[1]], body[2]) 296 | : buildFunction(context, params, body); 297 | const buildLambda = (context, [_lambda, param, body]) => buildNestedLambda(context, [param], body); 298 | const buildFreeze = (context, [_freeze, body]) => buildFunction(context, [], body); 299 | const buildDefun = (context, [_defun, symbol, params, body]) => 300 | assemble( 301 | (s, b) => Call$('d', [s, b]), 302 | inlineName(context, symbol), 303 | buildFunction(hasExternalCalls(symbol, body) ? context : context.sync(), params, body)); 304 | const buildCons = (context, expr) => { 305 | const { result, state } = produceState(x => isForm(x, 3, 'cons'), x => x[1], x => x[2], expr); 306 | return isEArray(state) || state === null 307 | ? assemble( 308 | (...xs) => ann('Cons', Call$('r', [Vector(xs)])), 309 | ...result.map(x => uncast(build(context.now(), x)))) 310 | : assemble( 311 | (x, ...xs) => ann('Cons', Call$('r', [Vector(xs), x])), 312 | uncast(build(context.now(), state)), 313 | ...result.map(x => uncast(build(context.now(), x)))); 314 | }; 315 | const buildSet = (context, [_set, symbol, value]) => 316 | isSymbol(symbol) && !context.has(symbol) 317 | ? assemble( 318 | v => inject(nameOf(symbol), x => Call(Member(x, Id('set')), [v])), 319 | uncast(build(context.now(), value))) 320 | : assemble( 321 | (s, v) => Call(Member(Call(Member$('c'), [s]), Id('set')), [v]), 322 | inlineName(context, symbol), 323 | uncast(build(context.now(), value))); 324 | const buildValue = (context, [_value, symbol]) => 325 | isSymbol(symbol) && !context.has(symbol) 326 | ? inject(nameOf(symbol), x => Call(Member(x, Id('get')), [])) 327 | : assemble( 328 | s => Call$('valueOf', [s]), 329 | inlineName(context, symbol)); 330 | const buildInline = (context, [fExpr, ...argExprs]) => 331 | assemble( 332 | (...xs) => context.inlines.get(nameOf(fExpr))(...xs), 333 | ...argExprs.map(x => build(context.now(), x))); 334 | const buildApp = (context, [fExpr, ...argExprs]) => 335 | assemble( 336 | (f, ...args) => context.head 337 | ? Call$('t', [Call(f, args)], context.async) 338 | : Call$('b', [f, ...args]), 339 | context.has(fExpr) ? fabricate(SafeId(nameOf(fExpr))) : 340 | isArray(fExpr) ? uncast(build(context.now(), fExpr)) : 341 | isSymbol(fExpr) ? inject(nameOf(fExpr), x => Member(x, Id('f'))) : 342 | raise('not a valid application form'), 343 | ...argExprs.map(x => uncast(build(context.now(), x)))); 344 | const build = (context, expr) => 345 | isNumber(expr) ? fabricate(ann('Number', Literal(expr))) : 346 | isString(expr) ? fabricate(ann('String', Literal(expr))) : 347 | isEArray(expr) ? fabricate(ann('Null', Literal(null))) : 348 | isSymbol(expr) ? (context.has(expr) ? fabricate(variable(context, expr)) : idle(expr)) : 349 | isForm(expr, 3, 'and') ? buildAnd (context, expr) : 350 | isForm(expr, 4, 'if') ? buildIf (context, expr) : 351 | isForm(expr, 0, 'cond') ? buildCond (context, expr) : 352 | isForm(expr, 3, 'do') ? buildDo (context, expr) : 353 | isForm(expr, 4, 'let') ? buildLet (context, expr) : 354 | isForm(expr, 3, 'trap-error') ? buildTrap (context, expr) : 355 | isForm(expr, 3, 'lambda') ? buildLambda(context, expr) : 356 | isForm(expr, 2, 'freeze') ? buildFreeze(context, expr) : 357 | isForm(expr, 4, 'defun') ? buildDefun (context, expr) : 358 | isForm(expr, 3, 'cons') ? buildCons (context, expr) : 359 | isForm(expr, 3, 'set') ? buildSet (context, expr) : 360 | isForm(expr, 2, 'value') ? buildValue (context, expr) : 361 | canInline(context, expr) ? buildInline(context, expr) : 362 | isArray(expr) ? buildApp (context, expr) : 363 | raise('not a valid form'); 364 | const hoist = (fabr, async = false) => 365 | fabr.keys.length === 0 ? fabr.ast : Iife(fabr.keys.map(x => Id(x)), fabr.values, fabr.ast, async); 366 | 367 | module.exports = (options = {}) => { 368 | const context = new Context({ async: true, head: true, locals: new Map(), inlines: new Map() }); 369 | const construct = expr => uncast(build(context, expr)); 370 | const compile = expr => hoist(construct(expr), true); 371 | const globals = new Map(); 372 | const lookup = name => { 373 | let cell = globals.get(name); 374 | if (!cell) { 375 | cell = new Cell(name); 376 | globals.set(name, cell); 377 | } 378 | return cell; 379 | }; 380 | const valueOf = x => lookup(x).get(); 381 | const openRead = options.openRead || (() => raise('open(in) not supported')); 382 | const openWrite = options.openWrite || (() => raise('open(out) not supported')); 383 | const open = (path, mode) => 384 | mode === 'in' ? openRead (asString(valueOf('*home-directory*')) + path) : 385 | mode === 'out' ? openWrite(asString(valueOf('*home-directory*')) + path) : 386 | raise(`open only accepts symbols in or out, not ${mode}`); 387 | const isInStream = options.isInStream || (options.InStream && (x => x instanceof options.InStream)) || (() => false); 388 | const isOutStream = options.isOutStream || (options.OutStream && (x => x instanceof options.OutStream)) || (() => false); 389 | const asInStream = x => isInStream(x) ? x : raise('input stream expected'); 390 | const asOutStream = x => isOutStream(x) ? x : raise('output stream expected'); 391 | const isStream = x => isInStream(x) || isOutStream(x); 392 | const asStream = x => isStream(x) ? x : raise('stream expected'); 393 | const clock = options.clock || (() => Date.now() / 1000); 394 | const startTime = clock(); 395 | const getTime = mode => 396 | mode === 'unix' ? clock() : 397 | mode === 'run' ? clock() - startTime : 398 | raise(`get-time only accepts symbols unix or run, not ${mode}`); 399 | const showCons = x => { 400 | const { result, state } = produceState(isCons, x => x.head, x => x.tail, x); 401 | return `[${result.map(show).join(' ')}${state === null ? '' : ` | ${show(state)}`}]`; 402 | }; 403 | const show = x => 404 | x === null ? '[]' : 405 | isString(x) ? `"${x}"` : 406 | isSymbol(x) ? nameOf(x) : 407 | isCons(x) ? showCons(x) : 408 | isFunction(x) ? `` : 409 | isArray(x) ? `` : 410 | isError(x) ? `` : 411 | isStream(x) ? `` : 412 | `${x}`; 413 | const assign = (name, value) => lookup(name).set(value); 414 | const defun = (name, f) => (lookup(name).f = f.arity ? f : fun(f), symbolOf(name)); 415 | const inline = (name, dataType, paramTypes, f) => { 416 | const inliner = (...args) => { 417 | const ast = f(...args.map((a, i) => paramTypes[i] ? cast(paramTypes[i], a) : uncast(a))); 418 | return dataType ? ann(dataType, ast) : ast; 419 | }; 420 | inliner.arity = f.length; 421 | context.inlines.set(name, inliner); 422 | return inliner; 423 | }; 424 | const $ = { 425 | cons, toArray, toArrayTree, toList, toListTree, 426 | asJsBool, asShenBool, isEArray, isNeArray, asNeString, asNzNumber, globals, lookup, assign, defun, inline, 427 | isStream, isInStream, isOutStream, isNumber, isString, isSymbol, isCons, isList, isArray, isError, isFunction, 428 | asStream, asInStream, asOutStream, asNumber, asString, asSymbol, asCons, asList, asArray, asError, asFunction, 429 | symbolOf, nameOf, valueOf, show, equate, raise, fun, bounce, settle, assemble, construct, compile, 430 | b: bounce, d: defun, l: fun, r: toList, s, t: settle, c: lookup 431 | }; 432 | $.evalJs = ast => AsyncFunction('$', generate(isStatement(ast) ? Block(ast) : Return(ast)))($); 433 | $.evalKl = expr => $.evalJs(compile(toArrayTree(expr))); 434 | const out = options.stoutput; 435 | assign('*language*', 'JavaScript'); 436 | assign('*implementation*', options.implementation || 'Unknown'); 437 | assign('*release*', options.release || 'Unknown'); 438 | assign('*os*', options.os || 'Unknown'); 439 | assign('*port*', options.port || 'Unknown'); 440 | assign('*porters*', options.porters || 'Unknown'); 441 | assign('*stinput*', options.stinput || (() => raise('standard input not supported'))); 442 | assign('*stoutput*', out || (() => raise('standard output not supported'))); 443 | assign('*sterror*', options.sterror || out || (() => raise('standard output not supported'))); 444 | assign('*home-directory*', options.homeDirectory || ''); 445 | assign('shen-script.*instream-supported*', asShenBool(options.isInStream || options.InStream)); 446 | assign('shen-script.*outstream-supported*', asShenBool(options.isOutStream || options.OutStream)); 447 | defun('eval-kl', $.evalKl); 448 | defun('if', (b, x, y) => asJsBool(b) ? x : y); 449 | defun('and', (x, y) => asShenBool(asJsBool(x) && asJsBool(y))); 450 | defun('or', (x, y) => asShenBool(asJsBool(x) || asJsBool(y))); 451 | defun('open', (p, m) => open(asString(p), nameOf(asSymbol(m)))); 452 | defun('close', x => (asStream(x).close(), null)); 453 | defun('read-byte', x => asInStream(x).read()); 454 | defun('write-byte', (b, x) => (asOutStream(x).write(asNumber(b)), b)); 455 | defun('number?', x => asShenBool(isNumber(x))); 456 | defun('string?', x => asShenBool(isString(x))); 457 | defun('absvector?', x => asShenBool(isArray(x))); 458 | defun('cons?', x => asShenBool(isCons(x))); 459 | defun('hd', c => asCons(c).head); 460 | defun('tl', c => asCons(c).tail); 461 | defun('cons', cons); 462 | defun('tlstr', x => asNeString(x).substring(1)); 463 | defun('cn', (x, y) => asString(x) + asString(y)); 464 | defun('string->n', x => asNeString(x).charCodeAt(0)); 465 | defun('n->string', n => String.fromCharCode(asNumber(n))); 466 | defun('pos', (x, i) => asString(x)[asIndex(i, x)]); 467 | defun('str', show); 468 | defun('absvector', n => new Array(asNumber(n)).fill(null)); 469 | defun('<-address', (a, i) => asArray(a)[asIndex(i, a)]); 470 | defun('address->', (a, i, x) => (asArray(a)[asIndex(i, a)] = x, a)); 471 | defun('+', (x, y) => asNumber(x) + asNumber(y)); 472 | defun('-', (x, y) => asNumber(x) - asNumber(y)); 473 | defun('*', (x, y) => asNumber(x) * asNumber(y)); 474 | defun('/', (x, y) => asNumber(x) / asNzNumber(y)); 475 | defun('>', (x, y) => asShenBool(asNumber(x) > asNumber(y))); 476 | defun('<', (x, y) => asShenBool(asNumber(x) < asNumber(y))); 477 | defun('>=', (x, y) => asShenBool(asNumber(x) >= asNumber(y))); 478 | defun('<=', (x, y) => asShenBool(asNumber(x) <= asNumber(y))); 479 | defun('=', (x, y) => asShenBool(equate(x, y))); 480 | defun('intern', x => symbolOf(asString(x))); 481 | defun('get-time', x => getTime(nameOf(asSymbol(x)))); 482 | defun('simple-error', x => raise(asString(x))); 483 | defun('error-to-string', x => asError(x).message); 484 | defun('set', (x, y) => lookup(nameOf(asSymbol(x))).set(y)); 485 | defun('value', x => valueOf(nameOf(asSymbol(x)))); 486 | defun('type', (x, _) => x); 487 | inline('=', 'JsBool', [null, null], (x, y) => 488 | referenceEquatable(x, y) ? Binary('===', x, y) : Call$('equate', [x, y])); 489 | inline('not', 'JsBool', ['JsBool'], x => Unary('!', x)); 490 | inline('or', 'JsBool', ['JsBool', 'JsBool'], (x, y) => Binary('||', x, y)); 491 | inline('+', 'Number', ['Number', 'Number'], (x, y) => Binary('+', x, y)); 492 | inline('-', 'Number', ['Number', 'Number'], (x, y) => Binary('-', x, y)); 493 | inline('*', 'Number', ['Number', 'Number'], (x, y) => Binary('*', x, y)); 494 | inline('/', 'Number', ['Number', 'NzNumber'], (x, y) => Binary('/', x, y)); 495 | inline('<', 'JsBool', ['Number', 'Number'], (x, y) => Binary('<', x, y)); 496 | inline('>', 'JsBool', ['Number', 'Number'], (x, y) => Binary('>', x, y)); 497 | inline('<=', 'JsBool', ['Number', 'Number'], (x, y) => Binary('<=', x, y)); 498 | inline('>=', 'JsBool', ['Number', 'Number'], (x, y) => Binary('>=', x, y)); 499 | inline('cn', 'String', ['String', 'String'], (x, y) => Binary('+', x, y)); 500 | inline('str', 'String', [null], x => Call$('show', [x])); 501 | inline('intern', 'Symbol', ['String'], x => Call$('symbolOf', [x])); 502 | inline('number?', 'JsBool', [null], x => Call$('isNumber', [x])); 503 | inline('string?', 'JsBool', [null], x => Call$('isString', [x])); 504 | inline('cons?', 'JsBool', [null], x => Call$('isCons', [x])); 505 | inline('absvector?', 'JsBool', [null], x => Call$('isArray', [x])); 506 | inline('cons', 'Cons', [null, null], (x, y) => Call$('cons', [x, y])); 507 | inline('hd', null, ['Cons'], x => Member(x, Id('head'))); 508 | inline('tl', null, ['Cons'], x => Member(x, Id('tail'))); 509 | inline('error-to-string', 'String', ['Error'], x => Member(x, Id('message'))); 510 | inline('simple-error', null, ['String'], x => Call$('raise', [x])); 511 | inline('read-byte', 'Number', ['InStream'], x => Call(Member(x, Id('read')), [])); 512 | inline('write-byte', 'Number', [null, null], (x, y) => Call$f('write-byte', [x, y])); 513 | inline('get-time', 'Number', [null], x => Call$f('get-time', [x])); 514 | inline('string->n', 'Number', ['NeString'], x => Call(Member(x, Id('charCodeAt')), [Literal(0)])); 515 | inline('n->string', 'String', ['Number'], x => Call(Member(Id('String'), Id('fromCharCode')), [x])); 516 | inline('tlstr', 'String', ['NeString'], x => Call(Member(x, Id('substring')), [Literal(1)])); 517 | inline('pos', 'String', [null, null], (x, y) => Call$f('pos', [x, y])); 518 | inline('absvector', 'Array', [null], x => Call$f('absvector', [x])); 519 | inline('address->', 'Array', [null, null, null], (x, y, z) => Call$f('address->', [x, y, z])); 520 | return $; 521 | }; 522 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | language: 'JavaScript', 3 | port: '0.17.0', 4 | porters: 'Robert Koeninger' 5 | }; 6 | -------------------------------------------------------------------------------- /lib/config.node.js: -------------------------------------------------------------------------------- 1 | const { multiMatch } = require('./utils.js'); 2 | 3 | const implementation = 'Node.js'; 4 | const os = multiMatch(process.platform, 5 | [/win32|win64/i, 'Windows'], 6 | [/darwin|mac/i , 'macOS'], 7 | [/linux/i , 'Linux']); 8 | const release = process.version.slice(1); 9 | 10 | module.exports = { ...require('./config.js'), implementation, os, release }; 11 | -------------------------------------------------------------------------------- /lib/config.web.js: -------------------------------------------------------------------------------- 1 | const { multiMatch } = require('./utils.js'); 2 | 3 | const implementation = multiMatch( 4 | navigator.userAgent, 5 | [/edge/i, 'Edge'], 6 | [/trident|explorer/i, 'Internet Explorer'], 7 | [/chrome/i, 'Chrome'], 8 | [/opera/i, 'Opera'], 9 | [/firefox/i, 'Firefox'], 10 | [/safari/i, 'Safari'], 11 | [/vivaldi/i, 'Vivaldi'], 12 | [/android/i, 'Android']); 13 | const os = multiMatch( 14 | navigator.userAgent, 15 | [/win32|win64/i, 'Windows'], 16 | [/darwin|mac/i, 'macOS'], 17 | [/darwin|ios|iphone|ipad/i, 'iOS'], 18 | [/linux/i, 'Linux'], 19 | [/droid/i, 'Android'], 20 | [/x11/i, 'Unix']); 21 | const release = (() => { 22 | let ua = navigator.userAgent, 23 | tem, 24 | m = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*([\d.]+)/i) || []; 25 | if (/trident/i.test(m[1])) { 26 | tem = /\brv[ :]+([\d.]+)/g.exec(ua) || []; 27 | return (tem[1] || ''); 28 | } 29 | if (m[1] === 'Chrome'){ 30 | tem = ua.match(/\b(OPR|Edge)\/([\d.]+)/); 31 | if (tem != null) return tem[2]; 32 | } 33 | m = m[2] ? [m[1], m[2]] : [navigator.appName, navigator.appVersion, '-?']; 34 | if ((tem = ua.match(/version\/([\d.]+)/i)) != null) m.splice(1, 1, tem[1]); 35 | return m[1]; 36 | })(); 37 | 38 | module.exports = { ...require('./config.js'), implementation, os, release }; 39 | -------------------------------------------------------------------------------- /lib/frontend.js: -------------------------------------------------------------------------------- 1 | const { AsyncFunction, GeneratorFunction, flatMap, last, mapAsync, pairs } = require('./utils.js'); 2 | const ast = require('./ast.js'); 3 | const { 4 | Arrow, Assign, Async, Await, Binary, Block, Call, Catch, Class, Conditional, Const, Debugger, DoWhile, Empty, 5 | For, ForIn, ForOf, Generator, Id, If, Let, Literal, Member, NewObj, NewTarget, Procedure, Property, Return, 6 | SafeId, Sequence, Slot, Spread, Statement, Static, Super, This, Try, Unary, Var, Vector, While, Yield, YieldMany, 7 | generate 8 | } = ast; 9 | 10 | module.exports = async $ => { 11 | const { 12 | asFunction, asJsBool, asList, asNumber, asShenBool, asString, asSymbol, assign, compile, 13 | cons, defun, evalJs, fun, inline, isArray, isSymbol, lookup, nameOf, s, settle, symbolOf, 14 | toArray, toArrayTree, toList, toListTree, valueOf 15 | } = $; 16 | const signedfuncs = lookup('shen.*signedfuncs*'); 17 | const macroregCell = lookup('shen.*macroreg*'); 18 | const macrosCell = lookup('*macros*'); 19 | const propertyVectorCell = lookup('*property-vector*'); 20 | const caller = name => { 21 | const cell = lookup(name); 22 | return (...args) => settle(cell.f(...args)); 23 | }; 24 | const curryType = caller('shen.curry-type'); 25 | const incInfs = caller('shen.incinfs'); 26 | const newpv = caller('shen.newpv'); 27 | const unify = caller('unify!'); 28 | const load = caller('load'); 29 | const parse = caller('read-from-string'); 30 | const put = caller('put'); 31 | const evaluate = caller('eval'); 32 | const define = async (name, f) => { 33 | const id = symbolOf(name); 34 | await defun(name, f); 35 | await put(id, s`shen.lambda-form`, f, propertyVectorCell.get()); 36 | await put(id, s`arity`, f.length, propertyVectorCell.get()); 37 | return id; 38 | }; 39 | const setSignedFuncs = (id, type) => signedfuncs.set(cons(cons(id, type), signedfuncs.get())); 40 | const freeVariables = expr => 41 | isArray(expr) ? [...new Set(flatMap(expr, freeVariables)).values()] : 42 | isSymbol(expr) && nameOf(expr).charCodeAt(0) >= 65 && nameOf(expr).charCodeAt(0) <= 90 ? [expr] : 43 | []; 44 | const substitute = (map, expr) => 45 | isArray(expr) ? expr.map(x => substitute(map, x)) : 46 | map.has(expr) ? map.get(expr) : 47 | expr; 48 | const declareType = async (name, type) => { 49 | setSignedFuncs(symbolOf(name), type); 50 | await define('shen.type-signature-of-' + name, async (goal, proc, cont) => { 51 | const variableMap = new Map(await mapAsync(freeVariables(type), async x => [x, await newpv(proc)])); 52 | await incInfs(); 53 | return await unify(goal, substitute(variableMap, type), proc, cont); 54 | }); 55 | }; 56 | const defineTyped = async (name, type, f) => { 57 | const id = await define(name, f); 58 | await declareType(name, await curryType(toListTree(type))); 59 | return id; 60 | }; 61 | const defmacro = async (name, f) => { 62 | const macrofn = expr => { 63 | const result = toListTree(f(toArrayTree(expr))); 64 | return result === undefined ? expr : result; 65 | }; 66 | await define(name, macrofn); 67 | const macroreg = macroregCell.get(); 68 | const macros = macrosCell.get(); 69 | const id = symbolOf(name); 70 | if (!toArray(macroreg).includes(id)) { 71 | macroregCell.set(cons(id, macroreg)); 72 | macrosCell.set(cons(fun(x => macrofn(x)), macros)); 73 | } 74 | return id; 75 | }; 76 | const evalShen = expr => evaluate(toListTree(expr)); 77 | const execEach = async source => mapAsync(toArray(await parse(source)), evalShen); 78 | const exec = async source => last(await execEach(source)); 79 | const symbol = async (name, value) => { 80 | const index = name.lastIndexOf('.') + 1; 81 | const starredName = `${name.substr(0, index)}*${name.substr(index)}*`; 82 | assign(starredName, value); 83 | await define(name, () => valueOf(starredName)); 84 | return value; 85 | }; 86 | const sleep = duration => new Promise(resolve => setTimeout(() => resolve(null), duration)); 87 | const delay = (duration, callback) => setTimeout(() => settle(callback()), duration); 88 | const repeat = (duration, callback) => setInterval(() => settle(callback()), duration); 89 | 90 | /************************* 91 | * AST Builder Functions * 92 | *************************/ 93 | 94 | await define('js.ast.arguments', () => Id('arguments')); 95 | await define('js.ast.array', elements => Vector(toArray(elements))); 96 | await define('js.ast.arrow', (params, body) => Arrow(toArray(params), body)); 97 | await define('js.ast.assign', (x, y) => Assign(x, y)); 98 | await define('js.ast.async', arg => Async(arg)); 99 | await define('js.ast.await', arg => Await(arg)); 100 | await define('js.ast.binary', (op, x, y) => Binary(op, x, y)); 101 | await define('js.ast.block', body => Block(...toArray(body))); 102 | await define('js.ast.call', (f, args) => Call(f, toArray(args))); 103 | await define('js.ast.catch', (param, body) => Catch(param, body)); 104 | await define('js.ast.class', (id, base, slots) => Class(id, base, toArray(slots))); 105 | await define('js.ast.const', (id, init) => Const(id, init)); 106 | await define('js.ast.constructor', body => Slot('constructor', Id('constructor'), body)); 107 | await define('js.ast.debugger', () => Debugger()); 108 | await define('js.ast.do-while', (test, body) => DoWhile(test, body)); 109 | await define('js.ast.empty', () => Empty()); 110 | await define('js.ast.for', (x, y, z, body) => For(x, y, z, body)); 111 | await define('js.ast.for-await-of', (x, xs, body) => ForOf(x, xs, body, true)); 112 | await define('js.ast.for-in', (x, xs, body) => ForIn(x, xs, body)); 113 | await define('js.ast.for-of', (x, xs, body) => ForOf(x, xs, body)); 114 | await define('js.ast.function', (i, ps, body) => Procedure(i, toArray(ps), body)); 115 | await define('js.ast.function*', (i, ps, body) => Generator(i, toArray(ps), body)); 116 | await define('js.ast.getter', (id, body) => Slot('get', id, body)); 117 | await define('js.ast.id', name => Id(name)); 118 | await define('js.ast.if', (test, x, y) => If(test, x, y)); 119 | await define('js.ast.let', (id, init) => Let(id, init)); 120 | await define('js.ast.literal', value => Literal(value)); 121 | await define('js.ast.member', (obj, prop) => Member(obj, prop)); 122 | await define('js.ast.method', (id, body) => Slot('method', id, body)); 123 | await define('js.ast.new-target', () => NewTarget()); 124 | await define('js.ast.object', props => NewObj(pairs(toArray(props)).map(([x, y]) => Property(x, y)))); 125 | await define('js.ast.return', arg => Return(arg)); 126 | await define('js.ast.safe-id', name => SafeId(name)); 127 | await define('js.ast.setter', (id, body) => Slot('set', id, body)); 128 | await define('js.ast.sequence', exprs => Sequence(...toArray(exprs))); 129 | await define('js.ast.slot', (kind, id, body) => Slot(kind, id, body)); 130 | await define('js.ast.spread', arg => Spread(toArray(arg))); 131 | await define('js.ast.statement', expr => Statement(expr)); 132 | await define('js.ast.static', arg => Static(arg)); 133 | await define('js.ast.super', () => Super()); 134 | await define('js.ast.ternary', (x, y, z) => Conditional(x, y, z)); 135 | await define('js.ast.this', () => This()); 136 | await define('js.ast.try', (body, handler) => Try(body, handler)); 137 | await define('js.ast.update', (op, x, y) => Assign(x, y, op + '=')); 138 | await define('js.ast.unary', (op, x) => Unary(op, x)); 139 | await define('js.ast.var', (id, init) => Var(id, init)); 140 | await define('js.ast.while', (test, body) => While(test, body)); 141 | await define('js.ast.yield', arg => Yield(arg)); 142 | await define('js.ast.yield*', arg => YieldMany(arg)); 143 | 144 | await define('js.ast.compile', x => compile(toArrayTree(x))); 145 | await define('js.ast.eval', x => evalJs(x)); 146 | await define('js.ast.render', x => generate(x)); 147 | 148 | /***************** 149 | * Raw Operators * 150 | *****************/ 151 | 152 | await define('js.raw.==', (x, y) => x == y); 153 | await define('js.raw.===', (x, y) => x === y); 154 | await define('js.raw.!=', (x, y) => x != y); 155 | await define('js.raw.!==', (x, y) => x !== y); 156 | await define('js.raw.not', x => !x); 157 | await define('js.raw.and', (x, y) => x && y); 158 | await define('js.raw.or', (x, y) => x || y); 159 | await define('js.raw.+', (x, y) => x + y); 160 | await define('js.raw.-', (x, y) => x - y); 161 | await define('js.raw.*', (x, y) => x * y); 162 | await define('js.raw./', (x, y) => x / y); 163 | await define('js.raw.%', (x, y) => x % y); 164 | await define('js.raw.**', (x, y) => x ** y); 165 | await define('js.raw.<', (x, y) => x < y); 166 | await define('js.raw.>', (x, y) => x > y); 167 | await define('js.raw.<=', (x, y) => x <= y); 168 | await define('js.raw.>=', (x, y) => x >= y); 169 | await define('js.raw.bitwise.not', x => ~x); 170 | await define('js.raw.bitwise.and', (x, y) => x & y); 171 | await define('js.raw.bitwise.or', (x, y) => x | y); 172 | await define('js.raw.bitwise.xor', (x, y) => x ^ y); 173 | await define('js.raw.<<', (x, y) => x << y); 174 | await define('js.raw.>>', (x, y) => x >> y); 175 | await define('js.raw.>>>', (x, y) => x >>> y); 176 | await define('js.raw.delete', (x, y) => delete x[y]); 177 | await define('js.raw.eval', x => eval(x)); 178 | await define('js.raw.in', (x, y) => x in y); 179 | await define('js.raw.instanceof', (x, y) => x instanceof y); 180 | await define('js.raw.typeof', x => typeof x); 181 | await define('js.raw.void', x => void x); 182 | 183 | inline('js.raw.==', null, [null, null], (x, y) => Binary('==', x, y)); 184 | inline('js.raw.===', null, [null, null], (x, y) => Binary('===', x, y)); 185 | inline('js.raw.!=', null, [null, null], (x, y) => Binary('!=', x, y)); 186 | inline('js.raw.!==', null, [null, null], (x, y) => Binary('!==', x, y)); 187 | inline('js.raw.not', null, [null], x => Unary('!', x)); 188 | inline('js.raw.and', null, [null, null], (x, y) => Binary('&&', x, y)); 189 | inline('js.raw.or', null, [null, null], (x, y) => Binary('||', x, y)); 190 | inline('js.raw.+', null, [null, null], (x, y) => Binary('+', x, y)); 191 | inline('js.raw.-', null, [null, null], (x, y) => Binary('-', x, y)); 192 | inline('js.raw.*', null, [null, null], (x, y) => Binary('*', x, y)); 193 | inline('js.raw./', null, [null, null], (x, y) => Binary('/', x, y)); 194 | inline('js.raw.%', null, [null, null], (x, y) => Binary('%', x, y)); 195 | inline('js.raw.**', null, [null, null], (x, y) => Binary('**', x, y)); 196 | inline('js.raw.<', null, [null, null], (x, y) => Binary('<', x, y)); 197 | inline('js.raw.>', null, [null, null], (x, y) => Binary('>', x, y)); 198 | inline('js.raw.<=', null, [null, null], (x, y) => Binary('<=', x, y)); 199 | inline('js.raw.>=', null, [null, null], (x, y) => Binary('>=', x, y)); 200 | inline('js.raw.bitwise.not', null, [null], x => Unary('~', x)); 201 | inline('js.raw.bitwise.and', null, [null, null], (x, y) => Binary('&', x, y)); 202 | inline('js.raw.bitwise.or', null, [null, null], (x, y) => Binary('|', x, y)); 203 | inline('js.raw.bitwise.xor', null, [null, null], (x, y) => Binary('^', x, y)); 204 | inline('js.raw.<<', null, [null, null], (x, y) => Binary('<<', x, y)); 205 | inline('js.raw.>>', null, [null, null], (x, y) => Binary('>>', x, y)); 206 | inline('js.raw.>>>', null, [null, null], (x, y) => Binary('>>>', x, y)); 207 | inline('js.raw.delete', null, [null, null], (x, y) => Unary('delete', Member(x, y))); 208 | inline('js.raw.eval', null, [null], x => Call(Id('eval'), [x])); 209 | inline('js.raw.in', null, [null, null], (x, y) => Binary('in', x, y)); 210 | inline('js.raw.instanceof', null, [null, null], (x, y) => Binary('instanceof', x, y)); 211 | inline('js.raw.typeof', null, [null], x => Unary('typeof', x)); 212 | inline('js.raw.void', null, [null], x => Unary('void', x)); 213 | 214 | /****************************************** 215 | * Typed Operators and Standard Functions * 216 | ******************************************/ 217 | 218 | const funType = (first, ...rest) => 219 | rest.length === 0 220 | ? [s`-->`, symbolOf(first)] 221 | : [symbolOf(first), ...flatMap(rest, x => [s`-->`, symbolOf(x)])]; 222 | const A_B_boolean = funType('A', 'B', 'boolean'); 223 | const number_number = funType('number', 'number'); 224 | const number_number_number = funType('number', 'number', 'number'); 225 | const string_string = funType('string', 'string'); 226 | const string_A = funType('string', 'A'); 227 | const A_unit = funType('A', 'unit'); 228 | const string_number = funType('string', 'number'); 229 | const string_number_number = funType('string', 'number', 'number'); 230 | const A_boolean = funType('A', 'boolean'); 231 | const timeout_unit = funType('js.timeout', 'unit'); 232 | const number_unit = funType('number', 'unit'); 233 | const number_lazy_timeout = [s`number`, s`-->`, [s`lazy`, s`A`], s`-->`, s`js.timeout`]; 234 | 235 | await defineTyped('js.==', A_B_boolean, (x, y) => asShenBool(x == y)); 236 | await defineTyped('js.!=', A_B_boolean, (x, y) => asShenBool(x != y)); 237 | await defineTyped('js.===', A_B_boolean, (x, y) => asShenBool(x === y)); 238 | await defineTyped('js.!==', A_B_boolean, (x, y) => asShenBool(x !== y)); 239 | await defineTyped('js.%', number_number_number, (x, y) => asNumber(x) % asNumber(y)); 240 | await defineTyped('js.**', number_number_number, (x, y) => asNumber(x) ** asNumber(y)); 241 | await defineTyped('js.bitwise.not', number_number, x => ~asNumber(x)); 242 | await defineTyped('js.bitwise.and', number_number_number, (x, y) => asNumber(x) & asNumber(y)); 243 | await defineTyped('js.bitwise.or', number_number_number, (x, y) => asNumber(x) | asNumber(y)); 244 | await defineTyped('js.bitwise.xor', number_number_number, (x, y) => asNumber(x) ^ asNumber(y)); 245 | await defineTyped('js.<<', number_number_number, (x, y) => asNumber(x) << asNumber(y)); 246 | await defineTyped('js.>>', number_number_number, (x, y) => asNumber(x) >> asNumber(y)); 247 | await defineTyped('js.>>>', number_number_number, (x, y) => asNumber(x) >>> asNumber(y)); 248 | await defineTyped('js.clear', timeout_unit, x => (clearTimeout(x), null)); 249 | await defineTyped('js.decode-uri', string_string, x => decodeURI(asString(x))); 250 | await defineTyped('js.decode-uri-component', string_string, x => decodeURIComponent(asString(x))); 251 | await defineTyped('js.delay', number_lazy_timeout, (t, f) => delay(asNumber(t), asFunction(f))); 252 | await defineTyped('js.encode-uri', string_string, x => encodeURI(asString(x))); 253 | await defineTyped('js.encode-uri-component', string_string, x => encodeURIComponent(asString(x))); 254 | await defineTyped('js.eval', string_A, x => eval(asString(x))); 255 | await defineTyped('js.log', A_unit, x => (console.log(x), null)); 256 | await defineTyped('js.parse-float', string_number, x => parseFloat(asString(x))); 257 | await defineTyped('js.parse-int', string_number, x => parseInt(asString(x), 10)); 258 | await defineTyped('js.parse-int-with-radix', string_number_number, (x, r) => parseInt(asString(x), asNumber(r))); 259 | await defineTyped('js.repeat', number_lazy_timeout, (t, f) => repeat(asNumber(t), asFunction(f))); 260 | await defineTyped('js.sleep', number_unit, t => sleep(asNumber(t))); 261 | 262 | /****************** 263 | * JSON Functions * 264 | ******************/ 265 | 266 | await define('json.parse', s => JSON.parse(s)); 267 | await define('json.str', x => JSON.stringify(x)); 268 | 269 | /************************ 270 | * Recognisor Functions * 271 | ************************/ 272 | 273 | await defineTyped('js.array?', A_boolean, x => asShenBool(Array.isArray(x))); 274 | await defineTyped('js.async?', A_boolean, x => asShenBool(x instanceof AsyncFunction)); 275 | await defineTyped('js.boolean?', A_boolean, x => asShenBool(typeof x === 'boolean')); 276 | await defineTyped('js.defined?', A_boolean, x => asShenBool(typeof x !== 'undefined')); 277 | await defineTyped('js.false?', A_boolean, x => asShenBool(x === false)); 278 | await defineTyped('js.falsy?', A_boolean, x => asShenBool(!x)); 279 | await defineTyped('js.finite?', A_boolean, x => asShenBool(Number.isFinite(x))); 280 | await defineTyped('js.function?', A_boolean, x => asShenBool(typeof x === 'function')); 281 | await defineTyped('js.generator?', A_boolean, x => asShenBool(x instanceof GeneratorFunction)); 282 | await defineTyped('js.infinite?', A_boolean, x => asShenBool(!Number.isFinite(x))); 283 | await defineTyped('js.+infinity?', A_boolean, x => asShenBool(x === Infinity)); 284 | await defineTyped('js.-infinity?', A_boolean, x => asShenBool(x === -Infinity)); 285 | await defineTyped('js.integer?', A_boolean, x => asShenBool(Number.isInteger(x))); 286 | await defineTyped('js.+integer?', A_boolean, x => asShenBool(Number.isInteger(x) && x > 0)); 287 | await defineTyped('js.-integer?', A_boolean, x => asShenBool(Number.isInteger(x) && x < 0)); 288 | await defineTyped('js.nan?', A_boolean, x => asShenBool(Number.isNaN(x))); 289 | await defineTyped('js.null?', A_boolean, x => asShenBool(x === null)); 290 | await defineTyped('js.object?', A_boolean, x => asShenBool(typeof x === 'object' && x !== null && !Array.isArray(x))); 291 | await defineTyped('js.symbol?', A_boolean, x => asShenBool(typeof x === 'symbol')); 292 | await defineTyped('js.true?', A_boolean, x => asShenBool(x === true)); 293 | await defineTyped('js.truthy?', A_boolean, x => asShenBool(!!x)); 294 | await defineTyped('js.undefined?', A_boolean, x => asShenBool(typeof x === 'undefined')); 295 | 296 | /************************************** 297 | * Object Construction, Member Access * 298 | **************************************/ 299 | 300 | await define('js.get', (object, property) => { 301 | const value = object[property]; 302 | return typeof value === 'function' ? value.bind(object) : value; 303 | }); 304 | await defmacro('js.get-macro', expr => { 305 | if (isArray(expr) && expr[0] === s`.` && expr.length >= 2) { 306 | return expr.slice(2).reduce((whole, prop) => [s`js.get`, whole, prop], expr[1]); 307 | } 308 | }); 309 | await define('js.new', (Class, args) => new Class(...toArray(asList(args)))); 310 | await define('js.obj', values => 311 | pairs(toArray(asList(values))) 312 | .reduce((obj, [name, value]) => (obj[name] = value, obj), {})); 313 | await defmacro('js.obj-macro', expr => { 314 | if (isArray(expr) && expr[0] === s`{` && expr[expr.length - 1] === s`}`) { 315 | return [ 316 | s`js.obj`, 317 | expr.slice(1, expr.length - 1).reduceRight((tail, head) => [s`cons`, head, tail], []) 318 | ]; 319 | } 320 | }); 321 | await define('js.set', (object, property, value) => object[property] = value); 322 | 323 | /************************************** 324 | * Global Classes, Objects and Values * 325 | **************************************/ 326 | 327 | await symbol('js.Array', Array); 328 | await symbol('js.ArrayBuffer', ArrayBuffer); 329 | await symbol('js.AsyncFunction', AsyncFunction); 330 | await symbol('js.Boolean', Boolean); 331 | await symbol('js.console', console); 332 | await symbol('js.DataView', DataView); 333 | await symbol('js.Date', Date); 334 | await symbol('js.Function', Function); 335 | await symbol('js.GeneratorFunction', GeneratorFunction); 336 | await symbol('js.Infinity', Infinity); 337 | await symbol('js.JSON', JSON); 338 | await symbol('js.Map', Map); 339 | await symbol('js.Math', Math); 340 | await symbol('js.NaN', NaN); 341 | await symbol('js.Number', Number); 342 | await symbol('js.null', null); 343 | await symbol('js.Object', Object); 344 | await symbol('js.Promise', Promise); 345 | await symbol('js.Proxy', Proxy); 346 | await symbol('js.Reflect', Reflect); 347 | await symbol('js.RegExp', RegExp); 348 | await symbol('js.Set', Set); 349 | await symbol('js.String', String); 350 | await symbol('js.Symbol', Symbol); 351 | await symbol('js.undefined', undefined); 352 | await symbol('js.WeakMap', WeakMap); 353 | await symbol('js.WeakSet', WeakSet); 354 | await symbol('js.WebAssembly', WebAssembly); 355 | 356 | if (typeof Atomics !== 'undefined') { 357 | await symbol('js.Atomics', Atomics); 358 | } 359 | 360 | if (typeof globalThis !== 'undefined') { 361 | await symbol('js.globalThis', globalThis); 362 | } 363 | 364 | if (typeof SharedArrayBuffer !== 'undefined') { 365 | await symbol('js.SharedArrayBuffer', SharedArrayBuffer); 366 | } 367 | 368 | /************************ 369 | * ShenScript Internals * 370 | ************************/ 371 | 372 | await define('shen-script.lookup', x => lookup(nameOf(asSymbol(x)))); 373 | await define('shen-script.lookup-function', x => lookup(nameOf(asSymbol(x))).f || null); 374 | await symbol('shen-script.ast', ast); 375 | await symbol('shen-script.$', $); 376 | 377 | await define('shen-script.array->list', x => toList(x)); 378 | await define('shen-script.array->list+', (x, y) => toList(x, y)); 379 | await define('shen-script.array->list-tree', x => toListTree(x)); 380 | await define('shen-script.list->array', x => toArray(x)); 381 | await define('shen-script.list->array-tree', x => toArrayTree(x)); 382 | 383 | await define('shen-script.boolean.js->shen', x => asShenBool(x)); 384 | await define('shen-script.boolean.shen->js', x => asJsBool(x)); 385 | 386 | return Object.assign($, { 387 | caller, define, defineTyped, defmacro, evalShen, exec, execEach, load, parse, symbol 388 | }); 389 | }; 390 | -------------------------------------------------------------------------------- /lib/frontend.node.js: -------------------------------------------------------------------------------- 1 | const frontend = require('./frontend.js'); 2 | 3 | module.exports = async $ => { 4 | const { 5 | caller, define, defineTyped, defmacro, isArray, 6 | lookup, s, symbol, symbolOf, toList, valueOf 7 | } = $ = await frontend($); 8 | 9 | await defineTyped('node.exit', [s`number`, s`-->`, s`unit`], x => process.exit(x)); 10 | await defmacro('node.exit-macro', expr => { 11 | if (isArray(expr) && expr.length === 1 && expr[0] === s`node.exit`) { 12 | return [...expr, 0]; 13 | } 14 | }); 15 | 16 | await define('node.require', x => require(x)); 17 | await symbol('node.global', global); 18 | 19 | if (!lookup('js.globalThis').valueExists) { 20 | await symbol('js.globalThis', global); 21 | } 22 | 23 | /************************* 24 | * Declare Port Features * 25 | *************************/ 26 | 27 | const features = [ 28 | s`shen-script`, 29 | s`js`, 30 | s`node`, 31 | symbolOf(valueOf('*os*').toLowerCase())]; 32 | await caller('shen.x.features.initialise')(toList(features)); 33 | 34 | return $; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/frontend.web.js: -------------------------------------------------------------------------------- 1 | const { Id } = require('./ast.js'); 2 | const frontend = require('./frontend.js'); 3 | const { onReady } = require('./utils.js'); 4 | 5 | module.exports = async $ => { 6 | const { 7 | asShenBool, asString, caller, define, defineTyped, defun, evalJs, inline, isFunction, isNeArray, isString, 8 | isSymbol, lookup, nameOf, raise, s, settle, show, symbol, symbolOf, toArray, toArrayTree, toList, valueOf 9 | } = $ = await frontend($); 10 | 11 | await defineTyped('web.atob', [s`string`, s`-->`, s`string`], x => atob(asString(x))); 12 | await defineTyped('web.btoa', [s`string`, s`-->`, s`string`], x => btoa(asString(x))); 13 | 14 | await defun('y-or-n?', m => new Promise(f => f(asShenBool(confirm(m))))); 15 | await defineTyped('web.confirm?', [s`string`, s`-->`, s`boolean`], m => new Promise(f => f(asShenBool(confirm(m))))); 16 | await defineTyped('web.fetch-text', [s`string`, s`-->`, s`string`], url => fetch(url).then(x => x.text())); 17 | await defineTyped('web.fetch-json', [s`string`, s`-->`, s`A`], url => fetch(url).then(x => x.json())); 18 | await defineTyped('web.fetch-text*', [[s`list`, s`string`], s`-->`, [s`list`, s`string`]], 19 | urls => Promise.all(toArray(urls).map(url => fetch(url).then(x => x.text()))).then(x => toList(x))); 20 | await defineTyped('web.fetch-json*', [[s`list`, s`string`], s`-->`, [s`list`, s`A`]], 21 | urls => Promise.all(toArray(urls).map(url => fetch(url).then(x => x.json()))).then(x => toList(x))); 22 | 23 | await symbol('web.document', document); 24 | await symbol('web.navigator', navigator); 25 | await symbol('web.self', () => evalJs(Id('self'))); 26 | await symbol('web.window', window); 27 | 28 | inline('web.self', null, [], () => Id('self')); 29 | 30 | if (!lookup('js.globalThis').valueExists) { 31 | await symbol('js.globalThis', window); 32 | } 33 | 34 | await define('local-storage.clear', () => (localStorage.clear(), null)); 35 | await define('local-storage.get', key => localStorage.getItem(key)); 36 | await define('local-storage.has?', key => asShenBool(localStorage.getItem(key) !== undefined)); 37 | await define('local-storage.remove', key => (localStorage.removeItem(key), null)); 38 | await define('local-storage.set', (key, value) => (localStorage.setItem(key, value), null)); 39 | 40 | if (typeof Worker !== 'undefined') { 41 | await symbol('web.Worker', Worker); 42 | } 43 | 44 | /************************************* 45 | * DOM-Construction and Manipulation * 46 | *************************************/ 47 | 48 | const raiseInvalidForm = tree => raise(`dom.build: invalid form: ${show(tree)}`); 49 | const buildAttribute = (name, value) => Object.assign(document.createAttribute(name), { value }); 50 | const buildEventHandler = (event, f) => Object.assign(f, { event }); 51 | const buildElement = (element, children) => ( 52 | children.forEach(child => 53 | child instanceof Attr ? element.setAttributeNode(child) : 54 | isFunction(child) ? element['on' + child.event] = e => (settle(child(e)), null) : 55 | element.appendChild(child)), 56 | element); 57 | const buildTree = tree => 58 | isString(tree) ? document.createTextNode(tree) : 59 | !(isNeArray(tree) && isSymbol(tree[0])) ? raiseInvalidForm(tree) : 60 | nameOf(tree[0]).startsWith('@') ? buildAttribute(nameOf(tree[0]).substring(1), tree[1]) : 61 | nameOf(tree[0]).startsWith('!') ? buildEventHandler(nameOf(tree[0]).substring(1), tree[1]) : 62 | buildElement(document.createElement(nameOf(tree[0])), tree.slice(1).map(buildTree)); 63 | 64 | await define('dom.append', (parent, child) => (parent.appendChild(child), null)); 65 | await define('dom.build', tree => buildTree(toArrayTree(tree))); 66 | await define('dom.onready', f => (onReady(() => settle(f())), null)); 67 | await define('dom.prepend', (parent, child) => (parent.insertBefore(child, parent.firstChild), null)); 68 | await define('dom.query', selector => document.querySelector(selector)); 69 | await define('dom.query*', selector => toList([...document.querySelectorAll(selector).entries()])); 70 | await define('dom.remove', node => (node.parentNode && node.parentNode.removeChild(node), null)); 71 | await define('dom.replace', (target, node) => (target.parentNode && target.parentNode.replaceChild(node, target), null)); 72 | 73 | /************************* 74 | * Declare Port Features * 75 | *************************/ 76 | 77 | const features = [ 78 | s`shen-script`, 79 | s`js`, 80 | s`web`, 81 | symbolOf(valueOf('*implementation*').toLowerCase().split(' ').join('-')), 82 | symbolOf(valueOf('*os*').toLowerCase())]; 83 | await caller('shen.x.features.initialise')(toList(features)); 84 | 85 | return $; 86 | }; 87 | -------------------------------------------------------------------------------- /lib/overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = $ => { 2 | const { 3 | asArray, asCons, asNumber, asShenBool, cons, defun, equate, 4 | isArray, isCons, isSymbol, lookup, nameOf, raise, s, settle, toArray, toList 5 | } = $; 6 | const asMap = x => x instanceof Map ? x : raise('dict expected'); 7 | const isUpper = x => x >= 65 && x <= 90; 8 | const pvar = s`shen.pvar`; 9 | const tuple = s`shen.tuple`; 10 | const arity = s`arity`; 11 | const t$ = s`true`; 12 | const f$ = s`false`; 13 | const propertyVector = lookup('*property-vector*'); 14 | defun('@p', (x, y) => [tuple, x, y]); 15 | defun('shen.pvar?', x => asShenBool(isArray(x) && x.length > 0 && x[0] === pvar)); 16 | defun('shen.byte->digit', x => x - 48); 17 | defun('shen.numbyte?', x => asShenBool(x >= 48 && x <= 57)); 18 | defun('integer?', x => asShenBool(Number.isInteger(x))); 19 | defun('symbol?', x => asShenBool(isSymbol(x) && x !== t$ && x !== f$)); 20 | defun('variable?', x => asShenBool(isSymbol(x) && isUpper(nameOf(x).charCodeAt(0)))); 21 | defun('shen.fillvector', (xs, i, max, x) => asArray(xs).fill(x, asNumber(i), asNumber(max) + 1)); 22 | defun('shen.initialise_arity_table', xs => { 23 | for (; isCons(xs); xs = xs.tail.tail) { 24 | propertyVector.get().set(xs.head, cons(cons(arity, xs.tail.head), null)); 25 | } 26 | return null; 27 | }); 28 | defun('put', (x, p, y, d) => { 29 | const current = asMap(d).has(x) ? d.get(x) : null; 30 | const array = toArray(current); 31 | for (const element of array) { 32 | if (equate(p, asCons(element).head)) { 33 | element.tail = y; 34 | d.set(x, toList(array)); 35 | return y; 36 | } 37 | } 38 | array.push(cons(p, y)); 39 | d.set(x, toList(array)); 40 | return y; 41 | }); 42 | defun('shen.dict', _ => new Map()); 43 | defun('shen.dict?', x => asShenBool(x instanceof Map)); 44 | defun('shen.dict-count', d => asMap(d).size); 45 | defun('shen.dict->', (d, k, v) => (asMap(d).set(k, v), v)); 46 | defun('shen.<-dict', (d, k) => asMap(d).has(k) ? d.get(k) : raise('value not found in dict\r\n')); 47 | defun('shen.dict-rm', (d, k) => (asMap(d).delete(k), k)); 48 | defun('shen.dict-fold', async (d, f, acc) => { 49 | for (const [k, v] of asMap(d)) { 50 | acc = await settle(f(k, v, acc)); 51 | } 52 | return acc; 53 | }); 54 | defun('shen.dict-keys', d => toList([...asMap(d).keys()])); 55 | defun('shen.dict-values', d => toList([...asMap(d).values()])); 56 | const oldShow = $.show; 57 | $.show = x => x instanceof Map ? `` : oldShow(x); 58 | const credits = lookup('shen.credits').f; 59 | const pr = lookup('pr').f; 60 | const stoutput = lookup('*stoutput*'); 61 | defun('shen.credits', async () => { 62 | await settle(credits()); 63 | return await settle(pr('exit REPL with (node.exit)', stoutput.get())); 64 | }); 65 | return $; 66 | }; 67 | -------------------------------------------------------------------------------- /lib/shen.js: -------------------------------------------------------------------------------- 1 | const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null; 2 | 3 | module.exports = class Shen { 4 | constructor (options = {}) { 5 | const target = isNode ? 'node' : 'web'; 6 | const backend = require('./backend.js'); 7 | const config = require(`./config.${target}.js`); 8 | const kernel = require('./kernel.js'); 9 | const frontend = require(`./frontend.${target}.js`); 10 | return kernel(backend({ ...config, ...options })).then(frontend); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; 2 | const GeneratorFunction = Object.getPrototypeOf(function*() {}).constructor; 3 | 4 | class StringInStream { 5 | constructor(text) { 6 | this.text = text; 7 | this.pos = 0; 8 | } 9 | read() { 10 | return this.pos >= this.text.length 11 | ? -1 12 | : this.text.charCodeAt(this.pos++); 13 | } 14 | close() { 15 | return (this.pos = Infinity, null); 16 | } 17 | } 18 | 19 | const fetchRead = async path => new StringInStream(await (await fetch(path)).text()); 20 | const flatMap = (xs, f) => { 21 | const ys = []; 22 | for (const x of xs) { 23 | for (const y of f(x)) { 24 | ys.push(y); 25 | } 26 | } 27 | return ys; 28 | }; 29 | const most = a => a.length === 0 ? [] : a.slice(0, a.length - 1); 30 | const last = a => a.length === 0 ? undefined : a[a.length - 1]; 31 | const mapAsync = async (xs, f) => { 32 | const ys = []; 33 | for (const x of xs) { 34 | ys.push(await f(await x)); 35 | } 36 | return ys; 37 | }; 38 | const multiMatch = (key, ...pairs) => { 39 | for (const [regex, result] of pairs) { 40 | if (regex.test(key)) { 41 | return result; 42 | } 43 | } 44 | return 'Unknown'; 45 | }; 46 | const onReady = f => 47 | (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) 48 | ? f() 49 | : document.addEventListener('DOMContentLoaded', f); 50 | const pairs = (xs, message = 'array must be of even length') => 51 | xs.length % 2 === 0 52 | ? produce(x => x.length > 0, x => x.slice(0, 2), x => x.slice(2), xs) 53 | : raise(message); 54 | const produceState = (proceed, select, next, state, result = []) => { 55 | for (; proceed(state); state = next(state)) { 56 | result.push(select(state)); 57 | } 58 | return { result, state }; 59 | }; 60 | const produce = (proceed, select, next, state, result = []) => 61 | produceState(proceed, select, next, state, result).result; 62 | const raise = x => { throw new Error(x); }; 63 | const s = (x, y) => Symbol.for(String.raw(x, y)); 64 | 65 | module.exports = { 66 | AsyncFunction, GeneratorFunction, StringInStream, 67 | fetchRead, flatMap, last, mapAsync, most, multiMatch, onReady, pairs, produce, produceState, raise, s 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shen-script", 3 | "version": "0.17.0", 4 | "description": "Shen for JavaScript", 5 | "keywords": [ 6 | "shen", 7 | "language" 8 | ], 9 | "author": "Robert Koeninger", 10 | "license": "BSD-3-Clause", 11 | "repository": "github:rkoeninger/ShenScript", 12 | "main": "lib/shen.js", 13 | "files": [ 14 | "lib/*.js", 15 | "kernel/LICENSE.txt" 16 | ], 17 | "devDependencies": { 18 | "@babel/core": "^7.19.3", 19 | "@babel/preset-env": "^7.19.3", 20 | "babel-loader": "^8.2.5", 21 | "eslint": "^8.24.0", 22 | "follow-redirects": "^1.15.2", 23 | "kind-of": "^6.0.3", 24 | "minimist": "^1.2.6", 25 | "mocha": "^10.0.0", 26 | "mocha-each": "^2.0.1", 27 | "parsimmon": "^1.18.1", 28 | "rimraf": "^3.0.2", 29 | "ssri": "^9.0.1", 30 | "tar": "^6.1.11", 31 | "tempfile": "^4.0.0", 32 | "terser-webpack-plugin": "^5.3.6", 33 | "webpack": "^5.74.0", 34 | "webpack-cli": "^4.10.0" 35 | }, 36 | "dependencies": { 37 | "astring": "^1.8.3", 38 | "awaitify-stream": "^1.0.2" 39 | }, 40 | "scripts": { 41 | "lint": "eslint index.*.js lib/**/*.js scripts/**/*.js test/**/*.js --ignore-pattern lib/kernel.js", 42 | "test": "npm run test-backend && npm run test-frontend && npm run test-kernel", 43 | "fetch-kernel": "node scripts/fetch.js", 44 | "test-backend": "mocha test/backend/test.*.js --reporter dot --timeout 5000", 45 | "render-kernel": "node scripts/render.js", 46 | "test-kernel": "node test/kernel/test.kernel.js", 47 | "test-frontend": "mocha test/frontend/test.*.js --reporter dot --timeout 5000", 48 | "repl": "node scripts/repl.js", 49 | "start": "webpack-cli --watch --env.mode=development", 50 | "bundle-dev": "webpack-cli --env.mode=development", 51 | "bundle": "webpack-cli --env.mode=production", 52 | "bundle-min": "webpack-cli --env.mode=production --env.min", 53 | "bundles": "npm run bundle-dev && npm run bundle && npm run bundle-min" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | kernelVersion: '22.4', 3 | kernelPath: 'kernel', 4 | testsPath: 'kernel/tests', 5 | klPath: 'kernel/klambda', 6 | klExt: '.kl', 7 | klFiles: [ 8 | 'core', 9 | 'declarations', 10 | 'dict', 11 | 'init', 12 | 'load', 13 | 'macros', 14 | 'prolog', 15 | 'reader', 16 | 'sequent', 17 | 'sys', 18 | 't-star', 19 | 'toplevel', 20 | 'track', 21 | 'types', 22 | 'writer', 23 | 'yacc', 24 | 'extension-features', 25 | 'extension-launcher' 26 | ] 27 | }; 28 | -------------------------------------------------------------------------------- /scripts/fetch.js: -------------------------------------------------------------------------------- 1 | const follow = require('follow-redirects'); 2 | const fs = require('fs'); 3 | const rimraf = require('rimraf'); 4 | const tar = require('tar'); 5 | const { kernelVersion, kernelPath } = require('./config.js'); 6 | const { formatGrid } = require('./utils.js'); 7 | 8 | const request = url => new Promise((resolve, reject) => { 9 | follow[url.startsWith('https:') ? 'https' : 'http'].get(url, r => { 10 | if (r.statusCode < 200 || r.statusCode > 299) { 11 | reject(new Error('Failed to load page, status code: ' + r.statusCode)); 12 | } 13 | 14 | const data = []; 15 | r.on('data', x => data.push(x)); 16 | r.on('end', () => resolve(Buffer.concat(data))); 17 | }).on('error', e => reject(e)); 18 | }); 19 | 20 | const kernelFolderName = `ShenOSKernel-${kernelVersion}`; 21 | const kernelArchiveName = `${kernelFolderName}.tar.gz`; 22 | const kernelArchiveUrlBase = 'https://github.com/Shen-Language/shen-sources/releases/download'; 23 | const kernelArchiveUrl = `${kernelArchiveUrlBase}/shen-${kernelVersion}/${kernelArchiveName}`; 24 | 25 | const fetch = async () => { 26 | if (fs.existsSync(kernelPath)) { 27 | rimraf.sync(kernelPath); 28 | } 29 | 30 | const data = await request(kernelArchiveUrl); 31 | fs.writeFileSync(kernelArchiveName, data); 32 | await tar.extract({ file: kernelArchiveName, unlink: true }); 33 | fs.renameSync(kernelFolderName, kernelPath); 34 | fs.unlinkSync(kernelArchiveName); 35 | return formatGrid([`Shen ${kernelVersion}`, `${data.length} chars`]); 36 | }; 37 | 38 | fetch().then(console.log, console.error); 39 | -------------------------------------------------------------------------------- /scripts/parser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { alt, createLanguage, regexp, string } = require('parsimmon'); 3 | const { klPath, klFiles, klExt } = require('./config.js'); 4 | const { flatMap } = require('../lib/utils.js'); 5 | 6 | const language = createLanguage({ 7 | whitespace: _ => regexp(/\s*/m), 8 | numeric: _ => regexp(/-?\d+/).map(Number), 9 | textual: _ => regexp(/[^"]*/m).trim(string('"')), 10 | symbolic: _ => regexp(/[^\s()]+/).map(Symbol.for), 11 | value: r => alt(r.numeric, r.textual, r.symbolic, r.form), 12 | form: r => r.value.trim(r.whitespace).many().wrap(string('('), string(')')), 13 | file: r => r.value.trim(r.whitespace).many() 14 | }); 15 | const parseFile = s => language.file.tryParse(s); 16 | const parseForm = s => parseFile(s)[0]; 17 | const parseKernel = () => flatMap(klFiles, file => parseFile(fs.readFileSync(`${klPath}/${file}${klExt}`, 'utf-8'))); 18 | 19 | module.exports = { parseFile, parseForm, parseKernel }; 20 | -------------------------------------------------------------------------------- /scripts/render.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { parseKernel } = require('./parser.js'); 3 | const backend = require('../lib/backend.js'); 4 | const { 5 | Arrow, Assign, Block, Call, Const, Id, Literal, Member, Program, Return, Statement, 6 | generate 7 | } = require('../lib/ast.js'); 8 | const { formatDuration, formatGrid, measure } = require('./utils.js'); 9 | 10 | console.log('- parsing kernel...'); 11 | const measureParse = measure(parseKernel); 12 | console.log(` parsed in ${formatDuration(measureParse.duration)}`); 13 | 14 | console.log(`- creating backend...`); 15 | const measureBackend = measure(() => backend()); 16 | const { assemble, construct, isArray, s } = measureBackend.result; 17 | console.log(` created in ${formatDuration(measureBackend.duration)}`); 18 | 19 | console.log('- rendering kernel...'); 20 | const measureRender = measure(() => { 21 | const body = assemble( 22 | Block, 23 | ...measureParse.result.filter(isArray).map(construct), 24 | Assign(Id('$'), Call(Call(Id('require'), [Literal('./overrides.js')]), [Id('$')])), 25 | assemble(Statement, construct([s`shen.initialise`]))); 26 | return generate( 27 | Program([Statement(Assign( 28 | Member(Id('module'), Id('exports')), 29 | Arrow( 30 | [Id('$')], 31 | Block( 32 | ...Object.entries(body.subs).map(([key, value]) => Const(Id(key), value)), 33 | ...body.ast.body, 34 | Return(Id('$'))), 35 | true)))])); 36 | }); 37 | const syntax = measureRender.result; 38 | console.log(` rendered in ${formatDuration(measureRender.duration)}, ${syntax.length} chars`); 39 | 40 | console.log('- writing file...'); 41 | const measureWrite = measure(() => fs.writeFileSync(`lib/kernel.js`, syntax)); 42 | console.log(` written in ${formatDuration(measureWrite.duration)}`); 43 | console.log(); 44 | 45 | console.log(formatGrid(['kernel.js', `${syntax.length} chars`, formatDuration(measureRender.duration)])); 46 | -------------------------------------------------------------------------------- /scripts/repl.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { addAsyncFunctions } = require('awaitify-stream'); 3 | const Shen = require('../lib/shen.js'); 4 | 5 | const InStream = class { 6 | constructor(stream, name) { 7 | this.name = name; 8 | this.stream = addAsyncFunctions(stream); 9 | this.buf = ''; 10 | this.pos = 0; 11 | } 12 | async read() { 13 | if (this.pos < this.buf.length) { 14 | return this.buf[this.pos] === 13 ? (this.pos++, this.read()) : this.buf[this.pos++]; 15 | } 16 | const b = await this.stream.readAsync(); 17 | return b === null ? -1 : (this.buf = b, this.pos = 0, this.read()); 18 | } 19 | close() { return this.stream.close(); } 20 | }; 21 | 22 | const OutStream = class { 23 | constructor(stream, name) { 24 | this.name = name; 25 | this.stream = stream; 26 | } 27 | write(b) { return this.stream.write(String.fromCharCode(b)); } 28 | close() { return this.stream.close(); } 29 | }; 30 | 31 | (async () => { 32 | const { caller, toList } = await new Shen({ 33 | InStream, 34 | OutStream, 35 | openRead: path => new InStream(fs.createReadStream(path), `filein=${path}`), 36 | openWrite: path => new OutStream(fs.createWriteStream(path), `fileout=${path}`), 37 | stinput: new InStream(process.stdin, 'stinput'), 38 | stoutput: new OutStream(process.stdout, 'stoutput'), 39 | sterror: new OutStream(process.stderr, 'sterror') 40 | }); 41 | await caller('shen.x.launcher.main')(toList(['shen', 'repl'])); 42 | })(); 43 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const formatDuration = x => 2 | [[x / 60000, 'm'], [x / 1000 % 60, 's'], [x % 1000, 'ms']] 3 | .filter(([n, _]) => n >= 1) 4 | .map(([n, l]) => `${Math.floor(n)}${l}`) 5 | .join(' '); 6 | const padRight = (n, s) => s + ' '.repeat(n - s.length); 7 | const formatGrid = (...rows) => { 8 | if (rows.length === 0) { 9 | return; 10 | } 11 | rows = rows.map(r => r.map(x => '' + x)); 12 | const cols = [...rows[0].keys()].map(i => rows.map(r => r[i])); 13 | const widths = cols.map(c => c.reduce((x, y) => x > y.length ? x : y.length, 0)); 14 | const totalWidth = widths.reduce((x, y) => x + y, 0) + (cols.length - 1) * 3 + 4; 15 | const topBottom = '-'.repeat(totalWidth); 16 | return topBottom + '\n' + rows.map(r => `| ${r.map((s, i) => padRight(widths[i], s)).join(' | ')} |`).join('\n') + '\n' + topBottom; 17 | }; 18 | const measure = f => { 19 | const start = Date.now(); 20 | const result = f(); 21 | 22 | if (result && result.then) { 23 | return result.then(result => { 24 | const duration = Date.now() - start; 25 | return { duration, result }; 26 | }); 27 | } else { 28 | const duration = Date.now() - start; 29 | return { duration, result }; 30 | } 31 | }; 32 | 33 | module.exports = { formatDuration, formatGrid, measure }; 34 | -------------------------------------------------------------------------------- /test/backend/test.async.js: -------------------------------------------------------------------------------- 1 | const { equal, ok, rejects } = require('assert'); 2 | const forEach = require('mocha-each'); 3 | const { parseForm } = require('../../scripts/parser.js'); 4 | const backend = require('../../lib/backend.js'); 5 | 6 | const { cons, evalKl, lookup, s, settle, valueOf } = backend(); 7 | const exec = s => settle(evalKl(parseForm(s))); 8 | const f = name => lookup(name).f; 9 | 10 | describe('async', () => { 11 | describe('evaluation', () => { 12 | it('eval-kl', async () => { 13 | equal(5, await exec('(eval-kl (cons + (cons 2 (cons 3 ()))))')); 14 | equal(5, await f('eval-kl')(cons(s`+`, cons(2, cons(3, null))))); 15 | equal(5, await f('eval-kl')([s`+`, 2, 3])); 16 | equal(5, await evalKl(cons(s`+`, cons(2, cons(3, null))))); 17 | equal(5, await evalKl([s`+`, 2, 3])); 18 | }); 19 | }); 20 | 21 | describe('conditionals', () => { 22 | describe('cond', () => { 23 | it('should raise an error when there are no clauses', async () => { 24 | await rejects(exec('(cond)')); 25 | }); 26 | it('should act as an if-else chain', async () => { 27 | equal(2, await exec('(cond (false 1) (true 2) (false 3))')); 28 | }); 29 | }); 30 | describe('if', () => { 31 | it('should not evaluate both branches', async () => { 32 | equal(1, await exec('(tl (cons (if (= 0 0) (set x 1) (set x 2)) (value x)))')); 33 | equal(2, await exec('(tl (cons (if (= 0 1) (set x 1) (set x 2)) (value x)))')); 34 | }); 35 | it('should evaluate side effects in nested ifs', async () => { 36 | await exec('(if (trap-error (simple-error "fail") (lambda E (do (set x 1) true))) (set y 2) (set y 0))'); 37 | equal(1, valueOf('x')); 38 | equal(2, valueOf('y')); 39 | await exec('(if (trap-error (simple-error "fail") (lambda E (do (set x 3) false))) (set y 0) (set y 4))'); 40 | equal(3, valueOf('x')); 41 | equal(4, valueOf('y')); 42 | }); 43 | }); 44 | describe('and', () => { 45 | it('should return a Shen boolean', async () => { 46 | equal(s`true`, await exec('(and true true)')); 47 | equal(s`false`, await exec('(and true false)')); 48 | equal(s`false`, await exec('(and false true)')); 49 | equal(s`false`, await exec('(and false false)')); 50 | }); 51 | it('should do short-circuit evaluation', async () => { 52 | await exec('(and false (simple-error "should not get evaluated"))'); 53 | await rejects(exec('(and true (simple-error "should get evaluated"))')); 54 | }); 55 | }); 56 | describe('or', () => { 57 | it('should return a Shen boolean', async () => { 58 | equal(s`true`, await exec('(or true true)')); 59 | equal(s`true`, await exec('(or true false)')); 60 | equal(s`true`, await exec('(or false true)')); 61 | equal(s`false`, await exec('(or false false)')); 62 | }); 63 | it('should do short-circuit evaluation', async () => { 64 | await exec('(or true (simple-error "should not get evaluated"))'); 65 | await rejects(exec('(or false (simple-error "should get evaluated"))')); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('variable bindings', () => { 71 | describe('let', () => { 72 | it('should bind local variables', async () => { 73 | equal(123, await exec('(let X 123 X)')); 74 | }); 75 | it('should not bind local variables in value expression', async () => { 76 | equal('X', await exec('(let X (str X) X)')); 77 | await rejects(exec('(let X (+ 1 X) (* 2 X))')); 78 | }); 79 | it('should not bind local variables outside body expression', async () => { 80 | equal(s`X`, await exec('(tl (cons (let X 2 (+ 1 X)) X))')); 81 | }); 82 | it('should shadow outer bindings when nested', async () => { 83 | equal(3, await exec('(let X 1 (let X 2 (let X 3 X)))')); 84 | equal(3, await exec('(let X 1 (if (> X 0) (let X 2 (+ X 1)) 5))')); 85 | }); 86 | it('should be able to initialize inner variable in terms of outer variable', async () => { 87 | equal(2, await exec('(let X 1 (let X (+ X 1) X))')); 88 | }); 89 | it('should shadow outer lambda binding when nested', async () => { 90 | equal(8, await exec('((lambda X (let X 4 (+ X X))) 3)')); 91 | }); 92 | it('should shadow defun parameters in outer scope', async () => { 93 | await exec('(defun triple (X) (let X 4 (* 3 X)))'); 94 | equal(12, await exec('(triple 0)')); 95 | }); 96 | }); 97 | describe('lambda', () => { 98 | it('should shadow outer bindings when nested', async () => { 99 | equal(8, await exec('(((lambda X (lambda X (+ X X))) 3) 4)')); 100 | }); 101 | it('should shadow outer let binding when nested', async () => { 102 | equal(8, await exec('(let X 3 ((lambda X (+ X X)) 4))')); 103 | }); 104 | it('should shadow defun parameters in outer scope', async () => { 105 | await exec('(defun three (X) (lambda X (* 3 X)))'); 106 | equal(12, await exec('((three 2) 4)')); 107 | }); 108 | }); 109 | describe('set/value key optimization', () => { 110 | it('should not try to optimize a variable that holds the key value in (value)', async () => { 111 | await exec('(set z 31)'); 112 | equal(31, await exec('(let X z (value X))')); 113 | }); 114 | it('should not try to optimize a variable that holds the key value in (set)', async () => { 115 | await exec('(let X z (set X 37))'); 116 | equal(37, await exec('(value z)')); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('error handling', () => { 122 | describe('trap-error', () => { 123 | it('should provide error to handler', async () => { 124 | equal('hi', await exec('(trap-error (simple-error "hi") (lambda X (error-to-string X)))')); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('recursion', () => { 130 | forEach([[0, 1], [5, 120], [7, 5040]]).it('functions should be able to call themselves', async (n, r) => { 131 | await exec('(defun fac (N) (if (= 0 N) 1 (* N (fac (- N 1)))))'); 132 | equal(r, await settle(f('fac')(n))); 133 | }); 134 | describe('tail recursion', () => { 135 | const countDown = async body => { 136 | await evalKl([s`defun`, s`count-down`, [s`X`], parseForm(body)]); 137 | ok(await evalKl([s`count-down`, 20000])); 138 | }; 139 | it('should be possible without overflow', async () => { 140 | await countDown('(if (= 0 X) true (count-down (- X 1)))'); 141 | }); 142 | it('should optimize through an if true branch', async () => { 143 | await countDown('(if (> X 0) (count-down (- X 1)) true)'); 144 | }); 145 | it('should optimize through an if false branch', async () => { 146 | await countDown('(if (<= X 0) true (count-down (- X 1)))'); 147 | }); 148 | it('should optimize through nested if expressions', async () => { 149 | await countDown('(if (<= X 0) true (if true (count-down (- X 1)) false))'); 150 | }); 151 | it('should optimize through let body', async () => { 152 | await countDown('(if (<= X 0) true (let F 1 (count-down (- X F))))'); 153 | }); 154 | it('should optimize through a first cond consequent', async () => { 155 | await countDown('(cond ((> X 0) (count-down (- X 1))) (true true))'); 156 | }); 157 | it('should optimize through a last cond consequent', async () => { 158 | await countDown('(cond ((<= X 0) true) (true (count-down (- X 1))))'); 159 | }); 160 | it('should optimize through last expression in a do expression', async () => { 161 | await countDown('(do 0 (if (<= X 0) true (do 0 (count-down (- X 1)))))'); 162 | }); 163 | it('should optimize through handler of trap-error expression', async () => { 164 | await countDown('(trap-error (if (> X 0) (simple-error "recur") true) (lambda E (count-down (- X 1))))'); 165 | }); 166 | it('should optimize through freeze calls', async () => { 167 | await countDown('(if (<= X 0) true ((freeze (count-down (- X 1)))))'); 168 | }); 169 | it('should optimize through lambda calls', async () => { 170 | await countDown('(if (<= X 0) true ((lambda Y (count-down (- X Y))) 1))'); 171 | }); 172 | it('should optimize through nested lambdas', async () => { 173 | await countDown('(let F (lambda F (lambda X (if (<= X 0) true ((F F) (- X 1))))) ((F F) 20000))'); 174 | }); 175 | it('should be possible for mutually recursive functions without overflow', async () => { 176 | await exec('(defun even? (X) (if (= 0 X) true (odd? (- X 1))))'); 177 | await exec('(defun odd? (X) (if (= 0 X) false (even? (- X 1))))'); 178 | equal(s`true`, await exec('(even? 20000)')); 179 | }); 180 | }); 181 | }); 182 | 183 | describe('scope capture', () => { 184 | describe('lambda', () => { 185 | it('should capture local variables', async () => { 186 | equal(1, await exec('(let X 1 (let F (lambda Y X) (F 0)))')); 187 | }); 188 | it('should not have access to symbols outside lexical scope', async () => { 189 | equal(s`Y`, await exec('(let F (lambda X Y) (let Y 3 (F 0)))')); 190 | }); 191 | }); 192 | describe('freeze', () => { 193 | it('should capture local variables', async () => { 194 | equal(1, await exec('(let X 1 (let F (freeze X) (F)))')); 195 | }); 196 | it('should not have access to symbols outside lexical scope', async () => { 197 | equal(s`Y`, await exec('(let F (freeze Y) (let Y 3 (F)))')); 198 | }); 199 | }); 200 | describe('escaping', () => { 201 | it('$ should be usable as a variable', async () => { 202 | equal(3, await exec('(let $ 2 (+ 1 $))')); 203 | }); 204 | }); 205 | }); 206 | 207 | describe('applications', () => { 208 | it('argument expressions should be evaluated in order', async () => { 209 | await exec('((set x (lambda X (lambda Y (+ X Y)))) (set x 1) (set x 2))'); 210 | equal(2, valueOf('x')); 211 | }); 212 | it('partial application', async () => { 213 | equal(13, await exec('((+ 6) 7)')); 214 | }); 215 | it('curried application', async () => { 216 | equal(13, await exec('((lambda X (lambda Y (+ X Y))) 6 7)')); 217 | }); 218 | it('should raise error if too many arguments are applied', async () => { 219 | await rejects(exec('(+ 1 2 3)')); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/backend/test.eval.js: -------------------------------------------------------------------------------- 1 | const { equal, ok, rejects } = require('assert'); 2 | const forEach = require('mocha-each'); 3 | const { parseForm } = require('../../scripts/parser.js'); 4 | const backend = require('../../lib/backend.js'); 5 | 6 | const { cons, evalKl, lookup, s, settle, valueOf } = backend(); 7 | const exec = s => settle(evalKl(parseForm(s))); 8 | const f = name => lookup(name).f; 9 | 10 | describe('async', () => { 11 | describe('evaluation', () => { 12 | it('eval-kl', async () => { 13 | equal(5, await exec('(eval-kl (cons + (cons 2 (cons 3 ()))))')); 14 | equal(5, await f('eval-kl')(cons(s`+`, cons(2, cons(3, null))))); 15 | equal(5, await f('eval-kl')([s`+`, 2, 3])); 16 | equal(5, await evalKl(cons(s`+`, cons(2, cons(3, null))))); 17 | equal(5, await evalKl([s`+`, 2, 3])); 18 | }); 19 | }); 20 | 21 | describe('conditionals', () => { 22 | describe('cond', () => { 23 | it('should raise an error when there are no clauses', async () => { 24 | await rejects(exec('(cond)')); 25 | }); 26 | it('should act as an if-else chain', async () => { 27 | equal(2, await exec('(cond (false 1) (true 2) (false 3))')); 28 | }); 29 | }); 30 | describe('if', () => { 31 | it('should not evaluate both branches', async () => { 32 | equal(1, await exec('(tl (cons (if (= 0 0) (set x 1) (set x 2)) (value x)))')); 33 | equal(2, await exec('(tl (cons (if (= 0 1) (set x 1) (set x 2)) (value x)))')); 34 | }); 35 | it('should evaluate side effects in nested ifs', async () => { 36 | await exec('(if (trap-error (simple-error "fail") (lambda E (do (set x 1) true))) (set y 2) (set y 0))'); 37 | equal(1, valueOf('x')); 38 | equal(2, valueOf('y')); 39 | await exec('(if (trap-error (simple-error "fail") (lambda E (do (set x 3) false))) (set y 0) (set y 4))'); 40 | equal(3, valueOf('x')); 41 | equal(4, valueOf('y')); 42 | }); 43 | }); 44 | describe('and', () => { 45 | it('should return a Shen boolean', async () => { 46 | equal(s`true`, await exec('(and true true)')); 47 | equal(s`false`, await exec('(and true false)')); 48 | equal(s`false`, await exec('(and false true)')); 49 | equal(s`false`, await exec('(and false false)')); 50 | }); 51 | it('should do short-circuit evaluation', async () => { 52 | await exec('(and false (simple-error "should not get evaluated"))'); 53 | await rejects(exec('(and true (simple-error "should get evaluated"))')); 54 | }); 55 | }); 56 | describe('or', () => { 57 | it('should return a Shen boolean', async () => { 58 | equal(s`true`, await exec('(or true true)')); 59 | equal(s`true`, await exec('(or true false)')); 60 | equal(s`true`, await exec('(or false true)')); 61 | equal(s`false`, await exec('(or false false)')); 62 | }); 63 | it('should do short-circuit evaluation', async () => { 64 | await exec('(or true (simple-error "should not get evaluated"))'); 65 | await rejects(exec('(or false (simple-error "should get evaluated"))')); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('variable bindings', () => { 71 | describe('let', () => { 72 | it('should bind local variables', async () => { 73 | equal(123, await exec('(let X 123 X)')); 74 | }); 75 | it('should not bind local variables in value expression', async () => { 76 | equal('X', await exec('(let X (str X) X)')); 77 | await rejects(exec('(let X (+ 1 X) (* 2 X))')); 78 | }); 79 | it('should not bind local variables outside body expression', async () => { 80 | equal(s`X`, await exec('(tl (cons (let X 2 (+ 1 X)) X))')); 81 | }); 82 | it('should shadow outer bindings when nested', async () => { 83 | equal(3, await exec('(let X 1 (let X 2 (let X 3 X)))')); 84 | equal(3, await exec('(let X 1 (if (> X 0) (let X 2 (+ X 1)) 5))')); 85 | }); 86 | it('should be able to initialize inner variable in terms of outer variable', async () => { 87 | equal(2, await exec('(let X 1 (let X (+ X 1) X))')); 88 | }); 89 | it('should shadow outer lambda binding when nested', async () => { 90 | equal(8, await exec('((lambda X (let X 4 (+ X X))) 3)')); 91 | }); 92 | it('should shadow defun parameters in outer scope', async () => { 93 | await exec('(defun triple (X) (let X 4 (* 3 X)))'); 94 | equal(12, await exec('(triple 0)')); 95 | }); 96 | }); 97 | describe('lambda', () => { 98 | it('should shadow outer bindings when nested', async () => { 99 | equal(8, await exec('(((lambda X (lambda X (+ X X))) 3) 4)')); 100 | }); 101 | it('should shadow outer let binding when nested', async () => { 102 | equal(8, await exec('(let X 3 ((lambda X (+ X X)) 4))')); 103 | }); 104 | it('should shadow defun parameters in outer scope', async () => { 105 | await exec('(defun three (X) (lambda X (* 3 X)))'); 106 | equal(12, await exec('((three 2) 4)')); 107 | }); 108 | }); 109 | describe('set/value key optimization', () => { 110 | it('should not try to optimize a variable that holds the key value in (value)', async () => { 111 | await exec('(set z 31)'); 112 | equal(31, await exec('(let X z (value X))')); 113 | }); 114 | it('should not try to optimize a variable that holds the key value in (set)', async () => { 115 | await exec('(let X z (set X 37))'); 116 | equal(37, await exec('(value z)')); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('error handling', () => { 122 | describe('trap-error', () => { 123 | it('should provide error to handler', async () => { 124 | equal('hi', await exec('(trap-error (simple-error "hi") (lambda X (error-to-string X)))')); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('recursion', () => { 130 | forEach([[0, 1], [5, 120], [7, 5040]]).it('functions should be able to call themselves', async (n, r) => { 131 | await exec('(defun fac (N) (if (= 0 N) 1 (* N (fac (- N 1)))))'); 132 | equal(r, await settle(f('fac')(n))); 133 | }); 134 | describe('tail recursion', () => { 135 | const countDown = async body => { 136 | await evalKl([s`defun`, s`count-down`, [s`X`], parseForm(body)]); 137 | ok(await evalKl([s`count-down`, 20000])); 138 | }; 139 | it('should be possible without overflow', async () => { 140 | await countDown('(if (= 0 X) true (count-down (- X 1)))'); 141 | }); 142 | it('should optimize through an if true branch', async () => { 143 | await countDown('(if (> X 0) (count-down (- X 1)) true)'); 144 | }); 145 | it('should optimize through an if false branch', async () => { 146 | await countDown('(if (<= X 0) true (count-down (- X 1)))'); 147 | }); 148 | it('should optimize through nested if expressions', async () => { 149 | await countDown('(if (<= X 0) true (if true (count-down (- X 1)) false))'); 150 | }); 151 | it('should optimize through let body', async () => { 152 | await countDown('(if (<= X 0) true (let F 1 (count-down (- X F))))'); 153 | }); 154 | it('should optimize through a first cond consequent', async () => { 155 | await countDown('(cond ((> X 0) (count-down (- X 1))) (true true))'); 156 | }); 157 | it('should optimize through a last cond consequent', async () => { 158 | await countDown('(cond ((<= X 0) true) (true (count-down (- X 1))))'); 159 | }); 160 | it('should optimize through last expression in a do expression', async () => { 161 | await countDown('(do 0 (if (<= X 0) true (do 0 (count-down (- X 1)))))'); 162 | }); 163 | it('should optimize through handler of trap-error expression', async () => { 164 | await countDown('(trap-error (if (> X 0) (simple-error "recur") true) (lambda E (count-down (- X 1))))'); 165 | }); 166 | it('should optimize through freeze calls', async () => { 167 | await countDown('(if (<= X 0) true ((freeze (count-down (- X 1)))))'); 168 | }); 169 | it('should optimize through lambda calls', async () => { 170 | await countDown('(if (<= X 0) true ((lambda Y (count-down (- X Y))) 1))'); 171 | }); 172 | it('should optimize through nested lambdas', async () => { 173 | await countDown('(let F (lambda F (lambda X (if (<= X 0) true ((F F) (- X 1))))) ((F F) 20000))'); 174 | }); 175 | it('should be possible for mutually recursive functions without overflow', async () => { 176 | await exec('(defun even? (X) (if (= 0 X) true (odd? (- X 1))))'); 177 | await exec('(defun odd? (X) (if (= 0 X) false (even? (- X 1))))'); 178 | equal(s`true`, await exec('(even? 20000)')); 179 | }); 180 | }); 181 | }); 182 | 183 | describe('scope capture', () => { 184 | describe('lambda', () => { 185 | it('should capture local variables', async () => { 186 | equal(1, await exec('(let X 1 (let F (lambda Y X) (F 0)))')); 187 | }); 188 | it('should not have access to symbols outside lexical scope', async () => { 189 | equal(s`Y`, await exec('(let F (lambda X Y) (let Y 3 (F 0)))')); 190 | }); 191 | }); 192 | describe('freeze', () => { 193 | it('should capture local variables', async () => { 194 | equal(1, await exec('(let X 1 (let F (freeze X) (F)))')); 195 | }); 196 | it('should not have access to symbols outside lexical scope', async () => { 197 | equal(s`Y`, await exec('(let F (freeze Y) (let Y 3 (F)))')); 198 | }); 199 | }); 200 | describe('escaping', () => { 201 | it('$ should be usable as a variable', async () => { 202 | equal(3, await exec('(let $ 2 (+ 1 $))')); 203 | }); 204 | }); 205 | }); 206 | 207 | describe('applications', () => { 208 | it('argument expressions should be evaluated in order', async () => { 209 | await exec('((set x (lambda X (lambda Y (+ X Y)))) (set x 1) (set x 2))'); 210 | equal(2, valueOf('x')); 211 | }); 212 | it('partial application', async () => { 213 | equal(13, await exec('((+ 6) 7)')); 214 | }); 215 | it('curried application', async () => { 216 | equal(13, await exec('((lambda X (lambda Y (+ X Y))) 6 7)')); 217 | }); 218 | it('should raise error if too many arguments are applied', async () => { 219 | await rejects(exec('(+ 1 2 3)')); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/backend/test.parser.js: -------------------------------------------------------------------------------- 1 | const { equal } = require('assert'); 2 | const forEach = require('mocha-each'); 3 | const { s } = require('../../lib/utils.js'); 4 | const { parseForm } = require('../../scripts/parser.js'); 5 | 6 | describe('parsing', () => { 7 | describe('symbolic literals', () => { 8 | forEach(['abc', 'x\'{', '. { 9 | equal(s`${x}`, parseForm(x)); 10 | }); 11 | }); 12 | describe('string literals', () => { 13 | it('should parse empty strings', () => { 14 | equal('', parseForm('""')); 15 | }); 16 | forEach(['a\tb', 'a \n b', 'a b', 'a\r\n\vb']).it('should capture any whitespace', x => { 17 | equal(x, parseForm(`"${x}"`)); 18 | }); 19 | forEach(['~!@#$%', '^&*()_+`\'<', '>,./?;:']).it('should capture all ascii characters', x => { 20 | equal(x, parseForm(`"${x}"`)); 21 | }); 22 | }); 23 | describe('numberic literals', () => { 24 | it('should parse zero', () => { 25 | equal(0, parseForm('0')); 26 | }); 27 | forEach([[5, '5'], [287, '287'], [9456, '9456']]).it('should parse numbers', (n, x) => { 28 | equal(n, parseForm(x)); 29 | }); 30 | forEach([[-4, '-4'], [-143, '-143'], [-79, '-79']]).it('should parse negative numbers', (n, x) => { 31 | equal(n, parseForm(x)); 32 | }); 33 | }); 34 | describe('forms', () => { 35 | it('should parse empty forms as empty arrays', () => { 36 | equal(0, parseForm('()').length); 37 | }); 38 | it('should parse forms as arrays', () => { 39 | equal(s`abc`, parseForm('(abc)')[0]); 40 | equal(s`def`, parseForm('(abc def)')[1]); 41 | }); 42 | it('should parse nested forms as nested arrays', () => { 43 | const expr = parseForm('(if (>= 0 X) X (* -1 X))'); 44 | equal(4, expr.length); 45 | equal(s`if`, expr[0]); 46 | equal(3, expr[1].length); 47 | equal(s`>=`, expr[1][0]); 48 | equal(0, expr[1][1]); 49 | equal(s`X`, expr[1][2]); 50 | equal(s`X`, expr[2]); 51 | equal(3, expr[3].length); 52 | equal(s`*`, expr[3][0]); 53 | equal(-1, expr[3][1]); 54 | equal(s`X`, expr[3][2]); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/backend/test.primitive.js: -------------------------------------------------------------------------------- 1 | const { equal, ok, throws } = require('assert'); 2 | const forEach = require('mocha-each'); 3 | const backend = require('../../lib/backend.js'); 4 | 5 | const { asString, cons, isCons, isString, evalKl, lookup, s, equate } = backend(); 6 | const isShenBool = x => x === s`true` || x === s`false`; 7 | const values = [12, null, undefined, 'abc', s`asd`, 0, Infinity, [], cons(1, 2)]; 8 | const f = name => lookup(name).f; 9 | 10 | describe('primitive', () => { 11 | describe('math', () => { 12 | describe('+', () => { 13 | forEach([[1, 2, 3], [3400, -1250, 2150], [4637436, 283484734, 288122170]]).it('should add numbers', (x, y, z) => { 14 | equal(z, f('+')(x, y)); 15 | }); 16 | forEach([[undefined, 55], [125, NaN], [-4, 'qwerty']]).it('should raise error for non-numbers', (x, y) => { 17 | throws(() => f('+')(x, y)); 18 | }); 19 | }); 20 | describe('-', () => { 21 | it('should subtract numbers', () => { 22 | equal(69, f('-')(142, 73)); 23 | }); 24 | forEach([[undefined, 55], [125, NaN], [-4, 'qwerty']]).it('should raise error for non-numbers', (x, y) => { 25 | throws(() => f('-')(x, y)); 26 | }); 27 | }); 28 | describe('*', () => { 29 | it('should multiply numbers', () => { 30 | equal(24, f('*')(4, 6)); 31 | }); 32 | forEach([[undefined, 55], [125, NaN], [-4, 'qwerty']]).it('should raise error for non-numbers', (x, y) => { 33 | throws(() => f('*')(x, y)); 34 | }); 35 | forEach([34, -7, 449384736738485434.45945]).it('should return zero when multiplying by zero', x => { 36 | equal(0, f('*')(0, x)); 37 | }); 38 | }); 39 | describe('/', () => { 40 | it('should divide numbers', () => { 41 | equal(4, f('/')(24, 6)); 42 | }); 43 | forEach([[undefined, 55], [125, NaN], [-4, 'qwerty']]).it('should raise error for non-numbers', (x, y) => { 44 | throws(() => f('/')(x, y)); 45 | }); 46 | forEach([1, 0, -3]).it('should raise error when divisor is zero', x => { 47 | throws(() => f('/')(x, 0)); 48 | }); 49 | }); 50 | forEach(['<', '>', '<=', '>=', '=']).describe('%s', op => { 51 | forEach([[3, 5], [0.0002, -123213], [-34, 234234]]).it('should return a Shen boolean', (x, y) => { 52 | ok(isShenBool(f(op)(x, y))); 53 | }); 54 | }); 55 | }); 56 | 57 | describe('strings', () => { 58 | describe('pos', () => { 59 | forEach([-1, 3, 1.5, 'a', null]).it('should raise an error if index is out of range or not an integer', i => { 60 | throws(() => f('pos')('abc', i)); 61 | }); 62 | }); 63 | describe('tlstr', () => { 64 | forEach([['a', ''], ['12', '2'], ['#*%', '*%']]).it('should return all but first character of string', (x, y) => { 65 | equal(y, f('tlstr')(x)); 66 | }); 67 | it('should raise an error when given empty string', () => { 68 | throws(() => f('tlstr')('')); 69 | }); 70 | }); 71 | describe('cn', () => { 72 | forEach(['', 'lorem ipsum', '&`#%^@*']).it('should return non-empty argument when other is empty', x => { 73 | equal(x, f('cn')('', x)); 74 | equal(x, f('cn')(x, '')); 75 | }); 76 | }); 77 | describe('string->n', () => { 78 | it('should raise an error when given empty string', () => { 79 | throws(() => f('string->n')('')); 80 | }); 81 | forEach([[97, 'abc'], [63, '?12']]).it('should only return code point for first character', (n, x) => { 82 | equal(n, f('string->n')(x)); 83 | }); 84 | }); 85 | describe('n->string', () => { 86 | forEach([10, 45, 81, 76, 118]).it('should always return a string of length 1', n => { 87 | equal(1, asString(f('n->string')(n)).length); 88 | }); 89 | }); 90 | describe('str', () => { 91 | forEach(values).it('should return a string for any argument', x => { 92 | ok(isString(f('str')(x))); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('symbols', () => { 98 | describe('intern', () => { 99 | it('should return the same symbol for the same name', () => { 100 | equal(f('intern')('qwerty'), f('intern')('qwerty')); 101 | }); 102 | }); 103 | describe('value', () => { 104 | it('should accept idle symbols', () => { 105 | equal('JavaScript', f('value')(s`*language*`)); 106 | }); 107 | it('should raise error for symbol with no value', () => { 108 | throws(() => f('value')(s`qwerty`)); 109 | }); 110 | it('should raise error for non-symbol argument', () => { 111 | throws(() => f('value')(5)); 112 | throws(() => f('value')(cons(1, 2))); 113 | }); 114 | }); 115 | describe('set', () => { 116 | it('should return the assigned value', () => { 117 | equal(1, f('set')(s`x`, 1)); 118 | }); 119 | it('should allow value to be retrieved later', () => { 120 | f('set')(s`x`, 'abc'); 121 | equal("abc", f('value')(s`x`)); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('conses', () => { 127 | describe('cons', () => { 128 | forEach(values).describe('should accept values of any type', x => { 129 | forEach(values).it('and any other type', y => { 130 | ok(isCons(f('cons')(x, y))); 131 | }); 132 | }); 133 | }); 134 | describe('hd', () => { 135 | it('should raise an error on empty list', () => { 136 | throws(() => f('hd')(null)); 137 | }); 138 | forEach(values).it('should retrieve head value of any cons', x => { 139 | ok(equate(x, f('hd')(f('cons')(x, null)))); 140 | }); 141 | }); 142 | describe('tl', () => { 143 | it('should raise an error on empty list', () => { 144 | throws(() => f('tl')(null)); 145 | }); 146 | forEach(values).it('should retrieve head value of any cons', x => { 147 | ok(equate(x, f('tl')(f('cons')(null, x)))); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('absvectors', () => { 153 | it('should store values in specified index', () => { 154 | equal('hi', f('<-address')(f('address->')(f('absvector')(16), 3, 'hi'), 3)); 155 | }); 156 | describe('absvector', () => { 157 | forEach([-1, 'a', null, undefined]).it('should raise error when given non-positive-integer', n => { 158 | throws(() => f('absvector')(n)); 159 | }); 160 | forEach([0, 1, 2, 3, 4]).it('should initialize all values to null', i => { 161 | equal(null, f('<-address')(f('absvector')(5), i)); 162 | }); 163 | }); 164 | describe('<-address', () => { 165 | forEach([-1, 'a', null, undefined]).it('should raise error when given non-positive-integer', i => { 166 | throws(() => f('<-address')(f('absvector')(5), i)); 167 | }); 168 | }); 169 | describe('address->', () => { 170 | forEach([-1, 'a', null, undefined]).it('should raise error when given non-positive-integer', i => { 171 | throws(() => f('address->')(f('absvector')(5), i, null)); 172 | }); 173 | }); 174 | describe('absvector?', () => { 175 | forEach([0, 12, 4835]).it('should return true for values returned by (absvector N)', n => { 176 | equal(s`true`, f('absvector?')(f('absvector')(n))); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('errors', () => { 182 | describe('error-to-string', () => { 183 | forEach(values).it('should raise error when given non-error', x => { 184 | throws(() => f('error-to-string')(x)); 185 | }); 186 | }); 187 | }); 188 | 189 | describe('recognisors', () => { 190 | forEach(['cons?', 'number?', 'string?', 'absvector?']).describe('%s', op => { 191 | forEach(values).it('should return a Shen boolean', x => { 192 | ok(isShenBool(f(op)(x))); 193 | }); 194 | }); 195 | }); 196 | 197 | describe('equality', () => { 198 | describe('=', () => { 199 | it('should compare booleans', async () => { 200 | equal(s`true`, await evalKl([s`=`, s`true`, [s`and`, s`true`, s`true`]])); 201 | equal(s`false`, await evalKl([s`=`, s`true`, [s`and`, s`true`, s`false`]])); 202 | equal(s`true`, await evalKl([s`=`, [s`number?`, 746], [s`number?`, 419]])); 203 | equal(s`true`, await evalKl([s`=`, 25, [s`+`, 11, 14]])); 204 | }); 205 | }); 206 | describe('equal', () => { 207 | it('should handle Infinity', () => { 208 | ok(equate(Infinity, Infinity)); 209 | ok(equate(-Infinity, -Infinity)); 210 | ok(!equate(-Infinity, Infinity)); 211 | }); 212 | it('should compare functions based on reference equality', async () => { 213 | const u = await evalKl([s`lambda`, s`X`, 0]); 214 | const v = await evalKl([s`lambda`, s`X`, 0]); 215 | ok(equate(u, u)); 216 | ok(!equate(u, v)); 217 | }); 218 | it('should be able to compare objects', () => { 219 | ok(equate({}, {})); 220 | ok(!equate({}, null)); 221 | ok(equate({ a: 45, b: s`sym` }, { a: 45, b: s`sym` })); 222 | ok(equate({ ['key']: [{ a: 'abc', b: false }] }, { ['key']: [{ a: 'abc', b: false }] })); 223 | ok(!equate({ ['key']: [{ a: 'abc', b: false }] }, { ['key']: [{ a: 'abc', b: null }] })); 224 | }); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /test/frontend/test.ast.js: -------------------------------------------------------------------------------- 1 | const { equal, ok } = require('assert'); 2 | const backend = require('../../lib/backend.js'); 3 | const kernel = require('../../lib/kernel.js'); 4 | const frontend = require('../../lib/frontend.node.js'); 5 | 6 | describe('hack', () => it('dummy test so mocha runs the others', () => ok(true))); 7 | 8 | (async () => { 9 | const { exec } = await frontend(await kernel(backend())); 10 | 11 | describe('ast', () => { 12 | describe('eval', () => { 13 | it('should eval at runtime', async () => { 14 | equal(3, await exec('(js.ast.eval (js.ast.binary "+" (js.ast.literal 1) (js.ast.literal 2)))')); 15 | }); 16 | }); 17 | }); 18 | })(); 19 | -------------------------------------------------------------------------------- /test/frontend/test.interop.js: -------------------------------------------------------------------------------- 1 | const { equal, ok } = require('assert'); 2 | const backend = require('../../lib/backend.js'); 3 | const kernel = require('../../lib/kernel.js'); 4 | const frontend = require('../../lib/frontend.node.js'); 5 | 6 | describe('hack', () => it('dummy test so mocha runs the others', () => ok(true))); 7 | 8 | (async () => { 9 | const $ = await frontend(await kernel(backend())); 10 | const { caller, equate, exec, isArray, toList } = $; 11 | 12 | describe('interop', () => { 13 | describe('js.new', () => { 14 | it('should be able to construct globally referrable constructors', async () => { 15 | ok(isArray(await exec('(js.new (js.Array) [5])'))); 16 | equal(123, await exec('(js.new (js.Number) ["123"])')); 17 | }); 18 | }); 19 | describe('js.obj', () => { 20 | it('should construct js object from series of key-value pairs', async () => { 21 | ok(equate({ a: 1, b: 2 }, await exec('(js.obj ["a" 1 "b" 2])'))); 22 | }); 23 | it('should work with ({ ... }) macro', async () => { 24 | ok(equate({ a: 1, b: 2 }, await exec('({ "a" 1 "b" 2 })'))); 25 | }); 26 | it('should build nested objects with ({ ... }) macro', async () => { 27 | equal(42, (await exec('({ "x" ({ "y" ({ "z" 42 }) }) })')).x.y.z); 28 | }); 29 | }); 30 | describe('exec', () => { 31 | it('should work', async () => { 32 | equal(5, await exec('(+ 3 2)')); 33 | ok(equate(toList([1, 2, 3]), await exec('[1 2 3]'))); 34 | }); 35 | }); 36 | describe('.', () => { 37 | it('should access property on object', async () => { 38 | equal(3, await caller('js.get')({ y: 3 }, 'y')); 39 | }); 40 | it('should access chain of properties on object', async () => { 41 | equal(3, await exec('(. (js.obj ["x" (js.obj ["y" (js.obj ["z" 3])])]) "x" "y" "z")')); 42 | }); 43 | }); 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /test/kernel/test.kernel.js: -------------------------------------------------------------------------------- 1 | const dump = process.argv.includes('dump'); 2 | 3 | const fs = require('fs'); 4 | const tempfile = require('tempfile'); 5 | const config = require('../../lib/config.node.js'); 6 | const backend = require('../../lib/backend.js'); 7 | const kernel = require('../../lib/kernel.js'); 8 | const { testsPath } = require('../../scripts/config.js'); 9 | const { formatDuration, formatGrid, measure } = require('../../scripts/utils.js'); 10 | 11 | const InStream = class { 12 | constructor(buf) { 13 | this.buf = buf; 14 | this.pos = 0; 15 | } 16 | read() { return this.pos >= this.buf.length ? -1 : this.buf[this.pos++]; } 17 | close() {} 18 | }; 19 | 20 | const OutStream = class { 21 | constructor() { this.buffer = []; } 22 | write(b) { 23 | this.buffer.push(b); 24 | return b; 25 | } 26 | fromCharCodes() { return String.fromCharCode(...this.buffer); } 27 | }; 28 | 29 | const formatResult = (failures, ignored) => 30 | failures > ignored ? `${failures - ignored} (${ignored} ignored)` : 31 | ignored > 0 ? `success (${ignored} failures ignored)` : 32 | 'success'; 33 | 34 | (async () => { 35 | const stoutput = new OutStream(); 36 | 37 | console.log(`- creating backend...`); 38 | const measureBackend = measure(() => backend({ 39 | ...config, 40 | InStream, 41 | OutStream, 42 | openRead: path => new InStream(fs.readFileSync(path)), 43 | stoutput 44 | })); 45 | const $ = measureBackend.result; 46 | console.log(` created in ${formatDuration(measureBackend.duration)}`); 47 | 48 | console.log(`- creating kernel...`); 49 | const measureCreate = await measure(() => kernel($)); 50 | const { evalKl, s, valueOf } = measureCreate.result; 51 | console.log(` created in ${formatDuration(measureCreate.duration)}`); 52 | 53 | console.log('- running test suite...'); 54 | const measureRun = await measure(async () => { 55 | await evalKl([s`cd`, testsPath]); 56 | await evalKl([s`load`, 'README.shen']); 57 | await evalKl([s`load`, 'tests.shen']); 58 | }); 59 | const outputLog = stoutput.fromCharCodes(); 60 | const failures = valueOf('test-harness.*failed*'); 61 | const ignored = 0; 62 | console.log(` ran in ${formatDuration(measureRun.duration)}, ${formatResult(failures, ignored)}`); 63 | 64 | if (failures > ignored) { 65 | if (dump) { 66 | console.log(); 67 | console.log(outputLog); 68 | } else { 69 | const outputPath = tempfile('.log'); 70 | fs.writeFileSync(outputPath, outputLog); 71 | console.log(` output log written to ${outputPath}`); 72 | } 73 | } 74 | 75 | console.log(); 76 | console.log(formatGrid(['Test Suite', formatResult(failures, ignored), formatDuration(measureRun.duration)])); 77 | 78 | if (failures > ignored) { 79 | process.exit(1); 80 | } 81 | })(); 82 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = env => ({ 5 | mode: env.mode, 6 | entry: env.mode === 'development' ? './index.development.js' : './index.js', 7 | optimization: { 8 | minimize: !!env.min, 9 | minimizer: [ 10 | new TerserPlugin({ 11 | terserOptions: { 12 | keep_classnames: true, 13 | keep_fnames: true 14 | } 15 | }) 16 | ] 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: x => !x.includes('kernel.'), 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | plugins: ['@babel/plugin-proposal-object-rest-spread'] 27 | } 28 | } 29 | } 30 | ] 31 | }, 32 | output: { 33 | path: path.resolve(__dirname, 'dist/'), 34 | filename: `shen-script${env.mode === 'development' ? '.dev' : env.min ? '.min' : ''}.js` 35 | }, 36 | stats: { 37 | warningsFilter: w => w.includes('the request of a dependency is an expression') 38 | || w.includes('exceed') 39 | } 40 | }); 41 | --------------------------------------------------------------------------------