├── .gitignore ├── cli.sh ├── .prettierrc ├── README.md ├── src ├── data-manipulation.js ├── constants.js ├── utils.js ├── side-effects.js ├── BoxManager.js ├── sources.js ├── Linker.js ├── Module.js ├── errors.js ├── index.js ├── TestHarness.js ├── grammar.ne ├── Context.js └── grammar.cjs ├── cli.js ├── package.json └── examples ├── collections.srl └── numerals.srl /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | node --experimental-modules --no-warnings ./cli.js "$@" -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 4 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # srl 2 | 3 | Lambda calculus solver with side effects. 4 | 5 | ## Running tests in example folder 6 | 7 | After installing `srl` with `global` (or when using `npx`): 8 | 9 | $ srl examples/church.test.srl --run-tests -i examples/church.srl 10 | -------------------------------------------------------------------------------- /src/data-manipulation.js: -------------------------------------------------------------------------------- 1 | export function ofType(type) { 2 | return object => typeof object === 'object' && object.type === type 3 | } 4 | 5 | export function orDefault(value, defaultValue) { 6 | return value === null || value === undefined ? defaultValue : value 7 | } 8 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | import { main } from './src/index.js' 2 | import yargs from 'yargs' 3 | 4 | main(yargs.argv) 5 | .then(() => { 6 | process.exit(0) 7 | }) 8 | .catch(error => { 9 | if (yargs.argv.debug === true) { 10 | console.error(error) 11 | } 12 | 13 | console.log(`ERROR: ${error.message}`) 14 | process.exit(1) 15 | }) 16 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const TOKEN_TYPES = { 2 | IDENTIFIER: 'IDENTIFIER', 3 | SYMBOL: 'SYMBOL', 4 | EXPRESSION: 'EXPRESSION', 5 | DECLARATION: 'DECLARATION', 6 | ASSERTION: 'ASSERTION', 7 | IMPORT: 'IMPORT', 8 | PROGRAM: 'PROGRAM', 9 | BOX: 'BOX', 10 | UNIT: 'UNIT', 11 | MODULE: 'MODULE' 12 | } 13 | 14 | const VALUE_TYPES = {} 15 | 16 | export const helpText = `Usage: srl [...main modules] [-options] 17 | 18 | Options: 19 | --stdin read main module from stdin 20 | -i, --include include a module to link to from main modules 21 | --solve reduce rule to basic term from any main module 22 | --run-tests run all assertions in main modules 23 | ` 24 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | import fs from 'fs' 3 | import yargs from 'yargs' 4 | 5 | export const argv = yargs.argv 6 | 7 | export const format = input => 8 | input 9 | .map(entry => { 10 | if (Array.isArray(entry)) { 11 | return `(${format(entry)})` 12 | } 13 | 14 | if (entry.type === 'SYMBOL') { 15 | return `<${entry.value}>` 16 | } 17 | 18 | if (entry.type === 'BOX') { 19 | return `#${entry.value}` 20 | } 21 | 22 | if (entry.type === 'UNIT') { 23 | return `()` 24 | } 25 | 26 | return entry.value 27 | }) 28 | .join(' ') 29 | 30 | export const readFile = util.promisify(fs.readFile) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srl", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "compile": "nearleyc src/grammar.ne -o src/grammar.cjs" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "get-stdin": "^7.0.0", 15 | "glob": "^7.1.6", 16 | "glob-promise": "^3.4.0", 17 | "yargs": "^15.0.2" 18 | }, 19 | "devDependencies": { 20 | "moo": "^0.5.1", 21 | "nearley": "^2.19.0", 22 | "tap-colorize": "^1.2.0", 23 | "tap-difflet": "^0.7.2", 24 | "tap-min": "^2.0.0", 25 | "tap-mocha-reporter": "^5.0.0", 26 | "tap-spec": "^5.0.0", 27 | "tap-summary": "^4.0.0" 28 | }, 29 | "bin": { 30 | "srl": "./cli.sh" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/side-effects.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'increment!': ([box], ctx) => { 3 | if (box.type === 'BOX') { 4 | const currentValue = ctx.boxes.get(box.value) || 0 5 | ctx.boxes.set(box.value, currentValue + 1) 6 | } 7 | 8 | return true 9 | }, 10 | 'equals!': ([box, symbol], ctx) => { 11 | if (box.type === 'BOX' && symbol.type === 'SYMBOL') { 12 | const currentValue = ctx.boxes.get(box.value) || 0 13 | if (currentValue === parseFloat(symbol.value)) { 14 | return true 15 | } else { 16 | return { 17 | expected: symbol.value, 18 | actual: currentValue 19 | } 20 | } 21 | } 22 | 23 | return { 24 | expected: 'equals! [BOX] [SYMBOL]', 25 | actual: `equals! [${box.type}] [${symbol.type}]` 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/BoxManager.js: -------------------------------------------------------------------------------- 1 | export default class BoxManager { 2 | stack = [new Map()] 3 | 4 | get current() { 5 | return this.stack[this.stack.length - 1] 6 | } 7 | 8 | get isEmpty() { 9 | return this.stack.length === 1 10 | } 11 | 12 | push() { 13 | this.stack.push(new Map()) 14 | 15 | return this 16 | } 17 | pop() { 18 | if (!this.isEmpty) { 19 | this.stack.pop() 20 | } 21 | 22 | return this 23 | } 24 | 25 | save() { 26 | // TODO: IMPLEMENT 27 | return this 28 | } 29 | restore() { 30 | // TODO: IMPLEMENT 31 | return this 32 | } 33 | 34 | get(key) { 35 | return this.current.get(key) 36 | } 37 | set(key, value) { 38 | return this.current.set(key, value) 39 | } 40 | over(key, fn) { 41 | const newValue = fn(this.current.get(key)) 42 | this.current.set(key, newValue) 43 | return newValue 44 | } 45 | has(key) { 46 | return this.current.has(key) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/sources.js: -------------------------------------------------------------------------------- 1 | import { AbstractSourceError, NotImplementedError } from './errors.js' 2 | 3 | import getStdin from 'get-stdin' 4 | import path from 'path' 5 | 6 | import { readFile } from './utils.js' 7 | 8 | class Source { 9 | constructor(type) { 10 | this.type = type 11 | } 12 | 13 | async pull() { 14 | return Promise.reject(new AbstractSourceError()) 15 | } 16 | } 17 | 18 | export class StdinSource extends Source { 19 | constructor() { 20 | super('STDIN') 21 | } 22 | 23 | get name() { 24 | return `stdin` 25 | } 26 | 27 | async pull() { 28 | return await getStdin() 29 | } 30 | } 31 | 32 | export class FileSource extends Source { 33 | constructor(filePath) { 34 | super('FILE') 35 | 36 | this.filePath = path.resolve(filePath) 37 | } 38 | 39 | get name() { 40 | return `${this.filePath}` 41 | } 42 | 43 | async pull() { 44 | return await readFile(this.filePath, 'utf8') 45 | } 46 | } 47 | 48 | export class HttpSource extends Source { 49 | constructor(url) { 50 | super('HTTP') 51 | 52 | this.url = url 53 | } 54 | 55 | get name() { 56 | return `${this.url}` 57 | } 58 | 59 | async pull() { 60 | return Promise.reject(new NotImplementedError()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Linker.js: -------------------------------------------------------------------------------- 1 | import { DuplicateDeclarationError, UnknownModuleError } from './errors.js' 2 | 3 | export default class Linker { 4 | modules = new Map() 5 | 6 | constructor(modules) { 7 | for (let module of modules) { 8 | if (module.isInitialized) { 9 | this.modules.set(module.name, module) 10 | } else { 11 | throw new LinkError() 12 | } 13 | } 14 | } 15 | 16 | linkAll() { 17 | for (let [selfName, module] of this.modules) { 18 | const { context } = module 19 | 20 | for (let { name, identifiers } of context.imports) { 21 | const importedModule = this.modules.get(name) 22 | 23 | if (importedModule === undefined) { 24 | throw new UnknownModuleError(name) 25 | } 26 | 27 | const importedIdentifiers = importedModule.context.getImplicitImports( 28 | identifiers 29 | ) 30 | 31 | for (let [identifier, declaration] of importedIdentifiers) { 32 | if (module.hasDeclaration(identifier)) { 33 | throw new DuplicateDeclarationError(identifier) 34 | } else { 35 | module.addDeclaration(identifier, declaration) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | getMains() { 43 | return Array.from(this.modules.values()).filter( 44 | module => !module.isLibrary 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Module.js: -------------------------------------------------------------------------------- 1 | import nearley from 'nearley' 2 | import grammar from './grammar.cjs' 3 | 4 | import Context from './Context.js' 5 | 6 | import { 7 | ParsingError, 8 | AmbiguousGrammarError, 9 | UnexpectedEOFError 10 | } from './errors.js' 11 | 12 | export default class Module { 13 | context = new Context() 14 | parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)) 15 | rootAstNode = null 16 | name = null 17 | isInitialized = false 18 | 19 | constructor(source, isLibrary = false) { 20 | this.source = source 21 | this.isLibrary = isLibrary 22 | } 23 | 24 | async initialize() { 25 | const contents = await this.source.pull() 26 | 27 | try { 28 | this.parser.feed(contents) 29 | } catch (error) { 30 | console.log(error) 31 | throw new ParsingError(this.source, error.token) 32 | } 33 | 34 | if (this.parser.results.length > 1) { 35 | throw new AmbiguousGrammarError(this.parser.results.length) 36 | } 37 | 38 | if (this.parser.results.length === 0) { 39 | throw new UnexpectedEOFError() 40 | } 41 | 42 | this.rootAstNode = this.parser.results[0] 43 | 44 | this.context.loadProgram(this.rootAstNode.statements) 45 | 46 | this.name = this.context.getModuleDeclaration().name 47 | this.isInitialized = true 48 | } 49 | 50 | hasDeclaration(name) { 51 | return this.context.hasDeclaration(name) 52 | } 53 | 54 | getDeclaration(name) { 55 | return this.context.getDeclaration(name) 56 | } 57 | 58 | addDeclaration(name, declaration) { 59 | return this.context.addDeclaration(name, declaration) 60 | } 61 | 62 | evaluate(expression, options = {}) { 63 | return this.context.evaluate(expression, options) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | class DomainError extends Error { 2 | constructor(message) { 3 | super(message) 4 | 5 | this.name = this.constructor.name 6 | 7 | Error.captureStackTrace(this, this.constructor) 8 | } 9 | } 10 | 11 | class ModuleError extends DomainError {} 12 | 13 | export class AbstractSourceError extends ModuleError { 14 | constructor() { 15 | super(`Cannot pull from an abstract source`) 16 | } 17 | } 18 | 19 | export class NotImplementedError extends DomainError { 20 | constructor() { 21 | super(`Not implemented yet`) 22 | } 23 | } 24 | 25 | export class ParsingError extends ModuleError { 26 | constructor(source, token) { 27 | super(`Parsing error in ${source.name} at ${token.line}:${token.col}`) 28 | } 29 | } 30 | 31 | export class AmbiguousGrammarError extends ModuleError { 32 | constructor(resultCount) { 33 | super( 34 | `Ambiguous grammar detected - number of possible parsings: ${resultCount}` 35 | ) 36 | } 37 | } 38 | 39 | export class UnexpectedEOFError extends ModuleError { 40 | constructor() { 41 | super(`Unexpected end of input`) 42 | } 43 | } 44 | 45 | export class UnknownDeclarationError extends DomainError { 46 | constructor(name) { 47 | super(`Cannot find declaration '${name}'`) 48 | } 49 | } 50 | 51 | export class UnknownModuleError extends DomainError { 52 | constructor(name) { 53 | super(`Cannot find module '${name}'`) 54 | } 55 | } 56 | 57 | export class UnknownEffectError extends DomainError { 58 | constructor(name) { 59 | super(`Cannot find effect '${name}'`) 60 | } 61 | } 62 | 63 | export class UnexpectedResultError extends DomainError { 64 | constructor() { 65 | super(`Unexpected solution to a declaration`) 66 | } 67 | } 68 | 69 | export class DuplicateDeclarationError extends DomainError { 70 | constructor(name) { 71 | super( 72 | `Cannot import '${name}' as it would overwrite existing declaration` 73 | ) 74 | } 75 | } 76 | 77 | export class IrreducibleExpressionError extends DomainError { 78 | constructor(type) { 79 | super(`Cannot reduce expression starting with ${type}`) 80 | } 81 | } 82 | 83 | export class InsufficientArgumentsError extends DomainError { 84 | constructor() { 85 | super(`Not enough arguments to make a reduction`) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/collections.srl: -------------------------------------------------------------------------------- 1 | module Collections 2 | 3 | True 1 2 := 1 4 | False 1 2 := 2 5 | 6 | assert {`True` should choose first of its arguments} 7 | True <1> <2> = <1> 8 | assert {`False` should choose second of its arguments} 9 | False <1> <2> = <2> 10 | 11 | if Bool if-true if-false := Bool if-true if-false 12 | 13 | assert {`if True` should behave like `True`} 14 | if True <1> <2> = True <1> <2> 15 | assert {`if False` should behave like `False`} 16 | if False <1> <2> = False <1> <2> 17 | 18 | pair 1 2 f := f 1 2 19 | first Pair := Pair True 20 | 21 | assert {`first` should extract the head of a pair} 22 | first (pair <1> <2>) = <1> 23 | 24 | second Pair := Pair False 25 | 26 | assert {`second` should extract the tail of a pair} 27 | second (pair <1> <2>) = <2> 28 | 29 | Nil := pair True True 30 | is-Nil P := P True 31 | 32 | assert {`is-Nil Nil` should return True} 33 | is-Nil Nil = True 34 | 35 | . h t := pair False (pair h t) 36 | a-list := <1> . <2> . <3> . <4> . Nil 37 | 38 | assert {`is-Nil a-list` should return False} 39 | is-Nil a-list = False 40 | 41 | head L := first (second L) 42 | tail L := second (second L) 43 | 44 | {Check if `head` and `tail` work for all list elements} 45 | assert head a-list = <1> 46 | assert tail a-list = <2> . <3> . <4> . Nil 47 | assert head (tail a-list) = <2> 48 | assert tail (tail a-list) = <3> . <4> . Nil 49 | assert head (tail (tail a-list)) = <3> 50 | assert tail (tail (tail a-list)) = . <4> Nil 51 | assert head (tail (tail (tail a-list))) = <4> 52 | assert tail (tail (tail (tail a-list))) = Nil 53 | 54 | last L := if (is-Nil (tail L)) (head L) (last (tail L)) 55 | nth N L := head (N tail L) 56 | 57 | 0 f x := x 58 | 1 f x := f x 59 | 2 f x := f (f x) 60 | 3 f x := f (f (f x)) 61 | 4 f x := f (f (f (f x))) 62 | 5 f x := f (f (f (f (f x)))) 63 | 6 f x := f (f (f (f (f (f x))))) 64 | 65 | {Check if `nth` and `last` work for a list} 66 | assert last a-list = <4> 67 | assert nth 0 a-list = <1> 68 | assert nth 1 a-list = <2> 69 | assert nth 2 a-list = <3> 70 | assert nth 3 a-list = <4> 71 | assert is-Nil (nth 3 a-list) = False 72 | 73 | { 74 | Check if out of bounds `nth` is `Nil` 75 | Also, using an out of bounds element without 76 | checking for `Nil` is undefined behaviour 77 | } 78 | assert is-Nil (nth 4 a-list) = True 79 | assert is-Nil (nth 5 a-list) = True 80 | assert is-Nil (nth 6 a-list) = True -------------------------------------------------------------------------------- /examples/numerals.srl: -------------------------------------------------------------------------------- 1 | module Numerals 2 | 3 | True a b := a 4 | False a b := b 5 | 6 | and A B := A B A 7 | 8 | {Truth table for `and` operator} 9 | assert and False False = False 10 | assert and False True = False 11 | assert and True False = False 12 | assert and True True = True 13 | 14 | or A B := A A B 15 | 16 | {Truth table for `or` operator} 17 | assert or False False = False 18 | assert or False True = True 19 | assert or True False = True 20 | assert or True True = True 21 | 22 | not A := A False True 23 | 24 | {Truth table for `not` operator} 25 | assert not True = False 26 | assert not False = True 27 | 28 | first a b := a 29 | second a b := b 30 | 31 | {`first` and `second` should return respective arguments} 32 | assert first <1> () = <1> 33 | assert second () <2> = <2> 34 | 35 | 0 f x := x 36 | 37 | assert {0 should not apply a function} 38 | 0 (first False) True = True 39 | 40 | 1 f x := f x 41 | 42 | assert {1 should apply function once} 43 | 1 (first False) True = False 44 | 45 | 2 f x := f (f x) 46 | 47 | assert {2 plus 2 should increment the box 4 times} 48 | plus 2 2 (eat! #a) () = () 49 | | equals! #a <4> 50 | 51 | plus A B f x := A f (B f x) 52 | 53 | assert {0 plus 0 should not apply a function} 54 | plus 0 0 (first False) True = True 55 | assert {0 plus 1 should apply function at least once} 56 | plus 0 1 (first False) True = False 57 | assert {1 plus 1 should increment the box 2 times} 58 | plus 1 1 (eat! #a) () = () 59 | | equals! #a <2> 60 | 61 | succ A f x := f (A f x) 62 | 63 | assert {succ 1 should increment the box 2 times} 64 | succ 1 (eat! #a) () = () 65 | | equals! #a <2> 66 | 67 | mul A B f x := A (B f) x 68 | 69 | assert {mul 2 3 should increment the box 6 times} 70 | mul 2 3 (eat! #a) () = () 71 | | equals! #a <6> 72 | 73 | 3 f x := f (f (f x)) 74 | exp A B := B A 75 | 76 | assert {exp 2 3 should increment the box 8 times} 77 | exp 2 3 (eat! #a) () = () 78 | | equals! #a <8> 79 | 80 | minus A B := B pred A 81 | 82 | assert {minus 3 2 should increment the box 1 time} 83 | minus 3 2 (eat! #a) () = () 84 | | equals! #a <1> 85 | 86 | id x := x 87 | first x y := x 88 | inc f g h := h (g f) 89 | pred A f x := A (inc f) (first x) id 90 | 91 | assert {pred 3 should increment the box 2 times} 92 | pred 3 (eat! #a) () = () 93 | | equals! #a <2> 94 | 95 | is-zero A := A (first False) True 96 | 97 | assert {is-zero 0 should be True} 98 | is-zero 0 = True 99 | assert {is-zero 1 should be False} 100 | is-zero 1 = False 101 | assert {is-zero (pred 1) should be True} 102 | is-zero (pred 1) = True 103 | assert {is-zero (succ 0) should be False} 104 | is-zero (succ 0) = False 105 | 106 | {Set up testing side-effect} 107 | eat! B *rest := *rest 108 | | increment! B -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob-promise' 2 | 3 | import Module from './Module.js' 4 | import Linker from './Linker.js' 5 | import TestHarness from './TestHarness.js' 6 | import { FileSource, StdinSource } from './sources.js' 7 | 8 | import { UnknownDeclarationError, UnexpectedResultError } from './errors.js' 9 | 10 | import { helpText } from './constants.js' 11 | import { format } from './utils.js' 12 | 13 | export async function main(flags) { 14 | // Check if help has been requested 15 | if (flags.help === true || flags.h === true) { 16 | console.log(helpText) 17 | return process.exit(0) 18 | } 19 | 20 | // Create all modules that have been included in CLI command 21 | const modules = [] 22 | 23 | if (flags.stdin === true) { 24 | const source = new StdinSource() 25 | modules.push(new Module(source)) 26 | } 27 | 28 | if (Array.isArray(flags._)) { 29 | const files = await Promise.all( 30 | flags._.map(path => glob(path)) 31 | ).then(aFiles => aFiles.reduce((acc, arr) => [...acc, ...arr], [])) 32 | 33 | for (let filePath of files) { 34 | const source = new FileSource(filePath) 35 | modules.push(new Module(source)) 36 | } 37 | } 38 | 39 | if (Array.isArray(flags.import)) { 40 | for (let filePath of flags.import) { 41 | const source = new FileSource(await glob(filePath)) 42 | modules.push(new Module(source, true)) 43 | } 44 | } else if (typeof flags.import === 'string') { 45 | const source = new FileSource(flags.import) 46 | modules.push(new Module(source, true)) 47 | } 48 | 49 | // Initialize all modules in parallel 50 | await Promise.all(modules.map(mod => mod.initialize())) 51 | 52 | // Create a linker... 53 | const linker = new Linker(modules) 54 | 55 | // and link all modules 56 | linker.linkAll() 57 | 58 | // Get all modules that are not library imports (main modules) 59 | const mains = linker.getMains() 60 | 61 | // Decide what to do next: 62 | // 1. Run all assertions in main modules 63 | if (flags.runAssertions === true) { 64 | const testHarness = new TestHarness(mains, flags) 65 | 66 | return testHarness.run() 67 | } else if (typeof flags.solve === 'string') { 68 | // 2. Solve a specific declaration 69 | const declarationName = flags.solve 70 | 71 | const mod = mains.find(mod => mod.hasDeclaration(declarationName)) 72 | 73 | if (mod === undefined) { 74 | throw new UnknownDeclarationError(declarationName) 75 | } 76 | 77 | const declaration = mod.getDeclaration(declarationName) 78 | 79 | // TODO: if declaration has inputs, throw and ask for them 80 | 81 | const result = mod.evaluate(declaration.body, { 82 | onStart: data => { 83 | console.log(format(data)) 84 | }, 85 | onStep: data => { 86 | console.log(format(data)) 87 | // TODO: Display trace step by step 88 | }, 89 | onEnd: data => { 90 | console.log(format(data)) 91 | } 92 | }) 93 | 94 | // TODO: Display output 95 | 96 | if (!result) { 97 | throw new UnexpectedResultError() 98 | } 99 | 100 | return 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/TestHarness.js: -------------------------------------------------------------------------------- 1 | import { format } from './utils.js' 2 | import { inspect } from 'util' 3 | import assert from 'assert' 4 | 5 | const templates = { 6 | getHeader: () => [`TAP version 13`], 7 | getCount: testCount => [`1..${testCount}`], 8 | getModuleHeader: module => [`# ${module.name} (${module.source.name})`], 9 | getSkipped: (testIndex, description) => [ 10 | `ok ${testIndex} -${description} # SKIP` 11 | ], 12 | getSuccess: (testIndex, description) => [`ok ${testIndex} -${description}`], 13 | getFailure: (testIndex, description, details) => [ 14 | `not ok ${testIndex} -${description}`, 15 | ` ---`, 16 | details.error ? ` error: ${details.error}` : null, 17 | details.expected ? ` expected: ${format(details.expected)}` : null, 18 | details.actual ? ` actual: ${format(details.actual)}` : null, 19 | details.stack ? ` stack: ${format(details.stack)}` : null, 20 | ` ...` 21 | ] 22 | } 23 | 24 | export default class TestHarness { 25 | constructor(modules, flags) { 26 | this.flags = flags 27 | this.modules = modules 28 | } 29 | 30 | get testCount() { 31 | return this.modules.reduce( 32 | (sum, module) => 33 | sum + 34 | Array.from(module.context.asserts).reduce( 35 | (sum, assert) => 1 + assert.sideEffects.length + sum, 36 | 0 37 | ), 38 | 0 39 | ) 40 | } 41 | 42 | run() { 43 | const state = { 44 | testIndex: 0, 45 | failedCount: 0, 46 | skippedCount: 0, 47 | passedCount: 0, 48 | testCount: 0 49 | } 50 | 51 | let results = [ 52 | templates.getHeader(), 53 | templates.getCount(this.testCount) 54 | ] 55 | 56 | for (let module of this.modules) { 57 | const context = module.context 58 | 59 | results.push(templates.getModuleHeader(module)) 60 | 61 | for (let { 62 | test, 63 | expect, 64 | comment, 65 | sideEffects 66 | } of context.asserts) { 67 | state.testIndex += 1 68 | 69 | context.boxes.push() 70 | 71 | const testDescription = ` ${ 72 | comment ? comment + ' ' : '' 73 | }(${format(test)})` 74 | 75 | let result 76 | let evaluatedExpect 77 | let error 78 | 79 | try { 80 | result = context.evaluate( 81 | test, 82 | this.flags.trace 83 | ? { 84 | onStart: data => 85 | console.log(`---\n${format(data)}`), 86 | onStep: data => console.log(format(data)) 87 | } 88 | : {} 89 | ) 90 | 91 | try { 92 | evaluatedExpect = context.evaluate(expect) 93 | } catch (e) { 94 | evaluatedExpect = expect 95 | } 96 | 97 | assert.deepStrictEqual(evaluatedExpect, result) 98 | 99 | state.passedCount += 1 100 | results.push( 101 | templates.getSuccess(state.testIndex, testDescription) 102 | ) 103 | 104 | for (let effect of sideEffects) { 105 | state.testIndex += 1 106 | 107 | const effectDescription = `- side-effect: ${format( 108 | effect 109 | )}` 110 | 111 | const result = context.runSideEffect(effect, [], []) 112 | 113 | if (result === true) { 114 | results.push( 115 | templates.getSuccess( 116 | state.testIndex, 117 | effectDescription 118 | ) 119 | ) 120 | } else { 121 | results.push( 122 | templates.getFailure( 123 | state.testIndex, 124 | effectDescription, 125 | result 126 | ) 127 | ) 128 | } 129 | } 130 | } catch (e) { 131 | state.failedCount += 1 132 | 133 | error = e 134 | 135 | results.push( 136 | templates.getFailure(state.testIndex, testDescription, { 137 | error: 138 | error instanceof assert.AssertionError 139 | ? null 140 | : error, 141 | expected: evaluatedExpect, 142 | actual: result, 143 | stack: test 144 | }) 145 | ) 146 | 147 | for (let effect of sideEffects) { 148 | state.testIndex += 1 149 | 150 | results.push( 151 | templates.getSkipped( 152 | state.testIndex, 153 | `- side-effect` 154 | ) 155 | ) 156 | } 157 | } finally { 158 | state.testCount += 1 159 | } 160 | 161 | context.boxes.pop() 162 | } 163 | } 164 | 165 | const resultString = results 166 | .reduce((acc, strs) => [...acc, strs.filter(Boolean).join('\n')]) 167 | .join('\n') 168 | 169 | console.log(resultString) 170 | 171 | process.exit(state.failedCount > 0 ? 1 : 0) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/grammar.ne: -------------------------------------------------------------------------------- 1 | @{% 2 | const moo = require('moo') 3 | 4 | const lexer = moo.states({ 5 | main: { 6 | p_left: '(', 7 | p_right: ')', 8 | cb_left: { match: '{', push: 'comment' }, 9 | pipe: '|', 10 | box: '#', 11 | arrow: '=>>', 12 | as: ':=', 13 | colon: ':', 14 | eq: '=', 15 | star: '*', 16 | id: { match: /[a-zA-Z0-9$!?\.\-]+/, type: moo.keywords({ 17 | assert: ['assert'], 18 | imp: ["import"], 19 | mod: ["module"] 20 | }) }, 21 | ws: /[ \t]+/, 22 | sym: /\<[0-9]+\>/, 23 | nl: { match: /\r?\n/, lineBreaks: true } 24 | }, 25 | comment: { 26 | comment: { match: /[^}]+/, lineBreaks: true }, 27 | cb_right: { match:'}', pop: true } 28 | } 29 | }) 30 | 31 | const identity = a => a 32 | const nth = (n, f) => (a) => f(a[n - 1]) 33 | const map = (f) => (a) => a.map(f) 34 | const second = (a) => a[1] 35 | 36 | const converge = (fs, c) => (a) => c(...fs.map(f => f(a))) 37 | const concat = (...els) => els.reduce((r, e) => [...r, ...(Array.isArray(e) ? e : [e]) ], []) 38 | 39 | const nil = () => null 40 | const lift = x => [x] 41 | 42 | const IDENTIFIER = (token, starToken) => ({ 43 | type: 'IDENTIFIER', 44 | value: token.value, 45 | hasStar: starToken !== null 46 | }) 47 | 48 | const SYMBOL = (token) => ({ 49 | type: 'SYMBOL', 50 | value: parseFloat(token.value.substr(1)) 51 | }) 52 | 53 | const EXPRESSION = (body) => ({ 54 | type: 'EXPRESSION', 55 | value: body 56 | }) 57 | 58 | const DECLARATION = (name, args, body, se) => ({ 59 | type: 'DECLARATION', 60 | name, 61 | args, 62 | body, 63 | sideEffects: se 64 | }) 65 | 66 | const ASSERTION = (name, body, comment, se) => ({ 67 | type: 'ASSERTION', 68 | name, 69 | body, 70 | comment: comment && comment.value, 71 | sideEffects: se 72 | }) 73 | 74 | const IMPORT = (name, ids) => ({ 75 | type: 'IMPORT', 76 | name, 77 | ids 78 | }) 79 | 80 | const PROGRAM = (statements, body) => ({ 81 | type: 'PROGRAM', 82 | statements: statements.filter(Boolean), 83 | }) 84 | 85 | const BOX = (identifier) => ({ 86 | type: 'BOX', 87 | value: identifier.value 88 | }) 89 | 90 | const MODULE = (name) => ({ 91 | type: 'MODULE', 92 | name: name.value 93 | }) 94 | 95 | const STAR = {} 96 | 97 | const UNIT = { 98 | type: 'UNIT' 99 | } 100 | %} 101 | 102 | @lexer lexer 103 | 104 | Program -> NewLine:* EntryList NewLine:* 105 | {% ([_1, declarations, _2]) => PROGRAM(declarations) %} 106 | 107 | EntryList -> Entry (NewLine:+ Entry):* 108 | {% converge([nth(1, identity), nth(2, map(second))], concat) %} 109 | 110 | Entry -> 111 | Module {% nth(1, identity) %} 112 | | Declaration {% nth(1, identity) %} 113 | | Assertion {% nth(1, identity) %} 114 | | Import {% nth(1, identity) %} 115 | | Comment {% nil %} 116 | 117 | Module -> %mod _ Identifier {% ([_m, _1, name]) => MODULE(name) %} 118 | 119 | Declaration -> 120 | Identifier ArgumentList:? _:? Assign _:? Expression SideEffect:* 121 | {% ([name, args, _1, _2, _3, body, se]) => DECLARATION(name, args, body, se) %} 122 | 123 | SideEffect -> NewLine _:* Pipe _:+ Expression {% nth(5, identity) %} 124 | 125 | Assertion -> Assert _ (Comment AnyWhitespace:+):? Expression _:? Equals _:? AssertionBody SideEffect:* 126 | {% ([_a, _1, comment, name, _2, _3, _4, body, se ]) => ASSERTION(name, body, comment && comment[0], se) %} 127 | 128 | Comment -> %cb_left %comment %cb_right {% ([_, comment]) => comment %} 129 | 130 | AssertionBody -> 131 | Expression {% nth(1, identity) %} 132 | | Symbol {% nth(1, lift) %} 133 | | Box {% nth(1, lift) %} 134 | | Unit {% nth(1, lift) %} 135 | 136 | Import -> ImportKeyword _ Identifier _ Arrow _ ImportList 137 | {% ([_1, _i, name, _2, _a, _3, ids]) => IMPORT(name, ids) %} 138 | 139 | ImportList -> 140 | Star {% () => STAR %} 141 | | Identifier (ImportSeparator Identifier):* {% converge([nth(1, identity), nth(2, map(second))], concat) %} 142 | 143 | ImportSeparator -> _ {% nil %} | ImportLineBreak {% nil %} 144 | 145 | ImportLineBreak -> NewLine _ Arrow _ {% nil %} 146 | 147 | Expression -> 148 | Identifier (_ Value):* 149 | {% converge([nth(1, identity), nth(2, map(second))], concat) %} 150 | | Value _ Identifier _ Value (_ Identifier _ Value):+ {% 151 | ([v1, _1, id, _2, v2, tails], l, reject) => { 152 | const result = [[id, v1], [id, v2], ...tails.map(([_1, id, _2, value]) => [id, value])] 153 | 154 | const isSameId = result.every(([tid]) => tid.type === id.type && tid.value === id.value) 155 | 156 | if (isSameId) { 157 | let [op, val] = result[result.length - 1] 158 | return result.slice(0, -1).reduceRight((acc, [id, value]) => [id, value, acc], val) 159 | } else { 160 | return reject 161 | } 162 | } 163 | %} 164 | 165 | Value -> 166 | Symbol {% nth(1, identity) %} 167 | | Identifier {% nth(1, identity) %} 168 | | Box {% nth(1, identity) %} 169 | | %p_left Expression %p_right {% nth(2, identity) %} 170 | | Unit {% nth(1, identity) %} 171 | 172 | ArgumentList -> (_ Identifier):+ {% nth(1, map(second)) %} 173 | 174 | Box -> %box Identifier {% nth(2, BOX) %} 175 | Identifier -> Star:? %id {% ([star, id]) => IDENTIFIER(id, star) %} 176 | Symbol -> %sym {% nth(1, SYMBOL) %} 177 | Unit -> %p_left %p_right {% () => UNIT %} 178 | 179 | AnyWhitespace -> NewLine {% nil %} | _ {% nil %} 180 | EmptyLine -> NewLine NewLine {% nil %} 181 | ImportKeyword -> %imp {% nil %} 182 | Assert -> %assert {% nil %} 183 | NewLine -> %nl {% nil %} 184 | Equals -> %eq {% nil %} 185 | Assign -> %as {% nil %} 186 | _ -> %ws {% nil %} 187 | Pipe -> %pipe {% nil %} 188 | Star -> %star {% () => STAR %} 189 | Dot -> %dot {% nil %} 190 | Arrow -> %arrow {% nil %} 191 | Colon -> %colon {% nil %} -------------------------------------------------------------------------------- /src/Context.js: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | 3 | import { format } from './utils.js' 4 | import BoxManager from './BoxManager.js' 5 | import { ofType, orDefault } from './data-manipulation.js' 6 | 7 | import SIDE_EFFECTS from './side-effects.js' 8 | 9 | import { 10 | IrreducibleExpressionError, 11 | UnknownDeclarationError, 12 | InsufficientArgumentsError, 13 | UnknownEffectError 14 | } from './errors.js' 15 | 16 | export default class Context { 17 | moduleDeclaration = null 18 | declarations = new Map() 19 | asserts = new Set() 20 | imports = new Set() 21 | 22 | boxes = new BoxManager() 23 | 24 | getModuleDeclaration() { 25 | return this.moduleDeclaration 26 | } 27 | 28 | getDeclaration(name) { 29 | return this.declarations.get(name) 30 | } 31 | 32 | hasDeclaration(name) { 33 | return this.declarations.has(name) 34 | } 35 | 36 | addDeclaration(name, declaration) { 37 | return this.declarations.set(name, declaration) 38 | } 39 | 40 | loadProgram(statements) { 41 | statements 42 | .filter(ofType('DECLARATION')) 43 | .forEach(({ name, args, body, sideEffects }) => 44 | this.declarations.set(name.value, { 45 | name: name.value, 46 | args: args, 47 | body: body, 48 | sideEffects: orDefault(sideEffects, []) 49 | }) 50 | ) 51 | 52 | statements 53 | .filter(ofType('ASSERTION')) 54 | .forEach(({ name, body, comment, sideEffects }) => 55 | this.asserts.add({ 56 | test: name, 57 | expect: body, 58 | comment: comment, 59 | sideEffects: orDefault(sideEffects, []) 60 | }) 61 | ) 62 | 63 | statements.filter(ofType('IMPORT')).forEach(({ name, ids }) => 64 | this.imports.add({ 65 | name: name.value, 66 | identifiers: ids 67 | }) 68 | ) 69 | 70 | this.moduleDeclaration = statements.find( 71 | statement => statement.type === 'MODULE' 72 | ) 73 | } 74 | 75 | getImplicitImports(identifiers) { 76 | let queue = identifiers.slice() 77 | let result = new Map() 78 | let seen = new Set() 79 | 80 | while (queue.length > 0) { 81 | const identifier = queue.shift() 82 | 83 | if (this.hasDeclaration(identifier.value)) { 84 | const declaration = this.getDeclaration(identifier.value) 85 | 86 | if (seen.has(declaration.name)) { 87 | continue 88 | } else { 89 | seen.add(declaration.name) 90 | result.set(declaration.name, declaration) 91 | 92 | queue.push(...declaration.body.flat()) 93 | } 94 | } 95 | } 96 | 97 | return result 98 | } 99 | 100 | replace(subject, target, replacement) { 101 | const result = subject.reduce((acc, entry) => { 102 | if (Array.isArray(entry)) { 103 | return [...acc, this.replace(entry, target, replacement)] 104 | } 105 | 106 | if (target.type === entry.type && target.value === entry.value) { 107 | if (entry.hasStar && Array.isArray(replacement)) { 108 | return [...acc, ...replacement] 109 | } else { 110 | return [...acc, replacement] 111 | } 112 | } else { 113 | return [...acc, entry] 114 | } 115 | }, []) 116 | return result 117 | } 118 | 119 | runSideEffect(effect, shape, rest) { 120 | const [head, ...tail] = effect 121 | const handler = SIDE_EFFECTS[head.value] 122 | 123 | if (!handler) { 124 | throw new UnknownEffectError(head.value) 125 | } 126 | 127 | let res = [...tail] 128 | let from = shape === null ? [] : [...shape] 129 | let args = [...rest] 130 | 131 | while (from.length > 0) { 132 | const argument = from.shift() 133 | 134 | let value 135 | if (argument.hasStar) { 136 | value = args 137 | } else { 138 | value = args.shift() 139 | } 140 | 141 | res = this.replace(res, argument, value) 142 | } 143 | 144 | return handler(res, this) 145 | } 146 | 147 | simplify(expression) { 148 | let [head, ...tail] = expression 149 | 150 | while (Array.isArray(head)) { 151 | ;[head, ...tail] = [...head, ...tail] 152 | } 153 | 154 | return [head, ...tail] 155 | } 156 | 157 | reduce(expression, shouldRunEffects = true) {} 158 | 159 | *solve(input, shouldRunEffects = true) { 160 | let expression = input 161 | 162 | while (true) { 163 | let [head, ...tail] = this.simplify(expression) 164 | 165 | if (tail.length === 0) { 166 | if (head.type === 'IDENTIFIER') { 167 | const declaration = this.getDeclaration(head.value) 168 | 169 | if ( 170 | declaration && 171 | declaration.args && 172 | declaration.args.length > 0 173 | ) { 174 | return [head] 175 | } 176 | } else { 177 | return [head] 178 | } 179 | } else { 180 | if (head.type !== 'IDENTIFIER') { 181 | throw new IrreducibleExpressionError(head.type) 182 | } 183 | } 184 | 185 | if (!this.hasDeclaration(head.value)) { 186 | throw new UnknownDeclarationError(head.value) 187 | } 188 | 189 | const declaration = this.getDeclaration(head.value) 190 | 191 | const from = [...(declaration.args || [])] 192 | const effects = [...declaration.sideEffects] 193 | let result = [...declaration.body] 194 | let args = [...tail] 195 | 196 | while (from.length > 0) { 197 | const argument = from.shift() 198 | let value 199 | 200 | if (argument.hasStar) { 201 | value = args.slice() 202 | args = [] 203 | } else { 204 | value = args.shift() 205 | } 206 | 207 | if (value === undefined) { 208 | return result 209 | } 210 | 211 | result = this.replace(result, argument, value) 212 | } 213 | 214 | if (Array.isArray(effects) && shouldRunEffects) { 215 | for (let effect of effects) { 216 | this.runSideEffect(effect, declaration.args, tail) 217 | } 218 | } 219 | 220 | expression = [...result, ...args] 221 | yield expression 222 | } 223 | } 224 | 225 | evaluate(expression, options = {}) { 226 | let result = expression 227 | ;(options.onStart || (() => {}))(expression) 228 | for (let res of this.solve(result)) { 229 | result = res 230 | ;(options.onStep || (() => {}))(result) 231 | } 232 | 233 | ;(options.onEnd || (() => {}))(result) 234 | 235 | return result 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/grammar.cjs: -------------------------------------------------------------------------------- 1 | // Generated automatically by nearley, version 2.19.0 2 | // http://github.com/Hardmath123/nearley 3 | (function () { 4 | function id(x) { return x[0]; } 5 | 6 | const moo = require('moo') 7 | 8 | const lexer = moo.states({ 9 | main: { 10 | p_left: '(', 11 | p_right: ')', 12 | cb_left: { match: '{', push: 'comment' }, 13 | pipe: '|', 14 | box: '#', 15 | arrow: '=>>', 16 | as: ':=', 17 | colon: ':', 18 | eq: '=', 19 | star: '*', 20 | id: { match: /[a-zA-Z0-9$!?\.\-]+/, type: moo.keywords({ 21 | assert: ['assert'], 22 | imp: ["import"], 23 | mod: ["module"] 24 | }) }, 25 | ws: /[ \t]+/, 26 | sym: /\<[0-9]+\>/, 27 | nl: { match: /\r?\n/, lineBreaks: true } 28 | }, 29 | comment: { 30 | comment: { match: /[^}]+/, lineBreaks: true }, 31 | cb_right: { match:'}', pop: true } 32 | } 33 | }) 34 | 35 | const identity = a => a 36 | const nth = (n, f) => (a) => f(a[n - 1]) 37 | const map = (f) => (a) => a.map(f) 38 | const second = (a) => a[1] 39 | 40 | const converge = (fs, c) => (a) => c(...fs.map(f => f(a))) 41 | const concat = (...els) => els.reduce((r, e) => [...r, ...(Array.isArray(e) ? e : [e]) ], []) 42 | 43 | const nil = () => null 44 | const lift = x => [x] 45 | 46 | const IDENTIFIER = (token, starToken) => ({ 47 | type: 'IDENTIFIER', 48 | value: token.value, 49 | hasStar: starToken !== null 50 | }) 51 | 52 | const SYMBOL = (token) => ({ 53 | type: 'SYMBOL', 54 | value: parseFloat(token.value.substr(1)) 55 | }) 56 | 57 | const EXPRESSION = (body) => ({ 58 | type: 'EXPRESSION', 59 | value: body 60 | }) 61 | 62 | const DECLARATION = (name, args, body, se) => ({ 63 | type: 'DECLARATION', 64 | name, 65 | args, 66 | body, 67 | sideEffects: se 68 | }) 69 | 70 | const ASSERTION = (name, body, comment, se) => ({ 71 | type: 'ASSERTION', 72 | name, 73 | body, 74 | comment: comment && comment.value, 75 | sideEffects: se 76 | }) 77 | 78 | const IMPORT = (name, ids) => ({ 79 | type: 'IMPORT', 80 | name, 81 | ids 82 | }) 83 | 84 | const PROGRAM = (statements, body) => ({ 85 | type: 'PROGRAM', 86 | statements: statements.filter(Boolean), 87 | }) 88 | 89 | const BOX = (identifier) => ({ 90 | type: 'BOX', 91 | value: identifier.value 92 | }) 93 | 94 | const MODULE = (name) => ({ 95 | type: 'MODULE', 96 | name: name.value 97 | }) 98 | 99 | const STAR = {} 100 | 101 | const UNIT = { 102 | type: 'UNIT' 103 | } 104 | var grammar = { 105 | Lexer: lexer, 106 | ParserRules: [ 107 | {"name": "Program$ebnf$1", "symbols": []}, 108 | {"name": "Program$ebnf$1", "symbols": ["Program$ebnf$1", "NewLine"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 109 | {"name": "Program$ebnf$2", "symbols": []}, 110 | {"name": "Program$ebnf$2", "symbols": ["Program$ebnf$2", "NewLine"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 111 | {"name": "Program", "symbols": ["Program$ebnf$1", "EntryList", "Program$ebnf$2"], "postprocess": ([_1, declarations, _2]) => PROGRAM(declarations)}, 112 | {"name": "EntryList$ebnf$1", "symbols": []}, 113 | {"name": "EntryList$ebnf$1$subexpression$1$ebnf$1", "symbols": ["NewLine"]}, 114 | {"name": "EntryList$ebnf$1$subexpression$1$ebnf$1", "symbols": ["EntryList$ebnf$1$subexpression$1$ebnf$1", "NewLine"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 115 | {"name": "EntryList$ebnf$1$subexpression$1", "symbols": ["EntryList$ebnf$1$subexpression$1$ebnf$1", "Entry"]}, 116 | {"name": "EntryList$ebnf$1", "symbols": ["EntryList$ebnf$1", "EntryList$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 117 | {"name": "EntryList", "symbols": ["Entry", "EntryList$ebnf$1"], "postprocess": converge([nth(1, identity), nth(2, map(second))], concat)}, 118 | {"name": "Entry", "symbols": ["Module"], "postprocess": nth(1, identity)}, 119 | {"name": "Entry", "symbols": ["Declaration"], "postprocess": nth(1, identity)}, 120 | {"name": "Entry", "symbols": ["Assertion"], "postprocess": nth(1, identity)}, 121 | {"name": "Entry", "symbols": ["Import"], "postprocess": nth(1, identity)}, 122 | {"name": "Entry", "symbols": ["Comment"], "postprocess": nil}, 123 | {"name": "Module", "symbols": [(lexer.has("mod") ? {type: "mod"} : mod), "_", "Identifier"], "postprocess": ([_m, _1, name]) => MODULE(name)}, 124 | {"name": "Declaration$ebnf$1", "symbols": ["ArgumentList"], "postprocess": id}, 125 | {"name": "Declaration$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, 126 | {"name": "Declaration$ebnf$2", "symbols": ["_"], "postprocess": id}, 127 | {"name": "Declaration$ebnf$2", "symbols": [], "postprocess": function(d) {return null;}}, 128 | {"name": "Declaration$ebnf$3", "symbols": ["_"], "postprocess": id}, 129 | {"name": "Declaration$ebnf$3", "symbols": [], "postprocess": function(d) {return null;}}, 130 | {"name": "Declaration$ebnf$4", "symbols": []}, 131 | {"name": "Declaration$ebnf$4", "symbols": ["Declaration$ebnf$4", "SideEffect"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 132 | {"name": "Declaration", "symbols": ["Identifier", "Declaration$ebnf$1", "Declaration$ebnf$2", "Assign", "Declaration$ebnf$3", "Expression", "Declaration$ebnf$4"], "postprocess": ([name, args, _1, _2, _3, body, se]) => DECLARATION(name, args, body, se)}, 133 | {"name": "SideEffect$ebnf$1", "symbols": []}, 134 | {"name": "SideEffect$ebnf$1", "symbols": ["SideEffect$ebnf$1", "_"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 135 | {"name": "SideEffect$ebnf$2", "symbols": ["_"]}, 136 | {"name": "SideEffect$ebnf$2", "symbols": ["SideEffect$ebnf$2", "_"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 137 | {"name": "SideEffect", "symbols": ["NewLine", "SideEffect$ebnf$1", "Pipe", "SideEffect$ebnf$2", "Expression"], "postprocess": nth(5, identity)}, 138 | {"name": "Assertion$ebnf$1$subexpression$1$ebnf$1", "symbols": ["AnyWhitespace"]}, 139 | {"name": "Assertion$ebnf$1$subexpression$1$ebnf$1", "symbols": ["Assertion$ebnf$1$subexpression$1$ebnf$1", "AnyWhitespace"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 140 | {"name": "Assertion$ebnf$1$subexpression$1", "symbols": ["Comment", "Assertion$ebnf$1$subexpression$1$ebnf$1"]}, 141 | {"name": "Assertion$ebnf$1", "symbols": ["Assertion$ebnf$1$subexpression$1"], "postprocess": id}, 142 | {"name": "Assertion$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, 143 | {"name": "Assertion$ebnf$2", "symbols": ["_"], "postprocess": id}, 144 | {"name": "Assertion$ebnf$2", "symbols": [], "postprocess": function(d) {return null;}}, 145 | {"name": "Assertion$ebnf$3", "symbols": ["_"], "postprocess": id}, 146 | {"name": "Assertion$ebnf$3", "symbols": [], "postprocess": function(d) {return null;}}, 147 | {"name": "Assertion$ebnf$4", "symbols": []}, 148 | {"name": "Assertion$ebnf$4", "symbols": ["Assertion$ebnf$4", "SideEffect"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 149 | {"name": "Assertion", "symbols": ["Assert", "_", "Assertion$ebnf$1", "Expression", "Assertion$ebnf$2", "Equals", "Assertion$ebnf$3", "AssertionBody", "Assertion$ebnf$4"], "postprocess": ([_a, _1, comment, name, _2, _3, _4, body, se ]) => ASSERTION(name, body, comment && comment[0], se)}, 150 | {"name": "Comment", "symbols": [(lexer.has("cb_left") ? {type: "cb_left"} : cb_left), (lexer.has("comment") ? {type: "comment"} : comment), (lexer.has("cb_right") ? {type: "cb_right"} : cb_right)], "postprocess": ([_, comment]) => comment}, 151 | {"name": "AssertionBody", "symbols": ["Expression"], "postprocess": nth(1, identity)}, 152 | {"name": "AssertionBody", "symbols": ["Symbol"], "postprocess": nth(1, lift)}, 153 | {"name": "AssertionBody", "symbols": ["Box"], "postprocess": nth(1, lift)}, 154 | {"name": "AssertionBody", "symbols": ["Unit"], "postprocess": nth(1, lift)}, 155 | {"name": "Import", "symbols": ["ImportKeyword", "_", "Identifier", "_", "Arrow", "_", "ImportList"], "postprocess": ([_1, _i, name, _2, _a, _3, ids]) => IMPORT(name, ids)}, 156 | {"name": "ImportList", "symbols": ["Star"], "postprocess": () => STAR}, 157 | {"name": "ImportList$ebnf$1", "symbols": []}, 158 | {"name": "ImportList$ebnf$1$subexpression$1", "symbols": ["ImportSeparator", "Identifier"]}, 159 | {"name": "ImportList$ebnf$1", "symbols": ["ImportList$ebnf$1", "ImportList$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 160 | {"name": "ImportList", "symbols": ["Identifier", "ImportList$ebnf$1"], "postprocess": converge([nth(1, identity), nth(2, map(second))], concat)}, 161 | {"name": "ImportSeparator", "symbols": ["_"], "postprocess": nil}, 162 | {"name": "ImportSeparator", "symbols": ["ImportLineBreak"], "postprocess": nil}, 163 | {"name": "ImportLineBreak", "symbols": ["NewLine", "_", "Arrow", "_"], "postprocess": nil}, 164 | {"name": "Expression$ebnf$1", "symbols": []}, 165 | {"name": "Expression$ebnf$1$subexpression$1", "symbols": ["_", "Value"]}, 166 | {"name": "Expression$ebnf$1", "symbols": ["Expression$ebnf$1", "Expression$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 167 | {"name": "Expression", "symbols": ["Identifier", "Expression$ebnf$1"], "postprocess": converge([nth(1, identity), nth(2, map(second))], concat)}, 168 | {"name": "Expression$ebnf$2$subexpression$1", "symbols": ["_", "Identifier", "_", "Value"]}, 169 | {"name": "Expression$ebnf$2", "symbols": ["Expression$ebnf$2$subexpression$1"]}, 170 | {"name": "Expression$ebnf$2$subexpression$2", "symbols": ["_", "Identifier", "_", "Value"]}, 171 | {"name": "Expression$ebnf$2", "symbols": ["Expression$ebnf$2", "Expression$ebnf$2$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 172 | {"name": "Expression", "symbols": ["Value", "_", "Identifier", "_", "Value", "Expression$ebnf$2"], "postprocess": 173 | ([v1, _1, id, _2, v2, tails], l, reject) => { 174 | const result = [[id, v1], [id, v2], ...tails.map(([_1, id, _2, value]) => [id, value])] 175 | 176 | const isSameId = result.every(([tid]) => tid.type === id.type && tid.value === id.value) 177 | 178 | if (isSameId) { 179 | let [op, val] = result[result.length - 1] 180 | return result.slice(0, -1).reduceRight((acc, [id, value]) => [id, value, acc], val) 181 | } else { 182 | return reject 183 | } 184 | } 185 | }, 186 | {"name": "Value", "symbols": ["Symbol"], "postprocess": nth(1, identity)}, 187 | {"name": "Value", "symbols": ["Identifier"], "postprocess": nth(1, identity)}, 188 | {"name": "Value", "symbols": ["Box"], "postprocess": nth(1, identity)}, 189 | {"name": "Value", "symbols": [(lexer.has("p_left") ? {type: "p_left"} : p_left), "Expression", (lexer.has("p_right") ? {type: "p_right"} : p_right)], "postprocess": nth(2, identity)}, 190 | {"name": "Value", "symbols": ["Unit"], "postprocess": nth(1, identity)}, 191 | {"name": "ArgumentList$ebnf$1$subexpression$1", "symbols": ["_", "Identifier"]}, 192 | {"name": "ArgumentList$ebnf$1", "symbols": ["ArgumentList$ebnf$1$subexpression$1"]}, 193 | {"name": "ArgumentList$ebnf$1$subexpression$2", "symbols": ["_", "Identifier"]}, 194 | {"name": "ArgumentList$ebnf$1", "symbols": ["ArgumentList$ebnf$1", "ArgumentList$ebnf$1$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, 195 | {"name": "ArgumentList", "symbols": ["ArgumentList$ebnf$1"], "postprocess": nth(1, map(second))}, 196 | {"name": "Box", "symbols": [(lexer.has("box") ? {type: "box"} : box), "Identifier"], "postprocess": nth(2, BOX)}, 197 | {"name": "Identifier$ebnf$1", "symbols": ["Star"], "postprocess": id}, 198 | {"name": "Identifier$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, 199 | {"name": "Identifier", "symbols": ["Identifier$ebnf$1", (lexer.has("id") ? {type: "id"} : id)], "postprocess": ([star, id]) => IDENTIFIER(id, star)}, 200 | {"name": "Symbol", "symbols": [(lexer.has("sym") ? {type: "sym"} : sym)], "postprocess": nth(1, SYMBOL)}, 201 | {"name": "Unit", "symbols": [(lexer.has("p_left") ? {type: "p_left"} : p_left), (lexer.has("p_right") ? {type: "p_right"} : p_right)], "postprocess": () => UNIT}, 202 | {"name": "AnyWhitespace", "symbols": ["NewLine"], "postprocess": nil}, 203 | {"name": "AnyWhitespace", "symbols": ["_"], "postprocess": nil}, 204 | {"name": "EmptyLine", "symbols": ["NewLine", "NewLine"], "postprocess": nil}, 205 | {"name": "ImportKeyword", "symbols": [(lexer.has("imp") ? {type: "imp"} : imp)], "postprocess": nil}, 206 | {"name": "Assert", "symbols": [(lexer.has("assert") ? {type: "assert"} : assert)], "postprocess": nil}, 207 | {"name": "NewLine", "symbols": [(lexer.has("nl") ? {type: "nl"} : nl)], "postprocess": nil}, 208 | {"name": "Equals", "symbols": [(lexer.has("eq") ? {type: "eq"} : eq)], "postprocess": nil}, 209 | {"name": "Assign", "symbols": [(lexer.has("as") ? {type: "as"} : as)], "postprocess": nil}, 210 | {"name": "_", "symbols": [(lexer.has("ws") ? {type: "ws"} : ws)], "postprocess": nil}, 211 | {"name": "Pipe", "symbols": [(lexer.has("pipe") ? {type: "pipe"} : pipe)], "postprocess": nil}, 212 | {"name": "Star", "symbols": [(lexer.has("star") ? {type: "star"} : star)], "postprocess": () => STAR}, 213 | {"name": "Dot", "symbols": [(lexer.has("dot") ? {type: "dot"} : dot)], "postprocess": nil}, 214 | {"name": "Arrow", "symbols": [(lexer.has("arrow") ? {type: "arrow"} : arrow)], "postprocess": nil}, 215 | {"name": "Colon", "symbols": [(lexer.has("colon") ? {type: "colon"} : colon)], "postprocess": nil} 216 | ] 217 | , ParserStart: "Program" 218 | } 219 | if (typeof module !== 'undefined'&& typeof module.exports !== 'undefined') { 220 | module.exports = grammar; 221 | } else { 222 | window.grammar = grammar; 223 | } 224 | })(); 225 | --------------------------------------------------------------------------------