├── .gitignore ├── .vscode ├── settings.json └── launch.json ├── test ├── modules │ ├── included.jaspr │ ├── exports-array.jaspr │ ├── has-includes.jaspr │ ├── recursive-include.jaspr │ ├── renamed-exports.jaspr │ ├── macros.jaspr │ ├── has-recursive-include.jaspr │ ├── hello-world.jaspr │ ├── simple-literate.jaspr.md │ ├── recursive-fn.jaspr │ └── literate-tests.jaspr.md ├── StandardLibraryTest.ts ├── Helpers.ts ├── ModuleTest.ts └── ParserTest.ts ├── examples └── factorial.jaspr ├── tsconfig.json ├── notes ├── modules.md └── design-rationale.md ├── LICENSE ├── package.json ├── src ├── ReservedNames.ts ├── Chan.ts ├── NativeFn.ts ├── Repl.ts ├── LiterateParser.ts ├── Fiber.ts ├── index.ts ├── PrettyPrint.ts ├── Jaspr.ts ├── JasprPrimitive.ts └── Module.ts ├── jaspr ├── objects.jaspr.md ├── jaspr.jaspr.md ├── macros.jaspr.md ├── signals-errors.jaspr.md ├── pattern-matching.jaspr.md ├── numbers.jaspr.md ├── concurrency.jaspr.md └── streams.jaspr.md ├── schema.jaspr └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": "en" 3 | } -------------------------------------------------------------------------------- /test/modules/included.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | 3 | included: 'included-value 4 | $export: {included} 5 | -------------------------------------------------------------------------------- /test/modules/exports-array.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.exports-array 3 | $version: "1.0" 4 | 5 | a: 1 6 | b: 2 7 | c: 3 8 | 9 | $export: [a, b] -------------------------------------------------------------------------------- /examples/factorial.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: example.factorial 3 | $version: “1.0” 4 | 5 | factorial: (λ if (≤ _ 1) 1 (× _ (factorial (dec _)))) 6 | $main: (inspect! (factorial 10)) 7 | -------------------------------------------------------------------------------- /test/modules/has-includes.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.has-includes 3 | $version: "1.0" 4 | $include: ["included.jaspr"] 5 | 6 | original: 'original-value 7 | $export: {original} 8 | -------------------------------------------------------------------------------- /test/modules/recursive-include.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $include: ["included.jaspr", "has-recursive-include.jaspr"] 3 | 4 | recursive-included: 'recursive-included-value 5 | $export: {recursive-included} 6 | -------------------------------------------------------------------------------- /test/modules/renamed-exports.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.renamed-exports 3 | $version: "1.0" 4 | 5 | a: 1 6 | b: 2 7 | c: 3 8 | 9 | $export: {one: a, two: b, uno: a, dos: b} 10 | -------------------------------------------------------------------------------- /test/modules/macros.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.macros 3 | $version: "1.0" 4 | 5 | macro.quote: ($closure {} ([] "" (0 $args)) {}) 6 | quoted: (quote quoted-value) 7 | 8 | $export: {quote, quoted} 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "outDir": "dist/", 5 | "removeComments": true, 6 | "strictNullChecks": true, 7 | "noImplicitAny": true, 8 | "sourceMap": true, 9 | "module": "commonjs" 10 | } 11 | } -------------------------------------------------------------------------------- /test/modules/has-recursive-include.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.has-recursive-include 3 | $version: "1.0" 4 | $include: ["included.jaspr", "recursive-include.jaspr"] 5 | 6 | original: 'original-value 7 | $export: {original} 8 | -------------------------------------------------------------------------------- /test/modules/hello-world.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.hello-world 3 | $version: "1.0" 4 | 5 | $import: {jaspr.primitive} 6 | 7 | hello-world: '"Hello, world!" 8 | $main: (jaspr.primitive.inspect! hello-world) 9 | 10 | $export: {hello-world} 11 | -------------------------------------------------------------------------------- /notes/modules.md: -------------------------------------------------------------------------------- 1 | 2 | - `jaspr` 3 | - `jaspr.unicode` 4 | - `jaspr.regex` 5 | - `jaspr.random` 6 | - `jaspr.time` 7 | - `jaspr.tree-map` 8 | - `jaspr.actor` 9 | - `jaspr.net` 10 | - `jaspr.net.http` 11 | - `jaspr.os` 12 | - `jaspr.os.file` 13 | - `jaspr.os.path` 14 | - `jaspr.os.console` 15 | - `jaspr.os.process` 16 | - `jaspr.dom` -------------------------------------------------------------------------------- /test/modules/simple-literate.jaspr.md: -------------------------------------------------------------------------------- 1 | # Simple Literate Program 2 | 3 | This is a literate program. 4 | 5 | $schema: "http://adam.nels.onl/schema/jaspr/module" 6 | $module: jaspr-tests.simple-literate 7 | $version: "1.0" 8 | $export: {indented, fenced} 9 | 10 | All indented code blocks are Jaspr code. 11 | 12 | indented: 'indented-value 13 | 14 | Fenced code blocks are also Jaspr code. 15 | 16 | ```jaspr 17 | fenced: 'fenced-value 18 | ``` 19 | -------------------------------------------------------------------------------- /test/modules/recursive-fn.jaspr: -------------------------------------------------------------------------------- 1 | $schema: "http://adam.nels.onl/schema/jaspr/module" 2 | $module: jaspr-tests.recursive-fn 3 | $version: "1.0" 4 | 5 | $import: {jaspr.primitive} 6 | 7 | factorial: 8 | (jaspr.primitive.closure 9 | {} 10 | (jaspr.primitive.if 11 | (jaspr.primitive.< (0 $args) 1) 12 | 1 13 | (jaspr.primitive.multiply 14 | (0 $args) 15 | (factorial (jaspr.primitive.subtract (0 $args) 1)))) 16 | {}) 17 | 18 | five-factorial: (factorial 5) 19 | 20 | $export: {factorial, five-factorial} 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Adam R. Nelson 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch Program", 12 | "program": "${workspaceRoot}\\index.js" 13 | }, 14 | { 15 | // Name of configuration; appears in the launch configuration drop down menu. 16 | "name": "Test", 17 | "type": "node", 18 | "request": "launch", 19 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 20 | "args": ["--no-timeouts", "${workspaceRoot}/dist/test/*Test.js"], 21 | "stopOnEntry": false 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /test/modules/literate-tests.jaspr.md: -------------------------------------------------------------------------------- 1 | # Literate Program with Tests 2 | 3 | $schema: "http://adam.nels.onl/schema/jaspr/module" 4 | $module: jaspr-tests.literate-tests 5 | $version: "1.0" 6 | $import: {jaspr.primitive} 7 | 8 | Literate Jaspr programs can contain unit tests. Unit tests are indented code blocks inside of blockquotes. Each expression becomes a unit test; a test passes if it returns a truthy value. 9 | 10 | > true ; trivial test, always passes 11 | > 12 | > (jaspr.primitive.is? (jaspr.primitive.add 2 2) 4) ; basic arithmetic 13 | 14 | ## Heading 1 15 | 16 | The sequence `;=` gets transformed into `jaspr.primitive.assertDeepEquals`. 17 | 18 | > (jaspr.primitive.add 2 2) ;= 4 19 | 20 | ## Heading 2 21 | 22 | The right side of `;=` is not evaluated. 23 | 24 | > ([] 'a 'b 'c) ;= ["a", "b", "c"] 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jaspr", 3 | "version": "0.1.171228", 4 | "description": "JaSPR (JSon PRocessing) is a JSON-based Lisp-like language with async-by-default semantics", 5 | "main": "dist/src/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "pretest": "tsc", 9 | "test": "mocha dist/test/*Test.js", 10 | "repl": "node ./dist/src/index.js", 11 | "prerepl": "tsc" 12 | }, 13 | "bin": {"jaspr": "dist/src/index.js"}, 14 | "author": "Adam R. Nelson ", 15 | "license": "ISC", 16 | "dependencies": { 17 | "chalk": "^2.3.0", 18 | "command-line-args": "^4.0.7", 19 | "command-line-usage": "^4.0.2", 20 | "lodash": "^4.17.4", 21 | "printable-characters": "^1.0.38", 22 | "string-length": "^2.0.0", 23 | "xregexp": "^3.2.0" 24 | }, 25 | "devDependencies": { 26 | "@types/chai": "^4.0.4", 27 | "@types/command-line-args": "^4.0.2", 28 | "@types/lodash": "^4.14.74", 29 | "@types/mocha": "^2.2.43", 30 | "@types/node": "^8.0.30", 31 | "@types/xregexp": "^3.0.29", 32 | "chai": "^4.1.2", 33 | "mocha": "^3.5.3", 34 | "source-map-support": "^0.4.18", 35 | "typescript": "^2.5.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ReservedNames.ts: -------------------------------------------------------------------------------- 1 | /** Prefix for all reserved names */ 2 | export const prefix = '$' 3 | 4 | export const primitiveModule = 'jaspr.primitive' 5 | export const stdlibModule = 'jaspr' 6 | export const version = '0.1.171228' 7 | 8 | export const closure = prefix + 'closure' 9 | export const code = prefix + 'code' 10 | export const args = prefix + 'args' 11 | export const chan = prefix + 'chan' 12 | export const dynamic = prefix + 'dynamic' 13 | export const default_ = prefix + 'default' 14 | export const syntaxQuote = prefix + 'syntaxQuote' 15 | export const unquote = prefix + 'unquote' 16 | export const unquoteSplicing = prefix + 'unquoteSplicing' 17 | 18 | export const if_ = prefix + 'if' 19 | export const then = prefix + 'then' 20 | export const junction = prefix + 'junction' 21 | export const eval_ = prefix + 'eval' 22 | export const macroexpand = prefix + 'macroexpand' 23 | export const contextGet = prefix + 'contextGet' 24 | export const dynamicLet = prefix + 'dynamicLet' 25 | export const dynamicGet = prefix + 'dynamicGet' 26 | export const apply = prefix + 'apply' 27 | export const arrayMake = prefix + 'arrayMake' 28 | export const objectMake = prefix + 'objectMake' 29 | export const jsSync = prefix + 'jsSync' 30 | export const jsAsync = prefix + 'jsAsync' 31 | 32 | /** Named here because it's the one built-in function (not special form) that 33 | * is called directly by Jaspr's macroexpand/eval process: `$syntaxQuote` 34 | * can expand to `arrayConcat` when `$unquoteSplicing` is used. */ 35 | export const arrayConcat = 'arrayConcat' 36 | export const assertEquals = 'assertEquals' 37 | 38 | export const arrayConcatQualified = `${primitiveModule}.${arrayConcat}@${version}` 39 | export const assertEqualsQualified = `${primitiveModule}.${assertEquals}@${version}` 40 | -------------------------------------------------------------------------------- /jaspr/objects.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ Array Operations][prev] | [🗏 Table of Contents][toc] | [String Operations ❧][next] 2 | :---|:---:|---: 3 | 4 | # Object Operations 5 | 6 | ; TODO: Define object operations 7 | 8 | ## `hasKey?` 9 | 10 | ## `hasKeys?` 11 | 12 | hasKeys?: 13 | (fn* args 14 | (assertArgs args "expected at least one argument" 15 | (define {obj: (last args)} 16 | (all? (\ hasKey? _ obj) (init args))))) 17 | 18 | ## `withKey` 19 | 20 | ## `withoutKey` 21 | 22 | ## `keys` 23 | 24 | ## `values` 25 | 26 | ## `entries` 27 | 28 | entries: (fn- obj (map (\ [] _ (_ obj)) (keys obj))) 29 | 30 | ## `fromEntries` 31 | 32 | fromEntries: 33 | (fn- xs (reduce (fn- accum kv (withKey (0 kv) (1 kv) accum)) {} xs)) 34 | 35 | ## `size` 36 | 37 | size: (fn- obj (len (kays obj))) 38 | 39 | ## `merge` 40 | 41 | merge: 42 | (fn* args 43 | (if args 44 | (assertArgs (object? (hd args)) "not an object" 45 | (reduce (fn- accum kv (withKey (0 kv) (1 kv) accum)) 46 | (hd args) 47 | (mapcat entries (tl args)))) 48 | {})) 49 | 50 | ## `mergeWith` 51 | 52 | ## `pick` 53 | 54 | ## `omit` 55 | 56 | ## `mapKeys` 57 | 58 | ## `mapValues` 59 | 60 | ## `mapEntries` 61 | 62 | ## `mapMerge` 63 | 64 | ## `filterKeys` 65 | 66 | ## `filterValues` 67 | 68 | ## `filterEntries` 69 | 70 | ## `subobject?` 71 | 72 | ## `superobject?` 73 | 74 | ## Exports 75 | 76 | $export: { 77 | hasKeys? entries fromEntries size merge mergeWith pick omit 78 | mapKeys mapValues mapEntries mapMerge filterKeys filterValues 79 | filterEntries subobject? superobject? 80 | ⪽:subobject? ⪾:superobject? 81 | } 82 | 83 | [☙ Array Operations][prev] | [🗏 Table of Contents][toc] | [String Operations ❧][next] 84 | :---|:---:|---: 85 | 86 | [toc]: jaspr.jaspr.md 87 | [prev]: arrays.jaspr.md 88 | [next]: strings.jaspr.md 89 | -------------------------------------------------------------------------------- /src/Chan.ts: -------------------------------------------------------------------------------- 1 | import {Jaspr, JasprObject, JasprError, magicSymbol, isObject} from './Jaspr' 2 | import * as Names from './ReservedNames' 3 | import {remove} from 'lodash' 4 | 5 | class Chan { 6 | sendQueue: [Jaspr, (sent: boolean) => void][] = [] 7 | recvQueue: Array<(val: {value: Jaspr, done: boolean}) => void> = [] 8 | magicObject: JasprObject 9 | closed = false 10 | 11 | constructor(magicObject: JasprObject) { 12 | this.magicObject = magicObject 13 | } 14 | 15 | send(msg: Jaspr, cb: (sent: boolean) => void): (() => void) | null { 16 | if (this.closed) { cb(false); return null } 17 | if (this.recvQueue.length > 0) { 18 | (this.recvQueue.shift())({value: msg, done: false}) 19 | cb(true) 20 | return null 21 | } else { 22 | const entry: [Jaspr, (sent: boolean) => void] = [msg, cb] 23 | this.sendQueue.push(entry) 24 | return () => {remove(this.sendQueue, x => x === entry)} 25 | } 26 | } 27 | 28 | recv(cb: (val: {value: Jaspr, done: boolean}) => void): (() => void) | null { 29 | if (this.closed) { 30 | cb({value: null, done: true}) 31 | return null 32 | } 33 | if (this.sendQueue.length > 0) { 34 | const [msg, sendCb] = this.sendQueue.shift() 35 | cb({value: msg, done: false}) 36 | sendCb(true) 37 | return null 38 | } else { 39 | this.recvQueue.push(cb) 40 | return () => {remove(this.recvQueue, x => x === cb)} 41 | } 42 | } 43 | 44 | close(): boolean { 45 | if (this.closed) return false 46 | this.closed = true 47 | for (let [_, cb] of this.sendQueue) cb(false) 48 | for (let cb of this.recvQueue) cb({value: null, done: true}) 49 | this.sendQueue = [] 50 | this.recvQueue = [] 51 | return true 52 | } 53 | } 54 | 55 | namespace Chan { 56 | export function make(): JasprObject { 57 | const obj: JasprObject = {[Names.chan]: true} 58 | obj[magicSymbol] = new Chan(obj) 59 | return obj 60 | } 61 | 62 | export function isChan(it: Jaspr) { 63 | return isObject(it) && magicSymbol in it && it[magicSymbol] instanceof Chan 64 | } 65 | } 66 | 67 | export default Chan 68 | -------------------------------------------------------------------------------- /schema.jaspr: -------------------------------------------------------------------------------- 1 | // JSON schema for Jaspr modules 2 | // Author: Adam Nelson 3 | 4 | id: "http://adam.nels.onl/schema/jaspr/module" 5 | $schema: "http://json-schema.org/draft-06/schema#" 6 | title: "Jaspr Module" 7 | description: "A module in the Jaspr programming language" 8 | type: object 9 | properties: { 10 | $module: { 11 | description: "Module name" 12 | type: string 13 | } 14 | $main: { 15 | description: " 16 | If present, this module is an executable script. 17 | The value of this property is the script's main function." 18 | type: {$ref: "#/definitions/expr"} 19 | } 20 | $import: {$ref: "#/definitions/imports"} 21 | $imports: {$ref: "#/definitions/imports"} 22 | $export: {$ref: "#/definitions/names"} 23 | $exports: {$ref: "#/definitions/names"} 24 | $doc: { 25 | description: "Documentation for the module, in Markdown format" 26 | type: string 27 | } 28 | $author: { 29 | description: "Name (and, optionally, email address) of the module's author" 30 | type: string 31 | } 32 | patternProperties: { 33 | "^(macro[.])?[^.,:`~'\"()\\[\\]{}\\s]+$": {$ref: "#/definitions/expr"} 34 | "^doc[.][^.,:`~'\"()\\[\\]{}\\s]+$": {type: string} 35 | } 36 | additionalProperties: false 37 | } 38 | not: [ 39 | {required: [$import, $imports]} 40 | {required: [$export, $exports]} 41 | ] 42 | anyOf: [ 43 | {required: [$module, $export]} 44 | {required: [$module, $exports]} 45 | {required: [$script]} 46 | ] 47 | 48 | definitions: { 49 | expr: { 50 | description: "A Jaspr expression" 51 | } 52 | name: { 53 | description: "A valid Jaspr identifier" 54 | type: string 55 | pattern: "^[^.,:`~'\"()\\[\\]{}\\s]+$" 56 | } 57 | names: { 58 | description: "A list of names imported/exported from a module" 59 | anyOf: [{ 60 | type: array 61 | items: {$ref: "#/definitions/name"} 62 | }, { 63 | type: object 64 | patternProperties: { 65 | "^[^.,:`~'\"()\\[\\]{}\\s]+$": {$ref: "#/definitions/name"} 66 | } 67 | additionalProperties: false 68 | }] 69 | } 70 | imports: { 71 | description: "A list or object specifying the modules that this module depends on" 72 | anyOf: [{ 73 | type: array 74 | items: {type: string} 75 | }, { 76 | type: object 77 | additionalProperties: {anyOf: [ 78 | {type: boolean} 79 | {$ref: "#/definitions/name"} 80 | {$ref: "#/definitions/names"} 81 | ]} 82 | }] 83 | } 84 | } -------------------------------------------------------------------------------- /jaspr/jaspr.jaspr.md: -------------------------------------------------------------------------------- 1 | 🗏 Table of Contents | [Syntax and Semantics ❧][next] 2 | :---:|---: 3 | 4 | # Jaspr 5 | 6 | Jaspr is a highly concurrent, dynamically-typed functional programming language based on Lisp and JSON. It is especially well suited to processing JSON data and creating/consuming JSON APIs. 7 | 8 | This document is both the documentation for the Jaspr programming language and a literate Jaspr program that defines the `jaspr` standard library module. 9 | 10 | $schema: “http://adam.nels.onl/schema/jaspr/module” 11 | $module: jaspr 12 | $version: 0.1.171228 13 | $author: “Adam R. Nelson ” 14 | 15 | ## Literate Code Blocks 16 | 17 | Because the files in this directory are literate programs, code blocks in Markdown have special semantics. 18 | 19 | Top-level code blocks are actual Jaspr code that is part of the standard library module. 20 | 21 | ; This is actual Jaspr code! 22 | 23 | Code blocks inside of blockquotes are unit tests. If the code contains a `;=` comment, it is an assertion that the code before the `;=` evaluates to the JSON data after it. 24 | 25 | > ; This is Jaspr unit test code! 26 | 27 | ## Table of Contents 28 | 29 | 1. [Syntax and Semantics](syntax.jaspr.md) 30 | 2. [Data Types](data-types.jaspr.md) 31 | 3. [Concurrency and Channels](concurrency.jaspr.md) 32 | 4. [Macros](macros.jaspr.md) 33 | 5. [Number Operations](numbers.jaspr.md) 34 | 6. [Array Operations](arrays.jaspr.md) 35 | 7. [Object Operations](objects.jaspr.md) 36 | 8. [String Operations](strings.jaspr.md) 37 | 9. [Pattern Matching](pattern-matching.jaspr.md) 38 | 10. [Signals and Error Handling](signals-errors.jaspr.md) 39 | 11. [Streams and Pipelines](streams.jaspr.md) 40 | 12. [Comparisons and Sorting](sorting.jaspr.md) 41 | 13. [Basic I/O](io.jaspr.md) 42 | 14. [Modules](modules.jaspr.md) 43 | 44 | This index file does not contain any Jaspr code; the standard library is defined in the rest of the files in this directory. 45 | 46 | $include: [ 47 | syntax.jaspr.md, 48 | data-types.jaspr.md, 49 | concurrency.jaspr.md, 50 | macros.jaspr.md, 51 | numbers.jaspr.md, 52 | arrays.jaspr.md, 53 | objects.jaspr.md, 54 | strings.jaspr.md, 55 | pattern-matching.jaspr.md, 56 | signals-errors.jaspr.md, 57 | streams.jaspr.md, 58 | //sorting.jaspr.md, 59 | //io.jaspr.md, 60 | //modules.jaspr.md 61 | ] 62 | 63 | --- 64 | 65 | 🗏 Table of Contents | [Syntax and Semantics ❧][next] 66 | :---:|---: 67 | 68 | [next]: syntax.jaspr.md 69 | -------------------------------------------------------------------------------- /src/NativeFn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Jaspr, JasprArray, JasprObject, JasprError, Callback, ErrCallback, Deferred, magicSymbol 3 | } from './Jaspr' 4 | import {Env} from './Interpreter' 5 | import * as Names from './ReservedNames' 6 | 7 | export type SyncFn = (this: Env, ...args: Jaspr[]) => Jaspr 8 | export type AsyncFn = (this: Env, args: Jaspr[], cb: ErrCallback) => void 9 | 10 | export abstract class NativeFn { 11 | readonly source: string[] 12 | constructor(source: string[]) {this.source = source} 13 | abstract call(env: Env, args: Jaspr[], cb: ErrCallback): void 14 | abstract toClosure(env: Env): JasprObject 15 | arity() {return this.source.length - 1} 16 | toString() { return `Native function (${this.source})` } 17 | } 18 | 19 | export class NativeSyncFn extends NativeFn { 20 | readonly fn: SyncFn 21 | 22 | constructor(fn: SyncFn | string, ...rest: string[]) { 23 | if (typeof fn === 'string') { 24 | super([fn, ...rest]) 25 | this.fn = new Function(fn, ...rest) 26 | } else { 27 | super([...new Array(fn.length).fill(''), fn.toString()]) 28 | this.fn = fn 29 | } 30 | } 31 | 32 | call(env: Env, args: Jaspr[], cb: ErrCallback) { 33 | let result: Jaspr 34 | try {result = this.fn.apply(env, args)} 35 | catch (err) { 36 | if (err instanceof Error) { 37 | return cb({err: 'NativeError', why: err.toString()}, null) 38 | } else return cb(err, null) 39 | } 40 | cb(null, result) 41 | } 42 | 43 | toClosure(env: Env): JasprObject { 44 | return { 45 | [env.closureName]: {}, 46 | [Names.code]: [[Names.jsSync, ...this.source], 47 | ...new Array(this.arity()).map((v, i) => [i, Names.args])], 48 | [magicSymbol]: this 49 | } 50 | } 51 | } 52 | 53 | export class NativeAsyncFn extends NativeFn { 54 | readonly fn: AsyncFn 55 | 56 | constructor(fn: AsyncFn | string, ...rest: string[]) { 57 | if (typeof fn === 'string') { 58 | super([fn, ...rest]) 59 | this.fn = new Function(fn, ...rest) 60 | } else { 61 | super([...new Array(fn.length).fill(''), fn.toString()]) 62 | this.fn = fn 63 | } 64 | } 65 | 66 | call(env: Env, args: Jaspr[], cb: ErrCallback) { 67 | this.fn.call(env, args, cb) 68 | } 69 | 70 | toClosure(env: Env): JasprObject { 71 | return { 72 | [env.closureName]: {}, 73 | [Names.code]: [[Names.jsAsync, ...this.source], 74 | ...new Array(this.arity()).map((v, i) => [i, Names.args])], 75 | [magicSymbol]: this 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/StandardLibraryTest.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Jaspr, JasprError, resolveFully, toString, toBool, isObject, magicSymbol 3 | } from '../src/Jaspr' 4 | import {Root, Branch} from '../src/Fiber' 5 | import {expandAndEval, waitFor} from '../src/Interpreter' 6 | import {readModuleFile, evalModule, ModuleSource, Module} from '../src/Module' 7 | import prim from '../src/JasprPrimitive' 8 | import * as Names from '../src/ReservedNames' 9 | import prettyPrint from '../src/PrettyPrint' 10 | import {NativeSyncFn} from '../src/NativeFn' 11 | import {expect, AssertionError} from 'chai' 12 | import * as path from 'path' 13 | 14 | const filename = path.resolve(__dirname, '..', '..', 'jaspr', 'jaspr.jaspr.md') 15 | let stdlib: Promise | null = null 16 | let root: Root | null 17 | 18 | describe('the standard library', () => { 19 | it('loads (takes ~10 seconds due to resolveFully)', function() { 20 | this.timeout(15000) 21 | return stdlib || Promise.reject('stdlib is null') 22 | }) 23 | 24 | before(() => { 25 | stdlib = new Promise((resolve, reject) => { 26 | let errored = false 27 | function fail(msg: string, err: Jaspr, raisedBy?: Branch): void { 28 | reject(new AssertionError( 29 | `\n${msg}: ${prettyPrint(err, false)}` /*+ 30 | (raisedBy ? `\n\nStack trace:\n${raisedBy.stackTraceString(false)}` : '')*/)) 31 | } 32 | const env = root = new Root((root, err, raisedBy, cb) => { 33 | if (errored) return cb(null) 34 | errored = true 35 | resolveFully(err, (resErr, err) => { 36 | if (resErr) return fail('error resolving error', resErr,) 37 | if (err) return fail('error evaluating module', err, raisedBy) 38 | errored = false 39 | cb(null) 40 | }) 41 | }) 42 | readModuleFile(filename, (err, modsrc) => { 43 | if (err) return fail('error loading module', err) 44 | evalModule(env, modsrc, { 45 | filename, localModules: new Map([ 46 | [Names.primitiveModule, Promise.resolve(prim(env))] 47 | ]) 48 | }).then( 49 | mod => resolveFully(mod, (err, mod) => resolve(mod)), 50 | err => fail('error loading module', err)) 51 | }) 52 | }) 53 | 54 | stdlib.then(mod => describe('standard library test', () => { 55 | for (let test of Object.keys(mod.test).sort()) { 56 | it(test, () => new Promise((resolve, reject) => { 57 | const env = root 58 | if (!env) return reject('env is null') 59 | const {fiber, cancel} = env.deferCancelable( 60 | (env, cb) => waitFor(expandAndEval(env, mod, [], { 61 | key: env.signalHandlerVar, 62 | value: new NativeSyncFn(function errorHandler(err) { 63 | if (isObject(err) && err[magicSymbol] instanceof Error) { 64 | reject(err[magicSymbol]) 65 | } else reject(new AssertionError(prettyPrint(err, false))) 66 | cancel() 67 | return null 68 | }).toClosure(env) 69 | }, mod.test[test]), cb)) 70 | fiber.await(v => { 71 | try { expect(toBool(v)).to.be.true } 72 | catch (ex) { return reject(ex) } 73 | resolve(v) 74 | }) 75 | })) 76 | } 77 | })) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /src/Repl.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import * as ReadLine from 'readline' 3 | import {Jaspr, Callback} from './Jaspr' 4 | import Parser from './Parser' 5 | const {blank} = require('printable-characters') 6 | 7 | let 8 | promptStacks: { 9 | prompt: string, message?: string, cb: Callback, onBlank?: () => void 10 | }[][] = [], 11 | currentCallback: any = null, currentOnBlank: any = null, currentPrompt = '>', 12 | currentPriority = 0, parser: Parser | null = null, 13 | readline: ReadLine.ReadLine | null = null 14 | 15 | function nextPrompt( 16 | {prompt, message, onBlank}: 17 | {prompt: string, message?: string, onBlank?: () => void}, 18 | priority: number, 19 | cb: Callback 20 | ) { 21 | parser = new Parser('REPL Input') 22 | currentPriority = priority 23 | currentCallback = cb 24 | currentPrompt = prompt 25 | currentOnBlank = onBlank || (() => { 26 | parser = new Parser('REPL Input') 27 | rl.setPrompt(prompt + ' ') 28 | rl.prompt() 29 | }) 30 | const rl = readline || (readline = makeReadline()) 31 | if (message) console.log(message) 32 | rl.setPrompt(prompt + ' ') 33 | rl.prompt() 34 | } 35 | 36 | function makeReadline(): ReadLine.ReadLine { 37 | const rl = ReadLine.createInterface({ 38 | input: process.stdin, 39 | output: process.stdout 40 | }) 41 | rl.on('line', input => { 42 | if (!currentCallback) return 43 | if (input.trim() === '') { 44 | currentOnBlank() 45 | for (let i = 0; i < promptStacks.length; i++) { 46 | if (!promptStacks[i]) continue 47 | const next = promptStacks[i].pop() 48 | if (next) return nextPrompt(next, i, next.cb) 49 | } 50 | currentCallback = null 51 | return 52 | } 53 | const p = parser || (parser = new Parser('REPL Input')) 54 | let result: Jaspr | undefined = undefined 55 | try { 56 | p.read(input) 57 | if (p.isDone()) { 58 | result = p.getOneResult() 59 | } else { 60 | rl.setPrompt(chalk.gray('…') + blank(currentPrompt)) 61 | rl.prompt() 62 | } 63 | } catch (ex) { 64 | console.error(ex) 65 | parser = new Parser('REPL Input') 66 | rl.setPrompt(currentPrompt + ' ') 67 | rl.prompt() 68 | } 69 | if (result !== undefined) { 70 | currentCallback(result) 71 | for (let i = 0; i < promptStacks.length; i++) { 72 | if (!promptStacks[i]) continue 73 | const next = promptStacks[i].pop() 74 | if (next) return nextPrompt(next, i, next.cb) 75 | } 76 | currentCallback = null 77 | } 78 | }).on('close', () => process.exit(0)) 79 | return rl 80 | } 81 | 82 | /** 83 | * Displays a REPL prompt, waits for user input, parses user input, then passes 84 | * the parsed input to `cb`¹. The prompt is displayed again if parsing fails. 85 | * 86 | * If a REPL prompt is already displayed, and `options.priority` is greater² 87 | * than the current REPL prompt's priority, this prompt will be added to a 88 | * queue, and will not display until all other pending prompts have been 89 | * displayed. 90 | * 91 | * --- 92 | * 93 | * __¹__ Yes, I'm aware that this isn't exactly a REPL because it contains 94 | * neither the *evaluate* nor the *loop* parts of that acronym… `index.ts` 95 | * contains the rest of the REPL logic, since this prompt is used for error 96 | * recovery as well as a REPL. 97 | * 98 | * __²__ Priority is reversed, it goes from high to low… don't ask, it was 99 | * easier to implement this way. 😬 100 | * 101 | * @param options Options for the REPL prompt: 102 | * - `prompt`: Required. The text to display for the prompt. 103 | * - `priority`: Required. Lower numbers can replace existing prompts with 104 | * higher numbers. 105 | * - `message`: Optional. A message to display before the first prompt. 106 | * - `onBlank`: Optional. Callback that is called if the user presses ENTER 107 | * without typing anything. 108 | * @param cb Callback that is called with the Jaspr value parsed from user input 109 | */ 110 | export default function repl( 111 | options: { 112 | prompt: string, 113 | priority: number, 114 | message?: string, 115 | onBlank?: () => void 116 | }, 117 | cb: Callback 118 | ) { 119 | if (currentCallback && options.priority >= currentPriority) { 120 | (promptStacks[options.priority] || (promptStacks[options.priority] = [])) 121 | .push(Object.assign(options, {cb})) 122 | } else { 123 | if (currentCallback) { 124 | (promptStacks[currentPriority] || (promptStacks[currentPriority] = [])) 125 | .push({prompt: currentPrompt, onBlank: currentOnBlank, cb: currentCallback}) 126 | } 127 | nextPrompt(options, options.priority, cb) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/Helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Jaspr, JasprArray, JasprObject, Callback, resolveFully, toString, 3 | Deferred, JsonObject, Err, ErrCallback 4 | } from '../src/Jaspr' 5 | import { 6 | Env, Action, Scope, emptyScope, raise, evalExpr, macroExpand, evalDefs, 7 | waitFor, DynamicMap 8 | } from '../src/Interpreter' 9 | import {Root} from '../src/Fiber' 10 | import newPrimitiveModule from '../src/JasprPrimitive' 11 | import prettyPrint from '../src/PrettyPrint' 12 | import {NativeSyncFn} from '../src/NativeFn' 13 | import {importModule} from '../src/Module' 14 | import * as _ from 'lodash' 15 | import * as assert from 'assert' 16 | import {expect} from 'chai' 17 | require('source-map-support').install({ 18 | handleUncaughtExceptions: false 19 | }) 20 | 21 | export interface Should { 22 | equal(value: Jaspr): CB 23 | pass(assertions: (result: T, done?: () => void) => void): CB 24 | } 25 | 26 | export class TestCase implements Should { 27 | promises: PromiseLike[] = [] 28 | readonly env: Env 29 | 30 | constructor(env: Env) { 31 | this.env = env 32 | } 33 | 34 | pushPromise(): () => {resolve: () => void, reject: (err: any) => void} { 35 | let resolve: any = null, reject: any = null 36 | this.promises.push(new Promise((resolve_, reject_) => { 37 | resolve = resolve_; reject = reject_ 38 | })) 39 | return () => { 40 | if (resolve == null || reject == null) { 41 | console.error( 42 | 'Promise resolve/reject not available. This should never happen!') 43 | process.exit(1) 44 | } 45 | return {resolve, reject} 46 | } 47 | } 48 | 49 | equal(value: Jaspr): Callback { 50 | const resolvers = this.pushPromise() 51 | return result => resolveFully(result, (err, result) => { 52 | const {resolve, reject} = resolvers() 53 | try { expect(result).to.deep.equal(value) } 54 | catch (err) { reject(err); return } 55 | resolve() 56 | }) 57 | } 58 | 59 | pass(assertions: (result: T, done: () => void) => void): Callback { 60 | const resolvers = this.pushPromise() 61 | return result => resolveFully(result, (err, result) => { 62 | const {resolve, reject} = resolvers() 63 | try { 64 | if (assertions.length <= 1) { 65 | assertions(result, null) 66 | resolve() 67 | } else assertions(result, resolve) 68 | } catch (err) { reject(err) } 69 | }) 70 | } 71 | 72 | raise(errType: Err, fn: (dynamics: DynamicMap, cb: Callback) => void): void { 73 | const resolvers = this.pushPromise() 74 | const handler = new NativeSyncFn(function(err) { 75 | const {resolve, reject} = resolvers() 76 | try { 77 | expect(err).to.be.an('object') 78 | expect(err).to.have.property('err').equal(errType, 'wrong error type') 79 | } catch (err) {reject(err)} 80 | resolve() 81 | return null 82 | }) 83 | const d = this.env.defer() 84 | fn({ 85 | key: this.env.signalHandlerVar, 86 | value: handler.toClosure(this.env) 87 | }, d.resolve.bind(d)) 88 | d.await(v => resolvers().reject(new assert.AssertionError({ 89 | message: 'no error was raised', 90 | actual: v, 91 | expected: {err: errType} 92 | }))) 93 | } 94 | 95 | get withoutError(): Should> { 96 | const should = this 97 | return { 98 | equal(value: Jaspr) { 99 | const resolvers = should.pushPromise(), cb = should.equal(value) 100 | return (err, result) => { 101 | const {resolve, reject} = resolvers() 102 | if (err) return reject(err) 103 | cb(result) 104 | resolve() 105 | } 106 | }, 107 | pass(assertions: (result: T, done?: () => void) => void) { 108 | const resolvers = should.pushPromise(), cb = should.pass(assertions) 109 | return (err, result) => { 110 | const {resolve, reject} = resolvers() 111 | if (err) return reject(err) 112 | cb(result) 113 | resolve() 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | export const withEnv = (body: (env: Env, should: TestCase) => void) => () => 121 | new Promise((resolve, reject) => { 122 | let errored = false 123 | const root = new Root((root, err, raisedBy, cb) => { 124 | if (errored) return cb(null) 125 | errored = true 126 | reject(new assert.AssertionError({ 127 | message: `\nUnhandled signal raised:\n\n${prettyPrint(err, false)}` 128 | })) 129 | root.cancel() 130 | }) 131 | if (body.length < 2) { 132 | body(root, null) 133 | resolve() 134 | } else { 135 | const testCase = new TestCase(root) 136 | body(root, testCase) 137 | expect(testCase.promises).to.not.be.empty 138 | Promise.all(testCase.promises).then(() => resolve(), reject) 139 | } 140 | }) 141 | -------------------------------------------------------------------------------- /src/LiterateParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Literate programming extension to the Jaspr parser (Parse.ts) 3 | * 4 | * Extracts Jaspr source code from .jaspr.md Markdown files. 5 | * 6 | * Code blocks are interpreted as code, quoted code blocks are interpreted as 7 | * unit tests. If a definition is contained in a block whose header is the name 8 | * of the definition, the text content of that block will become that 9 | * definintion's docstring. 10 | */ 11 | 12 | import {Json, JsonObject, isObject} from './Jaspr' 13 | import {isLegalName} from './Interpreter' 14 | import Parser from './Parser' 15 | import {reservedChar} from './Parser' 16 | import * as _ from 'lodash' 17 | 18 | const atxHeader = /^[ ]{0,3}(#{1,6})\s+([^#]*?)(\s+#*)?\s*$/ 19 | const setextHeader = /^[ ]{0,3}(-+|=+)\s*$/ 20 | const aboveSetextHeader = /^[ ]{0,3}([^\s>\-*].*)$/ 21 | const blockquote = /(^[ ]{0,3}>\s)(.*)$/ 22 | const emptyBlockquote = /^[ ]{0,3}>\s*$/ 23 | const codeFence = /^[ ]{0,3}(```+|~~~+)\s*(\w+)?[^`~]*$/ 24 | const indentedCode = /^( |[ ]{0,3}\t)(\s*\S+.*)$/ 25 | 26 | export const markdownExtensions = ['.md', '.mkd', '.markdown'] 27 | 28 | export function parseMarkdown(src: string, filename?: string): JsonObject { 29 | const srcParser = new Parser(filename) 30 | const tests: { [name: string]: Json } = {} 31 | const doc: { [name: string]: string } = {} 32 | const headers: string[] = [] 33 | let headerName: string | null = null, 34 | headerNameDepth: number | null = null, 35 | lastLine = "", 36 | currentDoc = "", 37 | closeFence: string | null = null, 38 | ignoreFence = false, 39 | testParser: Parser | null = null, 40 | inBlockquote = false 41 | 42 | function pushHeader(text: string, h: number) { 43 | while (headers.length > h) headers.pop() 44 | while (headers.length < h - 1) headers.push("") 45 | headers.push(text) 46 | if (headerName && headerNameDepth !== null && h <= headerNameDepth) { 47 | doc[headerName] = currentDoc 48 | currentDoc = "" 49 | headerName = null 50 | headerNameDepth = null 51 | } 52 | if (text.length > 2 && 53 | text.charAt(0) === '`' && 54 | text.charAt(text.length - 1) === '`' && 55 | isLegalName(text.substring(1, text.length - 1))) { 56 | headerName = text.substring(1, text.length - 1) 57 | headerNameDepth = h 58 | } 59 | } 60 | 61 | function pushTests() { 62 | if (testParser === null) return 63 | const currentTests = testParser.getManyResults() 64 | testParser = null 65 | if (currentTests.length === 0) return 66 | let prefix = '' 67 | for (let c of headerName || _.last(headers) || 'test') { 68 | if (c === ' ' || c === '.') prefix += '-' 69 | if (!reservedChar.test(c)) prefix += c 70 | } 71 | if (prefix === '') prefix = 'test' 72 | prefix += '-' 73 | for (let test of currentTests) { 74 | let n = 0 75 | while (tests.hasOwnProperty(prefix + n)) n++ 76 | tests[prefix + n] = test 77 | } 78 | } 79 | 80 | src.split(/\r?\n/).forEach((line, n) => { 81 | let match: RegExpExecArray | null 82 | if (match = blockquote.exec(line)) { 83 | inBlockquote = true 84 | if (!testParser) testParser = new Parser(filename, true) 85 | const [_, quoteIndent, rest] = match 86 | if (closeFence) { 87 | // TODO: Handle spaces in front of closing code fence 88 | if (rest.startsWith(closeFence)) closeFence = null 89 | else if (!ignoreFence) { 90 | testParser.read(rest + '\n', 91 | {filename, line: n + 1, column: quoteIndent.length}) 92 | } 93 | } else if (match = indentedCode.exec(rest)) { 94 | const [_, indent, code] = match 95 | testParser.read(code + '\n', 96 | {filename, line: n + 1, column: quoteIndent.length + indent.length}) 97 | } else if (match = codeFence.exec(rest)) { 98 | const [_, fence, lang] = match 99 | closeFence = fence 100 | ignoreFence = lang !== undefined && lang.length > 0 && lang !== 'jaspr' 101 | } 102 | } else if (inBlockquote && !emptyBlockquote.exec(line)) { 103 | inBlockquote = false 104 | pushTests() 105 | if (closeFence) throw new Parser.ParseError( 106 | `Blockquote containing code fence ${closeFence} closed without closing fence`, 107 | {filename, line: n + 1, column: 0}) 108 | } 109 | if (closeFence) { 110 | // TODO: Handle spaces in front of closing code fence 111 | if (line.startsWith(closeFence)) closeFence = null 112 | else if (!ignoreFence) { 113 | srcParser.read(line + '\n', {filename, line: n + 1, column: 0}) 114 | } 115 | } else if (match = indentedCode.exec(line)) { 116 | const [_, indent, code] = match 117 | srcParser.read(code + '\n', {filename, line: n + 1, column: indent.length}) 118 | } else if (match = codeFence.exec(line)) { 119 | const [_, fence, lang] = match 120 | closeFence = fence 121 | ignoreFence = lang !== undefined && lang.length > 0 && lang !== 'jaspr' 122 | } else if (match = atxHeader.exec(line)) { 123 | const [_, prefix, text, suffix] = match 124 | pushHeader(text, prefix.length - 1) 125 | } else if (match = setextHeader.exec(line)) { 126 | const headerMatch = aboveSetextHeader.exec(lastLine) 127 | if (headerMatch) { 128 | pushHeader(headerMatch[1], match[1].charAt(0) === '=' ? 0 : 1) 129 | } 130 | } 131 | if (headerName) currentDoc += line + '\n' 132 | lastLine = line 133 | }) 134 | pushTests() 135 | 136 | const result = srcParser.getOneResult() 137 | if (!isObject(result)) { 138 | throw new Parser.ParseError( 139 | "Literate Jaspr file must produce an object", srcParser) 140 | } 141 | return _.assign(result, _.mapKeys(doc, (v, k) => 'doc.' + k), 142 | _.mapKeys(tests, (v, k) => 'test.' + k)) 143 | } 144 | -------------------------------------------------------------------------------- /notes/design-rationale.md: -------------------------------------------------------------------------------- 1 | # Design Rationale 2 | 3 | I started out designing Jaspr simply because it interested me; I love functional programming, and the idea of a JSON-based Lisp seemed so obvious that I was amazed that no one had done it before. As Jaspr development continued, I decided to set out specific design goals for it, to help decide which features to keep over others. 4 | 5 | These goals are ordered from most important to least. 6 | 7 | 1. **Learnability** 8 | 9 | Jaspr should be extremely easy for experienced programmers to pick up and start using, with very few unique traits to learn. This is accomplished through minimalism, in a few ways: 10 | 11 | - Minimal data types: only the 6 JSON data types, with special cases for closures and channels. 12 | - Minimal syntax: many programmers already know both Lisp and JSON, and Jaspr's basic syntax is the union of these. 13 | - Minimal primitive operations: everything is immutable, the only special language features are macros and a unique approach to concurrency. 14 | - Minimal concurrency complexity: concurrency _just happens_, and users don't have to think about it; immutability makes this a non-issue. 15 | - Principle of least surprise: 16 | - All JSON is valid Jaspr 17 | - Only one kind of equality (😒 Scheme) 18 | - Only two kinds of sequence (arrays, channels) 19 | - `!` consistently denotes impure functions 20 | 21 | 2. **Compatibility** 22 | 23 | Jaspr is designed to interoperate with existing systems written in other languages. Its focus on JSON is central to this, but also important is that its choice of naming conventions, standard library features, and integrated formats (such as JSON Schema) make integration with other languages painless. 24 | 25 | This is one reason I'm considering changing Jaspr's naming convention to `camelCase`. The `kebab-case` names used by Lisps aren't syntactically supported by most languages, but idiomatic Jaspr objects should be easy to deserialize in other languages without having to think about compatibility. 26 | 27 | 3. **Correctness** 28 | 29 | Jaspr should make it easy to write correct code. This is a feature shared with many functional languages, and is a primary reason that Jaspr is functional, immutable, and has a limited set of data types. 30 | 31 | Most functional languages that emphasize correctness are statically-typed (ML, Haskell). Jaspr, like Scheme and Erlang, is functional but dynamically-typed. This is because static typing would impair Learnability (another complex detail to learn) and Compatibility (foreign APIs won't always fit into a rigid type system). 32 | 33 | However, most dynamically-typed languages, even functional ones, make unnecessary tradeoffs in correctness. Even without a type system, Jaspr's interpreter/compiler should warn when an undefined name is used, or when a function is passed the wrong number of arguments, or when a literal of the wrong type indicates that the arguments of a standard library function have been incorrectly transposed. This is accomplished through three features: 34 | 35 | - Simple pre-run checks for obvious problems, like undefined names 36 | - Check macros, which can warn if a function is given the wrong number of arguments or an argument is a literal of the wrong type 37 | - A Typescript-esque optional type system layer based on JSON Schema and check macros 38 | 39 | 4. **Portability** 40 | 41 | Multiple Jaspr implementations should be possible, with as much compatibility as possible. Implementations could be interpreted or compiled, and run on PC, mobile, browser, or even embedded in other applications. 42 | 43 | Lisps make this easy; most of the core language features can be hidden behind macros, and macros can selectively define functions based on features available only on certain platforms. 44 | 45 | Jaspr's concurrency should also be portable to different platforms, even browsers. Its underlying semantics are basically CPS, which runs equally well on threads or on a JavaScript event loop. 46 | 47 | 5. **Live Debugging** 48 | 49 | Jaspr's error handling and concurrency features make it possible to observe, interact with, and alter running programs. It should be possible to perform "surgery" on a running Jaspr VM, restarting a crashed fiber by inserting the value it was expecting, then patching the function that threw the exception. 50 | 51 | 6. **Potential Clarity** 52 | 53 | It was hard to find the right word for this one. "Expressiveness" isn't quite correct; languages like Scala and Haskell are extremely expressive, but not very readable. At the same time, Jaspr doesn't _enforce_ readability the way Java, Go, and Python do; as a Lisp, that would be almost impossible. 54 | 55 | Jaspr's goals in clarity and expressiveness are closest to Ruby: there's More Than One Way To Do It, lots of syntactic constructs that do similar things, and multiple aliases for the same functions to make sure that whatever you _think_ will work probably will. The end result is that, while it's certainly possible to write messy, unreadable Jaspr code, it's also possible to write extremely clear and elegant code, to an extent not possible in more rigid languages. 56 | 57 | This means providing built-in macros and even syntactic shortcuts for lots of conveniences: lambdas and pattern-matching functions leave out extra parentheses that would be required in other Lisps, raw strings and multi-line strings with smart quotes are supported, ES2015-style object punning is built-in. 58 | 59 | Jaspr borrows a few core macros from Arc, which has a similar approach: `if` works like `cond` without the extra parentheses, `\` is like Arc's square brackets, and `let*` and `case` don't need parentheses around each binding/case. It also borrows threading macros and syntax-quote from Clojure. 60 | 61 | 7. **Syntactic Beauty** 62 | 63 | This is something that I just can't help working on. There are very few languages that really, fully use Unicode; Perl 6 is the only one I can think of. Jaspr supports smart quotes and alternative paren/bracket/brace characters, has optional Unicode syntax-quote and comment characters, and has Unicode or even emoji aliases for many core library functions. 64 | -------------------------------------------------------------------------------- /jaspr/macros.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ Concurrency and Channels][prev] | [🗏 Table of Contents][toc] | [Number Operations ❧][next] 2 | :---|:---:|---: 3 | 4 | # Macros 5 | 6 | ## Macro Utilities 7 | 8 | A few functions are especially useful for generating Jaspr code in macros. 9 | 10 | ### `quote` 11 | 12 | Wraps its argument in the Jaspr quote macro, `""`. Typically used in macros, where including a literal quote in a syntax quote context would prevent an unquote from being evaluated. 13 | 14 | > (quote 42) ;= ["", 42] 15 | 16 | quote: (fn- x ([] "" x)) 17 | 18 | ### `gensym!` 19 | 20 | Returns a string that is guaranteed to be unique, distinct from every other string used anywhere in the program. How this is done is implementation-dependent, but the default approach is to generate a random [UUID][uuid]. 21 | 22 | gensym!: (fn- (p.gensym!)) 23 | 24 | `gensym!` is typically used in macros to generate names that are guaranteed not to collide with existing names. 25 | 26 | > (= (gensym!) (gensym!)) ;= false 27 | 28 | [uuid]: https://en.wikipedia.org/wiki/Universally_unique_identifier 29 | 30 | ## Lambda Macros 31 | 32 | These remove one level of parens. For example, `(\ foo _)` becomes `(fn _ (foo _))`. The named lambdas `\x`, `\y`, and `\z` allow nesting with different variable names. `\xy` is a two-argument lambda. 33 | 34 | macro.\: (fn* body `[fn- _ ~body]) 35 | macro.\x: (fn* body `[fn- x ~body]) 36 | macro.\y: (fn* body `[fn- y ~body]) 37 | macro.\z: (fn* body `[fn- z ~body]) 38 | macro.\xy: (fn* body `[fn- x y ~body]) 39 | 40 | ## Threading Macros 41 | 42 | ### `->` 43 | 44 | > (-> 4 (p.add 1) (p.multiply 3)) ;= 15 45 | > (-> 4 (p.subtract 1) (p.multiply 3)) ;= 9 46 | 47 | > (-> 1 [] []) ;= [[1]] 48 | 49 | macro.->: 50 | (fn* args 51 | (assertArgs args "expected one or more arguments" 52 | (if (= 1 (len args)) 53 | (0 args) 54 | (define {arg: (0 args) f: (1 args) rest: (tl (tl args))} 55 | `[-> ~(if (and f (array? f)) 56 | `[~(hd f) ~arg ~@(tl f)] 57 | `[~f ~arg]) 58 | ~@rest])))) 59 | 60 | ### `->>` 61 | 62 | > (->> 4 (p.add 1) (p.multiply 3)) ;= 15 63 | > (->> 4 (p.subtract 1) (p.multiply 3)) ;= -9 64 | 65 | > (->> 1 [] []) ;= [[1]] 66 | 67 | macro.->>: 68 | (fn* args 69 | (assertArgs args "expected one or more arguments" 70 | (if (= 1 (len args)) 71 | (0 args) 72 | (define {arg: (0 args) f: (1 args) rest: (tl (tl args))} 73 | `[->> ~(if (and f (array? f)) `[~@f ~arg] `[~f ~arg]) 74 | ~@rest])))) 75 | 76 | ### `\->` 77 | 78 | macro.\->: (fn* args `[\ -> _ ~@args]) 79 | 80 | ### `\->>` 81 | 82 | macro.\->>: (fn* args `[\ ->> _ ~@args]) 83 | 84 | ## Miscellaneous Macros 85 | 86 | ### `comment` 87 | 88 | The `comment` macro ignores its arguments and expands to `null`. 89 | 90 | > (comment This is a comment.) ;= null 91 | 92 | macro.comment: (closure {} null) 93 | 94 | ### `loopAs` 95 | 96 | `loopAs` is the idiomatic way to use an inline recursive function as a loop. It takes a function name and an object; the function name and the object's keys are included in the scope, the object is used as the initial argument. 97 | 98 | > (loopAs factorial {n: 5} 99 | > (if (<= n 1) 1 (mul n (factorial {n: (dec n)})))) ;= 120 100 | 101 | macro.loopAs: 102 | (fn- name args body 103 | (assertArgs (string? name) "name (1st arg) is not a literal string" 104 | (object? args) "start value (2nd arg) is not an object" 105 | `[define ~({} name `[closure {} 106 | (define ~(p.objectMake (fn- k `[~(quote k) (0 $args)]) (keys args)) 107 | ~body)]) 108 | (~name ~args)])) 109 | 110 | ### `doTimes` 111 | 112 | `(doTimes n body)` executes `body` `n` times. 113 | 114 | macro.doTimes: 115 | (fn- n body 116 | `[define {.n.: ~n} 117 | (if (and (integer? .n.) (>= .n. 0)) 118 | (loopAs next {.n.} (if .n. (do ~body (next {.n.: (dec .n.)})))) 119 | (raise { 120 | err: 'BadArgs, why: "not a nonnegative integer", fn: ~(myName), 121 | args: ~(quote ([] n body)) 122 | }))]) 123 | 124 | ### `unless` 125 | 126 | `(unless pred expr)` is equivalent to `(if pred null expr)`. 127 | 128 | > (unless false 42) ;= 42 129 | > (unless true 42) ;= null 130 | 131 | --- 132 | 133 | macro.unless: (fn- pred expr `[if ~pred null ~expr]) 134 | 135 | ### `any=?` 136 | 137 | `(any=? x y0 y1 ... yn)` is equivalent to `(or (= x y0) (= x y1) ... (= x yn))` (although it does not evaluate `x` more than once). 138 | 139 | > (any=? 20 0 10 20 30) ;= true 140 | > (any=? 30 0 10 20 30) ;= true 141 | > (any=? 40 0 10 20 30) ;= false 142 | 143 | --- 144 | 145 | macro.any=?: 146 | (fn* args 147 | (assertArgs args "expected at least one argument" 148 | (define {x: (gensym!)} 149 | `[define ~({} x (hd args)) 150 | ~(loopAs next {ys: (tl args)} 151 | (if (= 1 (len ys)) 152 | `[= ~x ~(hd ys)] 153 | `[or (= ~x ~(hd ys)) ~(next {ys: (tl ys)})]))]))) 154 | 155 | ### `case=` 156 | 157 | `(case= value case0 expr0 case1 expr1 ... casen exprn default)` tests each of `case0`...`casen` for equality to `value` using `=`, then evaluates and returns the corresponding `expr` to the first `case` that is equal to `value`. 158 | 159 | > (case= 1 0 'zero 1 'one 2 'two 'other) ;= "one" 160 | 161 | If no `case` is equal to `value`, `case=` evaluates and returns `default`. 162 | 163 | > (case= 42 0 'zero 1 'one 2 'two 'other) ;= "other" 164 | 165 | If `default` is not present, it is `null`. 166 | 167 | > (case= 42 0 'zero) ;= null 168 | 169 | --- 170 | 171 | macro.case=: 172 | (fn* args 173 | (assertArgs args "expected at least 1 argument" 174 | (define {v: (gensym!)} 175 | `[define ~({} v (hd args)) 176 | ~(loopAs cases {exprs: (tl args)} 177 | (if (no exprs) null 178 | (= 1 (len exprs)) (hd exprs) 179 | `[if (= ~v ~(0 exprs)) 180 | ~(1 exprs) 181 | ~(cases {exprs: (tl (tl exprs))})]))]))) 182 | 183 | ### `assert` 184 | 185 | `(assert predicate err)` raises `err` as a signal if `predicate` is false. `err` is not evaluated if `predicate` is true. 186 | 187 | macro.assert: (fn- p e `[if (no ~p) (raise ~e)]) 188 | 189 | ## Exports 190 | 191 | $export: { 192 | quote, gensym!, \, \x, \y, \z, \xy, ->, ->>, \->, \->>, comment, loopAs, 193 | unless, case=, assert, 194 | 195 | λ: \, λx: \x, λy: \y, λz: \z, λxy: \xy, →: ->, ↠: ->>, λ→: \->, λ↠: \->>, 196 | ⍝: comment 197 | } 198 | 199 | [☙ Concurrency and Channels][prev] | [🗏 Table of Contents][toc] | [Number Operations ❧][next] 200 | :---|:---:|---: 201 | 202 | [toc]: jaspr.jaspr.md 203 | [prev]: concurrency.jaspr.md 204 | [next]: numbers.jaspr.md 205 | -------------------------------------------------------------------------------- /test/ModuleTest.ts: -------------------------------------------------------------------------------- 1 | import {AssertionError} from 'assert' 2 | import {expect} from 'chai' 3 | import {Jaspr, JasprError, resolveFully, toString, magicSymbol} from '../src/Jaspr' 4 | import {Root, Branch} from '../src/Fiber' 5 | import prettyPrint from '../src/PrettyPrint' 6 | import * as Names from '../src/ReservedNames' 7 | import prim from '../src/JasprPrimitive' 8 | import { 9 | readModuleFile, evalModule, importModule, ModuleSource, Module 10 | } from '../src/Module' 11 | 12 | function loadModule( 13 | filename: string, 14 | importedAs: string | null, 15 | assertions: (module: Module) => void 16 | ): () => Promise { 17 | return () => new Promise((resolve, reject) => { 18 | function fail(msg: string, err: Jaspr, raisedBy?: Branch): void { 19 | reject(new AssertionError({ 20 | message: `\n${msg}: ${prettyPrint(err, false)}`/* + 21 | (raisedBy ? `\n\nStack trace:\n${raisedBy.stackTraceString(false)}` : '') */ 22 | })) 23 | } 24 | const env = new Root((root, err, raisedBy, cb) => { 25 | fail('error evaluating module', err, raisedBy) 26 | root.cancel() 27 | }) 28 | readModuleFile(`test/modules/${filename}`, (err, modsrc) => { 29 | if (err) return fail('error loading module', err) 30 | evalModule(env, modsrc, { 31 | filename, localModules: new Map([ 32 | [Names.primitiveModule, Promise.resolve(prim(env))] 33 | ]) 34 | }).then( 35 | mod => resolveFully(importedAs ? importModule(mod, importedAs) : mod, 36 | (err, mod) => { 37 | try { assertions(mod) } 38 | catch (ex) { reject(ex); return } 39 | resolve() 40 | }), 41 | err => fail('error loading module', err)) 42 | }) 43 | }) 44 | } 45 | 46 | describe('the module loader', () => { 47 | it('can load a module', loadModule('hello-world.jaspr', null, mod => { 48 | expect(mod.$module).to.equal('jaspr-tests.hello-world') 49 | expect(mod.$version).to.equal('1.0') 50 | expect(mod.$export).to.deep.equal({'hello-world': 'hello-world'}) 51 | expect(mod.value).to.have.property('hello-world').equal('Hello, world!') 52 | expect(mod.value).to.have.property('jaspr-tests.hello-world.hello-world').equal('Hello, world!') 53 | expect(mod.value).to.have.property('jaspr-tests.hello-world.hello-world@1.0').equal('Hello, world!') 54 | expect(mod.qualified).to.have.property('jaspr-tests.hello-world.hello-world').equal('jaspr-tests.hello-world.hello-world@1.0') 55 | expect(mod.qualified).to.have.property('hello-world').equal('jaspr-tests.hello-world.hello-world@1.0') 56 | })) 57 | it('can load a module as a named import', loadModule('hello-world.jaspr', 'hello', mod => { 58 | expect(mod).to.not.have.property('$module') 59 | expect(mod).to.not.have.property('$export') 60 | expect(mod.value).to.have.property('hello-world').equal('Hello, world!') 61 | expect(mod.value).to.have.property('hello.hello-world').equal('Hello, world!') 62 | expect(mod.value).to.have.property('jaspr-tests.hello-world.hello-world@1.0').equal('Hello, world!') 63 | expect(mod.value).to.not.have.property('jaspr-tests.hello-world.hello-world') 64 | expect(mod.value).to.not.have.property('hello.hello-world@1.0') 65 | expect(mod.qualified).to.have.property('hello-world').equal('jaspr-tests.hello-world.hello-world@1.0') 66 | expect(mod.qualified).to.have.property('hello.hello-world').equal('jaspr-tests.hello-world.hello-world@1.0') 67 | expect(mod.qualified).to.not.have.property('jaspr-tests.hello-world.hello-world') 68 | })) 69 | it('handles recursive functions', loadModule('recursive-fn.jaspr', null, mod => { 70 | expect(mod.$module).to.equal('jaspr-tests.recursive-fn') 71 | expect(mod.$version).to.equal('1.0') 72 | expect(mod.value).to.have.property('factorial').be.an('object') 73 | expect(mod.value).to.have.property('five-factorial').equal(120) 74 | expect(mod.value).to.have.property('jaspr-tests.recursive-fn.five-factorial').equal(120) 75 | expect(mod.qualified).to.have.property('factorial').equal('jaspr-tests.recursive-fn.factorial@1.0') 76 | })) 77 | it('can load macros', loadModule('macros.jaspr', null, mod => { 78 | expect(mod.$module).to.equal('jaspr-tests.macros') 79 | expect(mod.macro).to.have.property('quote').be.an('object') 80 | expect(mod.macro).to.have.property('jaspr-tests.macros.quote').be.an('object') 81 | expect(mod.macro).to.not.have.property('quoted') 82 | expect(mod.macro).to.not.have.property('jaspr-tests.macros.quoted') 83 | expect(mod.value).to.not.have.property('quote') 84 | expect(mod.value).to.not.have.property('jaspr-tests.macros.quote') 85 | expect(mod.value).to.have.property('quoted').equal('quoted-value') 86 | expect(mod.value).to.have.property('jaspr-tests.macros.quoted').equal('quoted-value') 87 | expect(mod.qualified).to.have.property('quote').equal('jaspr-tests.macros.quote@1.0') 88 | expect(mod.qualified).to.have.property('quoted').equal('jaspr-tests.macros.quoted@1.0') 89 | })) 90 | it('can load literate modules', loadModule('simple-literate.jaspr.md', null, mod => { 91 | expect(mod.$module).to.equal('jaspr-tests.simple-literate') 92 | expect(mod.value).to.have.property('indented').equal('indented-value') 93 | expect(mod.value).to.have.property('fenced').equal('fenced-value') 94 | })) 95 | it('extracts tests from literate modules', loadModule('literate-tests.jaspr.md', null, mod => { 96 | expect(mod.$module).to.equal('jaspr-tests.literate-tests') 97 | //expect(mod.value).to.be.empty 98 | expect(mod.test).to.be.an('object') 99 | expect(mod.test).to.have.property('Literate-Program-with-Tests-0').equal(true) 100 | expect(mod.test).to.have.property('Literate-Program-with-Tests-1').deep.equal( 101 | ['jaspr.primitive.is?', ['jaspr.primitive.add', 2, 2], 4]) 102 | expect(mod.test).to.have.property('Heading-1-0').deep.equal( 103 | [Names.assertEqualsQualified, ['jaspr.primitive.add', 2, 2], ['', 4]]) 104 | expect(mod.test).to.have.property('Heading-2-0').deep.equal( 105 | [Names.assertEqualsQualified, [[], ['', 'a'], ['', 'b'], ['', 'c']], ['', ['a', 'b', 'c']]]) 106 | })) 107 | it('loads included files', loadModule('has-includes.jaspr', null, mod => { 108 | expect(mod.$module).to.equal('jaspr-tests.has-includes') 109 | expect(mod.value).to.have.property('original').equal('original-value') 110 | expect(mod.value).to.have.property('included').equal('included-value') 111 | expect(mod.$export).to.deep.equal({original: 'original', included: 'included'}) 112 | })) 113 | it('handles recursive includes', loadModule('has-recursive-include.jaspr', null, mod => { 114 | expect(mod.$module).to.equal('jaspr-tests.has-recursive-include') 115 | expect(mod.value).to.have.property('original').equal('original-value') 116 | expect(mod.value).to.have.property('included').equal('included-value') 117 | expect(mod.value).to.have.property('recursive-included').equal('recursive-included-value') 118 | expect(mod.$export).to.deep.equal({ 119 | original: 'original', 120 | included: 'included', 121 | 'recursive-included': 'recursive-included' 122 | }) 123 | })) 124 | }) 125 | -------------------------------------------------------------------------------- /src/Fiber.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Jaspr, JasprError, Deferred, Callback, toString, isArray, magicSymbol 3 | } from './Jaspr' 4 | import { 5 | Env, FiberDescriptor, JasprDynamic, makeDynamic, waitFor 6 | } from './Interpreter' 7 | import prettyPrint from './PrettyPrint' 8 | import * as Names from './ReservedNames' 9 | import {NativeAsyncFn} from './NativeFn' 10 | import Chan from './Chan' 11 | import chalk from 'chalk' 12 | import {randomBytes} from 'crypto' 13 | 14 | export class Branch implements Env { 15 | readonly root: Root 16 | readonly parent: Branch 17 | readonly junctionBranches: Branch[] 18 | canceled = false 19 | readonly listeners = new Set<() => void>() 20 | 21 | constructor(root?: Root, parent?: Branch, junction?: Branch[]) { 22 | if (root && parent) { 23 | this.root = root 24 | this.parent = parent 25 | } else if (this instanceof Root) { 26 | this.root = this 27 | this.parent = this 28 | } else { 29 | throw new Error('non-root branch must have a parent') 30 | } 31 | this.junctionBranches = junction || [this] 32 | } 33 | 34 | isCanceled(): boolean { 35 | if (this.canceled) return true 36 | if (this.parent.isCanceled()) { 37 | this.canceled = true 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | cancel() { 44 | if (!this.isCanceled()) { 45 | this.canceled = true 46 | for (let listener of this.listeners) { 47 | listener() 48 | this.parent.removeOnCancel(listener) 49 | } 50 | this.listeners.clear() 51 | } 52 | } 53 | onCancel(listener: () => void) { 54 | if (!this.isCanceled() && !this.listeners.has(listener)) { 55 | this.listeners.add(listener) 56 | this.parent.onCancel(listener) 57 | } 58 | } 59 | removeOnCancel(listener: () => void) { 60 | if (!this.isCanceled()) { 61 | if (this.listeners.delete(listener)) { 62 | this.parent.removeOnCancel(listener) 63 | } 64 | } 65 | } 66 | 67 | defer( 68 | props: () => FiberDescriptor = () => ({action: 'external'}) 69 | ): Fiber { 70 | return new Fiber(this, props) 71 | } 72 | 73 | junction( 74 | fns: ((env: Env, cb: Callback) => void)[], 75 | props: () => FiberDescriptor = () => ({action: 'junction'}) 76 | ): Fiber { 77 | const junction = new Fiber(this, props) 78 | const branches = new Array(fns.length) 79 | let done = false 80 | for(let i = 0; i < branches.length; i++) { 81 | branches[i] = new Branch(this.root, this, branches) 82 | } 83 | fns.forEach((fn, i) => setImmediate(() => fn(branches[i], result => { 84 | if (!done) { 85 | done = true 86 | branches.forEach((b, j) => {if (i !== j) b.cancel()}) 87 | junction.resolve(result) 88 | } 89 | }))) 90 | return junction 91 | } 92 | 93 | gensym(name?: string) { 94 | const id = randomBytes(15).toString('base64') 95 | return name ? name + '_' + id : id 96 | } 97 | 98 | get closureName(): string { return this.root.closureName } 99 | get signalHandlerVar(): JasprDynamic { return this.root.signalHandlerVar } 100 | get nameVar(): JasprDynamic { return this.root.nameVar } 101 | 102 | unhandledError(err: Jaspr, cb: Callback) { 103 | this.root.errorHandler(this.root, err, this, cb) 104 | } 105 | } 106 | 107 | export class Fiber extends Deferred { 108 | readonly branch: Branch 109 | readonly props: () => FiberDescriptor 110 | 111 | constructor( 112 | branch: Branch, 113 | props: () => FiberDescriptor = () => ({action: 'external'}) 114 | ) { 115 | super() 116 | this.branch = branch 117 | this.props = props 118 | 119 | // Debug information: 120 | //console.log(this.descriptionString()) 121 | //this.await(v => console.log(prettyPrint(this.code) + ' -> ' + prettyPrint(v))) 122 | } 123 | 124 | isCanceled() { return this.branch.isCanceled() } 125 | 126 | descriptionString(color = true, history = new Set()): string { 127 | if (history.has(this)) { 128 | return color ? chalk.redBright('(CYCLE DETECTED)') : '(CYCLE DETECTED)' 129 | } 130 | let {action: str, code, name} = this.props() 131 | code = code instanceof Deferred && code.value !== undefined 132 | ? code.value : code 133 | if (name) str += ` (in ${name})` 134 | if (code !== undefined) { 135 | str += ':' 136 | if (code instanceof Fiber) { 137 | str += color ? chalk.gray(' waiting on result of…') 138 | : ' waiting on result of…' 139 | str += '\n ' 140 | history.add(this) 141 | str += code.descriptionString(color, history).replace(/\n/gm, '\n ') 142 | } else if (code instanceof Deferred) { 143 | str += color ? chalk.gray(` waiting on result of ${code}`) 144 | : ` waiting on result of ${code}` 145 | } else { 146 | str += ' ' + prettyPrint(code, color) 147 | } 148 | } 149 | if (this.value !== undefined) { 150 | str += '\n' 151 | str += color ? chalk.yellowBright('resolved to:') : 'resolved to:' 152 | str += ' ' + prettyPrint(this.value, color) 153 | } 154 | return str 155 | } 156 | 157 | /*stackTrace(): Fiber[] { 158 | return [this].concat(this.parent.stackTrace()) 159 | } 160 | 161 | stackTraceString(color = true): string { 162 | const trace = this.stackTrace() 163 | let str = trace[0].descriptionString(color) 164 | for (let i = 1; i < trace.length; i++) { 165 | const branch = i === trace.length - 1 ? '└ ' : '├ ' 166 | str += '\n' + (color ? chalk.gray(branch) : branch) 167 | str += trace[i].descriptionString(color).replace(/\n/gm, 168 | '\n' + (i === trace.length - 1 ? ' ' : (color ? chalk.gray('│ ') : '│ '))) 169 | } 170 | return str 171 | }*/ 172 | 173 | toString() { 174 | if (this.value !== undefined) return `` 175 | else { 176 | const {code} = this.props() 177 | return `code)}>` 178 | } 179 | } 180 | } 181 | 182 | export type ErrorHandler = 183 | (root: Root, err: Jaspr, raisedIn: Branch, cb: Callback) => void 184 | 185 | export class Root extends Branch { 186 | readonly errorHandler: ErrorHandler 187 | 188 | constructor( 189 | errorHandler: ErrorHandler = 190 | (root, err, raisedBy, cb) => { 191 | console.error(chalk.redBright('⚠ Unhandled Signal ⚠')) 192 | console.error(prettyPrint(err)) 193 | //console.error('\n' + chalk.gray('Stack trace:')) 194 | //console.error(raisedBy.stackTraceString()) 195 | root.cancel() 196 | } 197 | ) { 198 | super() 199 | this.errorHandler = errorHandler 200 | } 201 | 202 | isCanceled() { return this.canceled } 203 | 204 | deferCancelable( 205 | fn: (env: Env, cb: Callback) => void, 206 | dynamics: [JasprDynamic, Jaspr | Deferred][] = [] 207 | ): {fiber: Fiber, cancel: () => void} { 208 | const branch = new Branch(this, this) 209 | const fiber = new Fiber(branch) 210 | setImmediate(() => fn(branch, fiber.resolve.bind(fiber))) 211 | return {fiber, cancel: () => branch.cancel()} 212 | } 213 | 214 | unhandledError(err: Jaspr, cb: Callback) { 215 | this.errorHandler(this, err, this, cb) 216 | } 217 | 218 | readonly _closureName = this.gensym('closure') 219 | readonly _signalHandlerVar = makeDynamic( 220 | new NativeAsyncFn(function rootErrorHandler([err], cb) { 221 | this.unhandledError(err, v => cb(null, v)) 222 | }).toClosure(this)) 223 | readonly _nameVar = makeDynamic(null) 224 | get closureName() { return this._closureName } 225 | get signalHandlerVar() { return this._signalHandlerVar } 226 | get nameVar() { return this._nameVar } 227 | } 228 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as commandLineArgs from 'command-line-args' 2 | import * as _ from 'lodash' 3 | import * as path from 'path' 4 | import chalk from 'chalk' 5 | import {readFile, writeFile} from 'fs' 6 | import {Jaspr, Callback, resolveFully} from './Jaspr' 7 | import {Env, Scope, mergeScopes, expandAndEval, waitFor} from './Interpreter' 8 | import {Root, Branch, ErrorHandler} from './Fiber' 9 | import Parser from './Parser' 10 | import {parseMarkdown, markdownExtensions} from './LiterateParser' 11 | import {Module, ModuleSource, readModuleFile, evalModule, importModule} from './Module' 12 | import primitives from './JasprPrimitive' 13 | import {primitiveModule, stdlibModule, version} from './ReservedNames' 14 | import prettyPrint from './PrettyPrint' 15 | import repl from './Repl' 16 | 17 | const optionDefs = [ 18 | {name: 'convert', alias: 'c', type: String}, 19 | {name: 'literate', alias: 'l', type: Boolean}, 20 | {name: 'repl', alias: 'r', type: Boolean}, 21 | {name: 'stdlib', type: String}, 22 | {name: 'help', type: Boolean}, 23 | {name: 'src', type: String, multiple: true, defaultOption: true} 24 | ] 25 | 26 | const options = commandLineArgs(optionDefs) 27 | 28 | if (options.help) usage() 29 | else if (options.convert) { 30 | if (options.repl || options.stdlib || !_.isEmpty(options.src)) { 31 | usage() 32 | } else { 33 | const inFile = options.convert 34 | readFile(inFile, (err, data) => { 35 | if (err != null) { 36 | console.error(err) 37 | process.exit(1) 38 | } else { 39 | const converted = 40 | (options.literate || _.some(markdownExtensions, e => inFile.endsWith(e))) 41 | ? parseMarkdown(data.toString('utf8'), inFile) 42 | : (() => { 43 | const p = new Parser(inFile) 44 | p.read(data.toString('utf8')) 45 | return p.getOneResult() 46 | })() 47 | console.log(JSON.stringify(converted)) 48 | } 49 | }) 50 | } 51 | } else { 52 | const isRepl = options.repl || _.isEmpty(options.src) 53 | const stdlib = options.stdlib || path.resolve(__dirname, '..', '..', 'jaspr', 'jaspr.jaspr.md') 54 | const scope: Promise = 55 | new Promise(resolve => setImmediate(() => 56 | loadModules(root, [stdlib, ...options.src || []], !isRepl, mods => 57 | resolve(Promise.all(mods.values()))))) 58 | .then(mods => mergeScopes(root, ...mods.map(m => 59 | importModule(m, m.$module, m.$module === primitiveModule ? {} : undefined)))) 60 | var root = new Root(consoleSignalHandler(scope)) 61 | if (isRepl) { 62 | scope.then(scope => { 63 | console.log(chalk.greenBright('{ Jaspr: (JSON Lisp) }')) 64 | console.log(chalk.yellow('Version ' + version)) 65 | console.log(chalk.yellow('Adam R. Nelson ')) 66 | console.log() 67 | console.log(chalk.gray('Use CTRL-C to quit')) 68 | console.log() 69 | function loop(counter = 1, last?: Jaspr) { 70 | repl({ 71 | prompt: chalk.green('Jaspr') + ' ' + chalk.cyan('№' + counter) + '>', 72 | priority: 1, 73 | message: last 74 | ? chalk.green(`№${counter - 1} ⇒`) + ' ' + prettyPrint(last) 75 | : undefined 76 | }, input => { 77 | const {fiber, cancel} = root.deferCancelable((env, cb) => { 78 | env.onCancel(() => { 79 | console.warn(chalk.yellowBright(`№${counter} canceled`)) 80 | loop(counter + 1) 81 | }) 82 | waitFor(expandAndEval(env, scope, [], undefined, input), cb) 83 | }) 84 | fiber.await(output => 85 | resolveFully(output, (err, x) => loop(counter + 1, x))) 86 | }) 87 | } 88 | loop() 89 | }) 90 | } 91 | } 92 | 93 | function consoleSignalHandler(scope: Promise): ErrorHandler { 94 | let helpDisplayed = false 95 | return function errorHandler(root, err, raisedIn, cb) { 96 | const message = 97 | '\n🚨 ' + chalk.redBright('Unhandled Signal Encountered!') + '\n' + 98 | prettyPrint(err) + '\n' + (helpDisplayed ? ` 99 | Provide a resume value, or press ENTER to ${ 100 | raisedIn == root ? 'end the program' : 'cancel this fiber'}. 101 | ` : ` 102 | An unhandled signal has stopped one fiber of the running Jaspr program. 103 | (Other fibers may still be running!) 104 | 105 | The stopped fiber can be restarted by providing a ${ 106 | chalk.bold('resume value')} at the 107 | prompt below. Or, you can press ENTER to ${ 108 | raisedIn == root ? 'end the program' : 'cancel this fiber'}. 109 | `).replace(/^[ ]+/gm, '') 110 | helpDisplayed = true 111 | repl({prompt: chalk.red('!>'), message, priority: 0, onBlank: () => { 112 | if (root === raisedIn) { 113 | console.error('Ending program due to unhandled signal.') 114 | process.exit(1) 115 | } else { 116 | console.error('Canceling fiber.') 117 | raisedIn.cancel() 118 | } 119 | }}, resumeValue => scope.then(scope => 120 | waitFor(expandAndEval(root, scope, [], undefined, resumeValue), cb))) 121 | } 122 | } 123 | 124 | function loadModules( 125 | env: Env, filenames: string[], runMain: boolean, 126 | cb: (mods: Map>) => void 127 | ): void { 128 | (function loadNext(localModules: Map>): void { 129 | const filename = filenames.shift() 130 | if (filename === undefined) return cb(localModules) 131 | readModuleFile(filename, (err, modsrc) => { 132 | if (err != null || modsrc == null) { 133 | console.error(chalk.redBright(`⚠☠ Failed to load module: ${filename}`)) 134 | console.error(prettyPrint(err)) 135 | return process.exit(1) 136 | } 137 | (>localModules.get(primitiveModule)).then(prim => { 138 | const imported = importModule(prim, primitiveModule, {}) 139 | const stdlib = localModules.get(stdlibModule) 140 | if (stdlib) return stdlib.then(stdlib => 141 | mergeScopes(env, imported, importModule(stdlib))) 142 | else return imported 143 | }).then(scope => { 144 | const promise = evalModule(env, modsrc, { 145 | filename, localModules, runMain, scope 146 | }) 147 | if (modsrc.$module != null) localModules.set(modsrc.$module, promise) 148 | loadNext(localModules) 149 | }) 150 | }) 151 | })(new Map([[primitiveModule, Promise.resolve(primitives(env))]])) 152 | } 153 | 154 | function usage() { 155 | console.log(require('command-line-usage')([{ 156 | header: `Jaspr ${version}`, 157 | content: ` 158 | JavaScript reference implementation of an interpreter for the Jaspr 159 | programming language. Use this command to run Jaspr scripts, start a Jaspr 160 | REPL, or convert Jaspr source code into JSON. 161 | `.trim().replace(/\s+/gm, ' ') 162 | }, { 163 | header: 'Examples', 164 | content: ` 165 | [bold]{ jaspr} 166 | 167 | Starts a REPL 168 | 169 | [bold]{ jaspr foo.jaspr bar.jaspr} 170 | 171 | Executes foo.jaspr and bar.jaspr 172 | 173 | [bold]{ jaspr --convert foo.jaspr > foo.json} 174 | 175 | Converts the Jaspr file foo.jaspr to the JSON file foo.json 176 | `.trim() 177 | }, { 178 | header: 'Options', 179 | optionList: [{ 180 | name: 'help', 181 | description: 'Display this usage guide' 182 | }, { 183 | name: 'src', 184 | typeLabel: '[underline]{file} ...', 185 | description: 'Jaspr source files to load' 186 | }, { 187 | name: 'convert', 188 | typeLabel: '[underline]{file}', 189 | description: 'Jaspr file to convert to JSON (JSON is written to stdout)' 190 | }, { 191 | name: 'repl', 192 | description: 'Start a REPL even if source files are loaded' 193 | }, { 194 | name: 'literate', 195 | description: ` 196 | Treat all source files as Literate Jaspr (Jaspr embedded in Markdown), 197 | regardless of extension 198 | `.trim().replace(/\s+/gm, ' ') 199 | }] 200 | }, { 201 | header: 'Author', 202 | content: ` 203 | Adam R. Nelson 204 | GitHub: https://github.com/ar-nelson/jaspr 205 | `.trim() 206 | }])) 207 | } 208 | -------------------------------------------------------------------------------- /src/PrettyPrint.ts: -------------------------------------------------------------------------------- 1 | import {Jaspr, Deferred, isArray, isObject, isMagic} from './Jaspr' 2 | import {reservedChar} from './Parser' 3 | import chalk from 'chalk' 4 | import {join, sum, identity} from 'lodash' 5 | 6 | const defaultIndent = 2 7 | const maxLength = 80 8 | const maxEntries = 36 9 | const maxDepth = 36 10 | 11 | function spaces(n: number) { 12 | let s = '' 13 | for (let i = 0; i < n; i++) s += ' ' 14 | return s 15 | } 16 | 17 | const escapes: {[e: string]: string} = { 18 | '\n': '\\n', 19 | '\r': '\\r', 20 | '\f': '\\f', 21 | '\v': '\\v', 22 | '“': '\\“', 23 | '”': '\\”', 24 | '\\': '\\\\' 25 | } 26 | 27 | function quoteString( 28 | str: string, 29 | truncateAt = Infinity, 30 | quoteColor: (s: string) => string = identity, 31 | escapeColor: (s: string) => string = identity, 32 | ellipsisColor: (s: string) => string = identity 33 | ): string { 34 | let out = quoteColor('“'), len = 1 35 | for (let c of str) { 36 | if (escapes.hasOwnProperty(c)) { 37 | len++ 38 | c = escapeColor(escapes[c]) 39 | } 40 | if (len >= truncateAt) { 41 | out += ellipsisColor('…') 42 | break 43 | } 44 | out += c 45 | len++ 46 | } 47 | return out + quoteColor('”') 48 | } 49 | 50 | abstract class Form { 51 | abstract length(): number 52 | abstract toStringInline(color?: boolean): string 53 | abstract toStringBlock(color?: boolean, offset?: number, hanging?: number): string 54 | toString(color = true, offset = 0, hanging?: number) { 55 | if (this.length() < maxLength - offset) { 56 | return this.toStringInline(color) 57 | } else { 58 | return this.toStringBlock(color, offset, hanging) 59 | } 60 | } 61 | } 62 | 63 | class ArrayForm extends Form { 64 | elements: Form[] 65 | 66 | constructor(elements: Form[]) { 67 | super() 68 | this.elements = elements 69 | } 70 | 71 | length() { 72 | return 2 + sum(this.elements.map(x => x.length())) + 73 | (this.elements.length > 0 ? this.elements.length - 1 : 0) 74 | } 75 | 76 | toStringInline(color = true) { 77 | return (color ? chalk.cyan('[') : '[') + 78 | join(this.elements.map(x => x.toStringInline(color)), ' ') + 79 | (color ? chalk.cyan(']') : ']') 80 | } 81 | 82 | toStringBlock(color = true, offset = 0, hanging = offset) { 83 | const token: (x: string) => string = color ? chalk.cyan : identity 84 | return token('[') + '\n' + 85 | join(this.elements.map(x => 86 | spaces(hanging + defaultIndent) + 87 | x.toString(color, hanging + defaultIndent)), 88 | token(',') + '\n') + 89 | '\n' + spaces(hanging) + token(']') 90 | } 91 | } 92 | 93 | class ObjectForm extends Form { 94 | entries: { 95 | key: string, 96 | unquoted: boolean, 97 | len: number, 98 | form: Form 99 | }[] 100 | 101 | constructor(entries: [string, Form][]) { 102 | super() 103 | this.entries = entries.map(([key, form]) => { 104 | if (key === '' || reservedChar.test(key)) { 105 | return {key, unquoted: false, len: quoteString(key).length, form} 106 | } else { 107 | return {key, unquoted: true, len: key.length, form} 108 | } 109 | }) 110 | } 111 | 112 | length() { 113 | return 2 + sum(this.entries.map(({len, form}) => len + 1 + form.length())) + 114 | (this.entries.length > 0 ? this.entries.length - 1 : 0) 115 | } 116 | 117 | toStringInline(color = true) { 118 | const token: (x: string) => string = color ? chalk.green : identity 119 | return token('{') + 120 | join(this.entries.map(({key, unquoted, form: value}) => { 121 | if (unquoted) { 122 | return key + token(':') + value.toStringInline(color) 123 | } else { 124 | return quoteString(key, undefined, 125 | token, token, color ? chalk.gray : identity) + 126 | token(':') + value.toStringInline(color) 127 | } 128 | }), ' ') + token('}') 129 | } 130 | 131 | toStringBlock(color = true, offset = 0, hanging = offset) { 132 | const token: (x: string) => string = color ? chalk.green : identity 133 | return token('{') + '\n' + 134 | join(this.entries.map(({key, len, unquoted, form: value}) => { 135 | const keyStr = spaces(hanging + defaultIndent) + 136 | (unquoted ? key : quoteString(key, undefined, 137 | token, token, color ? chalk.gray : identity)) + 138 | token(':') + ' ' 139 | if (hanging + defaultIndent + len + 2 + value.length() < maxLength) { 140 | return keyStr + value.toStringInline(color) 141 | } else { 142 | return keyStr + value.toString(color, 143 | hanging + defaultIndent + len + 2, 144 | hanging + defaultIndent) 145 | } 146 | }), token(',') + '\n') + 147 | '\n' + spaces(hanging) + token('}') 148 | } 149 | } 150 | 151 | class StringForm extends Form { 152 | str: string 153 | unquoted: boolean 154 | len: number 155 | 156 | constructor(str: string) { 157 | super() 158 | this.str = str 159 | if (str === '' || reservedChar.test(str)) { 160 | this.unquoted = false 161 | this.len = quoteString(str).length 162 | } else { 163 | this.unquoted = true 164 | this.len = str.length 165 | } 166 | } 167 | 168 | length() { return this.len } 169 | 170 | toStringInline(color = true) { 171 | if (this.unquoted) return this.str 172 | else return quoteString(this.str, undefined, 173 | color ? chalk.gray : identity, 174 | color ? chalk.yellow : identity, 175 | color ? chalk.gray : identity) 176 | } 177 | 178 | toStringBlock(color = true, offset = 0, hanging = -1) { 179 | let out = '', len = maxLength - offset 180 | if (hanging >= 0 && offset + this.str.length > maxLength) { 181 | out += '\n' + spaces(hanging) 182 | len = maxLength - hanging 183 | } 184 | if (this.unquoted && this.str.length <= len) return out + this.str 185 | else return out + quoteString(this.str, len, 186 | color ? chalk.gray : identity, 187 | color ? chalk.yellow : identity, 188 | color ? chalk.gray : identity) 189 | } 190 | } 191 | 192 | class ConstantForm extends Form { 193 | str: string 194 | color: (s: string) => string 195 | 196 | constructor(str: string, color: (s: string) => string = identity) { 197 | super() 198 | this.str = str 199 | this.color = color 200 | } 201 | 202 | length() { return this.str.length } 203 | toStringInline(color = true) { return color ? this.color(this.str) : this.str } 204 | toStringBlock(color = true, offset = 0, hanging = -1) { 205 | if (hanging >= 0 && offset + this.str.length > maxLength) { 206 | return '\n' + spaces(hanging) + (color ? this.color(this.str) : this.str) 207 | } 208 | return color ? this.color(this.str) : this.str 209 | } 210 | } 211 | 212 | function buildForms(it: Jaspr | Deferred, depth = 0): Form { 213 | if (depth >= maxDepth) { 214 | return new ConstantForm('... (too deep)', chalk.gray) 215 | } else if (it === null) { 216 | return new ConstantForm('null', chalk.magentaBright) 217 | } else if (it === true) { 218 | return new ConstantForm('true', chalk.greenBright) 219 | } else if (it === false) { 220 | return new ConstantForm('false', chalk.redBright) 221 | } else if (typeof it === 'number') { 222 | return new ConstantForm('' + it, chalk.cyanBright) 223 | } else if (typeof it === 'string') { 224 | return new StringForm(it) 225 | } else if (it instanceof Deferred) { 226 | if (it.value !== undefined) { 227 | return buildForms(it.value, depth) 228 | } else { 229 | return new ConstantForm(it.toString(), chalk.yellow) 230 | } 231 | } else if (isArray(it)) { 232 | return new ArrayForm(it.map(e => buildForms(e, depth + 1))) 233 | } else if (isMagic(it)) { 234 | return new ConstantForm('(magic)', chalk.yellowBright) 235 | } else if (isObject(it)) { 236 | return new ObjectForm( 237 | Object.keys(it).map(k => [k, buildForms(it[k], depth + 2)])) 238 | } else { 239 | return new ConstantForm('' + it, chalk.yellow) 240 | } 241 | } 242 | 243 | export default function prettyPrint(it: Jaspr, color = true) { 244 | return buildForms(it).toString(color) 245 | } 246 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | `{Jaspr: (JSON Lisp)}` 2 | ====================== 3 | 4 | A new programming language. Jaspr is **minimal**, **functional**, and **concurrent by default**. 5 | 6 | ```jaspr 7 | quicksort: 8 | (fn [] [] 9 | . [x] ([] x) 10 | . xs (let pivot (→ xs len (÷ 2) floor) 11 | y (pivot xs) 12 | part (fn parts x 13 | (let key (if (< x y) 'lt 14 | (> x y) 'gt 15 | (= x y) 'eq 16 | (raise {err: “NotComparable”, x, y})) 17 | (update (λ cons x _) key parts))) 18 | {lt eq gt} (reduce part {lt: [], eq: [], gt: []} xs) 19 | 20 | (cat (quicksort lt) eq (quicksort gt)))) 21 | ``` 22 | 23 | What? 24 | ----- 25 | 26 | Jaspr is a functional Lisp, in the style of Clojure. All data in Jaspr is immutable JSON, including Jaspr source code itself. Jaspr's syntax is a superset of JSON, with syntax sugar to make it resemble Lisp code: 27 | 28 | | Jaspr | JSON | 29 | |------:|:-----| 30 | | `(+ 1 2)` | `["+", 1, 2]` | 31 | | `{a: b c: d e}` | `{"a": "b", "c": "d", "e": "e"}` | 32 | | `'[1 2 3]` | `["", [1, 2, 3]]` | 33 | | `((“autoquote” yes) [“autoquote” no])` | `[[["", "autoquote"], "yes"], ["autoquote", "no"]]` | 34 | | `null ; comment` | `null` | 35 | | `null // comment` | `null` | 36 | 37 | Strings are evaluated as symbols unless they are quoted (the empty string is the quote macro). There is no string/symbol split, because all Jaspr values must be one of the [6 JSON data types][json]. 38 | 39 | Evaluation is concurrent by default: function arguments and data structure elements have no set evaluation order, but are each evaluated in their own fibers (lightweight threads). This is similar to lazy evaluation in languages like Haskell, except that even unused subexpressions will always be evaluated eventually. 40 | 41 | Functions in Jaspr can have side effects, but there are no mutable data structures. Jaspr supports communication between fibers via channels; these are the same kinds of channels used in Go or in Clojure's `core/async`, and they are the only mutable state in the language. 42 | 43 | Jaspr source files are modules consisting of a single top-level JSON object, in which each key not starting with `$` is a definition. 44 | 45 | [json]: http://json.org/ 46 | 47 | Why? 48 | ---- 49 | 50 | Jaspr is a hobby project. I created it because I love designing programming languages, and the particular set of features that it has is especially interesting to me. Some of its features (concurrent-by-default evaluation, JSON as the base data structure of a Lisp) are seemingly obvious innovations that I'm genuinely surprised I haven't seen in any non-research language. 51 | 52 | I didn't design Jaspr to solve any particular problem, so I'm not sure if it will be useful to anyone yet. I do have some possible future applications for it, but they're more justifications than design goals: 53 | 54 | * Building and communicating with JSON-based HTTP APIs 55 | * Big Data processing, with automatic scaling across an arbitrary number of machines in Erlang-like fashion 56 | * Elm-like reactive functional UIs 57 | * Pretty much anything Clojure(script) is used for 58 | 59 | How? 60 | ---- 61 | 62 | This project is a reference implementation of a Jaspr interpreter in Node.js and TypeScript. After cloning the repo and installing dependencies with `npm install`, you can run the test suite with: 63 | 64 | npm test 65 | 66 | And start a REPL with: 67 | 68 | npm run repl 69 | 70 | The command-line interface is `index.js`, which, after compilation (triggered by `npm test`), should be located at `dist/src/index.js`. Run it with `--help` to get a usage message. It can run standalone Jaspr scripts, display the REPL, and convert Jaspr source into JSON. 71 | 72 | Q&A 73 | --- 74 | 75 | **Q: Is Jaspr production-ready?** 76 | 77 | A: Not even close, unfortunately. But you're welcome to try it out, report issues, and make feature requests! 78 | 79 | **Q: Is there any documentation yet?** 80 | 81 | A: The standard library is written as a literate program, which doubles as Markdown documentation. [You can read it here.][stdlib] It's intended as exhaustive documentation, not a tutorial; my plan is to write a tutorial once Jaspr's basic functionality is finalized. It's also still a WIP; some parts may be unfinished or refer to old features that have been changed/removed. 82 | 83 | Notably, all of the code samples in the documentation are also unit tests, so those at least should be guaranteed to work. 84 | 85 | [stdlib]: https://github.com/ar-nelson/jaspr/blob/master/jaspr/jaspr.jaspr.md 86 | 87 | **Q: Are those... _smart quotes_ in the code samples?** 88 | 89 | A: Yes! Extensive, pedantic Unicode support is another feature that I added to Jaspr solely because it interested me. The only other language I know of that does this is Perl 6. Jaspr's Unicode support is described [in the documentation][unicode]. 90 | 91 | But don't worry. You can write Jaspr in plain ASCII, with ordinary double quote characters, and it will still work. All Unicode function/macro names are just aliases for their ASCII names. 92 | 93 | [unicode]: https://github.com/ar-nelson/jaspr/blob/master/jaspr/syntax.jaspr.md#unicode 94 | 95 | **Q: Jaspr seems extremely similar to Clojure. Why would I use it instead of Clojure?** 96 | 97 | A: Right now, there isn't a good reason; Jaspr is too unfinished. Long-term, I'm still not sure what would motivate an experienced Clojurist to prefer Jaspr, but, if you haven't learned Clojure yet, Jaspr should be much simpler to pick up: fewer concepts to learn, more opinionated, smaller standard library without sacrificing much power. Clojure seems to have several overlapping ways to do everything, with the only benefits being backward compatibility or more fine-tuned control over performance. 98 | 99 | There's also an obvious advantage if you're working with JSON: while most programming languages, including Clojure, have some impedance mismatches when converting to JSON because they support additional data structures, everything in Jaspr is JSON. Period. 100 | 101 | **Q: If everything is JSON, how do functions and channels work?** 102 | 103 | A: I had to fudge the rules a little bit for functions and channels. They, along with [dynamic variable references][dynamic], are _magic objects_, the only exceptions to the “everything is immutable, referentially-transparent JSON” rule. 104 | 105 | Magic objects are JSON objects with two special qualities: they can be compared using address equality, and they can't be directly serialized to JSON. Other than that, all operations that work on JSON objects work on magic objects: their type is `object`, and they have keys with values. 106 | 107 | Functions are actually still plain JSON, and it's possible to create functions that aren't magic objects, so long as they aren't recursive. Jaspr takes after ultra-minimalistic Lisps like [newLISP][newlisp] and [PicoLisp][picolisp] in that the scope is just another data structure, and closures are just data structures containing a scope and code. However, scopes may contain self-references, thus functions must be unserializable to prevent infinite loops. But they are ordinary JSON objects in every other way, and this opens up interesting metaprogramming possibilities by allowing functions and macros to directly inspect a function's scope or code. 108 | 109 | [dynamic]: https://github.com/ar-nelson/jaspr/blob/master/jaspr/data-types.jaspr.md#dynamic-variables 110 | [newlisp]: http://www.newlisp.org/ 111 | [picolisp]: https://picolisp.com/ 112 | 113 | **Q: Isn't concurrent-by-default evaluation terrible for performance?** 114 | 115 | A: Not necessarily. While “every expression is evaluated in its own fiber” is a good mental model for Jaspr's semantics, in practice most Jaspr code can still be evaluated synchronously. This reference implementation does just that: the interpreter's `eval` function runs code in a straight-line, synchronous fashion until it encounters something that *must* be done asynchronously, at which point it returns an unresolved lazy value and allows its caller to move on to the next evaluatable subexpression. 116 | 117 | The interpreter is still *really* slow, though. That's an unavoidable consequence of writing an interpreter in JavaScript. I'm planning on fixing performance in two ways: by adding a JIT that generates JS source code, and by writing a JS transpiler for production use. 118 | 119 | **Q: Where does the name come from?** 120 | 121 | A: LISP = LISt Processing; JaSPR = JSon PRocessing. 122 | -------------------------------------------------------------------------------- /src/Jaspr.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import * as Names from './ReservedNames' 3 | import {reservedChar} from './Parser' 4 | 5 | /** 6 | * JSON data consists of `null`, booleans, numbers, strings, arrays, and objects. 7 | */ 8 | export type Json = null | boolean | number | string | JsonArray | JsonObject 9 | /** An array containing only JSON values */ 10 | export interface JsonArray extends Array {} 11 | /** An object containing only JSON values */ 12 | export interface JsonObject { [key: string]: Json } 13 | 14 | /** 15 | * Jaspr data is JSON with one additional case: Deferred values, which are lazy 16 | * values that have not yet resolved. 17 | * 18 | * Once all of the lazy values in Jaspr data are resolved and replaced with 19 | * their actual values, it is valid JSON. 20 | */ 21 | export type Jaspr = null | boolean | number | string | JasprArray | JasprObject 22 | /** An array containing only Jaspr values */ 23 | export interface JasprArray extends Array {} 24 | /** An object containing only Jaspr values */ 25 | export interface JasprObject { [key: string]: Jaspr | Deferred } 26 | 27 | /** Magic Jaspr values contain extra data stored in a hidden Symbol property. */ 28 | export const magicSymbol = Symbol("magic") 29 | 30 | /** Well-known error types */ 31 | export type Err = 32 | 'NoBinding' | 'NoKey' | 'NoMatch' | 'BadName' | 'BadArgs' | 'BadModule' | 33 | 'BadPattern' | 'NotCallable' | 'NoPrimitive' | 'NotJSON' | 'ParseFailed' | 34 | 'EvalFailed' | 'ReadFailed' | 'WriteFailed' | 'NativeError' | 35 | 'NotImplemented' | 'AssertFailed' 36 | 37 | /** An error signal object */ 38 | export interface JasprError extends JasprObject { 39 | /** The error type */ 40 | err: Err 41 | /** The error message */ 42 | why: string 43 | } 44 | 45 | /** A basic Jaspr callback, with no error argument */ 46 | export type Callback = (x: Jaspr) => void 47 | 48 | /** A Node-style callback with an error argument */ 49 | export type ErrCallback = (err: JasprError | null, x: T | null) => void 50 | 51 | /** 52 | * A Deferred object is a lazy value. It is a simplified promise, without 53 | * chaining, error handling, or any of the other ES2015 Promise features. 54 | */ 55 | export abstract class Deferred { 56 | value: Jaspr | undefined = undefined 57 | listeners: Callback[] = [] 58 | 59 | abstract isCanceled(): boolean 60 | 61 | /** 62 | * Calls `cb` when the Deferred value is available. May call it immediately 63 | * (synchronously) if it has already resolved. 64 | */ 65 | await(cb: Callback): void { 66 | if (this.isCanceled()) { 67 | this.listeners = [] 68 | return 69 | } 70 | if (this.value === undefined) this.listeners.push(cb) 71 | else cb(this.value) 72 | } 73 | 74 | /** 75 | * Resolves this Deferred value with an actual value. Throws an exception if 76 | * this Deferred has already been resolved. 77 | */ 78 | resolve(value: Jaspr): void { 79 | if (this.isCanceled()) { 80 | this.listeners = [] 81 | return 82 | } 83 | if (this.value === undefined) { 84 | this.value = value 85 | for (let listener of this.listeners) listener(value) 86 | this.listeners = [] 87 | } else throw new Error( 88 | `Double resolve of Deferred (old: ${toString(this.value)}, new: ${toString(value)})`) 89 | } 90 | 91 | toString() { 92 | if (this.value === undefined) return "(unresolved)" 93 | else return `(resolved: ${toString(this.value)})` 94 | } 95 | } 96 | 97 | /** Tests whether a Jaspr value is an array */ 98 | export const isArray: (it: Jaspr) => it is JasprArray = Array.isArray 99 | 100 | /** Tests whether a Jaspr value is an object */ 101 | export function isObject(it: Jaspr): it is JasprObject { 102 | return typeof it === 'object' && it != null && !isArray(it) && 103 | !(it instanceof Deferred) 104 | } 105 | 106 | /** Tests whether a Jaspr value is a magic object */ 107 | export function isMagic(it: Jaspr) { 108 | return isObject(it) && magicSymbol in it 109 | } 110 | 111 | /** 112 | * Returns the boolean value (_truthiness_) of a Jaspr value. `null`, `false`, 113 | * `0`, `NaN`. `""`, `[]`, and `{}` are _falsy_; all other values are _truthy_. 114 | */ 115 | export function toBool(it: Jaspr): boolean { 116 | if (typeof it === 'boolean') return it 117 | else if (typeof it === 'number') return it !== 0 && !isNaN(it) 118 | else if (typeof it === 'string') return it !== "" 119 | else if (isArray(it)) return it.length > 0 120 | else if (isObject(it)) return Object.keys(it).length > 0 121 | else return !!it 122 | } 123 | 124 | /** 125 | * Deeply searches `root` for {@link Deferred} values and waits for all of them 126 | * to resolve, then passes the fully-resolved `root` to the callback `cb`. 127 | * 128 | * @param root The value to resolve. 129 | * @param cb Callback that will be called when `root` has fully resolved. If 130 | * `jsonOnly` is `true`, `cb` may be called with an error instead. 131 | * @param jsonOnly If true, `cb` will be called with an error if `root` contains 132 | * any magic objects that are not valid JSON. 133 | */ 134 | export function resolveFully(root: Jaspr, cb: ErrCallback, jsonOnly = false): void { 135 | let pending = 1, stack: Jaspr[] = [], history = new Set() 136 | function loop(toPush: Jaspr) { 137 | pending-- 138 | stack.push(toPush) 139 | try { 140 | while (stack.length > 0) { 141 | const x = stack.pop() 142 | if (isArray(x)) { 143 | for (let i = 0; i < x.length; i++) { 144 | const el = x[i] 145 | if (el instanceof Deferred) { 146 | pending++ 147 | el.await(resolved => { 148 | x[i] = resolved 149 | setImmediate(loop, resolved) 150 | }) 151 | } else stack.push(el) 152 | } 153 | } else if (jsonOnly && isMagic(x)) { 154 | throw {err: 'NotJSON', why: 'No JSON representation for magic object', value: x} 155 | } else if (isObject(x) && !history.has(x)) { 156 | history.add(x) 157 | for (let k in x) { 158 | const el = x[k] 159 | if (el instanceof Deferred) { 160 | pending++ 161 | el.await(resolved => { 162 | x[k] = resolved 163 | setImmediate(loop, resolved) 164 | }) 165 | } else stack.push(el) 166 | } 167 | } 168 | } 169 | } catch (ex) { 170 | if (ex instanceof Error) throw ex 171 | else return cb(ex, null) 172 | } 173 | if (pending <= 0) return cb(null, root) 174 | } 175 | loop(root) 176 | } 177 | 178 | /** 179 | * Tests whether `key` is a Jaspr-accessible key in `obj` (that is, an 180 | * enumerable key in `obj` or any of its prototypes). 181 | * 182 | * This is needed because Jaspr uses prototypes to merge objects in some cases, 183 | * such as extending scopes with new definitions. 184 | * 185 | * @param obj The object that may contain `key`. 186 | * @param key The key to test for. 187 | */ 188 | export function has(obj: {}, key: string): boolean { 189 | for (let proto = obj; proto != null; proto = Object.getPrototypeOf(proto)) { 190 | if (Object.prototype.propertyIsEnumerable.call(proto, key)) return true 191 | } 192 | return false 193 | } 194 | 195 | export function toJson(x: Jaspr, cb: (err: JasprObject | null, json: Json) => void): void { 196 | return resolveFully(x, cb, true) 197 | } 198 | 199 | function quoteString(str: string): string { 200 | let out = '“' 201 | for (let c of str) switch (c) { 202 | case '\n': out += '\\n'; break 203 | case '\r': out += '\\r'; break 204 | case '\f': out += '\\f'; break 205 | case '\v': out += '\\v'; break 206 | case '“': out += '\\“'; break 207 | case '”': out += '\\”'; break 208 | case '\\': out += '\\\\'; break 209 | default: out += c 210 | } 211 | return out + '”' 212 | } 213 | 214 | /** 215 | * Returns a string representation of `it`. 216 | * 217 | * The returned string will be *mostly* valid Jaspr, except for unparseable 218 | * string representations of magic objects or unresolved {@link Deferred} 219 | * values. 220 | * 221 | * @param it The Jaspr value to return the string representation of. 222 | * @param bareString If true, if `it` is a string then the returned string will 223 | * just be `it`, unchanged. Useful for print functions. 224 | * @param alwaysQuote If true, strings in the returned string will always be 225 | * quoted. By default, `toString` only quotes strings if it is syntactically 226 | * necessary. 227 | */ 228 | export function toString(it: Jaspr, bareString = false, alwaysQuote = false): string { 229 | if (isMagic(it)) { 230 | return '(magic)' 231 | } else if (isObject(it)) { 232 | return `{${_.join(_.toPairs(it).map(([k, v]) => toString(k) + ': ' + toString(v)), ', ')}}` 233 | } else if (isArray(it)) { 234 | return `[${_.join(it.map(x => toString(x)), ', ')}]` 235 | } else if (typeof it === 'string') { 236 | if (bareString) return it 237 | if (alwaysQuote || it === '' || reservedChar.test(it) || it !== it.normalize('NFKC')) { 238 | return quoteString(it) 239 | } else return it 240 | } else if (it instanceof Deferred && it.value !== undefined) { 241 | return toString(it.value) 242 | } else { 243 | return '' + it 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /jaspr/signals-errors.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ Pattern Matching][prev] | [🗏 Table of Contents][toc] | [Streams and Pipelines ❧][next] 2 | :---|:---:|---: 3 | 4 | # Signals and Error Handling 5 | 6 | Errors in Jaspr are handled via _signals_. Signals are similar to exceptions in most programming languages, but, in addition to unwinding the stack and aborting a computation, they also allow the possibility of _resuming_ a computation at the point where a signal was raised. 7 | 8 | ## Raising a Signal 9 | 10 | Signals are raised by the `raise` function. Raising a signal means calling the function stored in the [dynamic variable][dynamic] `jaspr.primitive.signalHandler`. This function usually does one of four things: 11 | 12 | 1. It *catches* the signal, canceling the fiber that raised it and continuing from another point lower on the stack; 13 | 2. it *resumes* from the signal, returning a value that becomes the return value of `raise`, which allows the fiber to continue from where it left off; 14 | 3. it *re-raises* the signal with a different signal handler, potentially receiving a resume value which it then returns; or 15 | 4. it doesn't handle the signal, instead passing it to the *root signal handler*, which (usually) displays an error message and terminates the program. 16 | 17 | Signal handlers are usually created using the built-in functions `catch`, `resume`, `catchWith`, or `resumeWith`, all of which are careful to make sure that `signalHandler` is called in a context where it is not its own `signalHandler`, so as to avoid infinite loops if the handler itself raises a signal. 18 | 19 | [dynamic]: data-types.jaspr.md#dynamic-variables 20 | 21 | ## Error Objects 22 | 23 | The primary use of signals in Jaspr is to raise and handle _errors_. A Jaspr error is an object with an `err` key, whose value is a string _error type_. It usually has a `why` key as well, which contains either a string explaining the error or another error that caused this error. 24 | 25 | ### Well-known Error Types 26 | 27 | - `NoBinding`: Attempting to look up a name failed. Has `name` key. 28 | - `NoKey`: Attempted to look up a nonexistent key in an array or object. Has `key` and `in` keys. 29 | - `NoMatch`: Value didn't match any patterns in a `case` or `let`. Has `value` and `patterns` keys. 30 | - `NoPrimitive`: `$`-name is not a valid primitive special form. Has `callee` and `args` keys. 31 | - `BadName`: Attempted to bind a name that contains special characters or is otherwise invalid. 32 | - `BadArgs`: Arguments to a callable were wrong. This can cover a lot of cases; it usually includes `fn` and `args` keys for disambiguation. 33 | - `BadPattern`: Cannot parse a pattern. Has `pattern` key. 34 | - `NotCallable`: Object is not callable. Has `callee` and `args` keys. 35 | - `NotJSON`: Tried to convert a magic object to JSON. Has a `value` key. 36 | - `ParseFailed`: Failed to parse Jaspr or JSON source. Has `filename`, `line`, `column` keys. 37 | - `ReadFailed`: Filesystem error. 38 | - `WriteFailed`: Filesystem error. 39 | 40 | ## Handler Macros 41 | 42 | ### `catch` 43 | 44 | `(catch body pat₀ handler₀ pat₁ handler₁ … patₙ handlerₙ)` evaluates `body` with a signal handler. When a signal is thrown in `body`, 45 | 46 | 1. The fiber evaluating `body` is canceled. 47 | 2. The signal is matched against each pattern `pat₀`…`patₙ`. 48 | 3. If any `pat` matches, the corresponding `handler` is evaluated, and its return value becomes the return value of the `catch` form. 49 | 4. If no `pat` matches, the signal is re-raised. 50 | 5. If a parent signal handler resumes the re-raised signal, the resumed value becomes the return value of the `catch` form, and `body` is still canceled. 51 | 52 | > (catch 'pass 53 | > _ 'fail) ;= "pass" 54 | 55 | > (catch (await (raise 'err) 'pass) 56 | > _ 'fail) ;= "fail" 57 | 58 | > (catch (await (raise 'err) 'pass) 59 | > e e) ;= "err" 60 | 61 | > (catch (await (raise {err: 'bar, msg: "error message"}) 'pass) 62 | > {err: 'foo, msg} ([] 1 msg) 63 | > {err: 'bar, msg} ([] 2 msg) 64 | > {err: 'baz, msg} ([] 3 msg)) ;= [2, "error message"] 65 | 66 | If a `handler` raises a signal, that signal is handled by `catch`'s parent signal handler. 67 | 68 | > (catch (catch (raise 'inner) 69 | > _ (raise 'outer)) 70 | > err ([] err)) ;= [“outer”] 71 | 72 | `catch` only resolves once its return value has _deeply_ resolved, to guarantee that uncatchable signals aren't raised after `catch` has already returned. However, if `catch` spawns fibers that aren't incorporated into its return value---for example, with `do`---and those fibers are still running when `catch` resolves, they will be canceled. 73 | 74 | > (define {ch: (chan!), _: (await (sleep 200) (send! 'outer ch))} { 75 | > returned: (catch (do (await (sleep 100) (raise 'inner)) true) 76 | > x (do (send! x ch) false)), 77 | > raised: ('value (recv! ch)) 78 | > }) ;= {returned: true, raised: “outer”} 79 | 80 | `catch` raises a `BadArgs` error at macro expansion time if it has an even number of arguments, or a `BadPattern` error at macro expansion time if one of the `pat` patterns is not a valid pattern. 81 | 82 | --- 83 | 84 | macro.catch: 85 | (fn body … patterns 86 | `[catchWith (fn .err. (case .err. ~@patterns 87 | _ (raise .err.))) 88 | ~body]) 89 | 90 | ### `resume` 91 | 92 | `(resume body pat₀ handler₀ pat₁ handler₁ … patₙ handlerₙ)` evaluates `body` with a signal handler. When a signal is thrown in `body`, 93 | 94 | 1. The signal is matched against each pattern `pat₀`…`patₙ`. 95 | 2. If any `pat` matches, the corresponding `handler` is evaluated, and the signal is resumed with its return value. 96 | 3. If no `pat` matches, the signal is re-raised. 97 | 4. If a parent signal handler resumes the re-raised signal, the original signal is resumed with its resume value. 98 | 99 | > (resume 'pass 100 | > _ 'fail) ;= "pass" 101 | 102 | > (resume (raise 'fail) 103 | > _ 'resume) ;= "resume" 104 | 105 | > (resume (raise 'err) 106 | > e e) ;= "err" 107 | 108 | > (resume (cons null (raise {err: 'bar, msg: "error message"})) 109 | > {err: 'foo, msg} ([] 1 msg) 110 | > {err: 'bar, msg} ([] 2 msg) 111 | > {err: 'baz, msg} ([] 3 msg)) ;= [null, 2, "error message"] 112 | 113 | If a `handler` raises a signal, that signal is handled by `resume`'s parent signal handler. 114 | 115 | > (resume (resume (raise 'inner) 116 | > _ (raise 'outer)) 117 | > err ([] err)) ;= [“outer”] 118 | 119 | `resume` raises a `BadArgs` error at macro expansion time if it has an even number of arguments, or a `BadPattern` error at macro expansion time if one of the `pat` patterns is not a valid pattern. 120 | 121 | --- 122 | 123 | macro.resume: 124 | (fn body … patterns 125 | `[resumeWith (fn .err. (case .err. ~@patterns 126 | _ (raise .err.))) 127 | ~body]) 128 | 129 | ### `catchWith` 130 | 131 | `(catchWith handler body)` evaluates `body` with `handler` as its signal handler function. If any signal is raised in `body`, the fiber evaluating `body` will be canceled, the signal will be passed to `handler`, and the return value of `handler` will become the return value of the `catchWith` expression. 132 | 133 | > (catchWith (const 'fail) 'pass) ;= “pass” 134 | > (catchWith (const 'fail) (await (raise 'err) 'pass)) ;= “fail” 135 | > (catchWith id (await (raise 'err) 'pass)) ;= “err” 136 | 137 | If `handler` raises a signal, that signal is handled by `catchWith`'s parent signal handler. 138 | 139 | > (catchWith [] (catchWith (fn- x (raise 'outer)) 140 | > (raise 'inner))) ;= [“outer”] 141 | 142 | `catchWith` only resolves once its return value has _deeply_ resolved, to guarantee that uncatchable signals aren't raised after `catchWith` has already returned. However, if `catchWith` spawns fibers that aren't incorporated into its return value---for example, with `do`---and those fibers are still running when `catchWith` resolves, they will be canceled. 143 | 144 | > (define {ch: (chan!), _: (await (sleep 200) (send! 'outer ch))} { 145 | > returned: (catchWith (fn- x (do (send! x ch) false)) 146 | > (do (await (sleep 100) (raise 'inner)) true)), 147 | > raised: ('value (recv! ch)) 148 | > }) ;= {returned: true, raised: “outer”} 149 | 150 | The pattern-matching `catch` macro is better suited than `catchWith` to most use cases. 151 | 152 | --- 153 | 154 | macro.catchWith: 155 | (fn- handler body 156 | `[define {.ch.: (chan!) .last.: (getDynamic p.signalHandler) .hfn.: ~handler} 157 | (choice ('value (recv! .ch.)) 158 | (letDynamic p.signalHandler 159 | (fn- x (letDynamic p.signalHandler .last. 160 | (send! (.hfn. x) .ch.))) 161 | (await (send! ~body .ch.) (never))))]) 162 | 163 | ### `resumeWith` 164 | 165 | `(resumeWith handler body)` evaluates `body` with `handler` as its signal handler function. If any signal is raised in `body`, the `raise` call will return the result of calling `handler` with the signal. 166 | 167 | > (resumeWith (const 'resume) 'pass) ;= “pass” 168 | > (resumeWith (const 'resume) (raise 'fail)) ;= “resume” 169 | > (resumeWith id (raise 'err)) ;= “err” 170 | 171 | If `handler` raises a signal, that signal is handled by `resumeWith`'s parent signal handler. 172 | 173 | > (resumeWith [] 174 | > (resumeWith (fn- x (raise 'outer)) 175 | > {return: (raise 'inner)})) ;= {return: [“outer”]} 176 | 177 | The pattern-matching `resume` macro is better suited than `resumeWith` to most use cases. 178 | 179 | --- 180 | 181 | macro.resumeWith: 182 | (fn- handler body 183 | `[define {.last.: (getDynamic p.signalHandler) .hfn.: ~handler} 184 | (letDynamic p.signalHandler 185 | (fn- x (letDynamic p.signalHandler .last. (.hfn. x))) 186 | ~body)]) 187 | 188 | ## Exports 189 | 190 | $export: { 191 | catch resume catchWith resumeWith 192 | 🚨:resume 🚧:catch 193 | } 194 | 195 | [☙ Pattern Matching][prev] | [🗏 Table of Contents][toc] | [Streams and Pipelines ❧][next] 196 | :---|:---:|---: 197 | 198 | [toc]: jaspr.jaspr.md 199 | [prev]: pattern-matching.jaspr.md 200 | [next]: streams.jaspr.md 201 | -------------------------------------------------------------------------------- /test/ParserTest.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import Parser from '../src/Parser' 3 | import {readFileSync} from 'fs' 4 | require('source-map-support').install({ 5 | handleUncaughtExceptions: false 6 | }) 7 | 8 | function parse(str: string, filename = "") { 9 | const parser = new Parser(filename) 10 | parser.read(str) 11 | return parser.getOneResult() 12 | } 13 | 14 | describe('the parser', () => { 15 | it('parses top-level null', () => expect(parse('null')).to.equal(null)) 16 | it('parses top-level true', () => expect(parse('true')).to.equal(true)) 17 | it('parses top-level false', () => expect(parse('false')).to.equal(false)) 18 | it('parses top-level numbers', () => { 19 | expect(parse('1')).to.equal(1) 20 | expect(parse('100')).to.equal(100) 21 | expect(parse('1.0')).to.equal(1.0) 22 | expect(parse('3.14')).to.equal(3.14) 23 | expect(parse('-1')).to.equal(-1) 24 | expect(parse('+1')).to.equal(+1) 25 | expect(parse('-753.571')).to.equal(-753.571) 26 | expect(parse('1e2')).to.equal(1e2) 27 | expect(parse('1E2')).to.equal(1E2) 28 | expect(parse('1.23e45')).to.equal(1.23e45) 29 | expect(parse('1.23e+45')).to.equal(1.23e+45) 30 | expect(parse('1.23e-45')).to.equal(1.23e-45) 31 | expect(parse('1.23E+45')).to.equal(1.23E+45) 32 | expect(parse('1.23E-45')).to.equal(1.23E-45) 33 | expect(parse('-1.2e-3')).to.equal(-1.2e-3) 34 | }) 35 | it('parses top-level unquoted strings', () => { 36 | expect(parse('a')).to.equal('a') 37 | expect(parse('foo')).to.equal('foo') 38 | expect(parse('this-is-a-string')).to.equal('this-is-a-string') 39 | expect(parse('*')).to.equal('*') 40 | expect(parse('!@#$%^&*?<>-+_=\\/')).to.equal('!@#$%^&*?<>-+_=\\/') 41 | expect(parse('example.com')).to.equal('example.com') 42 | expect(parse('one1')).to.equal('one1') 43 | expect(parse('⚙foo')).to.equal('⚙foo') 44 | }) 45 | it('ignores whitespace around top-level unquoted strings', () => { 46 | expect(parse(' foo')).to.equal('foo') 47 | expect(parse('foo ')).to.equal('foo') 48 | expect(parse(' foo ')).to.equal('foo') 49 | expect(parse('\n\r\tfoo\v\f')).to.equal('foo') 50 | expect(parse(` 51 | 52 | foo 53 | 54 | `)).to.equal('foo') 55 | }) 56 | it('ignores line comments', () => { 57 | expect(parse(` 58 | // This is a line comment. 59 | foo 60 | `)).to.equal('foo') 61 | expect(parse(` 62 | // This is a line comment. 63 | // This is another. 64 | foo 65 | // This is a comment at the end of the file. 66 | `)).to.equal('foo') 67 | expect(parse(` 68 | //thisisalinecommentwithnospaces 69 | foo 70 | `)).to.equal('foo') 71 | expect(parse(` 72 | // This is a line comment. 73 | // This is another. 74 | foo 75 | // This is a comment at the end of the file`)).to.equal('foo') 76 | expect(parse(` 77 | // This // line // comment // contains // the // delimiter 78 | ////////////////////////////////////////////////////////// 79 | foo 80 | `)).to.equal('foo') 81 | expect(parse(` 82 | // JS style! 83 | ; Lisp style! 84 | 💭 Emoji style! 85 | foo 86 | `)).to.equal('foo') 87 | }) 88 | it('ignores block comments', () => { 89 | expect(parse('/* This is a block comment. */ foo')).to.equal('foo') 90 | expect(parse('foo /* This is a block comment. */')).to.equal('foo') 91 | expect(parse('/* bar */ foo /* baz */')).to.equal('foo') 92 | expect(parse('/*bar*/ foo /*baz*/')).to.equal('foo') 93 | expect(parse(`/** 94 | This is a documentation comment! 95 | 96 | @foo: bar fhqwhgads 97 | */ foo `)).to.equal('foo') 98 | }) 99 | it("doesn't mistake slashes in unquoted strings for comments", () => { 100 | expect(parse('/')).to.equal('/') 101 | expect(parse('b/w')).to.equal('b/w') 102 | expect(parse('/foo/')).to.equal('/foo/') 103 | expect(parse('-//-')).to.equal('-//-') 104 | expect(parse('-/*-')).to.equal('-/*-') 105 | expect(parse('*/')).to.equal('*/') 106 | expect(parse('*//')).to.equal('*//') 107 | }) 108 | it("allows single-character line comments adjacent to strings", () => { 109 | expect(parse('foo;bar')).to.equal('foo') 110 | }) 111 | it('parses top-level quoted strings', () => { 112 | expect(parse('""')).to.equal('') 113 | expect(parse('"a"')).to.equal('a') 114 | expect(parse('"foo"')).to.equal('foo') 115 | expect(parse('"this is a string"')).to.equal('this is a string') 116 | expect(parse('"\'"')).to.equal("'") 117 | expect(parse('"⚙foo"')).to.equal('⚙foo') 118 | expect(parse('"one\ntwo"')).to.equal('one\ntwo') 119 | }) 120 | it('supports all kinds of Unicode quotation marks', () => { 121 | // These should all say “Hello, world!” 122 | // If not, blame Google Translate. 123 | expect(parse('“Hello, world!”')).to.equal("Hello, world!") 124 | expect(parse('‘¡Hola Mundo!’')).to.equal("¡Hola Mundo!") 125 | expect(parse('„Hallo Welt!“')).to.equal("Hallo Welt!") 126 | expect(parse('‚Ahoj světe!‘')).to.equal("Ahoj světe!") 127 | expect(parse('„Hallo Wereld!”')).to.equal("Hallo Wereld!") 128 | expect(parse('”Hej världen!”')).to.equal("Hej världen!") 129 | expect(parse('’Здраво Свете!’')).to.equal("Здраво Свете!") 130 | expect(parse('«Bonjour monde!»')).to.equal("Bonjour monde!") 131 | expect(parse('‹Selam Dünya!›')).to.equal("Selam Dünya!") 132 | expect(parse('»Hej Verden!«')).to.equal("Hej Verden!") 133 | expect(parse('›Dobrý deň, svet!‹')).to.equal("Dobrý deň, svet!") 134 | expect(parse('»Hei maailma!»')).to.equal("Hei maailma!") 135 | expect(parse('「你好,世界!」')).to.equal("你好,世界!") 136 | expect(parse('『こんにちは世界!』')).to.equal("こんにちは世界!") 137 | expect(parse('《안녕, 세상!》')).to.equal("안녕, 세상!") 138 | expect(parse('〈안녕, 세상!〉')).to.equal("안녕, 세상!") 139 | }) 140 | it('normalizes Unicode in unquoted strings', () => { 141 | expect(parse("log₁₀")).to.equal("log10") 142 | expect(parse("√½")).to.equal("√1⁄2") 143 | expect(parse("fullwidth")).to.equal("fullwidth") 144 | }) 145 | it('supports standard JSON escapes in quoted strings', () => { 146 | expect(parse('"\\n"')).to.equal("\n") 147 | expect(parse('"\\r"')).to.equal("\r") 148 | expect(parse('"\\t"')).to.equal("\t") 149 | expect(parse('"\\f"')).to.equal("\f") 150 | expect(parse('"\\b"')).to.equal("\b") 151 | expect(parse('"\\v"')).to.equal("\v") 152 | expect(parse('"\\\\n"')).to.equal("\\n") 153 | expect(parse('"\\/"')).to.equal("/") 154 | expect(parse('"\\\'"')).to.equal("'") 155 | expect(parse('"\\""')).to.equal('"') 156 | }) 157 | it('can nest unrelated quote styles', () => { 158 | expect(parse('"“"')).to.equal("“") 159 | expect(parse('"”"')).to.equal("”") 160 | expect(parse('“"”')).to.equal('"') 161 | expect(parse('"I said, “Hello, world!”"')).to.equal('I said, “Hello, world!”') 162 | expect(parse('«I said, “Hello, world!”»')).to.equal('I said, “Hello, world!”') 163 | }) 164 | it('can nest the same quote style, if it has non-identical quote characters', () => { 165 | expect(parse('“ “ “ ” ” ”')).to.equal(" “ “ ” ” ") 166 | expect(parse('« « « » » »')).to.equal(" « « » » ") 167 | expect(parse('«««»»»')).to.equal("««»»") 168 | expect(parse('“I said, “Hello, world!””')).to.equal("I said, “Hello, world!”") 169 | }) 170 | it('parses empty structures', () => { 171 | expect(parse('()')).to.deep.equal([]) 172 | expect(parse('[]')).to.deep.equal([]) 173 | expect(parse('{}')).to.deep.equal({}) 174 | }) 175 | it('parses simple arrays without commas', () => { 176 | expect(parse('(1 2 3 4)')).to.deep.equal([1, 2, 3, 4]) 177 | expect(parse('[foo bar baz quux]')).to.deep.equal(['foo', 'bar', 'baz', 'quux']) 178 | }) 179 | it('parses simple arrays with commas', () => { 180 | expect(parse('(1, 2, 3, 4)')).to.deep.equal([1, 2, 3, 4]) 181 | expect(parse('[foo, bar, baz, quux]')).to.deep.equal(['foo', 'bar', 'baz', 'quux']) 182 | }) 183 | it('parses simple objects without commas', () => { 184 | expect(parse('{a: 1 b: 2}')).to.deep.equal({a: 1, b: 2}) 185 | expect(parse('{"foo": "bar" baz: quux}')).to.deep.equal({'foo': 'bar', 'baz': 'quux'}) 186 | }) 187 | it('parses simple objects with commas', () => { 188 | expect(parse('{a: 1, b: 2}')).to.deep.equal({a: 1, b: 2}) 189 | expect(parse('{"foo": "bar", baz: quux}')).to.deep.equal({'foo': 'bar', 'baz': 'quux'}) 190 | }) 191 | it('parses a top-level object without braces', () => { 192 | expect(parse('a: 1 b: 2')).to.deep.equal({a: 1, b: 2}) 193 | expect(parse('"foo": "bar" baz: quux')).to.deep.equal({'foo': 'bar', 'baz': 'quux'}) 194 | }) 195 | it('parses a top-level object without braces, with commas', () => { 196 | expect(parse('a: 1, b: 2')).to.deep.equal({a: 1, b: 2}) 197 | expect(parse('"foo": "bar", baz: quux')).to.deep.equal({'foo': 'bar', 'baz': 'quux'}) 198 | }) 199 | it ('parses top-level quoted forms', () => { 200 | expect(parse("'null")).to.deep.equal(["", null]) 201 | expect(parse("'(1 2 3 4)")).to.deep.equal(["", [1, 2, 3, 4]]) 202 | expect(parse("'{a: b, c: d}")).to.deep.equal(["", {a: 'b', c: 'd'}]) 203 | }) 204 | it('parses top-level syntax-quoted forms', () => { 205 | expect(parse("`null")).to.deep.equal(["$syntaxQuote", null]) 206 | expect(parse("`(1 2 3 4)")).to.deep.equal(["$syntaxQuote", [1, 2, 3, 4]]) 207 | expect(parse("`{a: b, c: d}")).to.deep.equal(["$syntaxQuote", {a: 'b', c: 'd'}]) 208 | }) 209 | it('quotes quoted strings inside parentheses', () => { 210 | expect(parse('("foo")')).to.deep.equal([["", "foo"]]) 211 | expect(parse('("foo" ("bar" "baz"))')).to.deep.equal( 212 | [["", "foo"], [["", "bar"], ["", "baz"]]]) 213 | }) 214 | it('does not quote unquoted strings inside parentheses', () => { 215 | expect(parse('(foo)')).to.deep.equal(["foo"]) 216 | expect(parse('(foo (bar baz))')).to.deep.equal(["foo", ["bar", "baz"]]) 217 | }) 218 | it('does not quote quoted strings inside brackets', () => { 219 | expect(parse('["foo"]')).to.deep.equal(["foo"]) 220 | expect(parse('["foo" ["bar" "baz"]]')).to.deep.equal(["foo", ["bar", "baz"]]) 221 | }) 222 | it('uses quoting style of innermost brackets/parens', () => { 223 | expect(parse('(["foo"])')).to.deep.equal([["foo"]]) 224 | expect(parse('("foo" [("bar") "baz"])')).to.deep.equal( 225 | [["", "foo"], [[["", "bar"]], "baz"]]) 226 | }) 227 | it('keeps innermost quoting style inside braces', () => { 228 | expect(parse('[{foo: "bar"}]')).to.deep.equal([{foo: "bar"}]) 229 | expect(parse('({foo: "bar"})')).to.deep.equal([{foo: ["", "bar"]}]) 230 | expect(parse('[({foo: "bar"})]')).to.deep.equal([[{foo: ["", "bar"]}]]) 231 | expect(parse('([{foo: "bar"}])')).to.deep.equal([[{foo: "bar"}]]) 232 | }) 233 | it("can parse the project's JSON configuration files", () => { 234 | const file1 = readFileSync('package.json').toString() 235 | expect(parse(file1, 'package.json')).to.deep.equal(JSON.parse(file1)) 236 | const file2 = readFileSync('package-lock.json').toString() 237 | expect(parse(file2, 'package-lock.json')).to.deep.equal(JSON.parse(file2)) 238 | const file3 = readFileSync('tsconfig.json').toString() 239 | expect(parse(file3, 'tsconfig.json')).to.deep.equal(JSON.parse(file3)) 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /jaspr/pattern-matching.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ String Operations][prev] | [🗏 Table of Contents][toc] | [Signals and Error Handling ❧][next] 2 | :---|:---:|---: 3 | 4 | # Pattern Matching 5 | 6 | The `case`, `fn`, and `let` macros interpret certain forms as patterns, and match those patterns against values. 7 | 8 | ## Patterns 9 | 10 | `null`, boolean, and number patterns match values equal to themselves, and do not bind any names. 11 | 12 | String patterns that are valid names (not containing `.` or any reserved character, and not starting with `$`) match anything, and bind the matched value to the string. The string `“_”` (underscore) does not bind any variables, and can be used to ignore parts of a pattern. 13 | 14 | Array patterns of length 2 starting with `“”` (quoted forms) match only values equal to the second element of the pattern, and do not bind any names. 15 | 16 | Array patterns whose first element is not `“”` (the quote macro) and which do not contain the string `“...”` match arrays of the same length, after matching each element of the pattern to each element of the array. 17 | 18 | Array patterns consisting of _n_ elements, then the string `“...”`, then a string that is a valid name (the _rest name_), match any array of length _n_ or greater and bind all except the first _n_ elements of the array to the rest name, after matching the first _n_ elements of the pattern to the first _n_ elements of the array. 19 | 20 | > **ℹ Tip:** Because unquoted strings are normalized (see [Unicode](syntax.jaspr.md#unicode)), the character `U+2026 HORIZONTAL ELLIPSIS (…)` can be used interchangeably with the sequence `...`. 21 | 22 | Object patterns match objects containing at least the same keys as the pattern, after matching the values of the pattern to the corresponding values of the object. 23 | 24 | All other values are not legal patterns; using an illegal pattern in a pattern-matching macro will raise a `BadPattern` error at macro expansion time. 25 | 26 | ### Pattern Code Generation Functions 27 | 28 | The pattern-matching macros use two internal, unexported functions to generate pattern-matching code: `makePatternTest` and `makePatternBindings`. 29 | 30 | `(makePatternTest pat val fn?)` generates the predicate that tests whether the result of evaluating `val` matches the pattern `pat`. The flag `fn?` is `true` when `makePatternTest` is called from `fn`; if it is true, `makePatternTest` omits the `array?` test for array patterns, because a function's arguments are always an array. 31 | 32 | makePatternTest: 33 | (fn- pat val fn? 34 | (if (or (null? pat) (boolean? pat) (number? pat)) 35 | `[p.is? ~val ~pat] 36 | (string? pat) 37 | true 38 | (object? pat) 39 | `[and (object? ~val) 40 | (hasKeys? ~@(map quote (keys pat)) ~val) 41 | ~@(map (\x makePatternTest (x pat) ([] (quote x) val) false) 42 | (keys pat))] 43 | (and (= (len pat) 2) (emptyString? (0 pat))) 44 | `[= ~val ~pat] 45 | (and (>= (len pat) 2) (= “...” (-2 pat)) (string? (-1 pat))) 46 | `[and ~(if fn? true `(array? ~val)) 47 | (>= (len ~val) ~(sub (len pat) 2)) 48 | ~@(makeArray 49 | (\x makePatternTest (x pat) ([] (quote x) val) false) 50 | (sub (len pat) 2))] 51 | (none? (\ or (= “...” _) (emptyString? _)) pat) 52 | `[and ~(if fn? true `(array? ~val)) 53 | (= (len ~val) ~(len pat)) 54 | ~@(makeArray 55 | (\x makePatternTest (x pat) ([] (quote x) val) false) 56 | (len pat))] 57 | (raise {err: “BadPattern”, pattern: pat}))) 58 | 59 | makePatternBindings: 60 | (fn- pat val 61 | (define { 62 | recur: (\->> (map (\x makePatternBindings (x pat) ([] (quote x) val))) 63 | (apply merge)) 64 | } (if (or (null? pat) (boolean? pat) (number? pat) (p.is? pat “_”)) 65 | {} 66 | (string? pat) 67 | (if (or (emptyString? pat) (substring? “.” pat)) 68 | (raise {err: “BadPattern”, pattern: pat}) 69 | ({} pat val)) 70 | (object? pat) 71 | (recur (keys pat)) 72 | (and (= (len pat) 2) (emptyString? (0 pat))) 73 | {} 74 | (and (>= (len pat) 2) (= “...” (-2 pat)) (string? (-1 pat))) 75 | (->> (indexes pat) 76 | (drop -2) 77 | recur 78 | (withKey (-1 pat) `[drop ~(sub (len pat) 2) ~val])) 79 | (recur (indexes pat))))) 80 | 81 | ## `case` 82 | 83 | `(case x pat₀ expr₀ pat₁ expr₁ … patₙ exprₙ)` matches each of the patterns `pat₀`…`patₙ` to `x` until one of them matches, then evaluates and returns the corresponding `expr` in a scope where unquoted strings in the pattern are bound to the corresponding elements of `x`. Only one of the `expr`s is evaluated. 84 | 85 | > (case 1 86 | > 0 “no” 87 | > 1 “yes” 88 | > 1 “no”) ;= “yes” 89 | 90 | > (case 42 x x) ;= 42 91 | 92 | > (case '[42] [x] x) ;= 42 93 | 94 | > (case “bar” 95 | > “foo” “no” 96 | > “bar” “yes” 97 | > “baz” “no”) ;= “yes” 98 | 99 | > (case '[1 2 3] 100 | > [] “no” 101 | > [1 2 3 4] “no” 102 | > [1 x y] `[~x ~y]) ;= [2, 3] 103 | 104 | > (case '[1 2 3] 105 | > [x … xs] `[~x ~xs]) ;= [1, [2, 3]] 106 | 107 | > (case [] 108 | > [x … xs] “no” 109 | > [] “yes”) ;= “yes” 110 | 111 | > (case {a: 1, b: 2} 112 | > {b c} “no” 113 | > {a b} `[~a ~b]) ;= [1, 2] 114 | 115 | > (case {a: 1, b: 2} 116 | > {a} “yes” 117 | > {a b} “no”) ;= “yes” 118 | 119 | > (case {a: 1, b: 2} 120 | > {a: 2 b} “no” 121 | > {a: 1 b: 3} “no” 122 | > {a: 1 b} “yes”) ;= “yes” 123 | 124 | `case` raises a `BadArgs` error at macro expansion time if it has an even number of arguments, a `BadPattern` error at macro expansion time if one of `pat₀`…`patₙ` is not a legal pattern, or a `NoMatch` error at runtime if no pattern matches `x`. 125 | 126 | --- 127 | 128 | macro.case: 129 | (fn* exprs 130 | (define { 131 | valExpr: (hd exprs), 132 | useLet: (or (array? valExpr) (object? valExpr)), 133 | val: (if useLet (gensym!) valExpr), 134 | clauses: (->> (tl exprs) 135 | (chunk 2) 136 | (mapcat (fn- pair 137 | (define {pat: (0 pair), expr: (1 pair)} 138 | `[~(makePatternTest pat val false) 139 | (define ~(makePatternBindings pat val) ~expr)])))), 140 | ifExpr: `[if ~@clauses (raise { 141 | err: “NoMatch”, 142 | fn: ~(myName), 143 | val: ~val 144 | })] 145 | } (if useLet `[define ~({} val valExpr) ~ifExpr] ifExpr))) 146 | 147 | ## `fn` 148 | 149 | `fn` defines a function using pattern matching. A `fn` form is made up of _clauses_ separated by the string `“.”`. Each clause is zero or more argument patterns, followed by a function body. When the function defined by `fn` is called, the array of arguments is matched against the argument patterns of each clause, in order, until one matches; if no clause matches, a `BadArgs` error is raised. 150 | 151 | > ((fn 42)) ;= 42 152 | 153 | > ((fn x (add 1 x)) 2) ;= 3 154 | 155 | > ((fn 1 'a 156 | > . 2 'b 157 | > . 3 'c) 2) ;= “b” 158 | 159 | > (define { 160 | > recursiveSum: (fn [] 0 161 | > . [x … xs] (add x (recursiveSum xs))) 162 | > } (recursiveSum '[1 2 3 4])) ;= 10 163 | 164 | --- 165 | 166 | macro.fn: 167 | (fn* args 168 | (define { 169 | clauses: (mapcat (fn- clause 170 | (define {pat: (init clause), body: (last clause)} 171 | `[~(makePatternTest pat argsName true) 172 | (define ~(makePatternBindings pat argsName) 173 | ~body)])) 174 | (split “.” args)), 175 | ifExpr: `[if ~@clauses (raise { 176 | err: “BadArgs”, 177 | why: “no pattern match for arguments”, 178 | fn: (myName), 179 | args: ~argsName 180 | })] 181 | } `[closure {} (define ~({} argsName '$args) ~ifExpr)])) 182 | 183 | ## `let` 184 | 185 | `let` is the sequential, pattern-matching variant of `define`. 186 | 187 | `(let pat₀ val₀ pat₁ val₁ … patₙ valₙ body)` evaluates `val₀`…`valₙ` in order, matching each pattern `pat` to the corresponding `val`, with the resulting bindings available when evaluating subsequent `val`s. It then returns the result of evaluating `body` with all bindings from all patterns in scope. 188 | 189 | > (let 42) ;= 42 190 | 191 | > (let x 91 192 | > x) ;= 91 193 | 194 | > (let [x y z] '[1 2 3] 195 | > y) ;= 2 196 | 197 | > (let x 1 198 | > y 2 199 | > {x y}) ;= {x: 1, y: 2} 200 | 201 | > (let [x … xs] '[1 2 3] 202 | > {x xs}) ;= {x: 1, xs: [2 3]} 203 | 204 | Unlike `define`, `let` does not allow recursive definitions. 205 | 206 | `let` raises a `BadArgs` error at macro expansion time if it has an even number of arguments, a `BadPattern` error at macro expansion time if one of `pat₀`…`patₙ` is not a legal pattern, or a `NoMatch` error at runtime if any `val` does not match its corresponding pattern. 207 | 208 | --- 209 | 210 | macro.let: 211 | (fn body body 212 | . pat val … rest 213 | `[case ~val 214 | ~pat (let ~@rest) 215 | .value. (raise { err: “NoMatch”, fn: ~(myName), 216 | pattern: ~(quote pat), value: .value. })]) 217 | 218 | ## `awaitLet` 219 | 220 | `awaitLet` is a combination of `let` and `await`. 221 | 222 | `(awaitLet pat₀ val₀ pat₁ val₁ … patₙ valₙ body)` evaluates `val₀`…`valₙ` in order, matching each pattern `pat` to the corresponding `val`, with the resulting bindings available when evaluating subsequent `val`s. 223 | 224 | > (awaitLet 42) ;= 42 225 | 226 | > (awaitLet x 91 227 | > x) ;= 91 228 | 229 | > (awaitLet [x y z] '[1 2 3] 230 | > y) ;= 2 231 | 232 | > (awaitLet x 1 233 | > y 2 234 | > {x y}) ;= {x: 1, y: 2} 235 | 236 | > (awaitLet [x … xs] '[1 2 3] 237 | > {x xs}) ;= {x: 1, xs: [2 3]} 238 | 239 | Each `val` is only evaluated after the previous `val` has resolved, as in `await`. Once all `val`s have resolved, `awaitLet` returns the result of evaluating `body` with all bindings from all patterns in scope. 240 | 241 | > (define {c: (chan!)} 242 | > (do (await (send! 10 c) (send! 20 c) (send! 30 c)) 243 | > (awaitLet {value: x} (recv! c) 244 | > {value: y} (recv! c) 245 | > {value: z} (recv! c) 246 | > ([] x y z)))) ;= [10, 20, 30] 247 | 248 | `awaitLet` raises a `BadArgs` error at macro expansion time if it has an even number of arguments, a `BadPattern` error at macro expansion time if one of `pat₀`…`patₙ` is not a legal pattern, or a `NoMatch` error at runtime if any `val` does not match its corresponding pattern. 249 | 250 | --- 251 | 252 | macro.awaitLet: 253 | (fn body body 254 | . pat val … rest 255 | `[define {.awaitLet.: ~val} 256 | (await .awaitLet. 257 | (case .awaitLet. 258 | ~pat (awaitLet ~@rest) 259 | _ (raise { err: “NoMatch”, fn: ~(myName), 260 | pattern: ~(quote pat), value: .awaitLet. })))]) 261 | 262 | ## Exports 263 | 264 | $export: {case, fn, let, awaitLet, letAwait:awaitLet, 🏷:let} 265 | 266 | [☙ String Operations][prev] | [🗏 Table of Contents][toc] | [Signals and Error Handling ❧][next] 267 | :---|:---:|---: 268 | 269 | [toc]: jaspr.jaspr.md 270 | [prev]: strings.jaspr.md 271 | [next]: signals-errors.jaspr.md 272 | -------------------------------------------------------------------------------- /src/JasprPrimitive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file defines the built-in `jaspr.primitive` module. 3 | * 4 | * `jaspr.primitive` is a module that comes bundled with every Jaspr 5 | * implementation. Unlike the standard library (`jaspr`), `jaspr.primitive` is 6 | * defined in the Jaspr implementation's host language, and the functions it 7 | * exports may exhibit undefined behavior if given arguments of the wrong type. 8 | */ 9 | 10 | import { 11 | Jaspr, JasprObject, Deferred, Callback, magicSymbol, toBool, isArray, 12 | isObject, isMagic, resolveFully, has, toString 13 | } from './Jaspr' 14 | import {isDynamic, makeDynamic, Env, qualify} from './Interpreter' 15 | import {currentSchema, Module} from './Module' 16 | import {NativeFn, NativeSyncFn, NativeAsyncFn} from './NativeFn' 17 | import Chan from './Chan' 18 | import * as Names from './ReservedNames' 19 | import prettyPrint from './PrettyPrint' 20 | import * as _ from 'lodash' 21 | import {expect, AssertionError} from 'chai' 22 | const unicodeLength = require('string-length') 23 | 24 | const moduleBase: Module = { 25 | $schema: currentSchema, 26 | $module: Names.primitiveModule, 27 | $version: Names.version, 28 | $author: 'Adam R. Nelson ', 29 | $doc: ` 30 | Low-level primitives used to implement the Jaspr standard library. 31 | Should not be used directly in production code; use the \`jaspr\` module 32 | instead. 33 | `.trim().replace(/\s+/gm, ' '), 34 | $main: null, $import: {}, $export: {}, 35 | value: {}, macro: {}, check: {}, doc: {}, test: {}, qualified: {} 36 | } 37 | 38 | const constants: JasprObject = { 39 | version: Names.version, 40 | Infinity: Infinity, 41 | '-Infinity': -Infinity, 42 | 'NaN': NaN, 43 | scopeKey: null, 44 | signalHandler: null, 45 | name: null 46 | } 47 | 48 | function wrap(fn: (x: any) => Jaspr): NativeFn { 49 | return new NativeSyncFn(function(x) { return fn(x) }) 50 | } 51 | 52 | const functions: {[name: string]: NativeFn} = { 53 | typeOf: new NativeSyncFn(function(it) { 54 | if (it === null) return 'null' 55 | switch (typeof it) { 56 | case 'boolean': return 'boolean' 57 | case 'number': return 'number' 58 | case 'string': return 'string' 59 | default: return isArray(it) ? 'array' : 'object' 60 | } 61 | }), 62 | 'gensym!': new NativeSyncFn(function(name?) { 63 | return this.gensym(name ? ''+name : undefined) 64 | }), 65 | 'inspect!': new NativeAsyncFn(function([x], cb) { 66 | resolveFully(x, (err, x) => { 67 | console.log(prettyPrint(x)) 68 | cb(null, x) 69 | }) 70 | }), 71 | sleep: new NativeAsyncFn(function([ms], cb) { 72 | setTimeout(cb, ms, null) 73 | }), 74 | bool: wrap(toBool), 75 | 'is?': new NativeSyncFn(function(a, b) { return a === b }), 76 | 'magic?': new NativeSyncFn(function(it) { return isMagic(it) }), 77 | [Names.assertEquals]: new NativeAsyncFn(function([a, b], cb) { 78 | resolveFully(a, (err, a) => resolveFully(b, (err, b) => { 79 | try { 80 | if (isMagic(a) || isMagic(b)) return cb(null, a === b) 81 | expect(a).to.deep.equal(b) 82 | cb(null, true) 83 | } catch (err) { 84 | if (err instanceof AssertionError) cb({ 85 | err: 'AssertFailed', why: err.message, 86 | [magicSymbol]: err 87 | }, null) 88 | else cb(err, null) 89 | } 90 | })) 91 | }), 92 | 93 | // channels 94 | 'chanMake!': new NativeSyncFn(function() { return Chan.make() }), 95 | 'chan?': new NativeSyncFn(function(it) { return Chan.isChan(it) }), 96 | 'chanSend!': new NativeAsyncFn(function([msg, chan], cb) { 97 | resolveFully(msg, (err, msg) => { 98 | const cancel = 99 | ((chan)[magicSymbol]).send(msg, x => cb(null, x)) 100 | if (cancel) this.onCancel(cancel) 101 | }) 102 | }), 103 | 'chanRecv!': new NativeAsyncFn(function([chan], cb) { 104 | const cancel = ((chan)[magicSymbol]).recv(x => cb(null, x)) 105 | if (cancel) this.onCancel(cancel) 106 | }), 107 | 'chanClose!': new NativeSyncFn(function(chan) { 108 | return ((chan)[magicSymbol]).close() 109 | }), 110 | 'chanClosed?': new NativeSyncFn(function(chan) { 111 | return ((chan)[magicSymbol]).closed 112 | }), 113 | 114 | // dynamic variables 115 | 'dynamicMake!': new NativeSyncFn(function(def) { return makeDynamic(def) }), 116 | 'dynamic?': new NativeSyncFn(function(it) { return isDynamic(it) }), 117 | 118 | // simple math 119 | '<': new NativeSyncFn(function(a, b) { return +(a) < +(b) }), 120 | '<=': new NativeSyncFn(function(a, b) { return +(a) <= +(b) }), 121 | add: new NativeSyncFn(function(a, b) { return +(a) + +(b) }), 122 | subtract: new NativeSyncFn(function(a, b) { return +(a) - +(b) }), 123 | multiply: new NativeSyncFn(function(a, b) { return +(a) * +(b) }), 124 | divide: new NativeSyncFn(function(a, b) { return +(a) / +(b) }), 125 | remainder: new NativeSyncFn(function(a, b) { return +(a) % +(b) }), 126 | modulus: new NativeSyncFn(function(a, b) { 127 | const x = +(a), y = +(b) 128 | return (Math.abs(x) * Math.sign(y)) % y 129 | }), 130 | negate: new NativeSyncFn(function(a) { return -(a) }), 131 | 132 | // advanced math 133 | 'random!': new NativeSyncFn(function() { return Math.random() }), 134 | pow: new NativeSyncFn(function(a, b) { return Math.pow(a, b) }), 135 | sqrt: wrap(Math.sqrt), 136 | cbrt: wrap(Math.cbrt), 137 | log: wrap(Math.log), 138 | log2: wrap(Math.log2), 139 | log10: wrap(Math.log10), 140 | floor: wrap(Math.floor), 141 | ceil: wrap(Math.ceil), 142 | round: wrap(Math.round), 143 | abs: wrap(Math.abs), 144 | sin: wrap(Math.sin), 145 | cos: wrap(Math.cos), 146 | tan: wrap(Math.tan), 147 | asin: wrap(Math.asin), 148 | acos: wrap(Math.acos), 149 | atan: wrap(Math.atan), 150 | sinh: wrap(Math.sinh), 151 | cosh: wrap(Math.cosh), 152 | tanh: wrap(Math.tanh), 153 | asinh: wrap(Math.asinh), 154 | acosh: wrap(Math.acosh), 155 | atanh: wrap(Math.atanh), 156 | atan2: new NativeSyncFn(function(a, b) { return Math.atan2(a, b) }), 157 | hypot: new NativeSyncFn(function(a, b) { return Math.hypot(a, b) }), 158 | 'finite?': wrap(isFinite), 159 | 'NaN?': wrap(isNaN), 160 | 161 | // string 162 | toString: new NativeAsyncFn(function([it], cb) { 163 | resolveFully(it, (err, it) => cb(null, toString(it, true))) 164 | }), 165 | toJSON: new NativeAsyncFn(function([it], cb) { 166 | resolveFully(it, (err, it) => { 167 | if (err) cb(err, null) 168 | else cb(null, JSON.stringify(it)) 169 | }) 170 | }), 171 | fromJSON: new NativeSyncFn(function(a) { return JSON.parse(''+(a)) }), 172 | stringCompare: new NativeSyncFn(function(x, y) { 173 | const a = ''+(x), b = ''+(y) 174 | if (a < b) return -1 175 | else if (a > b) return 1 176 | else return 0 177 | }), 178 | stringConcat: new NativeSyncFn(function(a, b) { return '' + a + b }), 179 | stringNativeIndexOf: new NativeSyncFn(function(needle, haystack, start) { 180 | return String.prototype.indexOf.call( 181 | ''+(haystack), ''+(needle), (start)|0) 182 | }), 183 | stringNativeLastIndexOf: new NativeSyncFn(function(needle, haystack, start) { 184 | return String.prototype.lastIndexOf.call( 185 | ''+(haystack), ''+(needle), (start)|0) 186 | }), 187 | stringNativeLength: new NativeSyncFn(function(str) { return (''+(str)).length }), 188 | stringUnicodeLength: new NativeSyncFn(function(str) { return unicodeLength(str) }), 189 | stringNativeSlice: new NativeSyncFn(function(start, end, str) { 190 | return String.prototype.slice.call( 191 | ''+(str), (start)|0, (end)|0) 192 | }), 193 | stringUnicodeSlice: new NativeSyncFn(function(start, end, str) { 194 | let out = '', index = 0 195 | let st = (start)|0, ed = (end)|0 196 | if (st < 0) st += unicodeLength(str) 197 | if (ed < 0) ed += unicodeLength(str) 198 | if (st < 0) st = 0 199 | for (let c of ''+(str)) { 200 | if (index >= ed) break 201 | else if (index >= st) out += c 202 | index++ 203 | } 204 | return out 205 | }), 206 | stringNativeCharAt: new NativeSyncFn(function(index, str) { 207 | return String.prototype.charAt.call( 208 | ''+(str), (index)|0) 209 | }), 210 | stringUnicodeCharAt: new NativeSyncFn(function(index, str) { 211 | let i = (index)|0 212 | if (i < 0) i += unicodeLength(str) 213 | if (i < 0) return '' 214 | for (let c of ''+(str)) if (i-- <= 0) return c 215 | return '' 216 | }), 217 | stringUnicodeCodePointAt: new NativeSyncFn(function(index, str) { 218 | let i = (index)|0 219 | if (i < 0) i += unicodeLength(str) 220 | if (i < 0) return null 221 | for (let c of ''+(str)) { 222 | if (i-- <= 0) return c.codePointAt(0) 223 | } 224 | return null 225 | }), 226 | stringNativeChars: new NativeSyncFn(function(inStr) { 227 | let str = ''+(inStr), out = new Array(str.length) 228 | for (let i = 0; i < out.length; i++) out[i] = str.charAt(i) 229 | return out 230 | }), 231 | stringUnicodeChars: new NativeSyncFn(function(str) { return [...''+(str)] }), 232 | stringUnicodeCodePoints: new NativeSyncFn(function(str) { 233 | return [...''+(str)].map(c => c.codePointAt(0)) 234 | }), 235 | stringNativeFromChars: new NativeSyncFn(function(chars) { 236 | return Array.prototype.reduce.call( 237 | chars, 238 | (a: string, b: string) => a + b, '') 239 | }), 240 | stringUnicodeFromCodePoints: new NativeSyncFn(function(codePoints) { 241 | return String.fromCodePoint(...codePoints) 242 | }), 243 | stringNFC: new NativeSyncFn(function(str) { 244 | return String.prototype.normalize.call(''+(str), 'NFC') 245 | }), 246 | stringNFD: new NativeSyncFn(function(str) { 247 | return String.prototype.normalize.call(''+(str), 'NFD') 248 | }), 249 | stringNFKC: new NativeSyncFn(function(str) { 250 | return String.prototype.normalize.call(''+(str), 'NFKC') 251 | }), 252 | stringNFKD: new NativeSyncFn(function(str) { 253 | return String.prototype.normalize.call(''+(str), 'NFKD') 254 | }), 255 | 256 | // arrays 257 | [Names.arrayConcat]: new NativeSyncFn(function(...args) { 258 | let out: Jaspr[] = [] 259 | for (let next of args) out = out.concat(next) 260 | return out 261 | }), 262 | arrayLength: new NativeSyncFn(function(a) { return (a).length }), 263 | arraySlice: new NativeSyncFn(function(start, end, a) { 264 | return Array.prototype.slice.call( 265 | a, (start)|0, (end)|0) 266 | }), 267 | 268 | // objects 269 | objectHas: new NativeSyncFn(function(key, obj) { 270 | return has(obj, ''+(key)) 271 | }), 272 | objectInsert: new NativeSyncFn(function(key, val, obj) { 273 | const out = Object.create(null), o = obj 274 | for (let oldKey in o) out[oldKey] = o[oldKey] 275 | out[''+(key)] = val 276 | return out 277 | }), 278 | objectDelete: new NativeSyncFn(function(key, obj) { 279 | const out = Object.create(null), k = ''+(key), o = obj 280 | for (let oldKey in o) if (oldKey !== k) out[oldKey] = o[oldKey] 281 | return out 282 | }), 283 | objectKeys: new NativeSyncFn(function(obj) { return Object.keys(obj) }), 284 | objectValues: new NativeSyncFn(function(obj) { 285 | const o = obj 286 | return Object.keys(o).map(k => o[k]) 287 | }) 288 | } 289 | 290 | const macros: {[name: string]: string} = { 291 | apply: Names.apply, 292 | arrayMake: Names.arrayMake, 293 | closure: Names.closure, 294 | contextGet: Names.contextGet, 295 | dynamicGet: Names.dynamicGet, 296 | dynamicLet: Names.dynamicLet, 297 | eval: Names.eval_, 298 | 'if': Names.if_, 299 | junction: Names.junction, 300 | macroexpand: Names.macroexpand, 301 | objectMake: Names.objectMake, 302 | then: Names.then 303 | } 304 | 305 | moduleBase.$export = 306 | _([constants, functions, macros]) 307 | .flatMap(_.keys).map(k => [k, k]) 308 | .fromPairs().value() 309 | 310 | moduleBase.qualified = 311 | _([constants, functions, macros]) 312 | .flatMap(_.keys) 313 | .flatMap(k => [ 314 | [k, qualify(moduleBase, k)], 315 | [`${Names.primitiveModule}.${k}`, qualify(moduleBase, k)]]) 316 | .fromPairs().value() 317 | 318 | export default function JasprPrimitive(env: Env): Module { 319 | const value = _(functions) 320 | .mapValues((fn: NativeFn) => fn.toClosure(env)) 321 | .assign(constants, { 322 | scopeKey: env.closureName, 323 | signalHandler: env.signalHandlerVar, 324 | name: env.nameVar 325 | }).value() 326 | const macro = _.mapValues(macros, name => 327 | new NativeSyncFn(function(...args) { 328 | let code: Jaspr[] = [name] 329 | for (let arg of args) code.push(arg) 330 | return code 331 | }).toClosure(env)) 332 | ;[value, macro].forEach(ctx => _.assignIn(ctx, 333 | _.mapKeys(ctx, (v, k) => `${Names.primitiveModule}.${k}`), 334 | _.mapKeys(ctx, (v, k) => qualify(moduleBase, k)) 335 | )) 336 | return _.create(moduleBase, {value, macro}) 337 | } 338 | -------------------------------------------------------------------------------- /jaspr/numbers.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ Macros][prev] | [🗏 Table of Contents][toc] | [Array Operations ❧][next] 2 | :---|:---:|---: 3 | 4 | # Number Operations 5 | 6 | ## Arithmetic 7 | 8 | ### `add` 9 | 10 | Returns the sum of its arguments. Raises a `BadArgs` error if any of its arguments are not numbers. 11 | 12 | > (add 1 2 3) ;= 6 13 | 14 | The alias `+` is preferred. 15 | 16 | --- 17 | 18 | add: 19 | (fn* args 20 | (case= (len args) 21 | 2 (assertArgs (number? (0 args)) "not a number" 22 | (number? (1 args)) "not a number" 23 | (p.add (0 args) (1 args))) 24 | 0 0 25 | (assertArgs (number? (hd args)) "not a number" 26 | (p.add (hd args) (apply add (tl args)))))) 27 | 28 | 29 | ### `dec` 30 | 31 | `(dec n)` returns the predecessor of `n` (i.e., `n` - 1). 32 | 33 | > (dec 4) ;= 3 34 | 35 | `dec` raises a `BadArgs` error if `n` is not a number. 36 | 37 | --- 38 | 39 | dec: (fn- n (assertArgs (number? n) "not a number" 40 | (p.subtract n 1))) 41 | 42 | ### `div` 43 | 44 | `(div dividend divisor)` returns the quotient of `dividend` and `divisor`. Division by zero returns `NaN`. `div` raises a `BadArgs` error if either of its arguments is not a number. 45 | 46 | --- 47 | 48 | div: 49 | (fn- dividend divisor 50 | (assertArgs (number? dividend) "dividend is not a number" 51 | (number? divisor) "divisor is not a number" 52 | (p.divide dividend divisor))) 53 | 54 | ### `inc` 55 | 56 | `(inc n)` returns the successor of `n` (i.e., `n` + 1). 57 | 58 | > (inc 3) ;= 4 59 | 60 | `inc` throws a `BadArgs` error if `n` is not a number. 61 | 62 | --- 63 | 64 | inc: (fn- n (assertArgs (number? n) "not a number" 65 | (p.add n 1))) 66 | 67 | ### `minus` 68 | 69 | `minus` (more commonly used via its alias `-`) is equivalent to either `neg` or `sub`, depending on its number of arguments. 70 | 71 | `(minus x)` returns the negation of the number `x`. 72 | 73 | > (minus 42) ;= -42 74 | > (minus -91) ;= 91 75 | 76 | `(minus x0 x1 … xn)` returns the value of `x0` − (`x1` − (… − `xn`)). 77 | 78 | > (minus 5 3) ;= 2 79 | > (minus 10 5 -10) ;= -5 80 | 81 | `minus` Raises a `BadArgs` error if it receives 0 arguments or if any of its arguments are not numbers. 82 | 83 | --- 84 | 85 | minus: (fn* args (if (p.is? 1 (len args)) 86 | (neg (0 args)) 87 | (apply sub args))) 88 | 89 | ### `mod` 90 | 91 | `(mod dividend divisor)` returns the modulus of `dividend` and `divisor`. Modulus by zero returns `NaN`. `mod` raises a `BadArgs` error if either of its arguments is not a number. 92 | 93 | The distinction between remainder (`rem`/`%`) and modulus (`mod`) is that the _remainder_ has the same sign as the _dividend_, while the _modulus_ has the same sign as the _divisor_. 94 | 95 | --- 96 | 97 | mod: 98 | (fn- dividend divisor 99 | (assertArgs (number? dividend) "dividend is not a number" 100 | (number? divisor) "divisor is not a number" 101 | (p.modulus dividend divisor))) 102 | 103 | ### `mul` 104 | 105 | Returns the product of its arguments. Raises a `BadArgs` error if any of its arguments are not numbers. 106 | 107 | > (mul 6 7 -1) ;= -42 108 | 109 | The alias `*` or `×` is preferred. 110 | 111 | --- 112 | 113 | mul: 114 | (fn* args 115 | (case= (len args) 116 | 2 (assertArgs (number? (0 args)) "not a number" 117 | (number? (1 args)) "not a number" 118 | (p.multiply (0 args) (1 args))) 119 | 0 1 120 | (assertArgs (number? (hd args)) "not a number" 121 | (p.multiply (hd args) (apply mul (tl args)))))) 122 | 123 | ### `neg` 124 | 125 | Negates its argument. Raises a `BadArgs` error if its argument is not a number. 126 | 127 | > (neg 42) ;= -42 128 | > (neg -91) ;= 91 129 | 130 | The alias `-` of `minus`, which has the same functionality when called with one argument, is preferred. 131 | 132 | --- 133 | 134 | neg: (fn- n (assertArgs (number? n) "not a number" 135 | (p.negate n))) 136 | 137 | ### `product` 138 | 139 | `(product xs)` returns the product of the numbers in the array `xs`. 140 | 141 | > (product '[2 3 4]) ;= 24 142 | 143 | `(product xs)` is semantically equivalent to `(apply mul xs)`, but it uses `fold` for better performance on large lists. It raises a `BadArgs` error if `xs` is not an array of numbers. 144 | 145 | --- 146 | 147 | product: (fn- xs (fold (\xy p.multiply x y) 1 xs)) 148 | 149 | ### `rem` 150 | 151 | `(rem dividend divisor)` returns the remainder of `dividend` and `divisor`. Division by zero returns `NaN`. `rem` raises a `BadArgs` error if either of its arguments is not a number. 152 | 153 | The distinction between remainder (`rem`/`%`) and modulus (`mod`) is that the _remainder_ has the same sign as the _dividend_, while the _modulus_ has the same sign as the _divisor_. 154 | 155 | --- 156 | 157 | rem: 158 | (fn- dividend divisor 159 | (assertArgs (number? dividend) "dividend is not a number" 160 | (number? divisor) "divisor is not a number" 161 | (p.remainder dividend divisor))) 162 | 163 | ### `sub` 164 | 165 | Returns the (right-associative) difference of its arguments. Raises a `BadArgs` error if any of its arguments are not numbers. 166 | 167 | > (sub 5 3) ;= 2 168 | > (sub 5 3 2) ;= 4 169 | 170 | The alias `-` of `minus`, which has the same functionality when called with more than one argument, is preferred. 171 | 172 | --- 173 | 174 | sub: 175 | (fn* args 176 | (case= (len args) 177 | 2 (assertArgs (number? (0 args)) "not a number" 178 | (number? (1 args)) "not a number" 179 | (p.subtract (0 args) (1 args))) 180 | 0 0 181 | (assertArgs (number? (hd args)) "not a number" 182 | (p.subtract (hd args) (apply sub (tl args)))))) 183 | 184 | ### `sum` 185 | 186 | `(sum xs)` returns the sum of the numbers in the array `xs`. 187 | 188 | > (sum '[2 3 4]) ;= 9 189 | 190 | `(sum xs)` is semantically equivalent to `(apply add xs)`, but it uses `fold` for better performance on large lists. It raises a `BadArgs` error if `xs` is not an array of numbers. 191 | 192 | --- 193 | 194 | sum: (fn- xs (fold (\xy p.add x y) 0 xs)) 195 | 196 | ## Exponents, Roots and Logarithms 197 | 198 | ### `cbrt` 199 | 200 | cbrt: (\ p.cbrt _) 201 | 202 | ### `pow` 203 | 204 | pow: (\xy p.pow x y) 205 | 206 | ### `sqrt` 207 | 208 | sqrt: (\ p.sqrt _) 209 | 210 | ### `log` 211 | 212 | log: (\ p.log _) 213 | 214 | ### `log2` 215 | 216 | log2: (\ p.log2 _) 217 | 218 | ### `log10` 219 | 220 | log10: (\ p.log10 _) 221 | 222 | ## Comparison 223 | 224 | ### `<` 225 | 226 | Less-than operator. Returns `true` if all of its arguments are ordered from least to greatest and none of them are equal, `false` otherwise. 227 | 228 | > (< 1 2) ;= true 229 | > (< 2 1) ;= false 230 | > (< 1 1) ;= false 231 | > (< 1 2 3 4 5) ;= true 232 | > (< 1 2 3 3 5) ;= false 233 | > (< 5 4 3 2 1) ;= false 234 | 235 | `<` raises a `BadArgs` error if any of its arguments are not numbers. 236 | 237 | --- 238 | 239 | <: 240 | (fn* args 241 | (assertArgs (p.< 1 (len args)) "expected 2 or more arguments" 242 | (number? (0 args)) "not a number" 243 | (number? (1 args)) "not a number" 244 | (and (p.< (0 args) (1 args)) 245 | (or (p.is? (len args) 2) 246 | (apply < (tl args)))))) 247 | 248 | ### `<=` 249 | 250 | Less-than-or-equal operator. Returns `true` if all of its arguments are ordered from least to greatest, `false` otherwise. 251 | 252 | > (<= 1 2) ;= true 253 | > (<= 2 1) ;= false 254 | > (<= 1 1) ;= true 255 | > (<= 1 2 3 4 5) ;= true 256 | > (<= 1 2 3 3 5) ;= true 257 | > (<= 5 4 3 2 1) ;= false 258 | 259 | `<=` raises a `BadArgs` error if any of its arguments are not numbers. 260 | 261 | --- 262 | 263 | <=: 264 | (fn* args 265 | (assertArgs (p.< 1 (len args)) "expected 2 or more arguments" 266 | (number? (0 args)) "not a number" 267 | (number? (1 args)) "not a number" 268 | (and (p.<= (0 args) (1 args)) 269 | (or (p.is? (len args) 2) 270 | (apply <= (tl args)))))) 271 | 272 | ### `>` 273 | 274 | Greater-than operator. Returns `true` if all of its arguments are ordered from greatest to least and none of them are equal, `false` otherwise. 275 | 276 | > (> 1 2) ;= false 277 | > (> 2 1) ;= true 278 | > (> 1 1) ;= false 279 | > (> 1 2 3 4 5) ;= false 280 | > (> 5 3 3 2 1) ;= false 281 | > (> 5 4 3 2 1) ;= true 282 | 283 | `>` raises a `BadArgs` error if any of its arguments are not numbers. 284 | 285 | --- 286 | 287 | >: 288 | (fn* args 289 | (assertArgs (p.< 1 (len args)) "expected 2 or more arguments" 290 | (number? (0 args)) "not a number" 291 | (number? (1 args)) "not a number" 292 | (and (p.< (1 args) (0 args)) 293 | (or (p.is? (len args) 2) 294 | (apply > (tl args)))))) 295 | 296 | ### `>=` 297 | 298 | Greater-than-or-equal operator. Returns `true` if all of its arguments are ordered from greatest to least, `false` otherwise. 299 | 300 | > (>= 1 2) ;= false 301 | > (>= 2 1) ;= true 302 | > (>= 1 1) ;= true 303 | > (>= 1 2 3 4 5) ;= false 304 | > (>= 5 3 3 2 1) ;= true 305 | > (>= 5 4 3 2 1) ;= true 306 | 307 | `>=` raises a `BadArgs` error if any of its arguments are not numbers. 308 | 309 | --- 310 | 311 | >=: 312 | (fn* args 313 | (assertArgs (p.< 1 (len args)) "expected 2 or more arguments" 314 | (number? (0 args)) "not a number" 315 | (number? (1 args)) "not a number" 316 | (and (p.<= (1 args) (0 args)) 317 | (or (p.is? (len args) 2) 318 | (apply >= (tl args)))))) 319 | 320 | ### `max` 321 | 322 | max: 323 | (fn* ns 324 | (case= (len ns) 325 | 0 0 326 | 1 (0 ns) 327 | 2 (define {a:(0 ns) b:(1 ns)} 328 | (if (> a b) a b)) 329 | (define {half: (floor (div (len ns) 2))} 330 | (max (apply max (p.arraySlice 0 half ns)) 331 | (apply max (p.arraySlice half (len ns) ns)))))) 332 | 333 | ### `min` 334 | 335 | min: 336 | (fn* ns 337 | (case= (len ns) 338 | 0 0 339 | 1 (0 ns) 340 | 2 (define {a:(0 ns) b:(1 ns)} 341 | (if (< a b) a b)) 342 | (define {half: (floor (div (len ns) 2))} 343 | (min (apply min (p.arraySlice 0 half ns)) 344 | (apply min (p.arraySlice half (len ns) ns)))))) 345 | 346 | ### `kronecker` 347 | 348 | The [Kronecker delta](https://en.wikipedia.org/wiki/Kronecker_delta) function _δ_. `(kronecker i j)` returns `1` if `i` = `j`, `0` otherwise. 349 | 350 | > (kronecker 42 42) ;= 1 351 | > (kronecker 1 2) ;= 0 352 | 353 | This function only becomes useful when it is called by its alias `δ`, as `(δ i j)` can be used as shorthand for `(if (= i j) 1 0)`. 354 | 355 | --- 356 | 357 | kronecker: (fn- i j (if (= i j) 1 0)) 358 | 359 | ## Predicates and Rounding 360 | 361 | floor: (\ p.floor _) 362 | ceil: (\ p.ceil _) 363 | round: (\ p.round _) 364 | abs: (\ p.abs _) 365 | sign: (\ if (pos? _) 1 (neg? _) -1 (NaN? _) NaN 0) 366 | pos?: (\ < 0 _) 367 | neg?: (\ > 0 _) 368 | zero?: (\ p.is? 0 _) 369 | even?: (\-> (mod 2) (= 0)) 370 | odd?: (\-> (mod 2) (= 1)) 371 | integer?: (\ and (number? _) (no (NaN? _)) (finite? _) (= _ (floor _))) 372 | finite?: (\ p.finite? _) 373 | infinite?: (\ no (p.finite? _)) 374 | NaN?: (\ p.NaN? _) 375 | 376 | ## Trigonometry 377 | 378 | sin: (\ p.sin _) 379 | cos: (\ p.cos _) 380 | tan: (\ p.tan _) 381 | asin: (\ p.asin _) 382 | acos: (\ p.acos _) 383 | atan: (\ p.atan _) 384 | atan2: (\xy p.atan2 x y) 385 | sinh: (\ p.sinh _) 386 | cosh: (\ p.cosh _) 387 | tanh: (\ p.tanh _) 388 | asinh: (\ p.asinh _) 389 | acosh: (\ p.acosh _) 390 | atanh: (\ p.atanh _) 391 | hypot: (\xy p.hypot x y) 392 | 393 | ## Constants 394 | 395 | pi: 3.141592653589793 396 | e: 2.718281828459045 397 | sqrt2: 1.4142135623730951 398 | sqrt1/2: 0.7071067811865476 399 | ln2: 0.6931471805599453 400 | ln10: 2.302585092994046 401 | log2e: 1.4426950408889634 402 | log10e: 0.4342944819032518 403 | 404 | ## Exports 405 | 406 | $export: { 407 | add sub mul neg div rem mod minus pow inc dec sum product < <= > >= 408 | min max kronecker pow sqrt cbrt log log2 log10 409 | finite? infinite? NaN? pos? neg? even? odd? zero? integer? 410 | floor ceil round abs sign 411 | sin cos tan asin acos atan atan2 sinh cosh tanh asinh acosh atanh hypot 412 | pi e sqrt2 sqrt1/2 ln2 ln10 log2e log10e 413 | 414 | +:add *:mul ×:mul ✕:mul -:minus −:minus ÷:div %:rem ↑:inc ↓:dec 415 | ∑:sum ∏:product √:sqrt ∛:cbrt expt:pow =<:<= ≤:<= ≥:>= ∞?:infinite? 416 | ⌊:floor ⌈:ceil |:abs ±:sign ∟:hypot ∠:atan2 π:pi √2:sqrt2 √½:sqrt1/2 417 | δ:kronecker 418 | 419 | ➕:add ➖:minus ✖:mul ➗:div 420 | } 421 | 422 | [☙ Macros][prev] | [🗏 Table of Contents][toc] | [Array Operations ❧][next] 423 | :---|:---:|---: 424 | 425 | [toc]: jaspr.jaspr.md 426 | [prev]: macros.jaspr.md 427 | [next]: arrays.jaspr.md 428 | -------------------------------------------------------------------------------- /src/Module.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import * as XRegExp from 'xregexp' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import { 6 | Jaspr, JasprObject, JasprError, Json, JsonObject, Deferred, Callback, 7 | ErrCallback, isArray, isObject, has 8 | } from './Jaspr' 9 | import { 10 | Scope, emptyScope, mergeScopes, Env, evalDefs, expandAndEval, isLegalName, 11 | Namespace, qualify, validateNames 12 | } from './Interpreter' 13 | import {prefix, primitiveModule} from './ReservedNames' 14 | import Parser from './Parser' 15 | import {parseMarkdown, markdownExtensions} from './LiterateParser' 16 | 17 | /** The JSON schema URL that all Jaspr modules must contain */ 18 | export const currentSchema = "http://adam.nels.onl/schema/jaspr/module" 19 | 20 | /** Enumeration of all valid module sources */ 21 | export type ImportSource = 'local' | 'file' | 'http' | 'git' 22 | 23 | /** 24 | * An import clause, from the `$import` section of a Jaspr module. 25 | * Contains a module name and version, and specifies where the module should be 26 | * loaded from and what names can be included in the top-level scope. 27 | */ 28 | export interface Import extends JsonObject { 29 | /** Path or URL to load the module from; format depends on `via` */ 30 | from: string 31 | /** Type of source to load the module from (http, file, git, etc.) */ 32 | via: ImportSource 33 | /** Name of the module to import */ 34 | module: string 35 | /** Optional module version; if not present, most recent version */ 36 | version: string | null 37 | /** 38 | * Names from the module to include in the top-level scope (unqualified). 39 | * May create aliases. `false` is equivalent to `{}`; `true` imports 40 | * everything. 41 | */ 42 | names: boolean | { [as: string]: string } 43 | } 44 | 45 | export interface ModuleSource { 46 | $schema: string 47 | $module?: string 48 | $version?: string 49 | $doc?: string 50 | $author?: string 51 | $main?: Json 52 | $import?: { [namespace: string]: Import } 53 | $export?: { [as: string]: string } 54 | [name: string]: Json | undefined 55 | } 56 | 57 | export interface Module extends Scope, Namespace { 58 | $schema: string 59 | $module: string | null 60 | $version: string | null 61 | $doc: string | null 62 | $author: string | null 63 | $main: Jaspr | Deferred 64 | $import: { [namespace: string]: Import } 65 | $export: { [as: string]: string } 66 | } 67 | 68 | const githubRegex = /^https?:\/\/(www[.])?github[.]com\/[^\/]+\/[^\/]+/ 69 | const httpRegex = /^https?:\/\/.+/ 70 | const moduleSegment = '(\\pL|\\p{Pd}|\\p{Pc})(\\pL|\\pN|\\p{Pd}|\\p{Pc}|\\p{Sk})*' 71 | const moduleNameRegex = XRegExp(`^${moduleSegment}([.]${moduleSegment})*$`, 'A') 72 | 73 | function importSource(from: string): ImportSource { 74 | if (githubRegex.test(from)) return 'git' 75 | else if (httpRegex.test(from)) return 'http' 76 | else if (moduleNameRegex.test(from)) return 'local' 77 | return 'file' 78 | } 79 | 80 | function normalizeImports(imports?: Json): { [namespace: string]: Import } { 81 | if (imports == null) return {} 82 | if (isArray(imports)) { 83 | imports = _(imports).map(name => { 84 | if (typeof name === 'string') return [name, name] 85 | else throw {err: 'import is not a string', import: name} 86 | }).fromPairs().value() 87 | } 88 | if (!isObject(imports)) throw {err: 'imports is not an object or array'} 89 | return _.mapValues(imports, (imp, name): Import => { 90 | if (!moduleNameRegex.test(name)) throw {err: 'illegal import name', name} 91 | if (typeof imp === 'boolean') { 92 | return {from: name, via: importSource(name), module: name, version: null, names: imp} 93 | } else if (typeof imp === 'string') { 94 | return {from: imp, via: importSource(imp), module: name, version: null, names: {}} 95 | } else if (isObject(imp)) { 96 | if (imp.hasOwnProperty('from')) { 97 | if (typeof imp.from !== 'string') { 98 | throw {err: 'import.from is not a string', name, import: imp} 99 | } 100 | } else imp.from = name 101 | switch (imp.via) { 102 | case 'local': case 'file': case 'http': case 'git': 103 | break 104 | case undefined: 105 | imp.via = importSource('' + imp.from) 106 | break 107 | default: 108 | throw { 109 | err: 'import.via is not a supported source', 110 | help: 'Supported sources are "local", "file", "http", and "git".', 111 | name, import: imp 112 | } 113 | } 114 | if (imp.hasOwnProperty('module')) { 115 | if (typeof imp.from !== 'string') { 116 | throw {err: 'import.module is not a string', name, import: imp} 117 | } 118 | } else imp.module = imp.via === 'local' ? imp.from : name 119 | imp.names = normalizeExports(imp.names, 'imported name') 120 | return imp 121 | } else throw {err: 'illegal import value', name, import: imp} 122 | }) 123 | } 124 | 125 | function normalizeExports(exports?: Json, name = 'export'): { [as: string]: string } { 126 | if (exports == null) return {} 127 | if (isArray(exports)) { 128 | exports = _(exports).map(key => { 129 | if (typeof key === 'string') return [key, key] 130 | else throw {err: `${name} is not a string`, key, value: key} 131 | }).fromPairs().value() 132 | } 133 | if (!isObject(exports)) throw {err: `${name}s is not an object or array`} 134 | for (let key in exports) { 135 | const value = exports[key] 136 | if (!isLegalName(key)) throw {err: `illegal ${name} key`, key, value} 137 | if (typeof value !== 'string') throw {err: `${name} is not a string`, key, value} 138 | if (!isLegalName(value)) throw {err: `illegal ${name} value`, key, value} 139 | } 140 | return exports 141 | } 142 | 143 | export function readModuleFile( 144 | filename: string, 145 | cb: ErrCallback, 146 | history?: string[] 147 | ): void { 148 | filename = path.normalize(filename) 149 | if (history) { 150 | if (history.indexOf(filename) >= 0) { 151 | return cb(null, {$schema: currentSchema}) 152 | } 153 | history.push(filename) 154 | } 155 | fs.readFile(filename, (err, data) => { 156 | // Error check 157 | if (err != null) return cb({ 158 | err: 'ReadFailed', why: 'failed to read module file', filename, 159 | 'nodeError': { 160 | name: err.name, 161 | message: err.message, 162 | errno: err.errno || null, 163 | path: err.path || null 164 | } 165 | }, null) 166 | 167 | // Parse 168 | let src: Json 169 | try { 170 | if (_.some(markdownExtensions, e => filename.endsWith(e))) { 171 | src = parseMarkdown(data.toString('utf8'), filename) 172 | } else { 173 | const parser = new Parser(filename) 174 | parser.read(data.toString('utf8')) 175 | src = parser.getOneResult() 176 | } 177 | } catch (ex) { 178 | if (ex instanceof Parser.ParseError) { 179 | const {filename, line, column} = ex.location 180 | return cb({ 181 | err: 'ParseFailed', why: ex.message, 182 | filename: filename || null, line, column 183 | }, null) 184 | } else throw ex 185 | } 186 | 187 | if (!isObject(src)) return cb({ 188 | err: 'BadModule', why: 'module is not an object', 189 | module: src, filename 190 | }, null) 191 | 192 | // Format imports and exports 193 | src.$import = normalizeImports(src.$import || src.$imports) 194 | delete src.$imports 195 | src.$export = normalizeExports(src.$export || src.$exports) 196 | delete src.$exports 197 | 198 | // Validate string properties if this is not an include file 199 | const done: (m: ModuleSource) => void = 200 | history ? src => cb(null, src) : 201 | src => { 202 | if (src.$schema !== currentSchema) return cb({ 203 | err: 'BadModule', why: 'bad or missing $schema property', 204 | help: ` 205 | Jaspr modules must have a $schema property, and that property must 206 | be a valid Jaspr module schema location. Currently, the only 207 | supported schema is "${currentSchema}". 208 | `.trim().replace(/\s+/gm, ' '), 209 | schema: src.$schema || null, filename 210 | }, null) 211 | for (let key of ['$module', '$doc', '$author']) { 212 | if (src.hasOwnProperty(key) && typeof src[key] !== 'string') { 213 | return cb({ 214 | err: 'BadModule', why: `${key} is not a string`, 215 | [key]: src[key], filename 216 | }, null) 217 | } 218 | } 219 | if (src.hasOwnProperty('$module') && 220 | !moduleNameRegex.test('' + src.$module)) { 221 | return cb({ 222 | err: 'BadModule', why: 'bad module name ($module property)', 223 | $module: src.$module, filename 224 | }, null) 225 | } 226 | cb(null, src) 227 | } 228 | 229 | // Load includes 230 | if (src.hasOwnProperty('$include')) { 231 | const includes = src.$include, includeHistory = history || [filename] 232 | delete src.$include 233 | if (isArray(includes)) { 234 | const includeNext = (mod: ModuleSource) => { 235 | const include = includes.pop() 236 | if (include === undefined) return done(mod) 237 | else if (typeof include !== 'string') return cb({ 238 | err: 'BadModule', why: 'include is not a string', 239 | include, filename 240 | }, null) 241 | const incFilename = 242 | path.isAbsolute(include) 243 | ? include : path.join(path.dirname(filename), include) 244 | readModuleFile(incFilename, (err, included) => { 245 | if (err) return cb(err, null) 246 | includeNext(mergeModules( 247 | mod, included, 248 | filename, incFilename)) 249 | }, includeHistory) 250 | } 251 | includeNext(src) 252 | } else cb({ 253 | err: 'BadModule', why: '$include is not an array', $include: includes, 254 | filename 255 | }, null) 256 | } else done(src) 257 | }) 258 | } 259 | 260 | function mergeModules( 261 | left: ModuleSource, right: ModuleSource, 262 | lFilename: string, rFilename: string 263 | ): ModuleSource { 264 | return _.assignInWith(left, right, (l: Json, r: Json, name: string): Json => { 265 | if (l === undefined) return r 266 | if (r === undefined) return l 267 | function mergeNames(ln: any, rn: any, kind: string): {[k: string]: string} { 268 | return _.assignInWith(ln, rn, (l: string, r: string, name: string): string => { 269 | if (l === undefined) return r 270 | if (r === undefined) return l 271 | if (l !== r) throw { 272 | err: 'BadModule', why: `include failed: duplicate ${kind}`, 273 | includer: lFilename, included: rFilename, name, 274 | 'includer-value': l, 'included-value': r 275 | } 276 | return l 277 | }) 278 | } 279 | switch (name) { 280 | case '$import': 281 | return _.assignInWith(l, r, 282 | (l: Import, r: Import, name: string): Import => { 283 | if (l === undefined) return r 284 | if (r === undefined) return l 285 | if (l.from !== r.from || l.via !== r.via || l.module !== r.module) { 286 | throw { 287 | err: 'BadModule', why: 'include failed: duplicate import', 288 | includer: lFilename, included: rFilename, name, 289 | 'includer-import': l, 'included-import': r 290 | } 291 | } 292 | return { 293 | from: l.from, via: l.via, module: l.module, version: null, 294 | names: mergeNames(l.names, r.names, 'imported value') 295 | } 296 | }) 297 | case '$export': 298 | return mergeNames(l, r, 'export') 299 | default: 300 | if (l === r) return l 301 | else throw { 302 | err: 'BadModule', why: 'include failed: duplicate name', 303 | includer: lFilename, included: rFilename, name, 304 | 'includer-value': l, 'included-value': r 305 | } 306 | } 307 | }) 308 | } 309 | 310 | function loadImport( 311 | alias: string, 312 | {from, via, module, version, names} : Import, 313 | filename: string, 314 | localModules = new Map>() 315 | ): Promise { 316 | if (!moduleNameRegex.test(alias)) return Promise.reject({ 317 | err: 'BadModule', why: 'illegal import alias; contains special characters', 318 | alias, filename 319 | }) 320 | if (via !== 'local') return Promise.reject({ 321 | err: 'NotImplemented', why: `import loader ${via} is not yet implemented`, 322 | filename 323 | }) 324 | const imported = localModules.get(from) 325 | if (imported) return imported.then(mod => { 326 | if (mod.$module == null) throw { 327 | err: 'BadModule', why: 'cannot import script module', module: alias, 328 | help: 'A module without a $module key is a script module, and cannot be imported.' 329 | } 330 | return importModule(mod, alias, names === true ? undefined : (names || {})) 331 | }) 332 | else return Promise.reject({ 333 | err: 'BadModule', why: 'local import not found', importedModule: from, 334 | filename 335 | }) 336 | } 337 | 338 | export function evalModule( 339 | env: Env, 340 | module: ModuleSource, 341 | options: { 342 | filename: string, 343 | scope?: Scope, 344 | runMain?: boolean, 345 | localModules?: Map> 346 | } 347 | ): Promise { 348 | const {runMain, filename} = options 349 | const {$schema, $module, $version, $main, $import, $export, $doc, $author} = module 350 | if ($module === undefined && $main === undefined) return Promise.reject({ 351 | err: 'BadModule', why: 'module must have either $module key or $main key', 352 | filename 353 | }) 354 | 355 | const imports: [string, Import][] = _.toPairs($import || {}) 356 | return (function nextImport(scope: Scope): Promise { 357 | const popped = imports.pop() 358 | if (popped) { 359 | const [alias, imp] = popped 360 | return loadImport(alias, imp, filename, options.localModules).then( 361 | imported => nextImport(mergeScopes(env, scope, imported))) 362 | } else { 363 | const defs: JasprObject = _.omit( 364 | _.pickBy(module, v => v !== undefined), 365 | ...Object.keys(module).filter(x => x.startsWith('$'))) 366 | const ns = {$module: $module || null, $version: $version || null} 367 | const nameError = validateNames(defs, ns) 368 | if (nameError != null) return Promise.reject(nameError) 369 | const mod = evalDefs(env, scope, [], undefined, defs, ns) 370 | 371 | // TODO: Remove this debug code! 372 | // ----------------------------------------------------------------------- 373 | for (let k in mod.value) { 374 | const v = mod.value[k] 375 | if (v instanceof Deferred) { 376 | const timeout = 377 | setTimeout(() => console.warn(`value.${k} has not resolved!`), 2500) 378 | v.await(() => clearTimeout(timeout)) 379 | } 380 | } 381 | for (let k in mod.macro) { 382 | const v = mod.macro[k] 383 | if (v instanceof Deferred) { 384 | const timeout = 385 | setTimeout(() => console.warn(`macro.${k} has not resolved!`), 2500) 386 | v.await(() => clearTimeout(timeout)) 387 | } 388 | } 389 | // ----------------------------------------------------------------------- 390 | 391 | return Promise.resolve(Object.assign(mod, { 392 | $schema, 393 | $module: $module || null, 394 | $version: $version || null, 395 | $doc: $doc || null, 396 | $author: $author || null, 397 | $import: $import || Object.create(null), 398 | $export: $export || Object.create(null), 399 | $main: options.runMain && $main !== undefined 400 | ? expandAndEval(env, mod, [], undefined, $main) 401 | : null 402 | })) 403 | } 404 | })(options.scope || emptyScope) 405 | } 406 | 407 | export function importModule( 408 | module: Module, 409 | alias = module.$module, 410 | names: {[name: string]: string} = 411 | _(module.$export).keys().map(k => [k, k]).fromPairs().value() 412 | ): Scope { 413 | function onlyExports(sc: {[name: string]: T}): {[name: string]: T} { 414 | const out: {[name: string]: T} = Object.create(null) 415 | _.forIn(module.$export, (exported, as) => { 416 | const qualified = qualify(module, exported) 417 | if (has(sc, qualified)) { 418 | out[qualify(module, as)] = sc[qualified] 419 | if (alias) out[`${alias}.${as}`] = sc[qualified] 420 | } 421 | }) 422 | // TODO: Raise BadModule if a name does not refer to an actual import 423 | _.forIn(names, (imported, as) => { 424 | const qualified = qualify(module, imported) 425 | if (has(out, qualified)) out[as] = out[qualified] 426 | }) 427 | return out 428 | } 429 | const out: Scope = 430 | _(module).omit('test', 'qualified') 431 | .omitBy((v, k) => k.startsWith('$')) 432 | .mapValues((v: Jaspr, k) => isObject(v) ? onlyExports(v) : v) 433 | .value() 434 | out.test = Object.create(null) 435 | out.qualified = Object.create(null) 436 | _.toPairs(names).concat(( 437 | alias ? _.keys(module.$export).map(k => [`${alias}.${k}`, k]) : []) 438 | ).forEach(([as, imported]) => 439 | out.qualified[as] = qualify(module, imported)) 440 | return out 441 | } 442 | -------------------------------------------------------------------------------- /jaspr/concurrency.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ Data Types][prev] | [🗏 Table of Contents][toc] | [Macros ❧][next] 2 | :---|:---:|---: 3 | 4 | # Concurrency and Channels 5 | 6 | ## Fibers 7 | 8 | Fibers are the basic unit of concurrency in Jaspr. Jaspr code does not deal with fibers directly; they are automatically managed by the language. 9 | 10 | A fiber is an execution context with a scope and a parent fiber, and it runs concurrently with other fibers. All of the fibers in a Jaspr program form a tree; the path from a given fiber back to the root is similar to a call stack. A fiber is _resolved_ when it returns a result. 11 | 12 | A new fiber is created whenever a Jaspr form is evaluated or a function is called. For example, evaluating `(+ foo bar)` first creates 1 fiber to evaluate the whole expression, then spawns 3 child fibers to evaluate each of `+`, `foo`, and `bar`. Once the `+` fiber has resolved, the parent fiber now evaluates the body of the function that `+` evaluated to. 13 | 14 | ### Laziness 15 | 16 | Data structures (arrays and objects) in Jaspr are lazy, although not to the extent of truly lazy languages like Haskell. When an array or object is constructed, all of its elements will be computed eventually in their own fibers, but not necessarily all at once. Parts of the structure may be used before other parts have resolved. 17 | 18 | For example, consider the following expression: 19 | 20 | > (hd ([] (p.add 1 1) (await (sleep 1000) 21 | > (inspect! “foo”) 22 | > 42))) ;= 2 23 | 24 | `hd` returns the first element of the array, `(+ 1 1)`, which evaluates to 2. The second element, which will take 1 second to compute and cause a side effect, is not relevant to the result. 25 | 26 | - In a strict language, the entire expression would block for 1 second before the result is available. 27 | - In a truly lazy (call-by-need) language, like Haskell, the expression would not block, and the side effect of printing `“foo”` would never occur, because that expression's value is never used. 28 | - In Jaspr, the expression does not block, but, once the result is available, the fiber computing the second element of the array _is still running_, and the program will still print `“foo”` after 1 second has passed. 29 | 30 | ## Concurrency Utility Functions 31 | 32 | ### `never` 33 | 34 | A function call that never returns. **Be careful:** using this can deadlock a program! 35 | 36 | never: (fn- (p.chanSend! null (p.chanMake!))) 37 | 38 | ### `sleep` 39 | 40 | `(sleep ms)` returns `null` after `ms` milliseconds have passed. 41 | 42 | sleep: (fn- ms (p.sleep ms)) 43 | 44 | ### `onCancel` 45 | 46 | TODO: Implement `onCancel`. 47 | 48 | ## Expression Chaining Forms 49 | 50 | ### `do` 51 | 52 | `(do expr₀ expr₁ … exprₙ)` evaluates `expr₀`…`exprₙ` concurrently, each in its own fiber. It returns the result of the last expression (`exprₙ`) without waiting on the others to resolve. 53 | 54 | > (do 1 2) ;= 2 55 | > (do (never) 42) ;= 42 56 | 57 | --- 58 | 59 | do: (fn* exprs (if exprs (-1 exprs) null)) 60 | 61 | ### `await` 62 | 63 | `(await expr₀ expr₁ … exprₙ)` evaluates `expr₀`…`exprₙ` in series. Each expression is evaluated only after the previous expression has resolved. It returns the value of the last expression in the chain. 64 | 65 | > (await 1 2) ;= 2 66 | 67 | `await` only waits for the top level of each value to resolve; e.g., if one of the expressions returns an array, `await` may continue to the next expression even though the elements of the array have not fully resolved. 68 | 69 | --- 70 | 71 | macro.await: 72 | (fn* exprs 73 | (if (no exprs) null 74 | (p.< (len exprs) 2) (hd exprs) 75 | `[p.then ~(hd exprs) (await ~@(tl exprs))])) 76 | 77 | ### `awaitAll` 78 | 79 | `(awaitAll expr₀ expr₁ … exprₙ)` evaluates `expr₀`…`exprₙ` in parallel. Once all of these expressions have resolved, it returns the value of the last expression in the argument list. 80 | 81 | > (awaitAll 1 2) ;= 2 82 | 83 | `awaitAll` only waits for the top level of each value to resolve; e.g., if one of the expressions returns an array, `awaitAll` may return even though the elements of the array have not fully resolved. 84 | 85 | --- 86 | 87 | macro.awaitAll: 88 | (fn* exprs 89 | (if (no exprs) null 90 | (p.is? 1 (len exprs)) (hd exprs) 91 | `[define {.a.: ~(hd exprs), .b.: (awaitAll ~@(tl exprs))} 92 | (p.then .a. .b.)])) 93 | 94 | ### `choice` 95 | 96 | `(choice expr₀ expr₁ … exprₙ)` evaluates `expr₀`…`exprₙ` in parallel and creates a _choice junction_ of the resulting fibers. A choice junction resolves to the value of the first fiber in the junction that resolves. Once the junction resolves, all of the unresolved fibers in the junction are _canceled_. A canceled fiber stops executing and aborts any pending `send!` or `recv!` operations; canceling a fiber cancels all of its children as well. 97 | 98 | > (choice 'fast (await (sleep 100) 'slow)) ;= “fast” 99 | > (choice (await (sleep 100) 'slow) 'fast) ;= “fast” 100 | > (choice 42 (never)) ;= 42 101 | 102 | Choice junctions are the only way to cancel fibers. A typical use case is to create timeouts. For example, `(choice (sleep 100) (recv! c))` will receive on `c` with a 100ms timeout, stopping the `recv!` operation once the timeout is up. `(choice (sleep 100) (send! x c))` will attempt to send `x` on `c` with a 100ms timeout, but, if nothing receives it, the `send!` will be canceled and a future `(recv! c)` will not receive `x`. 103 | 104 | > (define {ch: (chan!)} 105 | > (await (choice (send! 'canceled ch) (sleep 100)) 106 | > (do (send! 'ok ch) ('value (recv! ch))))) ;= “ok” 107 | 108 | Canceling a branch of a choice junction also cancels all branches of any choice junctions nested inside that branch. 109 | 110 | > (define {ch: (chan!)} 111 | > (await (choice (choice (send! 'canceled1 ch) (send! 'canceled2 ch)) 112 | > (sleep 100)) 113 | > (do (send! 'ok ch) ('value (recv! ch))))) ;= “ok” 114 | 115 | --- 116 | 117 | macro.choice: (fn* exprs `[p.junction ~@exprs]) 118 | 119 | ### `awaitOne` 120 | 121 | `(awaitOne xs)` blocks until at least one element of the array `xs` has resolved, then returns that element. This operation is called `race` in some JavaScript promise libraries. 122 | 123 | > (awaitOne ([] (await (sleep 300) 'foo) 124 | > (await (sleep 500) 'bar) 125 | > (await (sleep 100) 'baz))) ;= "baz" 126 | 127 | `awaitOne` raises a `BadArgs` error if `xs` is not an array, or if `xs` is empty. 128 | 129 | --- 130 | 131 | awaitOne: 132 | (fn- xs 133 | (assertArgs (array? xs) "not an array" 134 | xs "array cannot be empty" 135 | (loopAs next {i: 1, last: (hd xs)} 136 | (if (< i (len xs)) 137 | (next {i: (inc i), last: (choice last (i xs))}) 138 | last)))) 139 | 140 | ## Channels 141 | 142 | Channels are how Jaspr handles both mutable state and messaging between fibers. They are based on channels from Go, and function similarly: all channels can both send and receive messages, and sending and receiving both block until the sent message is received. 143 | 144 | Channels are the only mutable values in Go. They are [magic objects](data-types.jaspr.md#magic-objects) with the property `$chan: true`. Channels are not referentially transparent; all channels are structurally just `{$chan: true}`, but two channels with the same structure are not necessarily equal. Copying a channel with object operations like `withKey` or `withoutKey` will produce a new object that is no longer a channel. 145 | 146 | Values must be fully resolved before they can be sent on a channel. This prevents deadlocks that could result from a partially-resolved data structure being sent from a fiber that is then canceled (see [`choice`](#choice)), leaving parts of the value permanently unresolved. Closures are a special case: waiting for every member of a closure's scope to resolve could take a long time, so Jaspr keeps track of which scope entries were declared at the top level (which would not be inside any choice junction), and does not wait for those to resolve when sending a closure on a channel. 147 | 148 | ### `chan!` 149 | 150 | `(chan!)` creates a new channel. A channel is a magic object with the key `$chan: true`. 151 | 152 | > ('$chan (chan!)) ;= true 153 | 154 | Every call to `chan!` creates a unique object, even though all channels are structurally identical. Creating an updated copy of a channel (e.g., by using `put` or `update`) will result in a non-magic object that is not a channel. 155 | 156 | > (= (chan!) (chan!)) ;= false 157 | 158 | --- 159 | 160 | chan!: (fn- (p.chanMake!)) 161 | 162 | ### `send!` 163 | 164 | `(send! msg chan)` sends `msg` on the channel `chan`, then blocks until either `msg` is received or `chan` is closed. It returns `true` if `msg` was successfully received, `false` if `chan` was closed. 165 | 166 | > (define {c: (chan!)} (do (recv! c) (send! 42 c))) ;= true 167 | > (define {c: (chan!)} (await (close! c) (send! 42 c))) ;= false 168 | 169 | Unlike other Jaspr functions, `send!` is strict, not lazy. If `msg` or any element of `msg` is unresolved, `send!` blocks until `msg` has finished resolving. While `msg` is unresolved, the send has technically not yet occurred, so `recv!` calls will not yet be able to receive `msg`. 170 | 171 | > (define {c: (chan!)} 172 | > (do (send! ([] 1 (await (sleep 200) 2)) c) 173 | > (send! 42 c) 174 | > (await (recv! c) 175 | > (recv! c)))) ;= {value: [1, 2], done: false} 176 | 177 | `send!` raises a `BadArgs` error if `chan` is not a channel. 178 | 179 | --- 180 | 181 | send!: (fn- msg chan (assertArgs (chan? chan) “not a channel” 182 | (p.chanSend! msg chan))) 183 | 184 | ### `recv!` 185 | 186 | `(recv! chan)` blocks until it receives a message on `chan` or `chan` is closed. If a message `value` is successfully received, `recv!` returns `{value, done: false}`. If `chan` is closed before a message can be received, `recv!` returns `{value: null, done: true}`. 187 | 188 | > (define {c: (chan!)} 189 | > (do (send! 42 c) 190 | > (recv! c))) ;= {value: 42, done: false} 191 | 192 | > (define {c: (chan!)} 193 | > (do (close! c) 194 | > (recv! c))) ;= {value: null, done: true} 195 | 196 | `recv!` raises a `BadArgs` error if `chan` is not a channel. 197 | 198 | --- 199 | 200 | recv!: (fn- chan (assertArgs (chan? chan) “not a channel” 201 | (p.chanRecv! chan))) 202 | 203 | ### `close!` 204 | 205 | Closes a channel. Returns `true` if the channel was not yet closed, or `false` if the channel was already closed (and the `close!` call did nothing). 206 | 207 | > (define {c: (chan!)} (close! c)) ;= true 208 | > (define {c: (chan!)} (await (close! c) (close! c))) ;= false 209 | 210 | `close!` raises a `BadArgs` error if its argument is not a channel. 211 | 212 | --- 213 | 214 | close!: (fn- chan (assertArgs (chan? chan) “not a channel” 215 | (p.chanClose! chan))) 216 | 217 | ### `closed?` 218 | 219 | Returns a boolean indicating whether its argument, a channel, is closed. 220 | 221 | > (define {c: (chan!)} (closed? c)) ;= false 222 | > (define {c: (chan!)} (await (close! c) (closed? c))) ;= true 223 | 224 | `closed?` raises a `BadArgs` error if its argument is not a channel. 225 | 226 | --- 227 | 228 | closed?: (fn- chan (assertArgs (chan? chan) “not a channel” 229 | (p.chanClosed? chan))) 230 | 231 | ## Advanced Channel Operations 232 | 233 | ### `combine!` 234 | 235 | `(combine! chans)` returns a new channel. It continually receives on every channel in the array `chans` in parallel, and sends all received messages on the new channel. Messages from the same source channel will remain ordered relative to each other, but the ordering of messages from different source channels is undefined. The returned channel is closed once every channel in `chans` has closed. 236 | 237 | `combine!` raises a `BadArgs` error if `chans` is not an array of channels. 238 | 239 | ; TODO: Define combine! 240 | 241 | ### `distribute!` 242 | 243 | `(distribute! source sinks)` continually receives on the channel `source` and sends each message on every channel in the array `sinks` in parallel. It blocks until `source` closes and at least one sink has received every message, then returns `null`. 244 | 245 | > (define {in: (chan!), out1: (chan!), out2: (chan!)} 246 | > (awaitAll (await (send! 'foo in) 247 | > (send! 'bar in) 248 | > (close! in)) 249 | > (distribute! in ([] out1 out2)) 250 | > (define {a: ('value (recv! out1)), b: ('value (recv! out2))} 251 | > (await a b 252 | > (define {c: ('value (recv! out1)), d: ('value (recv! out2))} 253 | > ([] a b c d (closed? out1) (closed? out2))))))) 254 | > ;= ["foo", "foo", "bar", "bar", false, false] 255 | 256 | `distribute!` raises a `BadArgs` error if `source` is not a channel or `sinks` is not an array of channels. 257 | 258 | --- 259 | 260 | distribute!: 261 | (fn- source sinks 262 | (assertArgs (chan? source) "source is not a channel" 263 | (array? sinks) "not an array" 264 | (all? chan? sinks) "sink is not a channel" 265 | (loopAs next {} 266 | (define {recvd: (recv! source)} 267 | (if (no ('done recvd)) 268 | (await (awaitOne (map (\ send! ('value recvd) _) sinks)) 269 | (next {}))))))) 270 | 271 | ### `drain!` 272 | 273 | `(drain! source sink)` continually receives on the channel `source` and sends each message on the channel `sink`. It blocks until `source` closes and `sink` has received every message, then returns `null`. 274 | 275 | > (define {in: (chan!), out: (chan!)} 276 | > (awaitAll (await (send! 'foo in) 277 | > (send! 'bar in) 278 | > (close! in)) 279 | > (drain! in out) 280 | > (define {a: ('value (recv! out))} 281 | > (await a (define {b: ('value (recv! out))} 282 | > ([] a b (closed? out))))))) ;= ["foo", "bar", false] 283 | 284 | `drain!` raises a `BadArgs` error if either `source` or `sink` is not a channel. 285 | 286 | --- 287 | 288 | drain!: 289 | (fn- source sink 290 | (assertArgs (chan? source) "source is not a channel" 291 | (chan? sink) "sink is not a channel" 292 | (loopAs next {} 293 | (define {recvd: (recv! source)} 294 | (if (no ('done recvd)) 295 | (if (send! ('value recvd) sink) 296 | (next {}))))))) 297 | 298 | ### `sendAll!` 299 | 300 | `(sendAll! msgs chan)` sends all of the elements of the array `msgs` on the channel `chan`, in order, then returns a boolean representing whether the channel was closed. 301 | 302 | > (define {out: (chan!)} 303 | > (awaitAll (sendAll! '[foo bar] out) 304 | > (define {a: ('value (recv! out))} 305 | > (await a (define {b: ('value (recv! out))} 306 | > ([] a b (closed? out))))))) ;= ["foo", "bar", false] 307 | 308 | `sendAll!` raises a `BadArgs` error if `msgs` is not an array or `chan` is not a channel. 309 | 310 | --- 311 | 312 | sendAll!: 313 | (fn- msgs chan 314 | (assertArgs (array? msgs) "not an array" 315 | (chan? chan) "not a channel" 316 | (loopAs next {i: 0} 317 | (or (>= i (len msgs)) 318 | (and (send! (i msgs) chan) 319 | (next {i: (inc i)})))))) 320 | 321 | ## Mutable State 322 | 323 | ### Refs 324 | 325 | ; TODO: Test and document refs 326 | 327 | #### `ref!` 328 | 329 | refServer!: 330 | (fn- chan value 331 | (define {next: (recv! chan), msg: ('value next)} 332 | (if ('done next) null 333 | (hasKey? 'set msg) (refServer! chan ('set msg)) 334 | (hasKey? 'get msg) (do (send! value ('get msg)) 335 | (refServer! chan value)) 336 | (refServer! chan value)))) 337 | ref!: 338 | (fn- value 339 | (define {chan: (chan!)} 340 | (do (refServer! chan value) 341 | {ref: chan}))) 342 | 343 | #### `ref?` 344 | 345 | ref?: (fn- it (and (object? it) (hasKey? 'ref it) (chan? ('ref it)))) 346 | 347 | #### `get!` 348 | 349 | get!: 350 | (fn- ref 351 | (assertArgs (ref? ref) “not a ref” 352 | (define {chan: (chan!)} 353 | (do (send! {get: chan} ('chan ref)) 354 | ('value (recv! chan)))))) 355 | 356 | #### `set!` 357 | 358 | set!: 359 | (fn- value ref 360 | (assertArgs (ref? ref) “not a ref” 361 | (send! {set: value} ('chan ref)))) 362 | 363 | ### Queues 364 | 365 | ; TODO: Test and document queues 366 | 367 | #### `queue!` 368 | 369 | queueServer!: 370 | (fn- i o (define {next: (recv! i), msg: ('value next)} 371 | (if (no ('done next)) 372 | (do (send! msg o) (queueServer! i o))))) 373 | 374 | queue!: 375 | (fn- (define {enqueue: (chan!), dequeue: (chan!)} 376 | (do (queueServer! enqueue dequeue) 377 | {enqueue dequeue}))) 378 | 379 | #### `queue?` 380 | 381 | queue?: 382 | (fn- it (and (object? it) 383 | (hasKey? 'enqueue it) 384 | (chan? ('enqueue it)) 385 | (hasKey? 'dequeue it) 386 | (chan? ('dequeue it)))) 387 | 388 | #### `enqueue!` 389 | 390 | enqueue!: 391 | (fn- value queue 392 | (assertArgs (queue? queue) “not a queue” 393 | (send! value ('enqueue queue)))) 394 | 395 | #### `dequeue!` 396 | 397 | dequeue!: 398 | (fn- queue (assertArgs (queue? queue) “not a queue” 399 | ('value (recv! ('dequeue queue))))) 400 | 401 | ## Exports 402 | 403 | $export: { 404 | never sleep do await awaitAll inParallel:awaitAll inSeries:await choice 405 | awaitOne chan! send! recv! close! closed? 406 | 407 | combine! distribute! drain! sendAll! 408 | 409 | ref! ref? get! set! queue! queue? enqueue! dequeue! 410 | 411 | ⛔:never 💤:sleep ∥:do ∦:await ⋕:awaitAll ⏛:choice 📩:send! 📨:recv! 412 | } 413 | 414 | [☙ Data Types][prev] | [🗏 Table of Contents][toc] | [Macros ❧][next] 415 | :---|:---:|---: 416 | 417 | [toc]: jaspr.jaspr.md 418 | [prev]: data-types.jaspr.md 419 | [next]: macros.jaspr.md 420 | -------------------------------------------------------------------------------- /jaspr/streams.jaspr.md: -------------------------------------------------------------------------------- 1 | [☙ Signals and Errors][prev] | [🗏 Table of Contents][toc] | [Comparisons and Sorting ❧][next] 2 | :---|:---:|---: 3 | 4 | # Streams and Pipelines 5 | 6 | A stream is a one-way channel that produces a sequence of values, then closes. Unlike arrays, streams are inherently sequential and can only be iterated over once. 7 | 8 | Streams are useful when chaining sequence operations together, both for performance reasons (no intermediate arrays) and to guarantee sequential execution. 9 | 10 | ## Stream Producers 11 | 12 | ### `emptyStream!` 13 | 14 | `(emptyStream!)` returns a new, closed channel. 15 | 16 | > (closed? (emptyStream!)) ;= true 17 | 18 | emptyStream!: 19 | (fn- (let stream (chan!) (await (close! stream) stream))) 20 | 21 | ### `stream!` 22 | 23 | `(stream! xs)` returns a new channel and sends the elements of the array `xs` on that channel, in order. 24 | 25 | > (recv! (stream! '[1 2 3])) ;= {value: 1, done: false} 26 | 27 | > (define {stream: (stream! '[1 2 3])} 28 | > (await (recv! stream) 29 | > (recv! stream))) ;= {value: 2, done: false} 30 | 31 | > (define {stream: (stream! '[1 2 3])} 32 | > (await (recv! stream) 33 | > (recv! stream) 34 | > (recv! stream))) ;= {value: 3, done: false} 35 | 36 | After the last element of `xs` has been received, the returned channel is closed. 37 | 38 | > (define {stream: (stream! '[foo bar])} 39 | > (await (recv! stream) 40 | > (recv! stream) 41 | > (recv! stream))) ;= {value: null, done: true} 42 | 43 | `stream!` raises a `BadArgs` error if `xs` is not an array. 44 | 45 | --- 46 | 47 | stream!: (fn- xs (assertArgs (array? xs) "not an array" 48 | (apply streamOf! xs))) 49 | 50 | ### `streamOf!` 51 | 52 | `(streamOf! x0 x1 ... xn)` returns a new channel and sends the values `x0`...`xn` on that channel, in order. 53 | 54 | > (recv! (streamOf! 1 2 3)) ;= {value: 1, done: false} 55 | 56 | > (define {stream: (streamOf! 1 2 3)} 57 | > (await (recv! stream) 58 | > (recv! stream))) ;= {value: 2, done: false} 59 | 60 | > (define {stream: (streamOf! 1 2 3)} 61 | > (await (recv! stream) 62 | > (recv! stream) 63 | > (recv! stream))) ;= {value: 3, done: false} 64 | 65 | After the last element of `xs` has been received, the returned channel is closed. 66 | 67 | > (define {stream: (streamOf! 'foo 'bar)} 68 | > (await (recv! stream) 69 | > (recv! stream) 70 | > (recv! stream))) ;= {value: null, done: true} 71 | 72 | --- 73 | 74 | streamOf!: 75 | (fn* xs 76 | (let stream (chan!) 77 | max (len xs) 78 | _ (loopAs next {i: 0} 79 | (if (< i max) (await (send! (i xs) stream) (next {i: (inc i)})) 80 | (close! stream))) 81 | stream)) 82 | 83 | ### `streamChars!` 84 | 85 | streamChars!: 86 | (fn- str 87 | (assertArgs (string? str) "not a string" 88 | (let stream (chan!) 89 | max (chars str) 90 | _ (loopAs next {i: 0} 91 | (if (< i max) (await (send! (char i str) stream) 92 | (next {i: (inc i)})) 93 | (close! stream))) 94 | stream))) 95 | 96 | ### `streamCodePoints!` 97 | 98 | streamCodePoints!: 99 | (fn- str 100 | (assertArgs (string? str) "not a string" 101 | (let stream (chan!) 102 | max (codePoints str) 103 | _ (loopAs next {i: 0} 104 | (if (< i max) (await (send! (codePoint i str) stream) 105 | (next {i: (inc i)})) 106 | (close! stream))) 107 | stream))) 108 | 109 | ### `streamBytes!` 110 | 111 | streamBytes!: 112 | (fn- str 113 | (assertArgs (string? str) "not a string" 114 | (let stream (chan!) 115 | max (bytes str) 116 | _ (loopAs next {i: 0} 117 | (if (< i max) (await (send! (byte i str) stream) 118 | (next {i: (inc i)})) 119 | (close! stream))) 120 | stream))) 121 | 122 | ### `streamUnits!` 123 | 124 | streamUnits!: 125 | (fn- str 126 | (assertArgs (string? str) "not a string" 127 | (let stream (chan!) 128 | max (units str) 129 | _ (loopAs next {i: 0} 130 | (if (< i max) (await (send! (unit i str) stream) 131 | (next {i: (inc i)})) 132 | (close! stream))) 133 | stream))) 134 | 135 | ### `forever!` 136 | 137 | `(forever! x)` returns a new channel. It sends `x` on that channel repeatedly, in an infinite loop, until the channel is closed. 138 | 139 | > (define {foos: (forever! 'foo)} 140 | > ([] ('value (recv! foos)) 141 | > ('value (recv! foos)) 142 | > ('value (recv! foos)))) ;= ["foo", "foo", "foo"] 143 | 144 | --- 145 | 146 | forever!: 147 | (fn- x (define { out: (chan!) 148 | loop: (fn (if (send! x out) (loop))) } 149 | (do (loop) out))) 150 | 151 | ### `iterate!` 152 | 153 | ; TODO: Define iterate! 154 | 155 | ### `upto!` 156 | 157 | ; TODO: Define upto! 158 | 159 | ### `chain!` 160 | 161 | chain!: (comp chainEach! stream! []) 162 | 163 | ## Stream Converters 164 | 165 | ### `buffer!` 166 | 167 | ; TODO: Define buffer! 168 | 169 | ### `chainEach!` 170 | 171 | `chainEach!` transforms a stream of streams into a single stream. 172 | 173 | `(chainEach! chan)` returns a new channel. It continually receives on the channel `chan`, and, assuming the received values are themselves channels, continually receives on each received channel until the received channel closes, then sends those messages on the new channel. Once both `chan` and all channels received from `chan` have closed, it closes the new channel. 174 | 175 | > (->> (streamOf! (streamOf! 1 2 3) (streamOf! 4 5)) 176 | > chainEach! 177 | > collect!) ;= [1, 2, 3, 4, 5] 178 | 179 | `chainEach!` raises a `BadArgs` error if `chan` is not a channel, or if any message received on `chan` is not itself a channel. 180 | 181 | --- 182 | 183 | chainEach!: 184 | (fn in (let out (chan!) 185 | _ (await (forEach! (\ drain! _ out) in) 186 | (close! out)) 187 | out)) 188 | 189 | ### `chainMap!` 190 | 191 | `(chainMap! f chan)` returns a new channel. It continually receives on the channel `chan`, and, assuming the received messages are themselves channels, continually receives on each received channel until the received channel closes, then, for each received message `x`, sends `(f m)` on the new channel. Once both `chan` and all channels received from `chan` have closed, it closes the new channel. 192 | 193 | > (->> (streamOf! 10 20 30) 194 | > (chainMap! (\ streamOf! _ (inc _))) 195 | > collect!) ;= [10, 11, 20, 21, 30, 31] 196 | 197 | `chainMap!` raises a `BadArgs` error if `chan` is not a channel, or if `f` returns a non-channel value for any message received on `chan`. 198 | 199 | --- 200 | 201 | chainMap!: (comp chainEach! map!) 202 | 203 | ### `chunk!` 204 | 205 | ; TODO: Define chunk! 206 | 207 | ### `cycle!` 208 | 209 | `(cycle! chan)` returns a new channel. It continually receives on the channel `chan` and sends each message on the returned channel; once `chan` closes, the returned channel starts again from the beginning of the messages received from `chan`. 210 | 211 | > (->> (streamOf! 1 2 3) cycle! (take! 7)) ;= [1, 2, 3, 1, 2, 3, 1] 212 | 213 | `cycle!` raises a `BadArgs` error if `chan` is not a channel. 214 | 215 | --- 216 | 217 | cycle!: (fn in (let [init, saved] (fork! 2 in) 218 | (->> (collect! saved) forever! flatten! (chain! init)))) 219 | 220 | ### `drop!` 221 | 222 | `(drop! n chan)` receives and ignores `n` messages on `chan`, then returns `chan`. 223 | 224 | > (let c (streamOf! 1 2 3) 225 | > d (drop! 2 c) 226 | > (recv! d)) ;= {value: 3, done: false} 227 | 228 | `drop!` raises a `BadArgs` error if `n` is not a nonnegative integer or `chan` is not a channel. 229 | 230 | -- 231 | 232 | drop!: (fn n stream 233 | (await (forEach recv! (makeArray (const stream) n)) 234 | stream)) 235 | 236 | ### `filter!` 237 | 238 | `(filter! f chan)` returns a new channel. It receives every message on `chan`, and, for each message `m` received, sends `m` on the new channel if and only if `(f m)` is truthy. Once `chan` is closed, `filter!` closes the new channel. 239 | 240 | > (collect! (filter! (const true) (streamOf! 1 2 3))) ;= [1, 2, 3] 241 | > (collect! (filter! (const false) (streamOf! 1 2 3))) ;= [] 242 | > (collect! (filter! odd? (streamOf! 1 2 3))) ;= [1, 3] 243 | 244 | `filter!` raises a `BadArgs` error if `chan` is not a channel. 245 | 246 | --- 247 | 248 | filter!: 249 | (fn f in (let out (chan!) 250 | _ (await (forEach! (\ if (f _) (send! _ out)) in) 251 | (close! out)) 252 | out)) 253 | 254 | ### `flatten!` 255 | 256 | `(flatten! chan)` transforms a stream of arrays into a stream of the arrays' elements. 257 | 258 | > (->> (streamOf! '[1 2] '[3 4 5]) flatten! collect!) ;= [1, 2, 3, 4, 5] 259 | 260 | `flatten!` raises a `BadArgs` error if `chan` is not a channel, or if any message received on `chan` is not an array. 261 | 262 | --- 263 | 264 | flatten!: 265 | (fn in (let out (chan!) 266 | _ (await (forEach! (\ sendAll! _ out) in) 267 | (close! out)) 268 | out)) 269 | 270 | ### `flatMap!` 271 | 272 | flatMap!: (comp flatten! map!) 273 | 274 | ### `fork!` 275 | 276 | `(fork! n chan)` return an array of `n` channels. It receives every message on `chan`, then sends those messages on each channel in the returned array. Each returned channel is closed after every message from `chan` has been received on it. 277 | 278 | --- 279 | 280 | fork!: 281 | (fn n in 282 | (assertArgs (and (integer? n) (pos? n)) "not a positive integer" 283 | (chan? in) "not a channel" 284 | (let outs (makeArray (\ chan!) n) 285 | _ (loopAs next {last: outs} 286 | (let {value done} (recv! in) 287 | (if done (forEach (\xy await x (close! y)) last outs) 288 | (let sends (map (\ send! value _) outs) 289 | (await (awaitOne sends) 290 | (next {last: sends})))))) 291 | outs))) 292 | 293 | ### `map!` 294 | 295 | `(map! f chan)` returns a new channel. It receives every message on `chan`, and, for each message `m` received, sends `(f m)` on the new channel. Once `chan` has closed and the last mapped message has been received, it closes the new channel. 296 | 297 | `map!` and `map` can be used interchangeably (with `stream!` and `collect!` to convert arrays to/from channels). `map!` processes each element in series, while `map` processes them in parallel. 298 | 299 | --- 300 | 301 | map!: 302 | (fn f 303 | (emptyStream!) 304 | . f ... ins 305 | (let out (chan!) 306 | _ (await (apply forEach! (cons (comp (\ send! _ out) f) ins)) 307 | (close! out)) 308 | out)) 309 | 310 | ### `peek!` 311 | 312 | peek!: (fn f stream (map! (\ await (f _) _) stream)) 313 | 314 | ### `reject!` 315 | 316 | reject!: (fn f stream (filter! (comp no f) stream)) 317 | 318 | ### `roundRobin!` 319 | 320 | `(roundRobin! n input)` creates and returns an array of `n` output channels, then receives on the channel `input` and sends each message received from `input` on one of the output channels. It cycles through the output channels with each message, restarting from the beginning when the end is reached. Over time, each output channel will be sent an equal portion of the messages received from `input`. When `input` is closed, all output channels will be closed as well. 321 | 322 | > (->> (streamOf! 1 2 3 4 5 6 7 8 9) 323 | > (roundRobin! 3) 324 | > (map collect!)) ;= [[1, 4, 7], [2, 5, 8], [3, 6, 9]] 325 | 326 | `roundRobin!` raises a `BadArgs` error if `n` is not a positive integer or `input` is not a channel. 327 | 328 | --- 329 | 330 | roundRobin!: 331 | (fn n in 332 | (assertArgs (and (integer? n) (pos? n)) "not a positive integer" 333 | (chan? in) "not a channel" 334 | (let out (makeArray (\ chan!) n) 335 | _ (await (->> out forever! flatten! (forEach! send! in)) 336 | (forEach close! out)) 337 | out))) 338 | 339 | ### `zip!` 340 | 341 | zip!: (partial map! []) 342 | 343 | ## Stream Consumers 344 | 345 | ### `all?!` 346 | 347 | `(all?! f chan)` receives messages on `chan` until the result of applying the predicate `f` to one of them is falsy or `chan` is closed. If the result of applying `f` to any message is falsy, `all?!` returns `false`; if `chan` is closed, `all?!` returns `true`. 348 | 349 | > (all?! id (emptyStream!)) ;= true 350 | > (all?! number? (streamOf! 1 2 3)) ;= true 351 | > (all?! number? (streamOf! 1 null 3)) ;= false 352 | 353 | `all?!` raises a `BadArgs` error if `chan` is not a channel. 354 | 355 | --- 356 | 357 | all?!: 358 | (fn f chan 359 | (awaitLet {value done} (recv! chan) 360 | (or done (and (f value) (all?! f chan))))) 361 | 362 | ### `any?!` 363 | 364 | `(any?! f chan)` receives messages on `chan` until the result of applying the predicate `f` to one of them truthy or `chan` is closed. If the result of applying `f` to any message is truthy, `any?!` returns `true`; if `chan` is closed, `any?!` returns `false`. 365 | 366 | > (any?! id (emptyStream!)) ;= false 367 | > (any?! number? (streamOf! true false 1)) ;= true 368 | > (any?! number? (streamOf! true false null)) ;= false 369 | 370 | `any?!` raises a `BadArgs` error if `chan` is not a channel. 371 | 372 | --- 373 | 374 | any?!: 375 | (fn f chan 376 | (awaitLet {value done} (recv! chan) 377 | (and (no done) (or (f value) (any?! f chan))))) 378 | 379 | ### `collect!` 380 | 381 | `(collect! chan)` receives messages on `chan` until it closes, accumulating the messages, in order, into an array. When `chan` closes, it returns the array. 382 | 383 | > (collect! (streamOf! 1 2 3)) ;= [1, 2, 3] 384 | 385 | `collect!` raises a `BadArgs` error if `chan` is not a channel. 386 | 387 | --- 388 | 389 | collect!: (\ reduce! snoc [] _) 390 | 391 | ### `collectEntries!` 392 | 393 | `(collectEntries! chan)` receives messages on `chan` until it closes, accumulating the messages (which should be `[key, value]` pairs) into an object. When `chan` closes, it returns the object. 394 | 395 | > (collectEntries! (streamOf! '[foo 1] '[bar 2])) ;= {foo: 1, bar: 2} 396 | 397 | `collectEntries!` raises a `BadArgs` error if `chan` is not a channel, of if any message received on `chan` is not a 2-element array where the first element is a string. 398 | 399 | --- 400 | 401 | collectEntries!: 402 | (\ catch (reduce! (fn obj [k v] (withKey k v obj)) {} _) 403 | {err: 'BadArgs, fn} (raise { 404 | err: 'BadArgs, args: ([] _), fn: (myName), 405 | why: (if (= fn `reduce!) 406 | "not a channel" 407 | "stream element was not a [key, value] pair") 408 | })) 409 | 410 | ### `collectString!` 411 | 412 | `(collectString! chan)` receives messages on `chan` until it closes, accumulating the string representations of the messages, in order. When `chan` closes, it returns the concatenated strings. 413 | 414 | > (collectString! (streamOf! "the meaning of life is " 42 ".")) 415 | > ;= "the meaning of life is 42." 416 | 417 | `collectString!` raises a `BadArgs` error if `chan` is not a channel. 418 | 419 | --- 420 | 421 | collectString!: (\ reduce! str "" _) 422 | 423 | ### `count!` 424 | 425 | `(count! f chan)` receives messages from `chan` until `chan` is closed, and calls the predicate `f` on each of them. Once `chan` is closed, it returns the number of received messages for which `f` returned a truthy value. 426 | 427 | > (count! number? (streamOf! 1 'foo 2 'bar 3)) ;= 3 428 | 429 | `count!` raises a `BadArgs` error if `chan` is not a channel. 430 | 431 | --- 432 | 433 | count!: (fn f chan (reduce! (\xy if (f y) (inc x) x) 0 chan)) 434 | 435 | ### `forEach!` 436 | 437 | `(forEach! f stream)` receives repeatedly on the channel `stream`, and calls `f` with each received value before receiving the next one. When `stream` is closed, `forEach!` stops iterating and returns `null`. `f` is called only for its side effects. The elements of `stream` are iterated over in order, and `forEach!` waits for each call to `f` to resolve before starting the next one. 438 | 439 | > (let c (chan!) 440 | > _ (forEach! (\x send! x c) (streamOf! 1 2 3)) 441 | > (await (recv! c) (recv! c) (recv! c))) ;= {value: 3, done: false} 442 | 443 | `(forEach! f s₀ s₁ … sₙ)` receives on the channels `s₀`…`sₙ` at the same time, passing `n` + 1 arguments to `f`. Iteration stops once any of the streams is closed. 444 | 445 | > (let c1 (chan!) c2 (chan!) c3 (chan!) 446 | > ([] (forEach! (\xy send! y x) (streamOf! c1 c2 c3) (stream! '[a b c])) 447 | > ('value (recv! c1)) 448 | > ('value (recv! c2)) 449 | > ('value (recv! c3)))) ;= [null, "a", "b", "c"] 450 | 451 | `forEach!` does not resolve until every call to `f` has resolved. This behavior is similar to `await`. 452 | 453 | > (let chan (chan!) 454 | > (do (await (forEach! (\ await (sleep 100) (send! _ chan)) (streamOf! 1 2)) 455 | > (send! 3 chan)) 456 | > (await (recv! chan) 457 | > (recv! chan) 458 | > (recv! chan)))) ;= {value: 3, done: false} 459 | 460 | `forEach!` and `forEach` can be used interchangeably (with `stream!` and `collect!` to convert arrays to/from channels). `forEach!` processes each element in series, while `forEach` processes them in parallel. 461 | 462 | `forEach!` raises a `BadArgs` error if any `s` is not a channel. 463 | 464 | --- 465 | 466 | forEach!: 467 | (fn f 468 | null 469 | . f stream 470 | (loopAs next {} 471 | (awaitLet {value done} (recv! stream) 472 | (if (no done) (await (f value) (next {}))))) 473 | . f ... streams 474 | (loopAs next {} 475 | (let recvs (map recv! streams) 476 | (if (none? 'done recvs) 477 | (await (apply f (map 'value recvs)) (next {})))))) 478 | 479 | ### `forEachWhile!` 480 | 481 | ; TODO: Define forEachWhile! 482 | 483 | ### `none?!` 484 | 485 | `(none?! f chan)` receives messages on `chan` until the result of applying the predicate `f` to one of them truthy or `chan` is closed. If the result of applying `f` to a message is truthy, it returns `false` and closes `chan`; otherwise, it returns `true`. 486 | 487 | ### `reduce!` 488 | 489 | reduce!: 490 | (fn f init stream 491 | (assertArgs (chan? stream) "not a channel" 492 | (loopAs next {accum: init} 493 | (awaitLet {value done} (recv! stream) 494 | (if done accum (await accum (next {accum: (f accum value)}))))))) 495 | 496 | ### `take!` 497 | 498 | `(take! n chan)` receives `n` messages on the channel `chan`, then returns the received messages as an array. 499 | 500 | If `chan` is closed before `n` messages have been received, `take!` may return less than `n` messages. 501 | 502 | `take!` raises a `BadArgs` error if `n` is not a positive integer or if `chan` is not a channel. 503 | 504 | --- 505 | 506 | take!: 507 | (fn n chan 508 | (assertArgs (and (integer? n) (>= n 0)) "not a nonnegative integer" 509 | (chan? chan) "not a channel" 510 | (loopAs next {out: [], n} 511 | (if n (awaitLet {value done} (recv! chan) 512 | (if done out (next {out: (snoc out value), n: (dec n)}))) 513 | out)))) 514 | 515 | ## Exports 516 | 517 | $export: { 518 | emptyStream! stream! streamOf! streamChars! streamCodePoints! streamBytes! 519 | streamUnits! forever! iterate! upto! chain! 520 | 521 | buffer! chainEach! chainMap! chunk! cycle! drop! filter! flatten! flatMap! 522 | fork! map! peek! reject! roundRobin! zip! 523 | 524 | all?! any?! collect! collectEntries! collectString! count! forEach! 525 | forEachWhile! none?! reduce! take! 526 | } 527 | 528 | [☙ Signals and Errors][prev] | [🗏 Table of Contents][toc] | [Comparisons and Sorting ❧][next] 529 | :---|:---:|---: 530 | 531 | [toc]: jaspr.jaspr.md 532 | [prev]: signals-errors.jaspr.md 533 | [next]: sorting.jaspr.md 534 | --------------------------------------------------------------------------------