├── .gitignore ├── src └── joy │ ├── output.js │ ├── interpreter │ ├── util.js │ ├── dictionary.js │ ├── index.js │ ├── operand_defs.js │ ├── misc_defs.js │ ├── predicate_defs.js │ ├── types.js │ ├── combinator_defs.js │ └── operator_defs.js │ ├── stack.js │ ├── joy.js │ ├── fsm.js │ ├── lib │ ├── inilib.joy.js │ ├── agglib.joy.js │ ├── numlib.joy.js │ ├── seqlib.joy.js │ └── symlib.joy.js │ ├── parser.js │ └── lexer.js ├── LICENSE ├── static ├── index.html ├── styles.css └── index.js ├── package.json ├── test ├── joy.test.js └── lexer.test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output/ 2 | /coverage/ 3 | /dist/ 4 | /node_modules/ 5 | -------------------------------------------------------------------------------- /src/joy/output.js: -------------------------------------------------------------------------------- 1 | function Output (joy) { 2 | this.clear() 3 | } 4 | 5 | Output.prototype.clear = function clear () { 6 | this.value = '' 7 | return this 8 | } 9 | 10 | Output.prototype.write = function write (line) { 11 | this.value += line 12 | return this 13 | } 14 | 15 | Output.prototype.toString = function toString () { 16 | return this.value 17 | } 18 | 19 | module.exports = Output 20 | -------------------------------------------------------------------------------- /src/joy/interpreter/util.js: -------------------------------------------------------------------------------- 1 | const applyToTopN = n => f => stack => { 2 | const args = new Array(n) 3 | for (let i = n - 1; i >= 0; i -= 1) { 4 | args[i] = stack.pop() 5 | } 6 | stack.push(f.apply(null, args)) 7 | } 8 | 9 | exports.applyToTop = applyToTopN(1) 10 | exports.applyToTop2 = applyToTopN(2) 11 | exports.applyToTop3 = applyToTopN(3) 12 | exports.applyToTop4 = applyToTopN(4) 13 | exports.eq = (x, y) => x.equals(y) 14 | exports.ne = (x, y) => !exports.eq(x, y) 15 | exports.lte = (x, y) => x.lte(y) 16 | exports.lt = (x, y) => exports.lte(x, y) && !exports.eq(x, y) 17 | exports.gte = (x, y) => !exports.lt(x, y) 18 | exports.gt = (x, y) => !exports.lte(x, y) 19 | exports.cmp = (x, y) => { 20 | if (exports.eq(x, y)) { 21 | return 0 22 | } 23 | return exports.lte(x, y) ? -1 : 1 24 | } 25 | -------------------------------------------------------------------------------- /src/joy/stack.js: -------------------------------------------------------------------------------- 1 | function Stack () { 2 | var _this = this 3 | this._data = [] 4 | Object.defineProperty(this, 'length', { 5 | get: function get () { 6 | return _this._data.length 7 | } 8 | }) 9 | } 10 | 11 | Stack.prototype.pop = function pop () { 12 | if (this.length > 0) { 13 | return this._data.pop() 14 | } 15 | throw new RangeError('Cannot pop from empty stack') 16 | } 17 | 18 | Stack.prototype.push = function push (value) { 19 | this._data.push(value) 20 | } 21 | 22 | Stack.prototype.peek = function peek (n) { 23 | return n === 0 ? [] : this._data.slice(-n) 24 | } 25 | 26 | Stack.prototype.clear = function clear () { 27 | this._data.length = 0 28 | } 29 | 30 | Stack.prototype.restoreAfter = function restore (fn) { 31 | const memento = this._data.slice(0) 32 | const result = fn() 33 | this._data = memento 34 | return result 35 | } 36 | 37 | Stack.prototype.toString = function toSring () { 38 | return this._data.toString() 39 | } 40 | 41 | module.exports = Stack 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Jim Fitzpatrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Joy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

Try Joy

17 | 22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joy-js", 3 | "version": "0.1.0", 4 | "description": "A JavaScript interpreter for the Joy programming language", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/jimf/joy-js" 8 | }, 9 | "scripts": { 10 | "build": "rimraf dist && mkdir dist && cp static/index.html dist && npm run build-js && npm run build-css", 11 | "build-js": "browserify static/index.js | minify > dist/index.js", 12 | "build-css": "uglifycss static/styles.css > dist/styles.css", 13 | "deploy": "gh-pages -d dist", 14 | "lint": "standard", 15 | "pretest": "npm run lint", 16 | "start": "budo ./static/index.js --dir=./static --live --open --host=0.0.0.0", 17 | "test": "nyc tape test/*.js" 18 | }, 19 | "nyc": { 20 | "reporter": [ 21 | "lcov", 22 | "text" 23 | ] 24 | }, 25 | "keywords": [], 26 | "author": "Jim Fitzpatrick", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/jimf/joy-js/issues" 30 | }, 31 | "homepage": "https://github.com/jimf/joy-js", 32 | "dependencies": {}, 33 | "devDependencies": { 34 | "babel-minify": "^0.4.3", 35 | "browserify": "^16.2.2", 36 | "budo": "^11.2.2", 37 | "gh-pages": "^1.2.0", 38 | "nyc": "^11.8.0", 39 | "rimraf": "^2.6.2", 40 | "standard": "^11.0.1", 41 | "tape": "^4.9.0", 42 | "uglifycss": "0.0.29" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/joy/interpreter/dictionary.js: -------------------------------------------------------------------------------- 1 | const CombinatorDefs = require('./combinator_defs') 2 | const MiscDefs = require('./misc_defs') 3 | const OperandDefs = require('./operand_defs') 4 | const OperatorDefs = require('./operator_defs') 5 | const PredicateDefs = require('./predicate_defs') 6 | 7 | function Dictionary () { 8 | const definitions = {} 9 | 10 | function define (key, value) { 11 | if (Object.prototype.hasOwnProperty.call(definitions, key)) { 12 | throw new Error('Word "' + key + '" already defined') 13 | } 14 | definitions[key] = value 15 | } 16 | 17 | function get (key) { 18 | if (!Object.prototype.hasOwnProperty.call(definitions, key)) { 19 | throw new Error('Word "' + key + '" is not defined') 20 | } 21 | return definitions[key] 22 | } 23 | 24 | function keys () { 25 | return Object.keys(definitions) 26 | } 27 | 28 | return { define: define, get: get, keys: keys } 29 | } 30 | 31 | Dictionary.stdlib = function stdlib (opts) { 32 | const dict = Dictionary() 33 | function load (defs) { 34 | defs.forEach((def) => { 35 | dict.define(def.name, def) 36 | }) 37 | } 38 | opts = Object.assign({ dictionary: dict }, opts) 39 | load(CombinatorDefs(opts.execute)) 40 | load(OperandDefs(opts)) 41 | load(OperatorDefs(opts)) 42 | load(PredicateDefs) 43 | load(MiscDefs(opts)) 44 | return dict 45 | } 46 | 47 | module.exports = Dictionary 48 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | *, :before, :after { 2 | margin: 0; 3 | padding: 0; 4 | position: relative; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | } 9 | 10 | html { 11 | height: 100%; 12 | } 13 | 14 | body { 15 | background-color: #20262e; 16 | color: #fbfbfb; 17 | font-family: Jura; 18 | } 19 | 20 | header { 21 | height: 40px; 22 | } 23 | 24 | nav { 25 | margin: 10px; 26 | position: absolute; 27 | right: 0; 28 | top: 0; 29 | } 30 | 31 | nav li { 32 | list-style: none; 33 | } 34 | 35 | nav a { 36 | color: #c8c8c8; 37 | } 38 | 39 | .sections { 40 | border-top: 1px solid #2d333b; 41 | display: flex; 42 | height: calc(100vh - 40px); 43 | } 44 | 45 | .sections > div { 46 | width: 50%; 47 | } 48 | 49 | .input-section { 50 | border-right: 1px solid #2d333b; 51 | } 52 | 53 | .input-section label span { 54 | margin: 0 10px; 55 | position: absolute; 56 | z-index: 1; 57 | } 58 | 59 | textarea[name=input] { 60 | background-color: #20262e; 61 | border: none; 62 | color: #ed6e55; 63 | display: block; 64 | font-family: 'Roboto Mono', monospace; 65 | font-size: 12px; 66 | height: 100%; 67 | padding: 10px; 68 | resize: none; 69 | width: 100%; 70 | } 71 | 72 | textarea[name=input]:focus { 73 | outline: none; 74 | } 75 | 76 | .output-result { 77 | font-family: 'Roboto Mono', monospace; 78 | font-size: 13px; 79 | padding: 10px; 80 | white-space: pre; 81 | } 82 | 83 | @media (max-width: 768px) and (orientation: portrait) { 84 | .sections { 85 | flex-flow: row wrap; 86 | } 87 | 88 | .sections > div { 89 | height: 50%; 90 | width: 100%; 91 | } 92 | 93 | .input-section { 94 | border-right: none; 95 | border-bottom: 1px solid #2d333b; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /static/index.js: -------------------------------------------------------------------------------- 1 | const Joy = require('../src/joy/joy') 2 | 3 | function InputHistory () { 4 | const history = [] 5 | let pointer = 0 6 | 7 | function reset () { 8 | pointer = history.length 9 | } 10 | 11 | function append (input) { 12 | history.push(input) 13 | reset() 14 | } 15 | 16 | function prev () { 17 | if (pointer > 0) { pointer -= 1 } 18 | return history[pointer] 19 | } 20 | 21 | function next () { 22 | if (pointer < history.length) { pointer += 1 } 23 | return history[pointer] 24 | } 25 | 26 | return { 27 | append: append, 28 | prev: prev, 29 | next: next, 30 | reset: reset 31 | } 32 | } 33 | 34 | const inputEl = document.querySelector('[name=input]') 35 | const outputEl = document.querySelector('.output-result') 36 | const history = InputHistory() 37 | const joy = Joy() 38 | 39 | function runInput (input) { 40 | try { 41 | const result = joy.run(input) 42 | outputEl.textContent = result 43 | history.append(input) 44 | inputEl.value = '' 45 | } catch (e) { 46 | outputEl.textContent = e.toString() 47 | throw e 48 | } 49 | } 50 | 51 | function prevInput () { 52 | inputEl.value = history.prev() || '' 53 | } 54 | 55 | function nextInput () { 56 | inputEl.value = history.next() || '' 57 | } 58 | 59 | function resetInput () { 60 | history.reset() 61 | inputEl.value = '' 62 | } 63 | 64 | function onInputKeydown (e) { 65 | if (e.which === 13 /* Enter */ && !e.shiftKey) { 66 | e.preventDefault() 67 | runInput(inputEl.value.trim()) 68 | } else if (e.key === 'p' && e.ctrlKey) { 69 | e.preventDefault() 70 | prevInput() 71 | } else if (e.key === 'n' && e.ctrlKey) { 72 | e.preventDefault() 73 | nextInput() 74 | } else if (e.key === 'c' && e.ctrlKey) { 75 | e.preventDefault() 76 | resetInput() 77 | } 78 | } 79 | 80 | inputEl.addEventListener('keydown', onInputKeydown) 81 | -------------------------------------------------------------------------------- /src/joy/joy.js: -------------------------------------------------------------------------------- 1 | const Interpreter = require('./interpreter') 2 | const Output = require('./output') 3 | const Stack = require('./stack') 4 | 5 | function Joy () { 6 | const flags = { 7 | /** 8 | * Automatic output behavior with each instruction. 9 | * 0: No automatic output 10 | * 1: (Default) Output top of stack 11 | * 2: Output entire stack 12 | */ 13 | autoput: 1, 14 | 15 | /** 16 | * Input echoing behavior. 17 | * 0: (Default) No echo 18 | * 1: Echo input, no change 19 | * 2: Echo input, prefixed with tab 20 | * 3: Echo input, prefixed with line number, followed by tab 21 | */ 22 | echo: 0, 23 | 24 | /** 25 | * How to treat undefined words. 26 | * 0: No error (noop) 27 | * 1: (Default) Error 28 | */ 29 | undefError: 1 30 | } 31 | const stack = new Stack() 32 | const output = new Output() 33 | const getSetFlag = flag => (val) => { 34 | if (val === undefined) { return flags[flag] } 35 | flags[flag] = val 36 | } 37 | const interpreter = Interpreter(stack, { 38 | autoput: getSetFlag('autoput'), 39 | echo: getSetFlag('echo'), 40 | undefError: getSetFlag('undefError'), 41 | output: function (line) { 42 | output.write(line) 43 | } 44 | }) 45 | 46 | return { 47 | run: function run (input) { 48 | output.clear() 49 | 50 | switch (flags.echo) { 51 | case 0: /* do nothing */ break 52 | case 1: 53 | output.write(`${input}\n`) 54 | break 55 | case 2: 56 | output.write(`\t${input}\n`) 57 | break 58 | case 3: 59 | // FIXME: should track line numbers 60 | output.write(`1.\t${input}\n`) 61 | break 62 | } 63 | 64 | interpreter.run(input) 65 | 66 | switch (flags.autoput) { 67 | case 0: /* do nothing */ break 68 | case 1: 69 | output.write(stack.peek(1).toString() + '\n') 70 | break 71 | case 2: 72 | stack.peek(stack.length).forEach((item) => { 73 | output.write(item.toString() + '\n') 74 | }) 75 | break 76 | } 77 | 78 | return output.toString().replace(/\n$/, '') 79 | } 80 | } 81 | } 82 | 83 | module.exports = Joy 84 | -------------------------------------------------------------------------------- /src/joy/fsm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Construct a finite state machine for matching input string sequences. 3 | * 4 | * @param {object} opts Configuration options 5 | * @param {string} opts.initial Initial state 6 | * @param {string} [opts.default] Default state to transition to if next state is undefined 7 | * @param {string[]} opts.accepting Accept state(s) 8 | * @param {string} opts.stop State in which to stop accepting input 9 | * @return {object} 10 | */ 11 | function Fsm (opts) { 12 | var nextStates = Object.keys(opts.states).reduce(function (acc, s) { 13 | var rules = opts.states[s].map(function (rule) { 14 | if (!opts.states[rule[1]] && !opts.accepting.includes(rule[1])) { 15 | throw new Error('Programming Error: Transition defined for unknown state "' + rule[1] + '"') 16 | } 17 | var pred = rule[0].test 18 | ? function (c) { return rule[0].test(c) } 19 | : function (c) { return c === rule[0] } 20 | return [pred, rule[1], rule[2]] 21 | }) 22 | acc[s] = function (input, acc2) { 23 | var match = rules.find(function (rule) { return rule[0](input) }) 24 | return match && [match[1], match[2] ? match[2](acc2, input) : acc2] 25 | } 26 | return acc 27 | }, {}) 28 | 29 | function nextState (state, input, acc) { 30 | if (nextStates[state] === undefined) { return [opts.default, acc] } 31 | return nextStates[state](input, acc) || [opts.default, acc] 32 | } 33 | 34 | /** 35 | * Run characters of input against this fsm. 36 | * 37 | * @param {string} input Input string 38 | * @return {object|null} Result object if fsm reaches an accept state; null otherwise 39 | */ 40 | function run (input) { 41 | let currState = opts.initial 42 | let state 43 | let result = '' 44 | let acc = opts.seed 45 | 46 | for (let i = 0, len = input.length; i < len; i += 1) { 47 | const c = input.charAt(i) 48 | const next = nextState(currState, c, acc) 49 | state = next[0] 50 | acc = next[1] 51 | if (state === opts.stop) { break } 52 | result += c 53 | currState = state 54 | } 55 | 56 | return opts.accepting.includes(currState) 57 | ? { value: result, state: currState, acc: acc } 58 | : null 59 | } 60 | 61 | return { run: run } 62 | } 63 | 64 | module.exports = Fsm 65 | -------------------------------------------------------------------------------- /test/joy.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const Joy = require('../src/joy/joy') 3 | 4 | test('Joy examples', (t) => { 5 | const cases = [ 6 | { input: '2 3 + .', expected: '5' }, 7 | { input: '2.34 5.67 * .', expected: String(2.34 * 5.67) }, 8 | { input: '2 3 + dup * .', expected: '25' }, 9 | { input: '[1 2 3] [4 5 6 7] concat .', expected: '[1 2 3 4 5 6 7]' }, 10 | { input: '[ 3.14 42 [1 2 3] 0.003 ] dup concat .', expected: '[3.14 42 [1 2 3] 0.003 3.14 42 [1 2 3] 0.003]' }, 11 | { input: '[1 2 3 4] [dup *] map .', expected: '[1 4 9 16]' }, 12 | { input: '5 [1] [*] primrec .', expected: '120' }, 13 | { input: '20 3 4 + * 6 - 100 rem .', expected: '34' }, 14 | { input: '\'A 32 + succ succ .', expected: '\'c' }, 15 | { input: 'false true false not and not or.', expected: 'false' }, 16 | { input: '\'A \'E < 2 3 + 15 3 / = and.', expected: 'true' }, 17 | { input: '{1 3 5 7} {2 4 6 8} or {} or {3 4 5 6 7 8 9 10} and.', expected: '{3 4 5 6 7 8}' }, 18 | { input: '{3 7 5 1} {2 4 6 8} or {} or {3 4 5 6 7 8 9 10 10} and.', expected: '{3 4 5 6 7 8}' }, 19 | { input: '5 3 {2 1} cons cons 3 swap cons.', expected: '{1 2 3 5}' }, 20 | { input: '5 [6] [1 2] cons cons \'A swap cons.', expected: '[\'A 5 [6] 1 2]' }, 21 | { input: '"CECAB" first.', expected: '\'C' }, 22 | { input: '[\'A 5 [6] 1 2] first.', expected: '\'A' }, 23 | { input: '[\'A 5 [6] 1 2] rest.', expected: '[5 [6] 1 2]' }, 24 | { input: '{5 2 3} first.', expected: '2' }, 25 | { input: '{5 2 3} rest.', expected: '{3 5}' }, 26 | { input: '[1 2 3 4 5] rest rest rest rest first.', expected: '5' }, 27 | { input: '[1 2 3 4 5] 5 at.', expected: '5' }, 28 | { input: '2000 [1000 >] [2 /] [3 *] ifte .', expected: '1000' }, 29 | { input: '[] [2 8 3 6 5] [swons] step.', expected: '[5 6 3 8 2]' }, 30 | { input: '"John Smith" [\'Z >] filter .', expected: '"ohnmith"' }, 31 | { input: '[2 5 3] 0 [+] fold .', expected: '10' }, 32 | { input: '[2 5 3] 0 [dup * +] fold .', expected: '38' }, 33 | { input: '[3 4 5] [0 [+] fold] [size] cleave /.', expected: '4' } 34 | ] 35 | cases.forEach(({ input, expected }) => { 36 | t.equal(Joy().run(input), expected) 37 | }) 38 | t.end() 39 | }) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joy-js 2 | 3 | A JavaScript interpreter for the [Joy][] programming language. 4 | 5 | *Work in progress* 6 | 7 | ## Todo 8 | 9 | - [x] Lexer 10 | - [x] Parser 11 | - [ ] Interpreter (in progress) 12 | - [x] Operand words defined (Skipped: `conts`, `undefs`, `clock`, `stdin`, `stdout`, `stderr`) 13 | - [x] Operator words defined (Skipped: `frexp`, `strftime`, `srand`, `fclose`, `feof`, `ferror`, `fflush`, `fgetch`, `fgets`, `fopen`, `fread`, `fwrite`, `fremove`, `frename`, `fput`, `fputch`, `fputchars`, `fputstring`, `fseek`, `ftell`, `opcase`, `case`, `name`; partial functionality: `format`, `formatf`) 14 | - [x] Predicate words defined (Skipped: `user`, `file`) 15 | - [ ] Combinator words defined 16 | - [ ] "Miscellaneous" words defined 17 | - [ ] Demo site (simple site available, must run locally) 18 | - [x] Basic site 19 | - [x] Make responsive 20 | - [x] Publish to GitHub pages 21 | - [ ] Syntax-highlighted editor 22 | - [ ] Add output options 23 | - [ ] Joy tutorial or similar? 24 | - [ ] REPL 25 | 26 | ## How to run 27 | 28 | The interpreter may be run locally via the Node repl, or via a local express 29 | server. 30 | 31 | __Node.js repl:__ 32 | 33 | $ node 34 | > Joy = require('./src/joy/joy') 35 | > Joy.run('19 23 + .') 36 | '42' 37 | > 38 | 39 | __Dev server:__ 40 | 41 | $ npm install 42 | $ npm start 43 | 44 | ## Motivation 45 | 46 | This project started after the following [tweet](https://twitter.com/lorentzframe/status/997997523301117953) 47 | by [@lorentzframe](https://twitter.com/lorentzframe) popped into my Twitter 48 | timeline: 49 | 50 | > "SICP" by Abelson & Sussman should be read continuously, ~2 pages a day, 51 | > returning to page 1 every year. Ditto "Thinking Forth" by Leo Brodie, tho' 52 | > only ~1 page a day. The former teaches how to think, the latter how to 53 | > engineer. Both are in unpopular languages, on purpose.'" 54 | 55 | I own SICP, and while the suggestion of re-reading it yearly strikes me as a 56 | tad extreme, I do think the content is great. "Thinking Forth" on the other 57 | hand was completely new to me. Within a few days time I had found myself 58 | combing through content on Forth on the web, as well as related, concatenative 59 | / stack-based languages, including Joy. In that search, I stumbled on 60 | [this online Forth interpreter](https://brendanator.github.io/jsForth/) written 61 | in JavaScript, and immediately fell in love with the idea. I wanted to do 62 | something similar. Joy looked like a good target, as there is little in the way 63 | of actual working implementations, and the syntax and surface area are 64 | relatively small. Thus, joy-js was born. 65 | 66 | ## License 67 | 68 | MIT 69 | 70 | [Joy]: https://en.wikipedia.org/wiki/Joy_(programming_language) 71 | -------------------------------------------------------------------------------- /src/joy/lib/inilib.joy.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | (* FILE: inilib.joy *) 3 | 4 | LIBRA 5 | 6 | _inilib == true; 7 | 8 | 9 | (* - - - - - I N P U T O U T P U T - - - - *) 10 | 11 | newline == '\\n putch; 12 | putln == put newline; 13 | space == '\\032 putch; 14 | bell == '\\007 putch; 15 | (* this is now a primitive in raw Joy: 16 | putchars == [putch] step; 17 | *) 18 | putstrings == [putchars] step; 19 | 20 | ask == "Please " putchars putchars newline get; 21 | 22 | (* - - - - - O P E R A T O R S - - - - - *) 23 | 24 | dup2 == dupd dup swapd; 25 | pop2 == pop pop; 26 | newstack == [] unstack; 27 | truth == true; 28 | falsity == false; 29 | to-upper == ['a >=] [32 -] [] ifte; 30 | to-lower == ['a < ] [32 +] [] ifte; 31 | boolean == [logical] [set] sequor; 32 | numerical == [integer] [float] sequor; 33 | swoncat == swap concat; 34 | 35 | (* date and time *) 36 | 37 | weekdays == 38 | [ "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" 39 | "Saturday" "Sunday" ]; 40 | months == 41 | [ "JAN" "FEB" "MAR" "APR" "MAY" "JUN" 42 | "JUL" "AUG" "SEP" "OCT" "NOV" "DEC" ]; 43 | localtime-strings == 44 | time localtime 45 | [ [ 0 at 'd 4 4 format ] 46 | [ 1 at pred months of ] 47 | [ 2 at 'd 2 2 format ] 48 | [ 3 at 'd 2 2 format ] 49 | [ 4 at 'd 2 2 format ] 50 | [ 5 at 'd 2 2 format ] 51 | [ 6 at [] ["true"] ["false"] ifte ] 52 | [ 7 at 'd 5 5 format ] 53 | [ 8 at pred weekdays of ] ] 54 | [i] map 55 | popd; 56 | today == 57 | localtime-strings 58 | [ [8 at] [" "] [2 at] ["-"] [1 at] ["-"] [0 at rest rest] ] 59 | [i] map 60 | popd 61 | "" [concat] fold; 62 | now == 63 | localtime-strings 64 | 3 drop 65 | [ [0 at] [":"] [1 at] [":"] [2 at] ] 66 | [i] map 67 | popd 68 | "" [concat] fold; 69 | show-todaynow == 70 | today putchars space now putchars newline; 71 | 72 | (* program operators *) 73 | 74 | conjoin == [[false] ifte] cons cons; 75 | disjoin == [ifte] cons [true] swons cons; 76 | negate == [[false] [true] ifte] cons; 77 | 78 | (* - - - - - C O M B I N A T O R S - - - - - *) 79 | 80 | sequor == [pop true] swap ifte; 81 | sequand == [pop false] ifte; 82 | dipd == [dip] cons dip; 83 | dip2 == [dip] cons dip; 84 | dip3 == [dip2] cons dip; 85 | call == [] cons i; 86 | i2 == [dip] dip i; 87 | nullary2 == [nullary] cons dup i2 swapd; 88 | (* this is now a primitive in raw Joy: 89 | unary2 == [unary ] cons dup i2; 90 | *) 91 | repeat == dupd swap [i] dip2 while; 92 | forever == maxint swap times; 93 | 94 | (* library inclusion *) 95 | 96 | verbose == false; 97 | libload == 98 | [ '_ swons intern body null ] 99 | [ ".joy" concat include ] 100 | [ [ verbose ] 101 | [ putchars " is already loaded\\n" putchars ] 102 | [ pop ] 103 | ifte ] 104 | ifte; 105 | basic-libload == 106 | "agglib" libload 107 | "seqlib" libload 108 | "numlib" libload; 109 | special-libload == 110 | "mtrlib" libload 111 | "tutlib" libload 112 | "lazlib" libload 113 | "lsplib" libload 114 | "symlib" libload; 115 | 116 | all-libload == basic-libload special-libload; 117 | 118 | INILIB == "inilib.joy - the initial library, assumed everywhere\\n". 119 | (* end LIBRA *) 120 | 121 | "inilib is loaded\\n" putchars. 122 | 123 | (* END inilib.joy *) 124 | `.trim() 125 | -------------------------------------------------------------------------------- /src/joy/interpreter/index.js: -------------------------------------------------------------------------------- 1 | const Dictionary = require('./dictionary') 2 | const Parser = require('../parser') 3 | const T = require('./types') 4 | 5 | function tokenToType (token) { 6 | switch (token.type) { 7 | case 'IntegerConstant': return new T.JoyInt(token.value) 8 | case 'FloatConstant': return new T.JoyFloat(token.value) 9 | case 'CharacterConstant': return new T.JoyChar(token.value) 10 | case 'StringConstant': return new T.JoyString(token.value) 11 | case 'AtomicSymbol': return new T.JoySymbol(token.value) 12 | case 'Quotation': 13 | // if (token.term.factors.every(t => t.type && t.type.endsWith('Constant'))) { 14 | // return new T.JoyList(token.term.factors.map(tokenToType)) 15 | // } 16 | // break 17 | return new T.JoyList(token.term.factors.map(tokenToType)) 18 | case 'Set': return new T.JoySet(token.members.map(tokenToType)) 19 | case 'SimpleDefinition': return new T.JoyList(token.term.factors.map(tokenToType)) 20 | default: /* do nothing */ 21 | } 22 | throw new Error('Unhandled type conversion for token ' + token.type) 23 | } 24 | 25 | function arityToMessage (arity) { 26 | switch (arity) { 27 | case 1: return 'one parameter' 28 | case 2: return 'two parameters' 29 | case 3: return 'three parameters' 30 | case 4: return 'four parameters' 31 | case 5: return 'five parameters' 32 | } 33 | } 34 | 35 | function Interpreter (stack, options) { 36 | const definitions = Dictionary.stdlib( 37 | Object.assign({ execute: execute }, options) 38 | ) 39 | 40 | function evalBuiltin (def) { 41 | const arity = def.handlers[0][0].length 42 | if (stack.length < arity) { 43 | throw new Error(`run time error: ${arityToMessage(arity)} needed for ${def.name}`) 44 | } 45 | const params = stack.peek(arity) 46 | const handler = def.handlers.find(handlerDef => 47 | params.every((p, i) => { 48 | const paramType = handlerDef[0][i] 49 | return paramType === '*' || p[`is${paramType}`] 50 | })) 51 | if (!handler) { 52 | if (options.undefError() === 0) { return } 53 | throw new Error(`run time error: suitable parameters needed for ${def.name}`) 54 | } 55 | handler[1](stack) 56 | } 57 | 58 | function evalDefined (def) { 59 | def.definition.value.forEach((p) => { 60 | stack.push(p) 61 | execute() 62 | }) 63 | } 64 | 65 | function evalInstruction (val) { 66 | if (val.isSymbol) { 67 | let def 68 | try { 69 | def = definitions.get(val.value) 70 | } catch (e) { 71 | if (options.undefError() === 0) { return } 72 | throw e 73 | } 74 | if (def.handlers) { 75 | evalBuiltin(def) 76 | } else { 77 | evalDefined(def) 78 | } 79 | } else { 80 | stack.push(val) 81 | } 82 | } 83 | 84 | function execute () { 85 | if (stack.length && stack.peek(1)[0].isSymbol) { 86 | const p = stack.pop() 87 | evalInstruction(p, stack) 88 | } 89 | } 90 | 91 | function run (input) { 92 | const ast = Parser().parse(input) 93 | 94 | ast.requests.forEach((instructions) => { 95 | if (instructions.type === 'CompoundDefinition') { 96 | (instructions.public.definitions || []).forEach((simpleDef) => { 97 | // FIXME: certain compound defs are failing here. simpleDef.symbol 98 | // undefined. Investigate. See symlib.joy 99 | definitions.define(simpleDef.symbol.value, { 100 | name: simpleDef.symbol.value, 101 | definition: tokenToType(simpleDef) 102 | }) 103 | }) 104 | } else { 105 | instructions.factors.forEach((token) => { 106 | evalInstruction(tokenToType(token), stack) 107 | }) 108 | } 109 | }) 110 | } 111 | 112 | return { execute: execute, run: run } 113 | } 114 | 115 | module.exports = Interpreter 116 | -------------------------------------------------------------------------------- /src/joy/interpreter/operand_defs.js: -------------------------------------------------------------------------------- 1 | const T = require('./types') 2 | 3 | module.exports = opts => [ 4 | { 5 | name: 'false', 6 | signature: 'false : -> false', 7 | help: 'Pushes the value false.', 8 | handlers: [ 9 | [[], function (stack) { 10 | stack.push(new T.JoyBool(false)) 11 | }] 12 | ] 13 | }, 14 | 15 | { 16 | name: 'true', 17 | signature: 'true : -> true', 18 | help: 'Pushes the value true.', 19 | handlers: [ 20 | [[], function (stack) { 21 | stack.push(new T.JoyBool(true)) 22 | }] 23 | ] 24 | }, 25 | 26 | { 27 | name: 'maxint', 28 | signature: 'maxint : -> maxint', 29 | help: 'Pushes largest integer (platform dependent). Typically it is 32 bits.', 30 | handlers: [ 31 | [[], function (stack) { 32 | stack.push(new T.JoyInt(Number.MAX_SAFE_INTEGER)) 33 | }] 34 | ] 35 | }, 36 | 37 | { 38 | name: 'setsize', 39 | signature: 'setsize : -> setsize', 40 | help: ` 41 | Pushes the maximum number of elements in a set (platform dependent). 42 | Typically it is 32, and set members are in the range 0..31. 43 | `.trim(), 44 | handlers: [ 45 | [[], function (stack) { 46 | stack.push(new T.JoyInt(32)) 47 | }] 48 | ] 49 | }, 50 | 51 | { 52 | name: 'stack', 53 | signature: 'stack : .. X Y Z -> .. X Y Z [Z Y X ..]', 54 | help: 'Pushes the stack as a list.', 55 | handlers: [ 56 | [[], function (stack) { 57 | stack.push(new T.JoyList(stack.peek(stack.length))) 58 | }] 59 | ] 60 | }, 61 | 62 | /** 63 | * conts : -> [[P] [Q] ..] 64 | * Pushes current continuations. Buggy, do not use. 65 | */ 66 | 67 | { 68 | name: 'autoput', 69 | signature: 'autoput : -> I', 70 | help: 'Pushes current value of flag for automatic output, I = 0..2.', 71 | handlers: [ 72 | [[], function (stack) { 73 | stack.push(new T.JoyInt(opts.autoput())) 74 | }] 75 | ] 76 | }, 77 | 78 | { 79 | name: 'undeferror', 80 | signature: 'undeferror : -> I', 81 | help: 'Pushes current value of undefined-is-error flag.', 82 | handlers: [ 83 | [[], function (stack) { 84 | stack.push(new T.JoyInt(opts.undefError())) 85 | }] 86 | ] 87 | }, 88 | 89 | /** 90 | * undefs : -> 91 | * Push a list of all undefined symbols in the current symbol table. 92 | * TODO: Not sure what this is 93 | */ 94 | 95 | { 96 | name: 'echo', 97 | signature: 'echo : -> I', 98 | help: 'Pushes value of echo flag, I = 0..3.', 99 | handlers: [ 100 | [[], function (stack) { 101 | stack.push(opts.echo()) 102 | }] 103 | ] 104 | }, 105 | 106 | /** 107 | * clock : -> I 108 | * Pushes the integer value of current CPU usage in hundreds of a second. 109 | * TODO: Not sure what to do here 110 | */ 111 | 112 | { 113 | name: 'time', 114 | signature: 'time : -> I', 115 | help: 'Pushes the current time (in seconds since the Epoch).', 116 | handlers: [ 117 | [[], function (stack) { 118 | stack.push(new T.JoyInt(Math.floor(Date.now() / 1000))) 119 | }] 120 | ] 121 | }, 122 | 123 | { 124 | name: 'rand', 125 | signature: 'rand : -> I', 126 | help: 'I is a random integer.', 127 | handlers: [ 128 | [[], function (stack) { 129 | stack.push(new T.JoyInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))) 130 | }] 131 | ] 132 | } 133 | 134 | /** 135 | * stdin : -> S 136 | * Pushes the standard input stream. 137 | * TODO 138 | */ 139 | 140 | /** 141 | * stdout : -> S 142 | * Pushes the standard output stream. 143 | * TODO 144 | */ 145 | 146 | /** 147 | * stderr : -> S 148 | * Pushes the standard error stream. 149 | * TODO 150 | */ 151 | ] 152 | -------------------------------------------------------------------------------- /src/joy/lib/agglib.joy.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | (* FILE: agglib.joy *) 3 | 4 | LIBRA 5 | 6 | _agglib == true; 7 | 8 | (* - - - - - O P E R A T O R S - - - - - *) 9 | 10 | unitset == {} cons; 11 | unitstring == "" cons; 12 | unitlist == [] cons; 13 | pairset == {} cons cons; 14 | pairstring == "" cons cons; 15 | pairlist == [] cons cons; 16 | unpair == uncons uncons pop; 17 | second == rest first; 18 | third == rest rest first; 19 | fourth == 3 drop first; 20 | fifth == 4 drop first; 21 | string2set == {} swap shunt; 22 | elements == {} swap [swons] step; 23 | (* 24 | set2string == "" swap [chr swons] step; 25 | *) 26 | set2string == "" [[chr] dip cons] foldr; 27 | shunt == [swons] step; 28 | 29 | (* "dipped" versions *) 30 | 31 | nulld == [null] dip; 32 | consd == [cons] dip; 33 | swonsd == [swons] dip; 34 | unconsd == [uncons] dip; 35 | unswonsd == [unswons] dip; 36 | firstd == [first] dip; 37 | restd == [rest] dip; 38 | secondd == [secondd] dip; 39 | thirdd == [third] dip; 40 | 41 | (* on two operands *) 42 | 43 | null2 == nulld null or; 44 | cons2 == swapd cons consd; 45 | uncons2 == unconsd uncons swapd; 46 | swons2 == swapd swons swonsd; 47 | unswons2 == [unswons] dip unswons swapd; 48 | 49 | zip == [null2] [pop2 []] [uncons2] [[pairlist] dip cons] linrec; 50 | 51 | from-to == (* lo hi agg *) 52 | [] cons [pop pop] swoncat 53 | [>] swap 54 | [ [dup succ] dip ] 55 | [cons] 56 | linrec; 57 | from-to-list == [] from-to; 58 | from-to-set == {} from-to; 59 | from-to-string == "" from-to; 60 | 61 | (* - - - - - C O M B I N A T O R S - - - - - *) 62 | 63 | (* Left to Right *) 64 | 65 | (* inbuilt: step map fold filter split *) 66 | (* desirable: step2 map2 fold2 *) 67 | 68 | (* cartesian product -like *) 69 | pairstep == [dupd] swoncat [step pop] cons cons step; 70 | 71 | (* Right to Left *) 72 | 73 | mapr == 74 | [ [null] [] [uncons] ] dip (* P1 P2 P3 *) 75 | [dip cons] cons (* P4 *) 76 | linrec; 77 | foldr == 78 | [ [ [null] ] dip (* P1 *) 79 | [] cons [pop] swoncat (* P2 *) 80 | [uncons] ] dip (* P3 *) 81 | linrec; 82 | 83 | stepr2 == 84 | [ [null2] [pop pop] ] dip (* P1 P2 *) 85 | [dip] cons [dip] cons [uncons2] swoncat (* P3 *) 86 | tailrec; 87 | fold2 == rollupd stepr2; 88 | 89 | mapr2 == (* == zipwith B&W p 57 *) 90 | [ [null2] [pop2 []] [uncons2] ] dip (* P1 P2 P3 *) 91 | [dip cons] cons (* P4 *) 92 | linrec; 93 | foldr2 == 94 | [ [ [null2] ] dip (* P1 *) 95 | [] cons [pop2] swoncat (* P2 *) 96 | [uncons2] ] dip (* P3 *) 97 | linrec; 98 | interleave2 == [cons cons] foldr2; 99 | interleave2list == [] interleave2; 100 | 101 | sum == 0 [+] fold; 102 | average == [sum] [size] cleave / ; 103 | variance == (* [..] variance *) 104 | 0.0 swap dup (* 0.0 [..] [..] *) 105 | [sum] [size] cleave dup (* 0.0 [..] su n n *) 106 | [ / (* 0.0 [..] av n *) 107 | [ - dup * + ] cons (* 0.0 [..] [av - dup * +] n *) 108 | step ] (* sumsq n *) 109 | dip 110 | pred / ; 111 | 112 | AGGLIB == "agglib.joy - aggregate library\\n". 113 | (* end LIBRA *) 114 | 115 | "agglib is loaded\\n" putchars. 116 | 117 | (* END agglib.joy *) 118 | `.trim() 119 | -------------------------------------------------------------------------------- /src/joy/lib/numlib.joy.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | (* FILE: numlib.joy *) 3 | 4 | LIBRA 5 | 6 | _numlib == true; 7 | 8 | (* predicates *) 9 | 10 | positive == 0 >; 11 | negative == 0 <; 12 | even == 2 rem null; 13 | odd == even not; 14 | prime == 15 | 2 16 | [ [dup * >] nullary [rem 0 >] dip and ] 17 | [ succ ] 18 | while 19 | dup * < ; 20 | 21 | (* functions *) 22 | 23 | fact == [1 1] dip [dup [*] dip succ] times pop; 24 | fib == [1 0] dip [swap [+] unary] times popd; 25 | (* 26 | nfib == [1 1] dip [dup [+ succ] dip swap] times pop; 27 | *) 28 | gcd == [0 >] [dup rollup rem] while pop; 29 | 30 | fahrenheit == 9 * 5 / 32 + ; 31 | celsius == 32 - 5 * 9 / ; 32 | 33 | pi == 3.14159265; 34 | e == 1.0 exp; 35 | radians == pi * 180 / ; 36 | sindeg == radians sin; 37 | cosdeg == radians cos; 38 | tandeg == radians tan; 39 | 40 | (* find roots of the quadratic equation with coefficients a b c : 41 | a * X^2 + b * X + c = 0 *) 42 | qroots == (* a b c *) 43 | [ pop pop null ] (* a = 0 ? *) 44 | (* degenerate cases: *) 45 | [ [ pop null ] (* b = 0 ? *) 46 | [ [ null ] (* c = 0 ? *) 47 | [ [_INF] ] (* => [_INF] *) 48 | [ [] ] (* => [] *) 49 | ifte 50 | [ pop pop pop ] dip ] 51 | [ 0 swap - swap 1.0 * / (* float divisor *) 52 | [] cons popd ] (* => [ -c/b ] *) 53 | ifte ] 54 | (* standard cases: *) 55 | [ [ [ dup * swap ] dip 56 | 4 * * - ] unary (* b^2 - 4ac *) 57 | [ 0 < ] (* b^2 - 4ac negative ? *) 58 | [ pop pop pop [_COMPLEX] ] (* => [_COMPLEX] *) 59 | [ [ 0 swap - 1.0 * (* -b (floated) *) 60 | swap 2 * ] dip (* 2a *) 61 | [ 0 = ] (* b^2 - 4ac zero ? *) 62 | [ pop / [] cons ] (* => [-b / 2a] *) 63 | [ sqrt swapd dup2 - [+] dip (* -b+s -b-s *) 64 | [] cons cons (* [ -b+s -b-s ]*) 65 | swap [/] cons map ] (* => [(-b+s)/2a (-b-s)/2a]*) 66 | ifte ] 67 | ifte ] 68 | ifte; 69 | 70 | (* combinators *) 71 | 72 | deriv == [unary2 swap - 0.001 /] cons [dup 0.001 +] swoncat; 73 | 74 | (* Newton's method for finding roots of equations in one variable, 75 | for example to find the temperature at which Fahrenheit = Celsius: 76 | "using 5 as a guess, find the X for which X = fahrenheit(X) 77 | or X - fahrenheit(X) = 0 78 | in Joy: 5 [dup fahrenheit -] newton => -40 79 | or -100 [dup celsius -] newton => -40 80 | So -40 is the fixpoint for the two conversion functions. 81 | *) 82 | newton == (* Usage: guess [F] newton *) 83 | dup deriv (* guess [F] [D] *) 84 | [ pop i abs 0.0001 > ] (* too big ? *) 85 | [ [dupd] dip (* guess guess [F] [D] *) 86 | dup2 (* guess guess [F] [D] [F] [D] *) 87 | [[cleave / - ] dip] 88 | dip ] (* newguess [F] [D] *) 89 | while 90 | pop pop; 91 | use-newton == [[-] cons] dip swoncat 1 swap newton; 92 | cube-root == [dup dup * *] use-newton; 93 | 94 | NUMLIB == "numlib.joy - numerical library\\n". 95 | (* end LIBRA *) 96 | 97 | "numlib is loaded\\n" putchars. 98 | 99 | (* END numlib.joy *) 100 | `.trim() 101 | -------------------------------------------------------------------------------- /src/joy/lib/seqlib.joy.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | (* FILE: seqlib.joy *) 3 | 4 | "agglib" libload. 5 | 6 | LIBRA 7 | 8 | _seqlib == true; 9 | 10 | putlist == 11 | "[ " putchars 12 | [ null ] 13 | [ pop ] 14 | [ unswons 15 | put 16 | [ "\\n " putchars put ] step ] 17 | ifte 18 | "]\\n" putchars; 19 | reverse == [[]] [""] iflist swap shunt; 20 | reverselist == [] swap shunt; 21 | reversestring == "" swap shunt; 22 | flatten == [null] [] [uncons] [concat] linrec; 23 | restlist == [null] [[] cons] [dup rest] [cons] linrec; 24 | product == 1 [*] fold; 25 | (* this does short circuiting 26 | product == 27 | 1 swap 28 | [ null not ] 29 | [ [first null] 30 | [[pop 0] dip pop []] 31 | [uncons [*] dip] 32 | ifte ] 33 | while 34 | pop; 35 | *) 36 | scalarproduct == [0] dip2 37 | [null2] [pop2] [uncons2 [* +] dip2] tailrec; 38 | frontlist1 == (* Thompson p 247 *) 39 | [null] [[] cons] 40 | [uncons] 41 | [ [cons] map popd [] swons ] 42 | linrec; 43 | frontlist == (* also works for sets and strings *) 44 | [null] [[] cons] 45 | [uncons] 46 | [ [cons] map popd dup first rest swons ] 47 | linrec; 48 | subseqlist == (* Thompson p 247 *) 49 | [null] 50 | [[] cons] 51 | [ uncons dup 52 | [frontlist [cons] map popd] dip ] 53 | [concat] 54 | linrec; 55 | powerlist1 == 56 | [null] [[] cons] [uncons] 57 | [dup swapd [cons] map popd concat] linrec; 58 | powerlist2 == 59 | [null] [[] cons] [uncons] 60 | [dup swapd [cons] map popd swoncat] linrec; 61 | insertlist == (* Sequence Item -> List(Sequence) *) 62 | swons 63 | [ small ] 64 | [ unitlist ] 65 | [ dup (* keep original *) 66 | unswons unconsd swons ] (* take out second *) 67 | [ swap [swons] cons map (* swons in second *) 68 | cons ] (* cons in original *) 69 | linrec; 70 | permlist == 71 | [ small ] 72 | [ unitlist ] 73 | [ uncons ] 74 | [ swap [insertlist] cons map 75 | flatten ] 76 | linrec; 77 | qsort == 78 | [small] [] [uncons [>] split] [swapd cons concat] binrec; 79 | qsort1-1 == 80 | [small] 81 | [] 82 | [uncons unswonsd [first >] split [swons] dip2] 83 | [swapd cons concat] 84 | binrec; 85 | qsort1 == 86 | [small] [] [uncons [[first] unary2 >] split] [swapd cons concat] binrec; 87 | mk_qsort == 88 | [ [small] [] ] dip 89 | [ unary2 >] cons [split] cons [uncons] swoncat 90 | [ swapd cons concat ] 91 | binrec; 92 | merge == 93 | [ [ [null] [pop] ] 94 | [ [pop null] [popd] ] 95 | [ [unswons2 <] [unconsd] [cons] ] 96 | [ [unswons2 >] [uncons swapd] [cons] ] 97 | [ [uncons2] [cons cons] ] ] 98 | condlinrec; 99 | merge1 == 100 | [ [ [null] [pop] ] 101 | [ [pop null] [popd] ] 102 | [ [unswons2 [first] unary2 <] [unconsd] [cons] ] 103 | [ [unswons2 [first] unary2 >] [uncons swapd] [cons] ] 104 | [ [uncons2] [cons cons] ] ] 105 | condlinrec; 106 | insert == 107 | [pop null] [firstd >=] disjoin 108 | [ swons ] 109 | [ unconsd] 110 | [ cons ] 111 | linrec; 112 | insert-old == 113 | [ [ [pop null] [swons] ] 114 | [ [firstd >= ] [swons] ] 115 | [ [unconsd] [cons] ] ] 116 | condlinrec; 117 | delete == 118 | [ [ [pop null] [pop] ] 119 | [ [firstd >] [pop] ] 120 | [ [firstd =] [pop rest] ] 121 | [ [unconsd] [cons] ] ] 122 | condlinrec; 123 | transpose == (* READE p 133 *) 124 | [ [null] [true] [[null] some] ifte ] 125 | [ pop [] ] 126 | [ [[first] map] [[rest] map] cleave ] 127 | [ cons ] 128 | linrec; 129 | cartproduct == [[]] dip2 [pairlist swap [swons] dip] pairstep; 130 | orlist == [list] swap disjoin; 131 | orlistfilter == orlist [filter] cons; 132 | treeshunt == [swons] treestep; 133 | treeflatten == [] swap treeshunt reverse; 134 | treereverse == [] [reverse] [map] treegenrec; 135 | treestrip == [list] treefilter; 136 | (* 137 | treemap == [map] treerec; 138 | *) 139 | treemap == [] [map] treegenrec; 140 | treefilter == [] swap orlistfilter [map] treegenrec; 141 | treesample == [ [1 2 [3 4] 5 [[[6]]] 7 ] 8 ]; 142 | 143 | SEQLIB == "seqlib.joy - sequence library, assumes agglib.joy\\n". 144 | (* end LIBRA *) 145 | 146 | "seqlib is loaded\\n" putchars. 147 | 148 | (* END seqlib.joy *) 149 | `.trim() 150 | -------------------------------------------------------------------------------- /src/joy/parser.js: -------------------------------------------------------------------------------- 1 | const Lexer = require('./lexer') 2 | 3 | function Parser () { 4 | let input 5 | let pos 6 | let tokens 7 | 8 | function formatUnexpectedToken () { 9 | let lineStart = input.lastIndexOf('\n', tokens[pos].pos) 10 | let lineEnd = input.indexOf('\n', tokens[pos].pos) 11 | if (lineStart === -1) { 12 | lineStart = 0 13 | } else { 14 | lineStart += 1 // Skip the newline 15 | } 16 | if (lineEnd === -1) { lineEnd = input.length - 1 } 17 | const line = input.slice(lineStart, lineEnd) 18 | const charsUpToError = tokens[pos].pos - lineStart 19 | const spacesToCaret = new Array(charsUpToError).fill(' ').join('') 20 | return `${line}\n${spacesToCaret}^` 21 | } 22 | 23 | function expect (bool, msg) { 24 | if (!bool) { 25 | msg = msg || `Syntax Error: Unexpected token "${tokens[pos].value}"\n\n${formatUnexpectedToken()}` 26 | throw new Error(msg) 27 | } 28 | } 29 | 30 | function match (type, value) { 31 | const token = tokens[pos] 32 | const result = Boolean(token) && 33 | token.type === type && 34 | (arguments.length === 1 || token.value === value) && 35 | token 36 | if (result) { 37 | pos += 1 38 | } 39 | return result 40 | } 41 | 42 | function cycle () { 43 | const node = { 44 | type: 'Cycle', 45 | requests: [] 46 | } 47 | 48 | while (true) { 49 | let next = compoundDefinition() 50 | if (next) { 51 | node.requests.push(next) 52 | continue 53 | } 54 | next = term() 55 | if (next && end()) { 56 | node.requests.push(next) 57 | continue 58 | } 59 | break 60 | } 61 | 62 | return node 63 | } 64 | 65 | function compoundDefinition () { 66 | let result = hideIn() 67 | if (result) { return result } 68 | const mod = match('ReservedWord', 'MODULE') && match('AtomicSymbol') 69 | const priv = match('ReservedWord', 'PRIVATE') && definitionSequence() 70 | const pub = (match('ReservedWord', 'PUBLIC') || match('ReservedWord', 'DEFINE') || match('ReservedWord', 'LIBRA')) && definitionSequence() 71 | if ((mod || priv || pub) && end()) { 72 | return { 73 | type: 'CompoundDefinition', 74 | module: mod, 75 | private: priv, 76 | public: pub 77 | } 78 | } 79 | return false 80 | } 81 | 82 | function hideIn () { 83 | if (match('ReservedWord', 'HIDE')) { 84 | const priv = definitionSequence() 85 | expect(priv && match('ReservedWord', 'IN'), 'IN expected in HIDE-definition') 86 | const pub = definitionSequence() 87 | expect(pub && match('ReservedWord', 'END'), 'END expected in HIDE-definition') 88 | return { 89 | type: 'CompoundDefinition', 90 | module: false, 91 | private: priv, 92 | public: pub 93 | } 94 | } 95 | return false 96 | } 97 | 98 | function definitionSequence () { 99 | const definitions = [] 100 | let def = definition() 101 | while (def) { 102 | definitions.push(def) 103 | def = match('ReservedChar', ';') && definition() 104 | } 105 | if (definitions.length) { 106 | return { 107 | type: 'DefinitionSequence', 108 | definitions: definitions 109 | } 110 | } 111 | return false 112 | } 113 | 114 | function definition () { 115 | return simpleDefinition() || compoundDefinition() 116 | } 117 | 118 | function simpleDefinition () { 119 | const sym = match('AtomicSymbol') 120 | if (!(sym && match('ReservedWord', '=='))) { return false } 121 | const trm = term() 122 | return trm && { 123 | type: 'SimpleDefinition', 124 | symbol: sym, 125 | term: trm 126 | } 127 | } 128 | 129 | function term () { 130 | const node = { 131 | type: 'Term', 132 | factors: [] 133 | } 134 | let child = factor() 135 | while (child) { 136 | node.factors.push(child) 137 | child = factor() 138 | } 139 | return node 140 | } 141 | 142 | function factor () { 143 | return match('AtomicSymbol') || 144 | match('IntegerConstant') || 145 | match('FloatConstant') || 146 | match('CharacterConstant') || 147 | match('StringConstant') || 148 | set() || 149 | quotation() 150 | } 151 | 152 | function set () { 153 | if (match('ReservedChar', '{')) { 154 | const members = [] 155 | let member = match('CharacterConstant') || match('IntegerConstant') 156 | while (member) { 157 | members.push(member) 158 | member = match('CharacterConstant') || match('IntegerConstant') 159 | } 160 | if (match('ReservedChar', '}')) { 161 | return { 162 | type: 'Set', 163 | members: members 164 | } 165 | } 166 | } 167 | return false 168 | } 169 | 170 | function quotation () { 171 | if (match('ReservedChar', '[')) { 172 | const factors = term() 173 | if (factors && match('ReservedChar', ']')) { 174 | return { 175 | type: 'Quotation', 176 | term: factors 177 | } 178 | } 179 | } 180 | return false 181 | } 182 | 183 | function end () { 184 | return match('ReservedWord', 'END') || match('ReservedChar', '.') 185 | } 186 | 187 | return { 188 | parse: function parse (inp) { 189 | input = inp 190 | const lexer = Lexer(input) 191 | pos = 0 192 | tokens = lexer.tokenize() 193 | const ast = cycle() 194 | expect(pos === tokens.length) 195 | return ast 196 | } 197 | } 198 | } 199 | 200 | module.exports = Parser 201 | -------------------------------------------------------------------------------- /src/joy/interpreter/misc_defs.js: -------------------------------------------------------------------------------- 1 | const T = require('./types') 2 | 3 | function fitToWidth (words, w) { 4 | return words.reduce((acc, word) => { 5 | if (acc.length === 0) { return word } 6 | const newlineIdx = acc.lastIndexOf('\n') 7 | const lineLength = newlineIdx === -1 ? acc.length : acc.length - newlineIdx - 1 8 | return (lineLength + word.length + 1 <= w) 9 | ? `${acc} ${word}` 10 | : `${acc}\n${word}` 11 | }, '') 12 | } 13 | 14 | module.exports = (opts) => [ 15 | { 16 | name: 'help', 17 | signature: 'help : ->', 18 | help: ` 19 | Lists all defined symbols, including those from library files. 20 | Then lists all primitives of raw Joy 21 | (There is a variant: "_help" which lists hidden symbols). 22 | `.trim(), 23 | handlers: [ 24 | [[], function () { 25 | // NOTE: Not sure about proper ordering, or what definitions are considered primitive. 26 | const defs = opts.dictionary.keys() 27 | defs.reverse() 28 | opts.output(`${fitToWidth(defs, 72)}\n`) 29 | }] 30 | ] 31 | }, 32 | 33 | { 34 | name: 'helpdetail', 35 | signature: 'helpdetail : [ S1 S2 .. ]', 36 | help: 'Gives brief help on each symbol S in the list.', 37 | handlers: [ 38 | [['List'], function (stack) { 39 | const top = stack.pop() 40 | top.value.forEach((word) => { 41 | let def 42 | try { 43 | def = opts.dictionary.get(word.value) 44 | } catch (e) { 45 | // TODO: Ignore for now. Not sure what Joy does here. 46 | return 47 | } 48 | if (def.signature && def.help) { 49 | opts.output(`${def.signature}\n ${def.help}\n`) 50 | } else { 51 | // TODO: Handle == entries 52 | } 53 | }) 54 | }] 55 | ] 56 | }, 57 | 58 | /** 59 | * manual : -> 60 | * Writes this manual of all Joy primitives to output file. 61 | */ 62 | 63 | { 64 | name: 'setautoput', 65 | signature: 'setautoput : I ->', 66 | help: ` 67 | Sets value of flag for automatic put to I (if I = 0, none; 68 | if I = 1, put; if I = 2, stack. 69 | `.trim(), 70 | handlers: [ 71 | [['Integer'], function (stack) { 72 | // NOTE: Not sure what should happen for out-of-range values. 73 | const top = stack.pop() 74 | opts.autoput(top.value) 75 | }] 76 | ] 77 | }, 78 | 79 | { 80 | name: 'setundeferror', 81 | signature: 'setundeferror : I ->', 82 | help: ` 83 | Sets flag that controls behavior of undefined functions 84 | (0 = no error, 1 = error). 85 | `.trim(), 86 | handlers: [ 87 | [['Integer'], function (stack) { 88 | // NOTE: Not sure what should happen for out-of-range values. 89 | const top = stack.pop() 90 | opts.undefError(top.value) 91 | }] 92 | ] 93 | }, 94 | 95 | { 96 | name: 'setecho', 97 | signature: 'setecho : I ->', 98 | help: ` 99 | Sets value of echo flag for listing. 100 | I = 0: no echo, 1: echo, 2: with tab, 3: and linenumber. 101 | `.trim(), 102 | handlers: [ 103 | [['Integer'], function (stack) { 104 | // NOTE: Not sure what should happen for out-of-range values. 105 | const top = stack.pop() 106 | opts.echo(top.value) 107 | }] 108 | ] 109 | }, 110 | 111 | { 112 | name: 'gc', 113 | signature: '', 114 | help: '', 115 | handlers: [[[], Function.prototype]] // noop. JS is garbage collected as is. 116 | }, 117 | 118 | /** 119 | * system : "command" -> 120 | * Escapes to shell, executes string "command". 121 | * The string may cause execution of another program. 122 | * When that has finished, the process returns to Joy. 123 | * TODO: Not sure what to do for this. May just treat as noop. Doesn't apply for browser. Maybe node repl. 124 | */ 125 | 126 | /** 127 | * getenv : "variable" -> "value" 128 | * Retrieves the value of the environment variable "variable". 129 | * TODO: Only applicable to node repl. 130 | */ 131 | 132 | /** 133 | * argv : -> A 134 | * Creates an aggregate A containing the interpreter's command line arguments. 135 | * TODO: Not sure 136 | */ 137 | 138 | /** 139 | * argc : -> I 140 | * Pushes the number of command line arguments. This is quivalent to 'argv size'. 141 | * TODO: Not sure 142 | */ 143 | 144 | /** 145 | * get : -> F 146 | * Reads a factor from input and pushes it onto stack. 147 | * TODO: Will need to update main loop to allow for async defs and i/o. 148 | */ 149 | 150 | { 151 | name: 'put', 152 | signature: 'put : X ->', 153 | help: 'Writes X to output, pops X off stack.', 154 | handlers: [ 155 | [['*'], function (stack) { 156 | const top = stack.pop() 157 | opts.output(`${top.toString()}\n`) 158 | }] 159 | ] 160 | }, 161 | 162 | { 163 | name: 'putch', 164 | signature: 'putch : N ->', 165 | help: 'N : numeric, writes character whose ASCII is N.', 166 | handlers: [ 167 | [['Character'], function (stack) { 168 | const top = stack.pop() 169 | opts.output(top.value) 170 | }], 171 | [['Integer'], function (stack) { 172 | const top = stack.pop() 173 | opts.output(T.JoyChar.from(top).value) 174 | }] 175 | ] 176 | }, 177 | 178 | /** 179 | * putchars : "abc.." -> 180 | * Writes abc.. (without quotes) 181 | */ 182 | { 183 | name: 'putchars', 184 | signature: 'putchars : "abc.." ->', 185 | help: 'Writes abc.. (without quotes)', 186 | handlers: [ 187 | [['String'], function (stack) { 188 | const top = stack.pop() 189 | opts.output(top.value) 190 | }] 191 | ] 192 | } 193 | 194 | /** 195 | * include : "filnam.ext" -> 196 | * Transfers input to file whose name is "filnam.ext". 197 | * On end-of-file returns to previous input file. 198 | * TODO: Not sure if/how this will work yet. Thinking about implementing some 199 | * of the base libs and throwing for anything else. 200 | */ 201 | 202 | /** 203 | * abort : -> 204 | * Aborts execution of current Joy program, returns to Joy main cycle. 205 | */ 206 | 207 | /** 208 | * quit : -> 209 | * Exit from Joy. 210 | */ 211 | ] 212 | -------------------------------------------------------------------------------- /test/lexer.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var Lexer = require('../src/joy/lexer') 3 | 4 | var digits = '0123456789'.split('') 5 | var lower = 'abcdefghijklmnopqrstuvwxyz'.split('') 6 | var upper = lower.map(function (c) { return c.toUpperCase() }) 7 | 8 | function toNumber (input) { 9 | if (/^-?0\d+/.test(input)) { 10 | return parseInt(input, 8) 11 | } else if (/^-?0[xX]/.test(input)) { 12 | return parseInt(input, 16) 13 | } 14 | return Number(input) 15 | } 16 | 17 | test('Lexer skips whitespace', function (t) { 18 | t.deepEqual(Lexer(' \n ').tokenize(), []) 19 | t.end() 20 | }) 21 | 22 | test('Lexer skips comments', function (t) { 23 | t.deepEqual(Lexer('# comment').tokenize(), []) 24 | t.deepEqual(Lexer('(* comment *)').tokenize(), []) 25 | t.deepEqual(Lexer('(*\n*)').tokenize(), []) 26 | t.deepEqual(Lexer('(* multiline\n comment *)').tokenize(), []) 27 | t.end() 28 | }) 29 | 30 | test('Lexer recognizes reserved characters', function (t) { 31 | '[]{};.'.split('').forEach(function (c) { 32 | t.deepEqual(Lexer(c).tokenize(), [{ type: 'ReservedChar', rawValue: c, value: c, pos: 0 }]) 33 | }) 34 | t.end() 35 | }) 36 | 37 | test('Lexer recognizes integer constants', function (t) { 38 | var cases = digits 39 | .concat(digits.map(function (d) { return '-' + d })) 40 | .concat([ 41 | '00000042', 42 | '1234567890', 43 | '9999999999', 44 | '-9999999999', 45 | '01234', 46 | '-077', 47 | '0312', 48 | '0xFF', 49 | '-0xCCFF', 50 | '0XABCDEF' 51 | ]) 52 | cases.forEach(function (input) { 53 | t.deepEqual(Lexer(input).tokenize(), [{ type: 'IntegerConstant', rawValue: input, value: toNumber(input), pos: 0 }]) 54 | }) 55 | t.end() 56 | }) 57 | 58 | test('Lexer recognizes float constants', function (t) { 59 | var cases = digits.map(function (d) { return d + '.0' }) 60 | .concat(digits.map(function (d) { return '-' + d + '.0' })) 61 | .concat([ 62 | '42.666', 63 | '1234567890.0123456789', 64 | '9999999999.999999999', 65 | '-9999999999.999999999', 66 | '1.0e10', 67 | '-5.5E9' 68 | ]) 69 | cases.forEach(function (input) { 70 | t.deepEqual(Lexer(input).tokenize(), [{ type: 'FloatConstant', rawValue: input, value: toNumber(input), pos: 0 }]) 71 | }) 72 | t.end() 73 | }) 74 | 75 | test('Lexer recognizes character constants', function (t) { 76 | var special = '!@#$%^&*();:.<>-_=+|[]{}'.split('') 77 | var cases = digits 78 | .concat(lower) 79 | .concat(upper) 80 | .concat(special) 81 | .map(function (testcase) { return "'" + testcase }) 82 | cases.forEach(function (input) { 83 | t.deepEqual(Lexer(input).tokenize(), [{ type: 'CharacterConstant', rawValue: input, value: input.slice(1), pos: 0 }]) 84 | }) 85 | t.deepEqual(Lexer('\'\\n').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\n', value: '\n', pos: 0 }]) 86 | t.deepEqual(Lexer('\'\\t').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\t', value: '\t', pos: 0 }]) 87 | t.deepEqual(Lexer('\'\\b').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\b', value: '\b', pos: 0 }]) 88 | t.deepEqual(Lexer('\'\\r').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\r', value: '\r', pos: 0 }]) 89 | t.deepEqual(Lexer('\'\\f').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\f', value: '\f', pos: 0 }]) 90 | t.deepEqual(Lexer('\'\\\'').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\\'', value: "'", pos: 0 }]) 91 | t.deepEqual(Lexer('\'\\"').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\"', value: '"', pos: 0 }]) 92 | t.deepEqual(Lexer('\'\\042').tokenize(), [{ type: 'CharacterConstant', rawValue: '\'\\042', value: String.fromCharCode(34), pos: 0 }]) 93 | t.end() 94 | }) 95 | 96 | test('Lexer recognizes string constants', function (t) { 97 | var cases = [ 98 | ['""', ''], 99 | ['"hello world"', 'hello world'], 100 | ['"123"', '123'], 101 | ['"\\n"', '\n'], 102 | ['"\\t"', '\t'], 103 | ['"\\b"', '\b'], 104 | ['"\\r"', '\r'], 105 | ['"\\f"', '\f'], 106 | ['"\\\'"', "'"], 107 | ['"\\""', '"'], 108 | ['"\\042"', '"'], 109 | ['" "', ' '] 110 | ] 111 | cases.forEach(function ([input, value]) { 112 | t.deepEqual(Lexer(input).tokenize(), [{ type: 'StringConstant', rawValue: input, value: value, pos: 0 }]) 113 | }) 114 | t.end() 115 | }) 116 | 117 | test('Lexer recognizes reserved words', function (t) { 118 | ['MODULE', 'PRIVATE', 'HIDE', 'PUBLIC', 'IN', 'DEFINE', 'LIBRA', 'END'].forEach(function (input) { 119 | t.deepEqual(Lexer(input).tokenize(), [{ type: 'ReservedWord', rawValue: input, value: input, pos: 0 }]) 120 | }) 121 | t.end() 122 | }) 123 | 124 | test('Lexer recognizes atomic symbols', function (t) { 125 | var special = '!@$%^&*()-_+=\\|:<>,?/'.split('') 126 | var cases = lower 127 | .concat(upper) 128 | .concat(special) 129 | .concat([ 130 | // '-12345678901', // NOTE: This one seems odd. Not qualified as integer due to >10 digits. 131 | // '#hi', // NOTE: Is this right? Or should this be a comment? 132 | 'helloWorld123_-=' 133 | ]) 134 | cases.forEach(function (input) { 135 | t.deepEqual(Lexer(input).tokenize(), [{ type: 'AtomicSymbol', rawValue: input, value: input, pos: 0 }]) 136 | }) 137 | t.deepEqual(Lexer('true').tokenize(), [{ type: 'AtomicSymbol', rawValue: 'true', value: true, pos: 0 }]) 138 | t.deepEqual(Lexer('false').tokenize(), [{ type: 'AtomicSymbol', rawValue: 'false', value: false, pos: 0 }]) 139 | t.end() 140 | }) 141 | 142 | test('Lexer recognizes token sequences', function (t) { 143 | t.deepEqual(Lexer('[]').tokenize(), [ 144 | { type: 'ReservedChar', rawValue: '[', value: '[', pos: 0 }, 145 | { type: 'ReservedChar', rawValue: ']', value: ']', pos: 1 } 146 | ]) 147 | t.deepEqual(Lexer('{}').tokenize(), [ 148 | { type: 'ReservedChar', rawValue: '{', value: '{', pos: 0 }, 149 | { type: 'ReservedChar', rawValue: '}', value: '}', pos: 1 } 150 | ]) 151 | t.deepEqual(Lexer('{1 2}').tokenize(), [ 152 | { type: 'ReservedChar', rawValue: '{', value: '{', pos: 0 }, 153 | { type: 'IntegerConstant', rawValue: '1', value: 1, pos: 1 }, 154 | { type: 'IntegerConstant', rawValue: '2', value: 2, pos: 3 }, 155 | { type: 'ReservedChar', rawValue: '}', value: '}', pos: 4 } 156 | ]) 157 | t.end() 158 | }) 159 | 160 | test('Lexer throws tokenizing invalid input', function (t) { 161 | t.throws(Lexer('(*').tokenize, 'throws when multiline comment is not terminated') 162 | // t.throws(Lexer('1invalid').tokenize, 'throws when word is invalid') // Not throwing, but not getting parsed as an atom either. Not sure what is correct. 163 | // t.throws(Lexer('12345678901').tokenize, 'throws when integers exceed 10 digits') 164 | // t.throws(Lexer((Number.MAX_SAFE_INTEGER + 1).toString()).tokenize, 'throws when integers exceed platform max integer') 165 | t.throws(Lexer('"').tokenize, 'throws when string constant is not closed') 166 | t.throws(Lexer('"hello world').tokenize, 'throws when string constant is not closed') 167 | t.throws(Lexer('"hello\nworld"').tokenize, 'throws when string spans multiple lines') 168 | t.end() 169 | }) 170 | -------------------------------------------------------------------------------- /src/joy/interpreter/predicate_defs.js: -------------------------------------------------------------------------------- 1 | const { applyToTop, applyToTop2, eq, ne, lte, lt, gte, gt } = require('./util') 2 | const T = require('./types') 3 | 4 | module.exports = [ 5 | { 6 | name: 'null', 7 | signature: 'null : X -> B', 8 | help: 'Tests for empty aggregate X or zero numeric.', 9 | handlers: [ 10 | [['Integer'], applyToTop(x => new T.JoyBool(x.value === 0))], 11 | [['Character'], applyToTop(x => new T.JoyBool(x.value === String.fromCharCode(0)))], 12 | [['Aggregate'], applyToTop(x => new T.JoyBool(x.length === 0))] 13 | ] 14 | }, 15 | 16 | { 17 | name: 'small', 18 | signature: 'small : X -> B', 19 | help: 'Tests whether aggregate X has 0 or 1 members, or numeric 0 or 1.', 20 | handlers: [ 21 | [['Aggregate'], applyToTop(x => new T.JoyBool(x.length === 0 || x.length === 1))], 22 | [['Integer'], applyToTop(x => new T.JoyBool(x.value === 0 || x.value === 1))], 23 | [['Character'], applyToTop(x => new T.JoyBool(x.value === String.fromCharCode(0) || x.value === String.fromCharCode(1)))] 24 | ] 25 | }, 26 | 27 | { 28 | name: '>=', 29 | signature: '>= : X Y -> B', 30 | help: ` 31 | Either both X and Y are numeric or both are strings or symbols. 32 | Tests whether X greater than or equal to Y. Also supports float. 33 | `.trim(), 34 | handlers: [ 35 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyBool(gte(x, y)))], 36 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyBool(gte(x, y)))], 37 | [['String', 'String'], applyToTop2((x, y) => new T.JoyBool(gte(x, y)))], 38 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyBool(gte(x, y)))] 39 | ] 40 | }, 41 | 42 | { 43 | name: '>', 44 | signature: '> : X Y -> B', 45 | help: ` 46 | Either both X and Y are numeric or both are strings or symbols. 47 | Tests whether X greater than Y. Also supports float. 48 | `.trim(), 49 | handlers: [ 50 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyBool(gt(x, y)))], 51 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyBool(gt(x, y)))], 52 | [['String', 'String'], applyToTop2((x, y) => new T.JoyBool(gt(x, y)))], 53 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyBool(gt(x, y)))] 54 | ] 55 | }, 56 | 57 | { 58 | name: '<=', 59 | signature: '<= : X Y -> B', 60 | help: ` 61 | Either both X and Y are numeric or both are strings or symbols. 62 | Tests whether X less than or equal to Y. Also supports float. 63 | `.trim(), 64 | handlers: [ 65 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyBool(lte(x, y)))], 66 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyBool(lte(x, y)))], 67 | [['String', 'String'], applyToTop2((x, y) => new T.JoyBool(lte(x, y)))], 68 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyBool(lte(x, y)))] 69 | ] 70 | }, 71 | 72 | { 73 | name: '<', 74 | signature: '< : X Y -> B', 75 | help: ` 76 | Either both X and Y are numeric or both are strings or symbols. 77 | Tests whether X less than Y. Also supports float. 78 | `.trim(), 79 | handlers: [ 80 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyBool(lt(x, y)))], 81 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyBool(lt(x, y)))], 82 | [['String', 'String'], applyToTop2((x, y) => new T.JoyBool(lt(x, y)))], 83 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyBool(lt(x, y)))] 84 | ] 85 | }, 86 | 87 | { 88 | name: '!=', 89 | signature: '!= : X Y -> B', 90 | help: ` 91 | Either both X and Y are numeric or both are strings or symbols. 92 | Tests whether X not equal to Y. Also supports float. 93 | `.trim(), 94 | handlers: [ 95 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyBool(ne(x, y)))], 96 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyBool(ne(x, y)))], 97 | [['String', 'String'], applyToTop2((x, y) => new T.JoyBool(ne(x, y)))], 98 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyBool(ne(x, y)))] 99 | ] 100 | }, 101 | 102 | { 103 | name: '=', 104 | signature: '= : X Y -> B', 105 | help: ` 106 | Either both X and Y are numeric or both are strings or symbols. 107 | Tests whether X equal to Y. Also supports float. 108 | `.trim(), 109 | handlers: [ 110 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyBool(eq(x, y)))], 111 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyBool(eq(x, y)))], 112 | [['String', 'String'], applyToTop2((x, y) => new T.JoyBool(eq(x, y)))], 113 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyBool(eq(x, y)))] 114 | ] 115 | }, 116 | 117 | { 118 | name: 'equal', 119 | signature: 'equal : T U -> B', 120 | help: '(Recursively) tests whether trees T and U are identical.', 121 | handlers: [ 122 | [['List', 'List'], applyToTop2((x, y) => new T.JoyBool(eq(x, y)))] 123 | ] 124 | }, 125 | 126 | { 127 | name: 'has', 128 | signature: 'has : A X -> B', 129 | help: 'Tests whether aggregate A has X as a member.', 130 | handlers: [ 131 | [['List', '*'], applyToTop2((x, y) => new T.JoyBool(x.value.find(m => m.value === y.value)))], 132 | [['String', 'Character'], applyToTop2((x, y) => new T.JoyBool(x.value.indexOf(y.value) >= 0))], 133 | [['Set', 'Integer'], applyToTop2((x, y) => new T.JoyBool(x.has(y)))] 134 | ] 135 | }, 136 | 137 | { 138 | name: 'in', 139 | signature: 'in : X A -> B', 140 | help: 'Tests whether X is a member of aggregate A.', 141 | handlers: [ 142 | [['List', '*'], applyToTop2((x, y) => new T.JoyBool(y.value.find(m => m.value === x.value)))], 143 | [['String', 'Character'], applyToTop2((x, y) => new T.JoyBool(y.value.indexOf(x.value) >= 0))], 144 | [['Set', 'Integer'], applyToTop2((x, y) => new T.JoyBool(y.has(x)))] 145 | ] 146 | }, 147 | 148 | { 149 | name: 'integer', 150 | signature: 'integer : X -> B', 151 | help: 'Tests whether X is an integer.', 152 | handlers: [ 153 | [['*'], applyToTop(x => new T.JoyBool(!!x.isInteger))] 154 | ] 155 | }, 156 | 157 | { 158 | name: 'char', 159 | signature: 'char : X -> B', 160 | help: 'Tests whether X is a character.', 161 | handlers: [ 162 | [['*'], applyToTop(x => new T.JoyBool(!!x.isCharacter))] 163 | ] 164 | }, 165 | 166 | { 167 | name: 'logical', 168 | signature: 'logical : X -> B', 169 | help: 'Tests whether X is a logical.', 170 | handlers: [ 171 | [['*'], applyToTop(x => new T.JoyBool(!!x.isLogical))] 172 | ] 173 | }, 174 | 175 | { 176 | name: 'set', 177 | signature: 'set : X -> B', 178 | help: 'Tests whether X is a set.', 179 | handlers: [ 180 | [['*'], applyToTop(x => new T.JoyBool(!!x.isSet))] 181 | ] 182 | }, 183 | 184 | { 185 | name: 'string', 186 | signature: 'string : X -> B', 187 | help: 'Tests whether X is a string.', 188 | handlers: [ 189 | [['*'], applyToTop(x => new T.JoyBool(!!x.isString))] 190 | ] 191 | }, 192 | 193 | { 194 | name: 'list', 195 | signature: 'list : X -> B', 196 | help: 'Tests whether X is a list.', 197 | handlers: [ 198 | [['*'], applyToTop(x => new T.JoyBool(!!x.isList))] 199 | ] 200 | }, 201 | 202 | { 203 | name: 'leaf', 204 | signature: 'leaf : X -> B', 205 | help: 'Tests whether X is not a list.', 206 | handlers: [ 207 | [['*'], applyToTop(x => new T.JoyBool(!x.isList))] 208 | ] 209 | }, 210 | 211 | /** 212 | * user : X -> B 213 | * Tests whether X is a user-defined symbol. 214 | * TODO: not sure how this will be distinguished yet 215 | */ 216 | 217 | { 218 | name: 'float', 219 | signature: 'float : R -> B', 220 | help: 'Tests whether R is a float.', 221 | handlers: [ 222 | [['*'], applyToTop(x => new T.JoyBool(!!x.isFloat))] 223 | ] 224 | } 225 | 226 | /** 227 | * file : F -> B 228 | * Tests whether F is a file. 229 | * TODO: Not sure how file io will be implemented. 230 | */ 231 | ] 232 | -------------------------------------------------------------------------------- /src/joy/lexer.js: -------------------------------------------------------------------------------- 1 | var Fsm = require('./fsm') 2 | 3 | const escapedCharToValue = { 4 | '\'\\n': '\n', 5 | '\'\\t': '\t', 6 | '\'\\b': '\b', 7 | '\'\\r': '\r', 8 | '\'\\f': '\f', 9 | "'\\'": "'", 10 | '\'\\"': '"' 11 | } 12 | 13 | function Token (type, rawValue, value, pos) { 14 | this.type = type 15 | this.rawValue = rawValue 16 | this.value = value == null ? rawValue : value 17 | this.pos = pos 18 | } 19 | 20 | var NumberFsm = Fsm({ 21 | initial: 'Initial', 22 | stop: 'NoNextState', 23 | accepting: ['Integer', 'NumberZero', 'NumberWithDecimal', 'NumberWithExponent', 'OctalNumber', 'HexNumber'], 24 | default: 'NoNextState', 25 | states: { 26 | Initial: [ 27 | ['0', 'NumberZero'], 28 | ['-', 'BeginNegativeNumber'], 29 | [/[0-9]/, 'Integer'] 30 | ], 31 | Integer: [ 32 | [/[0-9]/, 'Integer'], 33 | ['.', 'BeginNumberWithDecimal'], 34 | [/[eE]/, 'BeginNumberWithExponent'] 35 | ], 36 | BeginNumberWithDecimal: [ 37 | [/[0-9]/, 'NumberWithDecimal'] 38 | ], 39 | NumberWithDecimal: [ 40 | [/[0-9]/, 'NumberWithDecimal'], 41 | [/[eE]/, 'BeginNumberWithExponent'] 42 | ], 43 | BeginNumberWithExponent: [ 44 | ['-', 'BeginNumberWithSignedExponent'], 45 | [/[0-9]/, 'NumberWithExponent'] 46 | ], 47 | BeginNumberWithSignedExponent: [ 48 | [/[0-9]/, 'NumberWithExponent'] 49 | ], 50 | NumberWithExponent: [ 51 | [/[0-9]/, 'NumberWithExponent'] 52 | ], 53 | NumberZero: [ 54 | [/[0-7]/, 'OctalNumber'], 55 | [/[xX]/, 'BeginHexNumber'], 56 | ['.', 'BeginNumberWithDecimal'] 57 | ], 58 | OctalNumber: [ 59 | [/[0-7]/, 'OctalNumber'] 60 | ], 61 | BeginHexNumber: [ 62 | [/[0-9A-F]/, 'HexNumber'] 63 | ], 64 | HexNumber: [ 65 | [/[0-9A-F]/, 'HexNumber'] 66 | ], 67 | BeginNegativeNumber: [ 68 | ['0', 'NumberZero'], 69 | [/[1-9]/, 'Integer'] 70 | ] 71 | } 72 | }) 73 | 74 | var CharacterFsm = Fsm({ 75 | initial: 'Initial', 76 | stop: 'NoNextState', 77 | accepting: ['Character', 'EscapedCharacter', 'CharacterEscapedAscii'], 78 | default: 'NoNextState', 79 | states: { 80 | Initial: [ 81 | ["'", 'BeginCharacter'] 82 | ], 83 | BeginCharacter: [ 84 | [/[^\\]/, 'Character'], 85 | ['\\', 'BeginEscapedCharacter'] 86 | ], 87 | BeginEscapedCharacter: [ 88 | [/[ntbrf'"]/, 'EscapedCharacter'], 89 | [/\d/, 'BeginEscapedAscii0'] 90 | ], 91 | BeginEscapedAscii0: [ 92 | [/\d/, 'BeginEscapedAscii1'] 93 | ], 94 | BeginEscapedAscii1: [ 95 | [/\d/, 'CharacterEscapedAscii'] 96 | ] 97 | } 98 | }) 99 | 100 | var StringFsm = Fsm({ 101 | initial: 'Initial', 102 | stop: 'NoNextState', 103 | accepting: ['String'], 104 | default: 'NoNextState', 105 | seed: '', 106 | states: { 107 | Initial: [ 108 | ['"', 'BeginString'] 109 | ], 110 | BeginString: [ 111 | ['"', 'String'], 112 | ['\\', 'BeginStringEscape'], 113 | [/./, 'BeginString', (acc, s) => acc + s] 114 | ], 115 | BeginStringEscape: [ 116 | [/[ntbrf'"]/, 'BeginString', (acc, s) => { 117 | switch (s) { 118 | case 'n': return acc + '\n' 119 | case 't': return acc + '\t' 120 | case 'b': return acc + '\b' 121 | case 'r': return acc + '\r' 122 | case 'f': return acc + '\f' 123 | case "'": return acc + "'" 124 | case '"': return acc + '"' 125 | } 126 | }], 127 | [/\d/, 'BeginStringEscapedAscii0', (acc, s) => acc + s] 128 | ], 129 | BeginStringEscapedAscii0: [ 130 | [/\d/, 'BeginStringEscapedAscii1', (acc, s) => acc + s] 131 | ], 132 | BeginStringEscapedAscii1: [ 133 | [/\d/, 'BeginString', (acc, s) => { 134 | const ascii = parseInt(acc.slice(-2) + s, 8) 135 | return acc.slice(0, -2) + String.fromCharCode(ascii) 136 | }] 137 | ] 138 | } 139 | }) 140 | 141 | function Lexer (input) { 142 | let pos = 0 143 | let lookahead = input.charAt(pos) 144 | const tokenMap = { 145 | '[': 'ReservedChar', 146 | ']': 'ReservedChar', 147 | '{': 'ReservedChar', 148 | '}': 'ReservedChar', 149 | ';': 'ReservedChar', 150 | '.': 'ReservedChar', 151 | '==': 'ReservedWord', 152 | 'MODULE': 'ReservedWord', 153 | 'PRIVATE': 'ReservedWord', 154 | 'HIDE': 'ReservedWord', 155 | 'PUBLIC': 'ReservedWord', 156 | 'IN': 'ReservedWord', 157 | 'DEFINE': 'ReservedWord', 158 | 'LIBRA': 'ReservedWord', 159 | 'END': 'ReservedWord' 160 | } 161 | 162 | function raiseParseError () { 163 | throw new Error('Parse error: Unexpected end of input') 164 | } 165 | 166 | function peek (n) { 167 | n = n || 1 168 | return input.slice(pos, pos + n) 169 | } 170 | 171 | function read (n) { 172 | n = n || 1 173 | const val = peek(n) 174 | if (val.length === n) { 175 | pos += n 176 | lookahead = input.charAt(pos) 177 | return val 178 | } 179 | raiseParseError() 180 | } 181 | 182 | function skipWhitespace () { 183 | while (lookahead === ' ' || lookahead === '\t' || lookahead === '\n') { read(1) } 184 | } 185 | 186 | function skipComment () { 187 | while (pos < input.length && lookahead !== '\n') { read(1) } 188 | } 189 | 190 | function skipCommentMultiline () { 191 | const endPos = input.indexOf('*)', pos) 192 | if (endPos === -1) { raiseParseError() } 193 | read((endPos + 2) - pos) 194 | } 195 | 196 | function recognizeNumber () { 197 | const currPos = pos 198 | const result = NumberFsm.run(input.slice(pos)) 199 | if (result === null) { return null } 200 | read(result.value.length) 201 | if (result.state === 'NumberWithDecimal' || result.state === 'NumberWithExponent') { 202 | return new Token('FloatConstant', result.value, parseFloat(result.value), currPos) 203 | } else if (result.state === 'OctalNumber') { 204 | return new Token('IntegerConstant', result.value, parseInt(result.value, 8), currPos) 205 | } else if (result.state === 'HexNumber') { 206 | return new Token('IntegerConstant', result.value, parseInt(result.value, 16), currPos) 207 | } 208 | return new Token('IntegerConstant', result.value, parseInt(result.value, 10), currPos) 209 | } 210 | 211 | function recognizeCharacter () { 212 | const currPos = pos 213 | const result = CharacterFsm.run(input.slice(pos)) 214 | if (result === null) { return null } 215 | read(result.value.length) 216 | let value 217 | if (result.state === 'Character') { 218 | value = result.value.charAt(1) 219 | } else if (result.state === 'EscapedCharacter') { 220 | value = escapedCharToValue[result.value] 221 | } else { 222 | const octal = parseInt(result.value.slice(2), 8) 223 | value = String.fromCharCode(octal) 224 | } 225 | return new Token('CharacterConstant', result.value, value, currPos) 226 | } 227 | 228 | function recognizeString () { 229 | const currPos = pos 230 | const result = StringFsm.run(input.slice(pos)) 231 | if (result === null) { return null } 232 | read(result.value.length) 233 | return new Token('StringConstant', result.value, result.acc, currPos) 234 | } 235 | 236 | function recognizeSymbol () { 237 | const startPos = pos 238 | let currPos = pos 239 | let result 240 | if (/[a-zA-Z!@$%^&*()\-_+=\\|:<>,?/]/.test(lookahead)) { 241 | currPos += 1 242 | while (/[a-zA-Z0-9=_-]/.test(input.charAt(currPos))) { 243 | currPos += 1 244 | } 245 | result = read(currPos - pos) 246 | if (result === 'true') { 247 | return new Token('AtomicSymbol', 'true', true, startPos) 248 | } else if (result === 'false') { 249 | return new Token('AtomicSymbol', 'false', false, startPos) 250 | } 251 | return new Token(tokenMap[result] || 'AtomicSymbol', result, null, startPos) 252 | } 253 | return null 254 | } 255 | 256 | function nextToken () { 257 | let token 258 | skipWhitespace() 259 | const currPos = pos 260 | switch (true) { 261 | case pos >= input.length: 262 | return null 263 | case tokenMap[lookahead] === 'ReservedChar': 264 | token = new Token('ReservedChar', lookahead, null, currPos) 265 | read(1) 266 | return token 267 | case lookahead === '#': 268 | skipComment() 269 | return nextToken() 270 | case peek(2) === '(*': 271 | skipCommentMultiline() 272 | return nextToken() 273 | case /[0-9]/.test(lookahead): 274 | return recognizeNumber() || raiseParseError() 275 | case lookahead === '-': 276 | return recognizeNumber() || recognizeSymbol() || raiseParseError() 277 | case lookahead === "'": 278 | return recognizeCharacter() || raiseParseError() 279 | case lookahead === '"': 280 | return recognizeString() || raiseParseError() 281 | default: 282 | return recognizeSymbol() || raiseParseError() 283 | } 284 | } 285 | 286 | return { 287 | tokenize: function tokenize () { 288 | const result = [] 289 | let token = nextToken() 290 | while (token !== null) { 291 | result.push(token) 292 | token = nextToken() 293 | } 294 | return result 295 | } 296 | } 297 | } 298 | 299 | module.exports = Lexer 300 | -------------------------------------------------------------------------------- /src/joy/interpreter/types.js: -------------------------------------------------------------------------------- 1 | function JoyBase (value) { 2 | this.value = value 3 | Object.defineProperty(this, 'isNonEmptyAggregate', { 4 | get: () => { 5 | if (!this.isAggregate) { return false } 6 | return (this.length || this.value.length) !== 0 7 | } 8 | }) 9 | } 10 | JoyBase.prototype.ap = function (other) { 11 | return other.map(this.value) 12 | } 13 | JoyBase.prototype.map = function (f) { 14 | return new this.constructor(f(this.value)) 15 | } 16 | JoyBase.prototype.toString = function () { 17 | return this.value.toString() 18 | } 19 | 20 | // function Aggregate () { 21 | 22 | // } 23 | 24 | function JoyInt (value) { 25 | JoyBase.call(this, value) 26 | this.isInteger = true 27 | this.isNumeric = true 28 | } 29 | JoyInt.prototype = Object.create(JoyBase.prototype) 30 | JoyInt.prototype.constructor = JoyInt 31 | JoyInt.from = function (other) { 32 | if (other.isFloat) { 33 | return new JoyInt(Math.trunc(other.value)) 34 | } else if (other.isCharacter) { 35 | return new JoyInt(other.toNumber()) 36 | } else if (typeof other === 'number') { 37 | return new JoyInt(Math.floor(other)) 38 | } else if (typeof other === 'string' && other.length === 1) { 39 | return new JoyInt(other.charCodeAt(0)) 40 | } 41 | throw new Error('Cannot convert ' + other + ' to Integer') 42 | } 43 | JoyInt.prototype.toNumber = function () { 44 | return this.value 45 | } 46 | JoyInt.prototype.equals = function (other) { 47 | return this.value === other.value 48 | } 49 | JoyInt.prototype.lte = function (other) { 50 | return this.equals(other) || this.value < other.value 51 | } 52 | 53 | function JoyFloat (value) { 54 | JoyBase.call(this, value) 55 | this.isFloat = true 56 | } 57 | JoyFloat.prototype = Object.create(JoyBase.prototype) 58 | JoyFloat.prototype.constructor = JoyFloat 59 | JoyFloat.from = function (other) { 60 | if (other.isInteger) { 61 | return new JoyFloat(other.value) 62 | } else if (typeof other === 'number') { 63 | return new JoyFloat(other) 64 | } else if (other.isString) { 65 | return new JoyFloat(parseFloat(other.value)) 66 | } else if (typeof other === 'string') { 67 | return new JoyFloat(parseFloat(other)) 68 | } 69 | throw new Error('Cannot convert ' + other + ' to Float') 70 | } 71 | JoyFloat.prototype.toString = function () { 72 | const retval = this.value.toString() 73 | return retval.includes('.') ? retval : `${retval}.0` 74 | } 75 | JoyFloat.prototype.equals = function (other) { 76 | return this.value === other.value 77 | } 78 | JoyFloat.prototype.lte = function (other) { 79 | return this.equals(other) || this.value < other.value 80 | } 81 | 82 | function JoyChar (value) { 83 | JoyBase.call(this, value) 84 | this.isCharacter = true 85 | this.isNumeric = true 86 | } 87 | JoyChar.prototype = Object.create(JoyBase.prototype) 88 | JoyChar.prototype.constructor = JoyChar 89 | JoyChar.from = function (other) { 90 | if (other.isInteger) { 91 | return new JoyChar(String.fromCharCode(other.value)) 92 | } else if (typeof other === 'number') { 93 | return new JoyChar(String.fromCharCode(other)) 94 | } else if (other.isString && other.value.length === 1) { 95 | return new JoyChar(other.value) 96 | } else if (typeof other === 'string' && other.length === 1) { 97 | return new JoyChar(other.value) 98 | } 99 | throw new Error('Cannot convert ' + other + ' to Character') 100 | } 101 | JoyChar.prototype.toNumber = function () { 102 | return this.value.charCodeAt(0) 103 | } 104 | JoyChar.prototype.toString = function () { 105 | // TODO: Add handling for escape sequences 106 | return `'${this.value.toString()}` 107 | } 108 | JoyChar.prototype.equals = function (other) { 109 | return this.value === other.value 110 | } 111 | JoyChar.prototype.lte = function (other) { 112 | return this.equals(other) || this.value < other.value 113 | } 114 | 115 | function JoyString (value) { 116 | JoyBase.call(this, value) 117 | this.isString = true 118 | this.isAggregate = true 119 | Object.defineProperty(this, 'length', { 120 | get: () => this.value.length 121 | }) 122 | } 123 | JoyString.prototype = Object.create(JoyBase.prototype) 124 | JoyString.prototype.constructor = JoyString 125 | JoyString.prototype.toString = function () { 126 | // TODO: Add handling for escape sequences 127 | return `"${this.value}"` 128 | } 129 | JoyString.from = function (other) { 130 | if (other.isCharacter) { 131 | return new JoyString(other.value) 132 | } 133 | throw new Error('Cannot convert ' + other + ' to String') 134 | } 135 | JoyString.prototype.first = function () { 136 | return new JoyChar(this.value.charAt(0)) 137 | } 138 | JoyString.prototype.rest = function () { 139 | return this.map(x => x.slice(1)) 140 | } 141 | JoyString.prototype.equals = function (other) { 142 | return this.value === other.value 143 | } 144 | JoyString.prototype.lte = function (other) { 145 | return this.equals(other) || this.value < other.value 146 | } 147 | 148 | function JoyBool (value) { 149 | JoyBase.call(this, value) 150 | this.isBool = true 151 | this.isLogical = true 152 | } 153 | JoyBool.prototype = Object.create(JoyBase.prototype) 154 | JoyBool.prototype.constructor = JoyBool 155 | 156 | function JoyList (value) { 157 | JoyBase.call(this, value) 158 | this.isList = true 159 | this.isAggregate = true 160 | Object.defineProperty(this, 'length', { 161 | get: () => this.value.length 162 | }) 163 | } 164 | JoyList.prototype = Object.create(JoyBase.prototype) 165 | JoyList.prototype.constructor = JoyList 166 | JoyList.prototype.toString = function () { 167 | return `[${this.value.map(x => x.toString()).join(' ')}]` 168 | } 169 | JoyList.prototype.first = function () { 170 | return this.value[0] 171 | } 172 | JoyList.prototype.rest = function () { 173 | return this.map(x => x.slice(1)) 174 | } 175 | JoyList.prototype.equals = function (other) { 176 | if (this.value.length !== other.value.length) { return false } 177 | return this.value.every((x, idx) => 178 | x.equals 179 | ? x.equals(other.value[idx]) 180 | : x.value === other.value[x].value) 181 | } 182 | 183 | const JOY_SET_SIZE = 32 184 | function JoySet (value) { 185 | JoyBase.call(this, null) 186 | this._values = {} 187 | this._smallest = JOY_SET_SIZE 188 | this._length = 0 189 | value.forEach((val) => { 190 | this.add(val) 191 | }) 192 | this.isSet = true 193 | this.isAggregate = true 194 | Object.defineProperty(this, 'length', { 195 | get: () => this._length 196 | }) 197 | } 198 | JoySet.prototype = Object.create(JoyBase.prototype) 199 | JoySet.prototype.constructor = JoySet 200 | JoySet.prototype.add = function (item) { 201 | // TODO: throw if value exceeds 31? 202 | const value = item.toNumber() 203 | if (this.has(value)) { return } 204 | this._values[value] = item 205 | this._length += 1 206 | if (value < this._smallest) { 207 | this._smallest = value 208 | } 209 | return this 210 | } 211 | JoySet.prototype.has = function (item) { 212 | return !!this._values[item.value] 213 | } 214 | JoySet.prototype.forEach = function (fn) { 215 | Object.keys(this._values).forEach((key) => { 216 | fn(this._values[key]) 217 | }) 218 | } 219 | JoySet.prototype.forEachOrdered = function (fn) { 220 | for (let i = 0; i < JOY_SET_SIZE; i += 1) { 221 | if (this._values[i] !== undefined) { 222 | fn(this._values[i]) 223 | } 224 | } 225 | } 226 | JoySet.prototype.union = function (other) { 227 | const result = new JoySet([]) 228 | this.forEach((x) => { result.add(x) }) 229 | other.forEach((x) => { result.add(x) }) 230 | return result 231 | } 232 | JoySet.prototype.intersect = function (other) { 233 | const result = new JoySet([]) 234 | this.forEach((x) => { 235 | if (other.has(x)) { 236 | result.add(x) 237 | } 238 | }) 239 | return result 240 | } 241 | JoySet.prototype.symmetricDifference = function (other) { 242 | const result = new JoySet([]) 243 | this.forEach((x) => { 244 | if (!other.has(x)) { 245 | result.add(x) 246 | } 247 | }) 248 | other.forEach((x) => { 249 | if (!this.has(x)) { 250 | result.add(x) 251 | } 252 | }) 253 | return result 254 | } 255 | JoySet.prototype.complement = function () { 256 | const result = new JoySet([]) 257 | for (let i = 0; i < JOY_SET_SIZE; i += 1) { 258 | const val = new JoyInt(i) 259 | if (!this.has(val)) { 260 | result.add(val) 261 | } 262 | } 263 | return result 264 | } 265 | JoySet.prototype.first = function () { 266 | return new JoyInt(this._smallest) 267 | } 268 | JoySet.prototype.rest = function () { 269 | const result = new JoySet([]) 270 | this.forEach(x => { 271 | if (x.value !== this._smallest) { 272 | result.add(x) 273 | } 274 | }) 275 | return result 276 | } 277 | JoySet.prototype.toString = function () { 278 | const entries = [] 279 | this.forEach((x) => { entries.push(x.toString()) }) 280 | return `{${entries.join(' ')}}` 281 | } 282 | JoySet.prototype.equals = function (other) { 283 | if (this.length !== other.length) { return false } 284 | let result = true 285 | this.forEach((val) => { 286 | result = result && other.has(val) 287 | }) 288 | return result 289 | } 290 | JoySet.prototype.drop = function (n) { 291 | const result = new JoySet([]) 292 | let dropped = 0 293 | this.forEachOrdered((x) => { 294 | if (dropped < n) { 295 | dropped += 1 296 | } else { 297 | result.add(x) 298 | } 299 | }) 300 | return result 301 | } 302 | JoySet.prototype.take = function (n) { 303 | const result = new JoySet([]) 304 | let taken = 0 305 | this.forEachOrdered((x) => { 306 | if (taken < n) { 307 | taken += 1 308 | result.add(x) 309 | } 310 | }) 311 | return result 312 | } 313 | 314 | function JoySymbol (value) { 315 | JoyBase.call(this, value) 316 | this.isSymbol = true 317 | } 318 | JoySymbol.prototype = Object.create(JoyBase.prototype) 319 | JoySymbol.prototype.constructor = JoySymbol 320 | JoySymbol.prototype.equals = function (other) { 321 | return this.value === other.value 322 | } 323 | JoySymbol.prototype.lte = function (other) { 324 | return this.equals(other) || this.value < other.value 325 | } 326 | 327 | module.exports = { 328 | JoyInt: JoyInt, 329 | JoyFloat: JoyFloat, 330 | JoyChar: JoyChar, 331 | JoyString: JoyString, 332 | JoyBool: JoyBool, 333 | JoyList: JoyList, 334 | JoySet: JoySet, 335 | JoySymbol: JoySymbol 336 | } 337 | -------------------------------------------------------------------------------- /src/joy/lib/symlib.joy.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | (* FILE: symlib.joy *) 3 | 4 | basic-libload. 5 | 6 | LIBRA 7 | 8 | _symlib == true; 9 | 10 | HIDE 11 | (* 12 | 21 translators, involving 6 notations (5 source, 5 target) 13 | All translators have time complexity linear in the lenth of input. 14 | 15 | Notations (S=source, T=target) 16 | Pol (ST) - Polish or prefix, without bracketing 17 | Rev (ST) - Reverse Polish or postfix, Joy without dup swap .. 18 | Cam (ST) - Cambridge, Lisp-like bracketing but no lambda 19 | Tre ( T) - Trees, similar to Cam but even atoms in brackets 20 | Inf (ST) - Infix, fully bracketed only for binary operators 21 | Min (S ) - Minimally bracketed, binaries with precedences 22 | Lexicon, based on Joy leaves (i.e. not lists or quotations)): 23 | a = atoms or operands, leaves not defined as operators 24 | u = unary operators, user defined as leaves 25 | b = binary operators, user defined as leaves 26 | [..] = Joy lists or quotations (i.e. not leaves) 27 | b1 b2 b3 = user defined binaries of increasing precedences 28 | Grammars for the notations: 29 | Pol ::= [P], P ::= a | u P | b P P | [..] 30 | Rev ::= [R], R ::= a | R u | R R b | [..] 31 | Cam ::= C , C ::= a | [u C] | [b C C] | [QUOTE [..]] 32 | Tre ::= T , T ::= [a] | [u T] | [b T T] | [QUOTE [..]] 33 | Inf ::= [I], I ::= a | u I | [I b I] | QUOTE [..] 34 | Min ::= [M1], M1 ::= M2 | M2 b1 M1 35 | M2 ::= M3 | M3 b2 M2 36 | M3 ::= M4 | M4 b3 M3 37 | M4 ::= a | u M4 | [M1] | QUOTE [..] 38 | 39 | Note that operator classes u b b1 b2 b3 need to be user defined as lists: 40 | unops == [succ..]; binops == [cons..]; 41 | bin1ops == [>..]; bin2ops == [+..]; bin3ops == [*..] 42 | *) 43 | 44 | (* from Cambridge to .. *) 45 | C2T == 46 | [ list ] 47 | [ unswons 48 | [ [ [unops in] 49 | [first C2T [] cons] dip swons ] 50 | [ [[QUOTE] in] 51 | swons ] 52 | [ [uncons first [C2T] dip C2T [] cons cons] 53 | dip swons ] ] 54 | cond ] 55 | [ [] cons ] 56 | ifte; 57 | C2I == 58 | [ list ] 59 | [ unswons 60 | [ [ [unops in] 61 | [first C2I] dip swons ] 62 | [ [[QUOTE] in] 63 | swons ] 64 | [ [uncons first [C2I] dip C2I] 65 | dip swons concat [] cons ] ] 66 | cond ] 67 | [ [] cons ] 68 | ifte; 69 | C2P == 70 | [ list ] 71 | 72 | [ unswons 73 | [ [ [unops in] 74 | [first C2P] dip ] 75 | [ [[QUOTE] in] 76 | pop first ] 77 | [ [uncons first swap [C2P] dip C2P] 78 | dip ] ] 79 | cond ] 80 | [ ] 81 | ifte; 82 | C2R == 83 | [ list ] 84 | 85 | [ unswons 86 | [ [ [unops in] 87 | swap first C2R ] 88 | [ [[QUOTE] in] 89 | pop first ] 90 | [ swap uncons first swap 91 | [C2R] dip C2R ] ] 92 | cond ] 93 | [ ] 94 | ifte; 95 | (* from Infix to .. *) 96 | I2C == 97 | unswons 98 | [ [ [unops in] 99 | [I2C [] cons] dip swons ] 100 | [ [[QUOTE] in] 101 | [unswons [] cons] dip swons ] 102 | [ [list] (* binary *) 103 | I2C swap uncons swapd 104 | I2C swons cons cons ] 105 | [ ] ] 106 | cond; 107 | I2T == 108 | unswons 109 | [ [ [unops in] 110 | [I2T [] cons] dip swons ] 111 | [ [[QUOTE] in] 112 | [unswons [] cons] dip swons ] 113 | [ [list] (* binary *) 114 | I2T swap uncons swapd 115 | I2T swons cons cons ] 116 | [ [] cons ] ] 117 | cond; 118 | (* from Polish to .. *) 119 | P2C == 120 | unswons 121 | [ [ [unops in] 122 | [P2C [] cons] dip swons ] 123 | [ [binops in] 124 | [P2C [] cons [P2C [] cons] dip swoncat] 125 | dip swons ] 126 | [ [list] 127 | [] cons [QUOTE] swoncat ] 128 | [ ] ] 129 | cond; 130 | P2T == 131 | unswons 132 | [ [ [unops in] 133 | [P2T [] cons] dip swons ] 134 | [ [binops in] 135 | [P2T [] cons [P2T [] cons] dip swoncat] 136 | dip swons ] 137 | [ [list] 138 | [] cons [QUOTE] swoncat ] 139 | [ [] cons ] ] 140 | cond; 141 | P2I == 142 | unswons 143 | [ [ [unops in] 144 | [P2I] dip swons ] 145 | [ [binops in] 146 | [P2I [P2I] dip swap] 147 | dip swons concat [] cons ] 148 | [ [list] 149 | [QUOTE] swoncat ] 150 | [ [] cons ] ] 151 | cond; 152 | (* from Reverse to .. *) 153 | R2C == 154 | [ [ [ [unops in] 155 | [[] cons] dip swons ] 156 | [ [binops in] 157 | [[] cons cons] dip swons ] 158 | [ [list] 159 | [] cons [QUOTE] swoncat ] 160 | [ ] ] 161 | cond ] 162 | step; 163 | R2T == 164 | [ [ [ [unops in] 165 | [[] cons] dip swons ] 166 | [ [binops in] 167 | [[] cons cons] dip swons ] 168 | [ [list] 169 | [] cons [QUOTE] swoncat ] 170 | [ [] cons ] ] 171 | cond ] 172 | step; 173 | R2I == 174 | [ [ [ [unops in] 175 | swons ] 176 | [ [binops in] 177 | swons concat [] cons ] 178 | [ [list] 179 | [] cons [QUOTE] swoncat ] 180 | [ [] cons ] ] 181 | cond ] 182 | step; 183 | 184 | X2Y == (* used by Pol2Rev and Rev2Pol *) 185 | unswons 186 | [ [ [unops in] 187 | [X2Y] dip swap ] 188 | [ [binops in] 189 | [X2Y X2Y] dip swap ] 190 | [ swap ] ] 191 | cond; 192 | 193 | new-infra == cons [] swap infra 194 | 195 | IN 196 | 197 | (* Ideally the following should be inside the HIDE. But since currently 198 | HIDE definitions cannot be mutually recursive, the are placed here. *) 199 | 200 | M12T == 201 | M22T 202 | [ pop [null not] [first bin1ops in] sequand ] 203 | [ swap uncons swapd unswons M12T 204 | rollupd [] cons cons cons ] 205 | [ ] 206 | ifte; 207 | M22T == 208 | M32T 209 | [ pop [null not] [first bin2ops in] sequand ] 210 | [ swap uncons swapd unswons M22T 211 | rollupd [] cons cons cons ] 212 | [ ] 213 | ifte; 214 | M32T == 215 | M42T 216 | [ pop [null not] [first bin3ops in] sequand ] 217 | [ swap uncons swapd unswons M32T 218 | rollupd [] cons cons cons ] 219 | [ ] 220 | ifte; 221 | M42T == 222 | [ [ [ unops in ] 223 | [ unswons M42T [] cons ] dip swons ] 224 | [ [ [QUOTE] in ] 225 | [ unswons [] cons ] dip swons ] 226 | [ [ list] 227 | unswons M12T popd ] 228 | [ [] cons ] ] 229 | cond; 230 | 231 | M12C == 232 | M22C 233 | [ pop [null not] [first bin1ops in] sequand ] 234 | [ swap uncons swapd unswons M12C 235 | rollupd [] cons cons cons ] 236 | [ ] 237 | ifte; 238 | M22C == 239 | M32C 240 | [ pop [null not] [first bin2ops in] sequand ] 241 | [ swap uncons swapd unswons M22C 242 | rollupd [] cons cons cons ] 243 | [ ] 244 | ifte; 245 | M32C == 246 | M42C 247 | [ pop [null not] [first bin3ops in] sequand ] 248 | [ swap uncons swapd unswons M32C 249 | rollupd [] cons cons cons ] 250 | [ ] 251 | ifte; 252 | M42C == 253 | [ [ [ unops in ] 254 | [ unswons M42C [] cons ] dip swons ] 255 | [ [ [QUOTE] in ] 256 | [ unswons [] cons ] dip swons ] 257 | [ [ list] 258 | unswons M12C popd ] 259 | [ ] ] 260 | cond; 261 | 262 | (* In following, the fast reverselist is counted as 0.5 pass *) 263 | 264 | Pol2Rev == [X2Y] new-infra rest reverselist; (* 1.5 passes *) 265 | Pol2Cam == P2C popd; (* 1 pass *) 266 | Pol2Inf == P2I popd; (* 1 pass *) 267 | Pol2Tre == P2T popd; (* 1 pass *) 268 | 269 | Rev2Pol == reverselist [X2Y] new-infra rest; (* 1.5 passes *) 270 | Rev2Cam == R2C; (* 1 pass *) 271 | Rev2Inf == R2I; (* 1 pass *) 272 | Rev2Tre == R2T; (* 1 pass *) 273 | 274 | Cam2Pol == [C2P] new-infra; (* 1 pass *) 275 | Cam2Rev == [C2R] new-infra; (* 1 pass *) 276 | Cam2Tre == C2T; (* 1 pass *) 277 | Cam2Inf == C2I; (* 1 pass *) 278 | 279 | Inf2Pol == Inf2Cam Cam2Pol; (* 2 passes *) 280 | Inf2Rev == Inf2Cam Cam2Rev; (* 2 passes *) 281 | Inf2Cam == I2C popd; (* 1 pass *) 282 | Inf2Tre == I2T popd; (* 1 pass *) 283 | 284 | Min2Pol == Min2Cam Cam2Pol; (* 2 passes *) 285 | Min2Rev == Min2Cam Cam2Rev; (* 2 passes *) 286 | Min2Cam == unswons M12C popd; (* 1 pass *) 287 | Min2Tre == unswons M12T popd; (* 1 pass *) 288 | Min2Inf == Min2Cam Cam2Inf (* 2 passes *) 289 | 290 | END; (* HIDE, end of 21 translators *) 291 | 292 | (* other stuff to go here *) 293 | 294 | SYMLIB == "symlib.joy - symbolic manipulation library\\n". 295 | 296 | (* end LIBRA *) 297 | "symlib is loaded\\n" putchars. 298 | 299 | (* END symlib.joy *) 300 | `.trim() 301 | -------------------------------------------------------------------------------- /src/joy/interpreter/combinator_defs.js: -------------------------------------------------------------------------------- 1 | const T = require('./types') 2 | 3 | function trampoline (fn) { 4 | return function () { 5 | let result = fn.apply(this, arguments) 6 | while (result instanceof Function) { 7 | result = result() 8 | } 9 | return result 10 | } 11 | } 12 | 13 | function dequote (stack, execute, quote) { 14 | quote = quote || stack.pop() 15 | quote.value.forEach((p) => { 16 | stack.push(p) 17 | execute() 18 | }) 19 | } 20 | 21 | module.exports = execute => [ 22 | { 23 | name: 'i', 24 | signature: 'i : [P] -> ...', 25 | help: 'Executes P. So, [P] i == P.', 26 | handlers: [ 27 | [['List'], function (stack) { 28 | dequote(stack, execute) 29 | }] 30 | ] 31 | }, 32 | 33 | { 34 | name: 'x', 35 | signature: 'x : [P]i -> ...', 36 | help: 'Executes P without popping [P]. So, [P] x == [P] P.', 37 | handlers: [ 38 | [['List'], function (stack) { 39 | const P = stack.pop() 40 | stack.push(P) 41 | dequote(stack, execute, P) 42 | }] 43 | ] 44 | }, 45 | 46 | { 47 | name: 'dip', 48 | signature: 'dip : X [P] -> ... X', 49 | help: 'Saves X, executes P, pushes X back.', 50 | handlers: [ 51 | [['*', 'List'], function (stack) { 52 | const top = stack.pop() 53 | const bottom = stack.pop() 54 | stack.push(top) 55 | dequote(stack, execute) 56 | stack.push(bottom) 57 | }] 58 | ] 59 | }, 60 | 61 | { 62 | name: 'app1', 63 | signature: 'app1 : X [P] -> R', 64 | help: 'Executes P, pushes result R on stack without X.', 65 | handlers: [ 66 | [['*', 'List'], function (stack) { 67 | const P = stack.pop() 68 | const X = stack.pop() 69 | stack.push(stack.restoreAfter(() => { 70 | stack.push(X) 71 | dequote(stack, execute, P) 72 | return stack.pop() 73 | })) 74 | }] 75 | ] 76 | }, 77 | 78 | /** 79 | * app11 : X Y [P] -> R 80 | * Executes P, pushes result R on stack. 81 | */ 82 | 83 | /** 84 | * app12 : X Y1 Y2 [P] -> R1 R2 85 | * Executes P twice, with Y1 and Y2, returns R1 and R2. 86 | */ 87 | 88 | { 89 | name: 'construct', 90 | signature: 'construct : [P] [[P1] [P2] ..] -> R1 R2 ..', 91 | help: ` 92 | Saves state of stack and then executes [P]. 93 | Then executes each [Pi] to give Ri pushed onto saved stack. 94 | `.trim(), 95 | handlers: [ 96 | [['List', 'List'], function (stack) { 97 | const Ps = stack.pop() 98 | const P = stack.pop() 99 | const Rs = stack.restoreAfter(() => { 100 | const result = [] 101 | dequote(stack, execute, P) 102 | Ps.forEach((p) => { 103 | dequote(stack, execute, p) 104 | result.push(stack.pop()) 105 | }) 106 | return result 107 | }) 108 | Rs.forEach((r) => { 109 | stack.push(r) 110 | }) 111 | }] 112 | ] 113 | }, 114 | 115 | { 116 | name: 'nullary', 117 | signature: 'nullary : [P] -> R', 118 | help: ` 119 | Executes P, which leaves R on top of the stack. 120 | No matter how many parameters this consumes, none are removed from the stack. 121 | `.trim(), 122 | handlers: [ 123 | [['List'], function (stack) { 124 | const P = stack.pop() 125 | stack.push(stack.restoreAfter(() => { 126 | dequote(stack, execute, P) 127 | return stack.pop() 128 | })) 129 | }] 130 | ] 131 | }, 132 | 133 | { 134 | name: 'unary', 135 | signature: 'unary : X [P] -> R', 136 | help: ` 137 | Executes P, which leaves R on top of the stack. 138 | No matter how many parameters this consumes, 139 | exactly one is removed from the stack. 140 | `.trim(), 141 | handlers: [ 142 | [['*', 'List'], function (stack) { 143 | const P = stack.pop() 144 | const X = stack.pop() 145 | stack.push(stack.restoreAfter(() => { 146 | stack.push(X) 147 | dequote(stack, execute, P) 148 | return stack.pop() 149 | })) 150 | }] 151 | ] 152 | }, 153 | 154 | /** 155 | * unary2 : X1 X2 [P] -> R1 R2 156 | * Executes P twice, with X1 and X2 on top of the stack. 157 | * Returns the two values R1 and R2. 158 | */ 159 | 160 | /** 161 | * unary3 : X1 X2 X3 [P] -> R1 R2 R3 162 | * Executes P three times, with Xi, returns Ri (i = 1..3). 163 | */ 164 | 165 | /** 166 | * unary4 : X1 X2 X3 X4 [P] -> R1 R2 R3 R4 167 | * Executes P four times, with Xi, returns Ri (i = 1..4). 168 | */ 169 | 170 | /** 171 | * app2 : X1 X2 [P] -> R1 R2 172 | * Obsolescent. == unary2 173 | */ 174 | 175 | /** 176 | * app3 : X1 X2 X3 [P] -> R1 R2 R3 177 | * Obsolescent. == unary3 178 | */ 179 | 180 | /** 181 | * app4 : X1 X2 X3 X4 [P] -> R1 R2 R3 R4 182 | * Obsolescent. == unary4 183 | */ 184 | 185 | { 186 | name: 'binary', 187 | signature: 'binary : X Y [P] -> R', 188 | help: ` 189 | Executes P, which leaves R on top of the stack. 190 | No matter how many parameters this consumes, 191 | exactly two are removed from the stack. 192 | `.trim(), 193 | handlers: [ 194 | [['*', '*', 'List'], function (stack) { 195 | const P = stack.pop() 196 | const Y = stack.pop() 197 | const X = stack.pop() 198 | stack.push(stack.restoreAfter(() => { 199 | stack.push(X) 200 | stack.push(Y) 201 | dequote(stack, execute, P) 202 | return stack.pop() 203 | })) 204 | }] 205 | ] 206 | }, 207 | 208 | { 209 | name: 'ternary', 210 | signature: 'ternary : X Y Z [P] -> R', 211 | help: ` 212 | Executes P, which leaves R on top of the stack. 213 | No matter how many parameters this consumes, 214 | exactly three are removed from the stack. 215 | `.trim(), 216 | handlers: [ 217 | [['*', '*', '*', 'List'], function (stack) { 218 | const P = stack.pop() 219 | const Z = stack.pop() 220 | const Y = stack.pop() 221 | const X = stack.pop() 222 | stack.push(stack.restoreAfter(() => { 223 | stack.push(X) 224 | stack.push(Y) 225 | stack.push(Z) 226 | dequote(stack, execute, P) 227 | return stack.pop() 228 | })) 229 | }] 230 | ] 231 | }, 232 | 233 | { 234 | name: 'cleave', 235 | signature: 'cleave : X [P1] [P2] -> R1 R2', 236 | help: 'Executes P1 and P2, each with X on top, producing two results.', 237 | handlers: [ 238 | [['*', 'List', 'List'], function (stack) { 239 | const P2 = stack.pop() 240 | const P1 = stack.pop() 241 | const X = stack.pop() 242 | stack.push(X) 243 | dequote(stack, execute, P1) 244 | stack.push(X) 245 | dequote(stack, execute, P2) 246 | }] 247 | ] 248 | }, 249 | 250 | { 251 | name: 'branch', 252 | signature: 'branch : B [T] [F] -> ...', 253 | help: 'If B is true, then executes T else executes F.', 254 | handlers: [ 255 | [['Bool', 'List', 'List'], function (stack) { 256 | const F = stack.pop() 257 | const T = stack.pop() 258 | const B = stack.pop() 259 | if (B.value) { 260 | dequote(stack, execute, T) 261 | } else { 262 | dequote(stack, execute, F) 263 | } 264 | }] 265 | ] 266 | }, 267 | 268 | { 269 | name: 'ifte', 270 | signature: 'ifte : [B] [T] [F] -> ...', 271 | help: 'Executes B. If that yields true, then executes T else executes F.', 272 | handlers: [ 273 | [['List', 'List', 'List'], function (stack) { 274 | const F = stack.pop() 275 | const T = stack.pop() 276 | const B = stack.pop() 277 | const result = stack.restoreAfter(() => { 278 | dequote(stack, execute, B) 279 | return stack.pop().value 280 | }) 281 | if (result) { 282 | dequote(stack, execute, T) 283 | } else { 284 | dequote(stack, execute, F) 285 | } 286 | }] 287 | ] 288 | }, 289 | 290 | { 291 | name: 'ifinteger', 292 | signature: 'ifinteger : X [T] [E] -> ...', 293 | help: 'If X is an integer, executes T else executes E.', 294 | handlers: [ 295 | [['*', 'List', 'List'], function (stack) { 296 | const E = stack.pop() 297 | const T = stack.pop() 298 | const X = stack.pop() 299 | dequote(stack, execute, X.isInteger ? T : E) 300 | }] 301 | ] 302 | }, 303 | 304 | { 305 | name: 'ifchar', 306 | signature: 'ifchar : X [T] [E] -> ...', 307 | help: 'If X is a character, executes T else executes E.', 308 | handlers: [ 309 | [['*', 'List', 'List'], function (stack) { 310 | const E = stack.pop() 311 | const T = stack.pop() 312 | const X = stack.pop() 313 | dequote(stack, execute, X.isCharacter ? T : E) 314 | }] 315 | ] 316 | }, 317 | 318 | { 319 | name: 'iflogical', 320 | signature: 'iflogical : X [T] [E] -> ...', 321 | help: 'If X is a logical or truth value, executes T else executes E.', 322 | handlers: [ 323 | [['*', 'List', 'List'], function (stack) { 324 | const E = stack.pop() 325 | const T = stack.pop() 326 | const X = stack.pop() 327 | dequote(stack, execute, X.isBool ? T : E) 328 | }] 329 | ] 330 | }, 331 | 332 | { 333 | name: 'ifset', 334 | signature: 'ifset : X [T] [E] -> ...', 335 | help: 'If X is a set, executes T else executes E.', 336 | handlers: [ 337 | [['*', 'List', 'List'], function (stack) { 338 | const E = stack.pop() 339 | const T = stack.pop() 340 | const X = stack.pop() 341 | dequote(stack, execute, X.isSet ? T : E) 342 | }] 343 | ] 344 | }, 345 | 346 | { 347 | name: 'ifstring', 348 | signature: 'ifstring : X [T] [E] -> ...', 349 | help: 'If X is a string, executes T else executes E.', 350 | handlers: [ 351 | [['*', 'List', 'List'], function (stack) { 352 | const E = stack.pop() 353 | const T = stack.pop() 354 | const X = stack.pop() 355 | dequote(stack, execute, X.isString ? T : E) 356 | }] 357 | ] 358 | }, 359 | 360 | { 361 | name: 'iflist', 362 | signature: 'iflist : X [T] [E] -> ...', 363 | help: 'If X is a list, executes T else executes E.', 364 | handlers: [ 365 | [['*', 'List', 'List'], function (stack) { 366 | const E = stack.pop() 367 | const T = stack.pop() 368 | const X = stack.pop() 369 | dequote(stack, execute, X.isList ? T : E) 370 | }] 371 | ] 372 | }, 373 | 374 | { 375 | name: 'iffloat', 376 | signature: 'iffloat : X [T] [E] -> ...', 377 | help: 'If X is a float, executes T else executes E.', 378 | handlers: [ 379 | [['*', 'List', 'List'], function (stack) { 380 | const E = stack.pop() 381 | const T = stack.pop() 382 | const X = stack.pop() 383 | dequote(stack, execute, X.isFloat ? T : E) 384 | }] 385 | ] 386 | }, 387 | 388 | /** 389 | * iffile : X [T] [E] -> ... 390 | * If X is a file, executes T else executes E. 391 | * TODO 392 | */ 393 | 394 | { 395 | name: 'cond', 396 | signature: 'cond : [..[[Bi] Ti]..[D]] -> ...', 397 | help: ` 398 | Tries each Bi. If that yields true, then executes Ti and exits. 399 | If no Bi yields true, executes default D. 400 | `.trim(), 401 | handlers: [ 402 | [['List'], function (stack) { 403 | const top = stack.pop() 404 | for (let i = 0, len = top.value.length - 1; i < len; i += 1) { 405 | const result = stack.restoreAfter(() => { 406 | stack.push(top.value[i].value[0]) 407 | dequote(stack, execute) 408 | return stack.pop().value 409 | }) 410 | if (result) { 411 | stack.push(top.value[i].value[1]) 412 | execute() 413 | return 414 | } 415 | } 416 | stack.push(top.value[top.value.length - 1]) 417 | dequote(stack, execute) 418 | }] 419 | ] 420 | }, 421 | 422 | { 423 | name: 'while', 424 | signature: 'while : [B] [D] -> ...', 425 | help: 'While executing B yields true executes D.', 426 | handlers: [ 427 | [['List', 'List'], function (stack) { 428 | const D = stack.pop() 429 | const B = stack.pop() 430 | while (true) { 431 | const result = stack.restoreAfter(() => { 432 | dequote(stack, execute, B) 433 | return stack.pop().value 434 | }) 435 | if (result) { 436 | dequote(stack, execute, D) 437 | } else { 438 | break 439 | } 440 | } 441 | }] 442 | ] 443 | }, 444 | 445 | { 446 | name: 'linrec', 447 | signature: 'linrec : [P] [T] [R1] [R2] -> ...', 448 | help: ` 449 | Executes P. If that yields true, executes T. 450 | Else executes R1, recurses, executes R2. 451 | `.trim(), 452 | handlers: [ 453 | [['List', 'List', 'List', 'List'], function (stack) { 454 | const R2 = stack.pop() 455 | const R1 = stack.pop() 456 | const T = stack.pop() 457 | const P = stack.pop() 458 | let n = 0 459 | 460 | // NOTE: Not sure if this approach is exactly right. It works for 461 | // calculating factorial. Not sure if it works generically. Approach 462 | // effectively does an unfold, followed by a fold. I suspect there is 463 | // a way to do this that uses constant space. 464 | while (true) { 465 | const result = stack.restoreAfter(() => { 466 | dequote(stack, execute, P) 467 | return stack.pop().value 468 | }) 469 | if (result) { 470 | dequote(stack, execute, T) 471 | break 472 | } else { 473 | dequote(stack, execute, R1) 474 | n += 1 475 | } 476 | } 477 | while (n > 0) { 478 | dequote(stack, execute, R2) 479 | n -= 1 480 | } 481 | }] 482 | ] 483 | }, 484 | 485 | { 486 | name: 'tailrec', 487 | signature: 'tailrec : [P] [T] [R1] -> ...', 488 | help: ` 489 | Executes P. If that yields true, executes T. 490 | Else executes R1, recurses. 491 | `.trim(), 492 | handlers: [ 493 | [['List', 'List', 'List'], function (stack) { 494 | const R1 = stack.pop() 495 | const T = stack.pop() 496 | const P = stack.pop() 497 | 498 | while (true) { 499 | const result = stack.restoreAfter(() => { 500 | dequote(stack, execute, P) 501 | return stack.pop().value 502 | }) 503 | if (result) { 504 | dequote(stack, execute, T) 505 | break 506 | } else { 507 | dequote(stack, execute, R1) 508 | } 509 | } 510 | }] 511 | ] 512 | }, 513 | 514 | /** 515 | * binrec : [B] [T] [R1] [R2] -> ... 516 | * Executes P. If that yields true, executes T. 517 | * Else uses R1 to produce two intermediates, recurses on both, 518 | * then executes R2 to combines their results. 519 | */ 520 | 521 | /** 522 | * genrec : [B] [T] [R1] [R2] -> ... 523 | * Executes B, if that yields true executes T. 524 | * Else executes R1 and then [[B] [T] [R1] [R2] genrec] R2. 525 | */ 526 | 527 | /** 528 | * condlinrec : [ [C1] [C2] .. [D] ] -> ... 529 | * Each [Ci] is of the forms [[B] [T]] or [[B] [R1] [R2]]. 530 | * Tries each B. If that yields true and there is just a [T], executes T and exit. 531 | * If there are [R1] and [R2], executes R1, recurses, executes R2. 532 | * Subsequent case are ignored. If no B yields true, then [D] is used. 533 | * It is then of the forms [[T]] or [[R1] [R2]]. For the former, executes T. 534 | * For the latter executes R1, recurses, executes R2. 535 | */ 536 | 537 | { 538 | name: 'step', 539 | signature: 'step : A [P] -> ...', 540 | help: ` 541 | Sequentially putting members of aggregate A onto stack, 542 | executes P for each member of A. 543 | `.trim(), 544 | handlers: [ 545 | [['List', 'List'], function (stack) { 546 | const P = stack.pop() 547 | const A = stack.pop() 548 | A.value.forEach((a) => { 549 | stack.push(a) 550 | stack.push(P) 551 | dequote(stack, execute) 552 | }) 553 | }], 554 | [['String', 'List'], function (stack) { 555 | const P = stack.pop() 556 | const A = stack.pop() 557 | A.value.split('').forEach((char) => { 558 | stack.push(new T.JoyChar(char)) 559 | stack.push(P) 560 | dequote(stack, execute) 561 | }) 562 | }], 563 | [['Set', 'List'], function (stack) { 564 | const P = stack.pop() 565 | const A = stack.pop() 566 | A.forEachOrdered((a) => { 567 | stack.push(a) 568 | stack.push(P) 569 | dequote(stack, execute) 570 | }) 571 | }] 572 | ] 573 | }, 574 | 575 | { 576 | name: 'fold', 577 | signature: 'fold : A V0 [P] -> V', 578 | help: ` 579 | Starting with value V0, sequentially pushes members of aggregate A 580 | and combines with binary operator P to produce value V. 581 | `.trim(), 582 | handlers: [ 583 | [['List', '*', 'List'], function (stack) { 584 | const P = stack.pop() 585 | const seed = stack.pop() 586 | const A = stack.pop() 587 | stack.push(seed) 588 | A.value.forEach((val) => { 589 | stack.push(val) 590 | dequote(stack, execute, P) 591 | }) 592 | }], 593 | [['String', 'Char', 'List'], function (stack) { 594 | const P = stack.pop() 595 | const seed = stack.pop() 596 | const A = stack.pop() 597 | stack.push(seed) 598 | A.value.split('').forEach((char) => { 599 | stack.push(new T.JoyChar(char)) 600 | dequote(stack, execute, P) 601 | }) 602 | }], 603 | [['Set', 'Integer', 'List'], function (stack) { 604 | const P = stack.pop() 605 | const seed = stack.pop() 606 | const A = stack.pop() 607 | stack.push(seed) 608 | A.forEach((val) => { 609 | stack.push(val) 610 | dequote(stack, execute, P) 611 | }) 612 | }] 613 | ] 614 | }, 615 | 616 | { 617 | name: 'map', 618 | signature: 'map : A [P] -> B', 619 | help: ` 620 | Executes P on each member of aggregate A, 621 | collects results in sametype aggregate B. 622 | `.trim(), 623 | handlers: [ 624 | [['List', 'List'], function (stack) { 625 | const top = stack.pop() 626 | const bottom = stack.pop() 627 | const result = [] 628 | bottom.value.forEach((item) => { 629 | stack.push(item) 630 | dequote(stack, execute, top) 631 | result.push(stack.pop()) 632 | }) 633 | stack.push(new T.JoyList(result)) 634 | }], 635 | [['String', 'List'], function (stack) { 636 | const top = stack.pop() 637 | const bottom = stack.pop() 638 | let result = '' 639 | bottom.value.split('').forEach((char) => { 640 | stack.push(new T.JoyChar(char)) 641 | dequote(stack, execute, top) 642 | result += stack.pop().value 643 | }) 644 | stack.push(new T.JoyString(result)) 645 | }], 646 | [['Set', 'List'], function (stack) { 647 | const top = stack.pop() 648 | const bottom = stack.pop() 649 | const result = new T.JoySet([]) 650 | bottom.forEach((item) => { 651 | stack.push(item) 652 | dequote(stack, execute, top) 653 | result.add(stack.pop()) 654 | }) 655 | stack.push(result) 656 | }] 657 | ] 658 | }, 659 | 660 | { 661 | name: 'times', 662 | signature: 'times : N [P] -> ...', 663 | help: 'N times executes P.', 664 | handlers: [ 665 | [['Integer', 'List'], function (stack) { 666 | const P = stack.pop() 667 | const N = stack.pop() 668 | for (let i = 0; i < N.value; i += 1) { 669 | dequote(stack, execute, P) 670 | } 671 | }] 672 | ] 673 | }, 674 | 675 | /** 676 | * infra : L1 [P] -> L2 677 | * Using list L1 as stack, executes P and returns a new list L2. 678 | * The first element of L1 is used as the top of stack, 679 | * and after execution of P the top of stack becomes the first element of L2. 680 | */ 681 | 682 | { 683 | name: 'primrec', 684 | signature: 'primrec : X [I] [C] -> R', 685 | help: ` 686 | Executes I to obtain an initial value R0. 687 | For integer X uses increasing positive integers to X, combines by C for new R. 688 | For aggregate X uses successive members and combines by C for new R. 689 | `.trim(), 690 | handlers: [ 691 | [['Integer', 'List', 'List'], function (stack) { 692 | const C = stack.pop() 693 | const I = stack.pop() 694 | 695 | const primrec = trampoline(function _primrec (n) { 696 | if (n === 0) { 697 | return I.value[0] 698 | } 699 | return function () { 700 | stack.push(new T.JoyInt(n)) 701 | const res = _primrec(n - 1) 702 | stack.push(res instanceof Function ? res() : res) 703 | C.value.forEach((p) => { 704 | stack.push(p) 705 | execute() 706 | }) 707 | return stack.pop() 708 | } 709 | }) 710 | 711 | stack.push(primrec(stack.pop().value)) 712 | }] 713 | // TODO: String, Set 714 | ] 715 | }, 716 | 717 | { 718 | name: 'filter', 719 | signature: 'filter : A [B] -> A1', 720 | help: 'Uses test B to filter aggregate A producing sametype aggregate A1.', 721 | handlers: [ 722 | [['List', 'List'], function (stack) { 723 | const B = stack.pop() 724 | const A = stack.pop() 725 | const result = [] 726 | A.value.forEach((a) => { 727 | stack.push(a) 728 | stack.push(B) 729 | dequote(stack, execute) 730 | if (stack.pop().value) { 731 | result.push(a) 732 | } 733 | }) 734 | stack.push(new T.JoyList(result)) 735 | }], 736 | [['String', 'List'], function (stack) { 737 | const B = stack.pop() 738 | const A = stack.pop() 739 | let result = '' 740 | A.value.split('').forEach((char) => { 741 | stack.push(new T.JoyChar(char)) 742 | stack.push(B) 743 | dequote(stack, execute) 744 | if (stack.pop().value) { 745 | result += char 746 | } 747 | }) 748 | stack.push(new T.JoyString(result)) 749 | }], 750 | [['Set', 'List'], function (stack) { 751 | const B = stack.pop() 752 | const A = stack.pop() 753 | const result = new T.JoySet([]) 754 | A.forEach((a) => { 755 | stack.push(a) 756 | stack.push(B) 757 | dequote(stack, execute) 758 | if (stack.pop().value) { 759 | result.add(a) 760 | } 761 | }) 762 | stack.push(result) 763 | }] 764 | ] 765 | }, 766 | 767 | { 768 | name: 'split', 769 | signature: 'split : A [B] -> A1 A2', 770 | help: 'Uses test B to split aggregate A into sametype aggregates A1 and A2 .', 771 | handlers: [ 772 | [['List', 'List'], function (stack) { 773 | const B = stack.pop() 774 | const A = stack.pop() 775 | const A1 = [] 776 | const A2 = [] 777 | A.value.forEach((a) => { 778 | stack.push(a) 779 | stack.push(B) 780 | dequote(stack, execute) 781 | if (stack.pop().value) { 782 | A1.push(a) 783 | } else { 784 | A2.push(a) 785 | } 786 | }) 787 | stack.push(new T.JoyList(A1)) 788 | stack.push(new T.JoyList(A2)) 789 | }], 790 | [['String', 'List'], function (stack) { 791 | const B = stack.pop() 792 | const A = stack.pop() 793 | let A1 = '' 794 | let A2 = '' 795 | A.value.split('').forEach((char) => { 796 | stack.push(new T.JoyChar(char)) 797 | stack.push(B) 798 | dequote(stack, execute) 799 | if (stack.pop().value) { 800 | A1 += char 801 | } else { 802 | A2 += char 803 | } 804 | }) 805 | stack.push(new T.JoyString(A1)) 806 | stack.push(new T.JoyString(A2)) 807 | }], 808 | [['Set', 'List'], function (stack) { 809 | const B = stack.pop() 810 | const A = stack.pop() 811 | const A1 = new T.JoySet([]) 812 | const A2 = new T.JoySet([]) 813 | A.forEach((a) => { 814 | stack.push(a) 815 | stack.push(B) 816 | dequote(stack, execute) 817 | if (stack.pop().value) { 818 | A1.add(a) 819 | } else { 820 | A2.add(a) 821 | } 822 | }) 823 | stack.push(A1) 824 | stack.push(A2) 825 | }] 826 | ] 827 | }, 828 | 829 | { 830 | name: 'some', 831 | signature: 'some : A [B] -> X', 832 | help: 'Applies test B to members of aggregate A, X = true if some pass.', 833 | handlers: [ 834 | [['List', 'List'], function (stack) { 835 | const B = stack.pop() 836 | const A = stack.pop() 837 | for (let i = 0, len = A.value.length; i < len; i += 1) { 838 | const result = stack.restoreAfter(() => { 839 | stack.push(A.value[i]) 840 | dequote(stack, execute, B) 841 | return stack.pop().value 842 | }) 843 | if (result) { 844 | stack.push(new T.JoyBool(true)) 845 | return 846 | } 847 | } 848 | stack.push(new T.JoyBool(false)) 849 | }], 850 | [['String', 'List'], function (stack) { 851 | const B = stack.pop() 852 | const A = stack.pop() 853 | for (let i = 0, len = A.value.length; i < len; i += 1) { 854 | const result = stack.restoreAfter(() => { 855 | stack.push(new T.JoyChar(A.value.charAt([i]))) 856 | dequote(stack, execute, B) 857 | return stack.pop().value 858 | }) 859 | if (result) { 860 | stack.push(new T.JoyBool(true)) 861 | return 862 | } 863 | } 864 | stack.push(new T.JoyBool(false)) 865 | }], 866 | [['Set', 'List'], function (stack) { 867 | const B = stack.pop() 868 | const A = stack.pop() 869 | let result = false 870 | A.forEach((a) => { 871 | result = result || stack.restoreAfter(() => { 872 | stack.push(a) 873 | dequote(stack, execute, B) 874 | return stack.pop().value 875 | }) 876 | }) 877 | stack.push(new T.JoyBool(result)) 878 | }] 879 | ] 880 | }, 881 | 882 | { 883 | name: 'all', 884 | signature: 'all : A [B] -> X', 885 | help: 'Applies test B to members of aggregate A, X = true if all pass.', 886 | handlers: [ 887 | [['List', 'List'], function (stack) { 888 | const B = stack.pop() 889 | const A = stack.pop() 890 | for (let i = 0, len = A.value.length; i < len; i += 1) { 891 | const result = stack.restoreAfter(() => { 892 | stack.push(A.value[i]) 893 | dequote(stack, execute, B) 894 | return stack.pop().value 895 | }) 896 | if (!result) { 897 | stack.push(new T.JoyBool(false)) 898 | return 899 | } 900 | } 901 | stack.push(new T.JoyBool(true)) 902 | }], 903 | [['String', 'List'], function (stack) { 904 | const B = stack.pop() 905 | const A = stack.pop() 906 | for (let i = 0, len = A.value.length; i < len; i += 1) { 907 | const result = stack.restoreAfter(() => { 908 | stack.push(new T.JoyChar(A.value.charAt([i]))) 909 | dequote(stack, execute, B) 910 | return stack.pop().value 911 | }) 912 | if (!result) { 913 | stack.push(new T.JoyBool(false)) 914 | return 915 | } 916 | } 917 | stack.push(new T.JoyBool(true)) 918 | }], 919 | [['Set', 'List'], function (stack) { 920 | const B = stack.pop() 921 | const A = stack.pop() 922 | let result = true 923 | A.forEach((a) => { 924 | result = result && stack.restoreAfter(() => { 925 | stack.push(a) 926 | dequote(stack, execute, B) 927 | return stack.pop().value 928 | }) 929 | }) 930 | stack.push(new T.JoyBool(result)) 931 | }] 932 | ] 933 | } 934 | 935 | /** 936 | * treestep : T [P] -> ... 937 | * Recursively traverses leaves of tree T, executes P for each leaf. 938 | */ 939 | 940 | /** 941 | * treerec : T [O] [C] -> ... 942 | * T is a tree. If T is a leaf, executes O. Else executes [[O] [C] treerec] C. 943 | */ 944 | 945 | /** 946 | * treegenrec : T [O1] [O2] [C] -> ... 947 | * T is a tree. If T is a leaf, executes O1. 948 | * Else executes O2 and then [[O1] [O2] [C] treegenrec] C. 949 | */ 950 | ] 951 | -------------------------------------------------------------------------------- /src/joy/interpreter/operator_defs.js: -------------------------------------------------------------------------------- 1 | const { applyToTop, applyToTop2, applyToTop3, applyToTop4, cmp } = require('./util') 2 | const T = require('./types') 3 | 4 | const map = f => x => x.map(f) 5 | const liftA2 = f => (x, y) => x.map(f).ap(y) 6 | 7 | const lpad = (s, w, c) => { 8 | let result = s 9 | while (result.length < w) { 10 | result = `${c || ' '}${result}` 11 | } 12 | return result 13 | } 14 | 15 | module.exports = (opts) => [ 16 | { 17 | name: 'id', 18 | signature: 'id : ->', 19 | help: 'Any program of the form P id Q is equivalent to just P Q.', 20 | handlers: [[[], Function.prototype]] 21 | }, 22 | 23 | { 24 | name: 'dup', 25 | signature: 'dup : X -> X X', 26 | help: 'Pushes an extra copy of X onto stack.', 27 | handlers: [ 28 | [['*'], function (stack) { 29 | const top = stack.pop() 30 | stack.push(top) 31 | stack.push(top) 32 | }] 33 | ] 34 | }, 35 | 36 | { 37 | name: 'swap', 38 | signature: 'swap : X Y -> Y X', 39 | help: 'Interchanges X and Y on top of the stack.', 40 | handlers: [ 41 | [['*', '*'], function (stack) { 42 | const top = stack.pop() 43 | const bottom = stack.pop() 44 | stack.push(top) 45 | stack.push(bottom) 46 | }] 47 | ] 48 | }, 49 | 50 | { 51 | name: 'rollup', 52 | signature: 'rollup : X Y Z -> Z X Y', 53 | help: 'Moves X and Y up, moves Z down', 54 | handlers: [ 55 | [['*', '*', '*'], function (stack) { 56 | const top = stack.pop() 57 | const middle = stack.pop() 58 | const bottom = stack.pop() 59 | stack.push(top) 60 | stack.push(bottom) 61 | stack.push(middle) 62 | }] 63 | ] 64 | }, 65 | 66 | { 67 | name: 'rolldown', 68 | signature: 'rolldown : X Y Z -> Y Z X', 69 | help: 'Moves Y and Z down, moves X up', 70 | handlers: [ 71 | [['*', '*', '*'], function (stack) { 72 | const top = stack.pop() 73 | const middle = stack.pop() 74 | const bottom = stack.pop() 75 | stack.push(middle) 76 | stack.push(top) 77 | stack.push(bottom) 78 | }] 79 | ] 80 | }, 81 | 82 | { 83 | name: 'rotate', 84 | signature: 'rotate : X Y Z -> Z Y X', 85 | help: 'Interchanges X and Z', 86 | handlers: [ 87 | [['*', '*', '*'], function (stack) { 88 | const top = stack.pop() 89 | const middle = stack.pop() 90 | const bottom = stack.pop() 91 | stack.push(top) 92 | stack.push(middle) 93 | stack.push(bottom) 94 | }] 95 | ] 96 | }, 97 | 98 | { 99 | name: 'popd', 100 | signature: 'popd : Y Z -> Z', 101 | help: 'As if defined by: popd == [pop] dip', 102 | handlers: [ 103 | [['*', '*'], function (stack) { 104 | const top = stack.pop() 105 | stack.pop() 106 | stack.push(top) 107 | }] 108 | ] 109 | }, 110 | 111 | { 112 | name: 'dupd', 113 | signature: 'dupd : Y Z -> Y Y Z', 114 | help: 'As if defined by: dupd == [dup] dip', 115 | handlers: [ 116 | [['*', '*'], function (stack) { 117 | const top = stack.pop() 118 | const bottom = stack.pop() 119 | stack.push(bottom) 120 | stack.push(bottom) 121 | stack.push(top) 122 | }] 123 | ] 124 | }, 125 | 126 | { 127 | name: 'swapd', 128 | signature: 'swapd : X Y Z -> Y X Z', 129 | help: 'As if defined by: swapd == [swap] dip', 130 | handlers: [ 131 | [['*', '*', '*'], function (stack) { 132 | const top = stack.pop() 133 | const middle = stack.pop() 134 | const bottom = stack.pop() 135 | stack.push(middle) 136 | stack.push(bottom) 137 | stack.push(top) 138 | }] 139 | ] 140 | }, 141 | 142 | { 143 | name: 'rollupd', 144 | signature: 'rollupd : X Y Z W -> Z X Y W', 145 | help: 'As if defined by: rollupd == [rollup] dip', 146 | handlers: [ 147 | [['*', '*', '*', '*'], function (stack) { 148 | const top = stack.pop() 149 | const middleTop = stack.pop() 150 | const middleBot = stack.pop() 151 | const bottom = stack.pop() 152 | stack.push(middleTop) 153 | stack.push(bottom) 154 | stack.push(middleBot) 155 | stack.push(top) 156 | }] 157 | ] 158 | }, 159 | 160 | { 161 | name: 'rolldownd', 162 | signature: 'rolldownd : X Y Z W -> Y Z X W', 163 | help: 'As if defined by: rolldownd == [rolldown] dip', 164 | handlers: [ 165 | [['*', '*', '*', '*'], function (stack) { 166 | const top = stack.pop() 167 | const middleTop = stack.pop() 168 | const middleBot = stack.pop() 169 | const bottom = stack.pop() 170 | stack.push(middleBot) 171 | stack.push(middleTop) 172 | stack.push(bottom) 173 | stack.push(top) 174 | }] 175 | ] 176 | }, 177 | 178 | { 179 | name: 'rotated', 180 | signature: 'rotated : X Y Z W -> Z Y X W', 181 | help: 'As if defined by: rotated == [rotate] dip', 182 | handlers: [ 183 | [['*', '*', '*', '*'], function (stack) { 184 | const top = stack.pop() 185 | const middleTop = stack.pop() 186 | const middleBot = stack.pop() 187 | const bottom = stack.pop() 188 | stack.push(middleTop) 189 | stack.push(middleBot) 190 | stack.push(bottom) 191 | stack.push(top) 192 | }] 193 | ] 194 | }, 195 | 196 | { 197 | name: 'pop', 198 | signature: 'pop : X ->', 199 | help: 'Removes X from top of the stack.', 200 | handlers: [ 201 | [['*'], function (stack) { 202 | stack.pop() 203 | }] 204 | ] 205 | }, 206 | 207 | { 208 | name: 'choice', 209 | signature: 'choice : B T F -> X', 210 | help: 'If B is true, then X = T else X = F.', 211 | handlers: [ 212 | [['Bool', '*', '*'], function (stack) { 213 | const top = stack.pop() 214 | const middle = stack.pop() 215 | const bottom = stack.pop() 216 | stack.push(bottom === true ? middle : top) 217 | }] 218 | ] 219 | }, 220 | 221 | { 222 | name: 'or', 223 | signature: 'or : X Y -> Z', 224 | help: 'Z is the union of sets X and Y, logical disjunction for truth values.', 225 | handlers: [ 226 | [['Bool', 'Bool'], applyToTop2(liftA2(x => y => x || y))], 227 | [['Set', 'Set'], applyToTop2((x, y) => x.union(y))] 228 | ] 229 | }, 230 | 231 | { 232 | name: 'xor', 233 | signature: 'xor : X Y -> Z', 234 | help: ` 235 | Z is the symmetric difference of sets X and Y, 236 | logical exclusive disjunction for truth values. 237 | `.trim(), 238 | handlers: [ 239 | [['Bool', 'Bool'], applyToTop2(liftA2(x => y => (x || y) && !(x && y)))], 240 | [['Set', 'Set'], applyToTop2((x, y) => x.symmetricDifference(y))] 241 | ] 242 | }, 243 | 244 | { 245 | name: 'and', 246 | signature: 'and : X Y -> Z', 247 | help: 'Z is the intersection of sets X and Y, logical conjunction for truth values.', 248 | handlers: [ 249 | [['Bool', 'Bool'], applyToTop2(liftA2(x => y => x && y))], 250 | [['Set', 'Set'], applyToTop2((x, y) => x.intersect(y))] 251 | ] 252 | }, 253 | 254 | { 255 | name: 'not', 256 | signature: 'not : X -> Y', 257 | help: 'Y is the complement of set X, logical negation for truth values.', 258 | handlers: [ 259 | [['Bool'], applyToTop(map(x => !x))], 260 | [['Set'], applyToTop(x => x.complement())] 261 | ] 262 | }, 263 | 264 | { 265 | name: '+', 266 | signature: '+ : M I -> N', 267 | help: ` 268 | Numeric N is the result of adding integer I to numeric M. 269 | Also supports float. 270 | `.trim(), 271 | handlers: [ 272 | [['Numeric', 'Numeric'], applyToTop2((x, y) => { 273 | if (x.isCharacter || y.isCharacter) { 274 | return T.JoyChar.from(x.toNumber() + y.toNumber()) 275 | } 276 | return new T.JoyInt(x.value + y.value) 277 | })], 278 | [['Float', 'Float'], applyToTop2(liftA2(x => y => x + y))] 279 | ] 280 | }, 281 | 282 | { 283 | name: '-', 284 | signature: '- : M I -> N', 285 | help: ` 286 | Numeric N is the result of subtracting integer I from numeric M. 287 | Also supports float. 288 | `.trim(), 289 | handlers: [ 290 | [['Numeric', 'Numeric'], applyToTop2((x, y) => { 291 | if (x.isCharacter || y.isCharacter) { 292 | return T.JoyChar.from(x.toNumber() - y.toNumber()) 293 | } 294 | return new T.JoyInt(x.value - y.value) 295 | })], 296 | [['Float', 'Float'], applyToTop2(liftA2(x => y => x - y))] 297 | ] 298 | }, 299 | 300 | { 301 | name: '*', 302 | signature: '* : I J -> K', 303 | help: 'Integer K is the product of integers I and J. Also supports float.', 304 | handlers: [ 305 | [['Integer', 'Integer'], applyToTop2(liftA2(x => y => x * y))], 306 | [['Float', 'Float'], applyToTop2(liftA2(x => y => x * y))] 307 | ] 308 | }, 309 | 310 | { 311 | name: '/', 312 | signature: '/ : I J -> K', 313 | help: 'Integer K is the (rounded) ratio of integers I and J. Also supports float.', 314 | handlers: [ 315 | [['Integer', 'Integer'], applyToTop2(liftA2(x => y => x / y))], 316 | [['Float', 'Float'], applyToTop2(liftA2(x => y => x / y))] 317 | ] 318 | }, 319 | 320 | { 321 | name: 'rem', 322 | signature: 'rem : I J -> K', 323 | help: 'Integer K is the remainder of dividing I by J. Also supports float.', 324 | handlers: [ 325 | [['Integer', 'Integer'], applyToTop2(liftA2(x => y => x % y))], 326 | [['Float', 'Float'], applyToTop2(liftA2(x => y => x % y))] // Should this return Int? 327 | ] 328 | }, 329 | 330 | { 331 | name: 'div', 332 | signature: 'div : I J -> K L', 333 | help: 'Integers K and L are the quotient and remainder of dividing I by J.', 334 | handlers: [ 335 | [['Integer', 'Integer'], function (stack) { 336 | const top = stack.pop() 337 | const bottom = stack.pop() 338 | stack.push(top.map(() => Math.floor(bottom / top))) 339 | stack.push(top.map(() => Math.floor(bottom % top))) 340 | }] 341 | ] 342 | }, 343 | 344 | { 345 | name: 'sign', 346 | signature: 'sign : N1 -> N2', 347 | help: ` 348 | Integer N2 is the sign (-1 or 0 or +1) of integer N1, 349 | or float N2 is the sign (-1.0 or 0.0 or 1.0) of float N1. 350 | `.trim(), 351 | handlers: [ 352 | [['Integer'], applyToTop(map(x => { 353 | if (x === 0) { return 0 } 354 | return x < 0 ? -1 : 1 355 | }))], 356 | [['Float'], applyToTop(map(x => { 357 | if (x === 0) { return 0 } 358 | return x < 0 ? -1 : 1 359 | }))] 360 | ] 361 | }, 362 | 363 | { 364 | name: 'neg', 365 | signature: 'neg : I -> J', 366 | help: 'Integer J is the negative of integer I. Also supports float.', 367 | handlers: [ 368 | [['Integer'], applyToTop(map(x => -x))], 369 | [['Float'], applyToTop(map(x => -x))] 370 | ] 371 | }, 372 | 373 | { 374 | name: 'abs', 375 | signature: 'abs : N1 -> N2', 376 | help: ` 377 | Integer N2 is the absolute value (0,1,2..) of integer N1, 378 | or float N2 is the absolute value (0.0 ..) of float N1 379 | `.trim(), 380 | handlers: [ 381 | [['Integer'], applyToTop(map(Math.abs))], 382 | [['Float'], applyToTop(map(Math.abs))] 383 | ] 384 | }, 385 | 386 | { 387 | name: 'acos', 388 | signature: 'acos : F -> G', 389 | help: 'G is the arc cosine of F.', 390 | handlers: [ 391 | [['Float'], applyToTop(map(Math.acos))] 392 | ] 393 | }, 394 | 395 | { 396 | name: 'asin', 397 | signature: 'asin : F -> G', 398 | help: 'G is the arc sine of F.', 399 | handlers: [ 400 | [['Float'], applyToTop(map(Math.asin))] 401 | ] 402 | }, 403 | 404 | { 405 | name: 'atan', 406 | signature: 'atan : F -> G', 407 | help: 'G is the arc tangent of F.', 408 | handlers: [ 409 | [['Float'], applyToTop(map(Math.atan))] 410 | ] 411 | }, 412 | 413 | { 414 | name: 'atan2', 415 | signature: 'atan2 : F G -> H', 416 | help: 'H is the arc tangent of F / G.', 417 | handlers: [ 418 | [['Float', 'Float'], applyToTop2(liftA2(x => y => Math.atan(x / y)))] 419 | ] 420 | }, 421 | 422 | { 423 | name: 'ceil', 424 | signature: 'ceil : F -> G', 425 | help: 'G is the float ceiling of F.', 426 | handlers: [ 427 | [['Float'], applyToTop(map(Math.ceil))] 428 | ] 429 | }, 430 | 431 | { 432 | name: 'cos', 433 | signature: 'cos : F -> G', 434 | help: 'G is the cosine of F.', 435 | handlers: [ 436 | [['Float'], applyToTop(map(Math.cos))] 437 | ] 438 | }, 439 | 440 | { 441 | name: 'cosh', 442 | signature: 'cosh : F -> G', 443 | help: 'G is the hyperbolic cosine of F.', 444 | handlers: [ 445 | [['Float'], applyToTop(map(Math.cosh))] 446 | ] 447 | }, 448 | 449 | { 450 | name: 'exp', 451 | signature: 'exp : F -> G', 452 | help: 'G is e (2.718281828...) raised to the Fth power.', 453 | handlers: [ 454 | [['Integer'], applyToTop(x => T.JoyFloat.from(x).map(Math.exp))] 455 | ] 456 | }, 457 | 458 | { 459 | name: 'floor', 460 | signature: 'floor : F -> G', 461 | help: 'G is the floor of F.', 462 | handlers: [ 463 | [['Float'], applyToTop(map(Math.floor))] 464 | ] 465 | }, 466 | 467 | /** 468 | * frexp : F -> G I 469 | * G is the mantissa and I is the exponent of F. 470 | * Unless F = 0, 0.5 <= abs(G) < 1.0. 471 | * TODO (not easy in JS) 472 | */ 473 | 474 | { 475 | name: 'ldexp', 476 | signature: 'ldexp : F I -> G', 477 | help: 'G is F times 2 to the Ith power.', 478 | handlers: [ 479 | [['Float', 'Integer'], applyToTop2((F, I) => 480 | new T.JoyFloat(x => y => x * y) 481 | .ap(F) 482 | .ap(T.JoyFloat.from(I.map(x => Math.pow(2, x)))))] 483 | ] 484 | }, 485 | 486 | { 487 | name: 'log', 488 | signature: 'log : F -> G', 489 | help: 'G is the natural logarithm of F.', 490 | handlers: [ 491 | [['Float'], applyToTop(map(Math.log))] 492 | ] 493 | }, 494 | 495 | { 496 | name: 'log10', 497 | signature: 'log10 : F -> G', 498 | help: 'G is the common logarithm of F.', 499 | handlers: [ 500 | [['Float'], applyToTop(map(Math.log10))] 501 | ] 502 | }, 503 | 504 | { 505 | name: 'modf', 506 | signature: 'modf : F -> G H', 507 | help: ` 508 | G is the fractional part and H is the integer part 509 | (but expressed as a float) of F. 510 | `.trim(), 511 | handlers: [ 512 | [['Float'], function (stack) { 513 | const top = stack.pop() 514 | const intPart = top.map(Math.floor) 515 | stack.push(new T.JoyFloat(x => y => x % y).ap(top).ap(intPart)) 516 | stack.push(intPart) 517 | }] 518 | ] 519 | }, 520 | 521 | { 522 | name: 'pow', 523 | signature: 'pow : F G -> H', 524 | help: 'H is F raised to the Gth power.', 525 | handlers: [ 526 | [['Float', 'Integer'], applyToTop2((F, G) => 527 | new T.JoyFloat(x => y => Math.pow(x, y)) 528 | .ap(F) 529 | .ap(T.JoyFloat.from(G)))] 530 | ] 531 | }, 532 | 533 | { 534 | name: 'sin', 535 | signature: 'sin : F -> G', 536 | help: 'G is the sine of F.', 537 | handlers: [ 538 | [['Float'], applyToTop(map(Math.sin))] 539 | ] 540 | }, 541 | 542 | { 543 | name: 'sinh', 544 | signature: 'sinh : F -> G', 545 | help: 'G is the hyperbolic sine of F.', 546 | handlers: [ 547 | [['Float'], applyToTop(map(Math.sinh))] 548 | ] 549 | }, 550 | 551 | { 552 | name: 'sqrt', 553 | signature: 'sqrt : F -> G', 554 | help: 'G is the square root of F.', 555 | handlers: [ 556 | [['Float'], applyToTop(map(Math.sqrt))] 557 | ] 558 | }, 559 | 560 | { 561 | name: 'tan', 562 | signature: 'tan : F -> G', 563 | help: 'G is the tangent of F.', 564 | handlers: [ 565 | [['Float'], applyToTop(map(Math.tan))] 566 | ] 567 | }, 568 | 569 | { 570 | name: 'tanh', 571 | signature: 'tanh : F -> G', 572 | help: 'G is the hyperbolic tangent of F.', 573 | handlers: [ 574 | [['Float'], applyToTop(map(Math.tanh))] 575 | ] 576 | }, 577 | 578 | { 579 | name: 'trunc', 580 | signature: 'trunc : F -> I', 581 | help: 'I is an integer equal to the float F truncated toward zero.', 582 | handlers: [ 583 | [['Float'], applyToTop(T.JoyInt.from)] 584 | ] 585 | }, 586 | 587 | { 588 | name: 'localtime', 589 | signature: 'localtime : I -> T', 590 | help: ` 591 | Converts a time I into a list T representing local time: 592 | [year month day hour minute second isdst yearday weekday]. 593 | Month is 1 = January ... 12 = December; 594 | isdst is a Boolean flagging daylight savings/summer time; 595 | weekday is 0 = Monday ... 7 = Sunday. 596 | `.trim(), 597 | handlers: [ 598 | [['Integer'], applyToTop(x => { 599 | const d = new Date(x.value * 1000) 600 | const jan = new Date(d.getFullYear(), 0, 1) 601 | const jul = new Date(d.getFullYear(), 6, 1) 602 | const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()) 603 | const day = d.getDay() 604 | return new T.JoyList([ 605 | new T.JoyInt(d.getFullYear()), 606 | new T.JoyInt(d.getMonth() + 1), 607 | new T.JoyInt(d.getDate()), 608 | new T.JoyInt(d.getHours()), 609 | new T.JoyInt(d.getMinutes()), 610 | new T.JoyInt(d.getSeconds()), 611 | new T.JoyBool(d.getTimezoneOffset() < stdOffset), 612 | new T.JoyInt(d.getYear()), 613 | new T.JoyInt(day === 0 ? 6 : day - 1) 614 | ]) 615 | })] 616 | ] 617 | }, 618 | 619 | { 620 | name: 'gmtime', 621 | signature: 'gmtime : I -> T', 622 | help: ` 623 | Converts a time I into a list T representing universal time: 624 | [year month day hour minute second isdst yearday weekday]. 625 | Month is 1 = January ... 12 = December; 626 | isdst is false; weekday is 0 = Monday ... 7 = Sunday. 627 | `.trim(), 628 | handlers: [ 629 | [['Integer'], applyToTop(x => { 630 | const d = new Date(x.value * 1000) 631 | const jan = new Date(d.getUTCFullYear(), 0, 1) 632 | const jul = new Date(d.getUTCFullYear(), 6, 1) 633 | const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()) 634 | const day = d.getUTCDay() 635 | return new T.JoyList([ 636 | new T.JoyInt(d.getUTCFullYear()), 637 | new T.JoyInt(d.getUTCMonth() + 1), 638 | new T.JoyInt(d.getUTCDate()), 639 | new T.JoyInt(d.getUTCHours()), 640 | new T.JoyInt(d.getUTCMinutes()), 641 | new T.JoyInt(d.getUTCSeconds()), 642 | new T.JoyBool(d.getTimezoneOffset() < stdOffset), 643 | new T.JoyInt(d.getYear()), 644 | new T.JoyInt(day === 0 ? 6 : day - 1) 645 | ]) 646 | })] 647 | ] 648 | }, 649 | 650 | { 651 | name: 'mktime', 652 | signature: 'mktime : T -> I', 653 | help: ` 654 | Converts a list T representing local time into a time I. 655 | T is in the format generated by localtime. 656 | `.trim(), 657 | handlers: [ 658 | [['List'], applyToTop(x => { 659 | const vals = x.value.map(y => y.value) 660 | const d = new Date(vals[0], vals[1] - 1, vals[2], vals[3], vals[4], vals[5]) 661 | return new T.JoyInt(d.getTime() / 1000) 662 | })] 663 | ] 664 | }, 665 | 666 | /** 667 | * strftime : T S1 -> S2 668 | * Formats a list T in the format of localtime or gmtime 669 | * using string S1 and pushes the result S2. 670 | * TODO 671 | */ 672 | 673 | { 674 | name: 'strtol', 675 | signature: 'strtol : S I -> J', 676 | help: ` 677 | String S is converted to the integer J using base I. 678 | If I = 0, assumes base 10, 679 | but leading "0" means base 8 and leading "0x" means base 16. 680 | `.trim(), 681 | handlers: [ 682 | [['String', 'Integer'], applyToTop2((S, I) => { 683 | let base = I.value === 0 ? 10 : I.value 684 | if (S.value.startsWith('0') && S.value !== '0') { 685 | base = 8 686 | } else if (S.value.startsWith('0x') || S.value.startsWith('0X')) { 687 | base = 16 688 | } 689 | return T.JoyInt(parseInt(S.value, base)) 690 | })] 691 | ] 692 | }, 693 | 694 | { 695 | name: 'strtod', 696 | signature: 'strtod : S -> R', 697 | help: 'String S is converted to the float R.', 698 | handlers: [ 699 | [['String'], applyToTop(T.JoyFloat.from)] 700 | ] 701 | }, 702 | 703 | { 704 | name: 'format', 705 | signature: 'format : N C I J -> S', 706 | help: ` 707 | S is the formatted version of N in mode C 708 | ('d or 'i = decimal, 'o = octal, 'x or 709 | 'X = hex with lower or upper case letters) 710 | with maximum width I and minimum width J. 711 | `.trim(), 712 | handlers: [ 713 | [['Integer', 'Character', 'Integer', 'Integer'], applyToTop4((N, C, I, J) => { 714 | // TODO: Still some todos. Joy uses sprintf under the hood, which is 715 | // non-trivial. Also, should this work for characters? 716 | if (!/[dioxX]/.test(C.value)) { 717 | throw new Error('run time error: one of: d i o x X needed for format') 718 | } 719 | if (/[oxX]/.test(C.value)) { 720 | throw new Error(`run time error: ${C.value} not yet implemented for format`) 721 | } 722 | if (J.value === 0 && N.value === 0) { return '' } 723 | // Not sure if max width is being handled correctly. 724 | return new T.JoyString(lpad(N.value.toString(), J.value, '0').slice(0, I.value)) 725 | })] 726 | ] 727 | }, 728 | 729 | { 730 | name: 'formatf', 731 | signature: 'formatf : F C I J -> S', 732 | help: ` 733 | S is the formatted version of F in mode C 734 | ('e or 'E = exponential, 'f = fractional, 735 | 'g or G = general with lower or upper case letters) 736 | with maximum width I and precision J. 737 | `.trim(), 738 | handlers: [ 739 | [['Float', 'Character', 'Integer', 'Integer'], applyToTop4((F, C, I, J) => { 740 | // TODO: Still some todos. Joy uses sprintf under the hood, which is 741 | // non-trivial. 742 | if (!/[eEfgG]/.test(C.value)) { 743 | throw new Error('run time error: one of: e E f g G needed for format') 744 | } 745 | // TODO: Do something with C 746 | if (J.value === 0 && F.value === 0) { return '' } 747 | // Not sure if max width is being handled correctly. 748 | return new T.JoyString(F.value.toFixed(J.value).slice(0, I.value)) 749 | })] 750 | ] 751 | }, 752 | 753 | /** 754 | * srand : I -> 755 | * Sets the random integer seed to integer I. 756 | * TODO: JavaScript does not expose the seed for its rng. May need to revisit if this is needed. 757 | */ 758 | 759 | { 760 | name: 'pred', 761 | signature: 'pred : M -> N', 762 | help: 'Numeric N is the predecessor of numeric M.', 763 | handlers: [ 764 | [['Numeric'], applyToTop(x => x.constructor.from(x.toNumber() - 1))] 765 | ] 766 | }, 767 | 768 | { 769 | name: 'succ', 770 | signature: 'succ : M -> N', 771 | help: 'Numeric N is the successor of numeric M.', 772 | handlers: [ 773 | [['Numeric'], applyToTop(x => x.constructor.from(x.toNumber() + 1))] 774 | ] 775 | }, 776 | 777 | { 778 | name: 'max', 779 | signature: 'max : N1 N2 -> N', 780 | help: 'N is the maximum of numeric values N1 and N2. Also supports float.', 781 | handlers: [ 782 | [['Character', 'Character'], applyToTop2(liftA2(x => y => x >= y ? x : y))], 783 | [['Integer', 'Integer'], applyToTop2(liftA2(x => y => Math.max(x, y)))], 784 | [['Float', 'Float'], applyToTop2(liftA2(x => y => Math.max(x, y)))] 785 | ] 786 | }, 787 | 788 | { 789 | name: 'min', 790 | signature: 'min : N1 N2 -> N', 791 | help: 'N is the minimum of numeric values N1 and N2. Also supports float.', 792 | handlers: [ 793 | [['Character', 'Character'], applyToTop2((N1, N2) => new T.JoyChar(x => y => x <= y ? x : y).ap(N1).ap(N2))], 794 | [['Integer', 'Integer'], applyToTop2(liftA2(x => y => Math.min(x, y)))], 795 | [['Float', 'Float'], applyToTop2(liftA2(x => y => Math.min(x, y)))] 796 | ] 797 | }, 798 | 799 | /** 800 | * fclose : S -> 801 | * Stream S is closed and removed from the stack. 802 | */ 803 | 804 | /** 805 | * feof : S -> S B 806 | * B is the end-of-file status of stream S. 807 | */ 808 | 809 | /** 810 | * ferror : S -> S B 811 | * B is the error status of stream S. 812 | */ 813 | 814 | /** 815 | * fflush : S -> S 816 | * Flush stream S, forcing all buffered output to be written. 817 | */ 818 | 819 | /** 820 | * fgetch : S -> S C 821 | * C is the next available character from stream S. 822 | */ 823 | 824 | /** 825 | * fgets : S -> S L 826 | * L is the next available line (as a string) from stream S. 827 | */ 828 | 829 | /** 830 | * fopen : P M -> S 831 | * The file system object with pathname P is opened with mode M (r, w, a, etc.) 832 | * and stream object S is pushed; if the open fails, file:NULL is pushed. 833 | */ 834 | 835 | /** 836 | * fread : S I -> S L 837 | * I bytes are read from the current position of stream S 838 | * and returned as a list of I integers. 839 | */ 840 | 841 | /** 842 | * fwrite : S L -> S 843 | * A list of integers are written as bytes to the current position of stream S. 844 | */ 845 | 846 | /** 847 | * fremove : P -> B 848 | * The file system object with pathname P is removed from the file system. 849 | * is a boolean indicating success or failure. 850 | */ 851 | 852 | /** 853 | * frename : P1 P2 -> B 854 | * The file system object with pathname P1 is renamed to P2. 855 | * B is a boolean indicating success or failure. 856 | */ 857 | 858 | /** 859 | * fput : S X -> S 860 | * Writes X to stream S, pops X off stack. 861 | */ 862 | 863 | /** 864 | * fputch : S C -> S 865 | * The character C is written to the current position of stream S. 866 | */ 867 | 868 | /** 869 | * fputchars : S "abc.." -> S 870 | * The string abc.. (no quotes) is written to the current position of stream S. 871 | */ 872 | 873 | /** 874 | * fputstring : S "abc.." -> S 875 | * == fputchars, as a temporary alternative. 876 | */ 877 | 878 | /** 879 | * fseek : S P W -> S 880 | * Stream S is repositioned to position P relative to whence-point W, 881 | * where W = 0, 1, 2 for beginning, current position, end respectively. 882 | */ 883 | 884 | /** 885 | * ftell : S -> S I 886 | * I is the current position of stream S. 887 | */ 888 | 889 | { 890 | name: 'unstack', 891 | signature: 'unstack : [X Y ..] -> ..Y X', 892 | help: 'The list [X Y ..] becomes the new stack.', 893 | handlers: [ 894 | [['List'], function (stack) { 895 | const top = stack.pop() 896 | stack.clear() 897 | for (let i = top.value.length - 1; i >= 0; i -= 1) { 898 | stack.push(top.value[i]) 899 | } 900 | }] 901 | ] 902 | }, 903 | 904 | { 905 | name: 'cons', 906 | signature: 'cons : X A -> B', 907 | help: 'Aggregate B is A with a new member X (first member for sequences).', 908 | handlers: [ 909 | [['*', 'List'], applyToTop2((X, A) => A.map(xs => [X].concat(xs)))], 910 | [['Character', 'String'], applyToTop2((X, A) => A.map(cs => X.value.concat(cs)))], 911 | [['Integer', 'Set'], applyToTop2((X, A) => A.union(new T.JoySet([X])))] 912 | // TODO: Character into Set? I don't really understand the semantics of character sets 913 | ] 914 | }, 915 | 916 | { 917 | name: 'swons', 918 | signature: 'swons : A X -> B', 919 | help: 'Aggregate B is A with a new member X (first member for sequences).', 920 | handlers: [ 921 | [['List', '*'], applyToTop2((A, X) => A.map(xs => [X].concat(xs)))], 922 | [['String', 'Character'], applyToTop2((A, X) => A.map(cs => X.value.concat(cs)))], 923 | [['Set', 'Integer'], applyToTop2((A, X) => A.union(new T.JoySet([X])))] 924 | // TODO: Character into Set? I don't really understand the semantics of character sets 925 | ] 926 | }, 927 | 928 | { 929 | name: 'first', 930 | signature: 'first : A -> F', 931 | help: 'F is the first member of the non-empty aggregate A.', 932 | handlers: [ 933 | [['NonEmptyAggregate'], applyToTop(x => x.first())] 934 | ] 935 | }, 936 | 937 | { 938 | name: 'rest', 939 | signature: 'rest : A -> R', 940 | help: 'R is the non-empty aggregate A with its first member removed.', 941 | handlers: [ 942 | [['NonEmptyAggregate'], applyToTop(x => x.rest())] 943 | ] 944 | }, 945 | 946 | { 947 | name: 'compare', 948 | signature: 'compare : A B -> I', 949 | help: ` 950 | I (=-1,0,+1) is the comparison of aggregates A and B. 951 | The values correspond to the predicates <=, =, >=. 952 | `.trim(), 953 | handlers: [ 954 | // NOTE: Help mentions aggregates, but then points to predicates. The 955 | // predicate types made more sense, so I went with those. Might not be 956 | // correct. 957 | [['Numeric', 'Numeric'], applyToTop2((x, y) => new T.JoyInt(cmp(x, y)))], 958 | [['Float', 'Float'], applyToTop2((x, y) => new T.JoyInt(cmp(x, y)))], 959 | [['String', 'String'], applyToTop2((x, y) => new T.JoyInt(cmp(x, y)))], 960 | [['Symbol', 'Symbol'], applyToTop2((x, y) => new T.JoyInt(cmp(x, y)))] 961 | ] 962 | }, 963 | 964 | { 965 | name: 'at', 966 | signature: 'at : A I -> X', 967 | help: 'X (= A[I]) is the member of A at position I.', 968 | handlers: [ 969 | [['List', 'Integer'], applyToTop2((A, I) => A.value[I.value - 1])], 970 | [['String', 'Integer'], applyToTop2((A, I) => A.value.charAt(I.value - 1))] 971 | ] 972 | }, 973 | 974 | { 975 | name: 'of', 976 | signature: 'of : I A -> X', 977 | help: 'X (= A[I]) is the I-th member of aggregate A.', 978 | handlers: [ 979 | [['List', 'Integer'], applyToTop2((I, A) => A.value[I.value - 1])], 980 | [['String', 'Integer'], applyToTop2((I, A) => A.value.charAt(I.value - 1))] 981 | ] 982 | }, 983 | 984 | { 985 | name: 'size', 986 | signature: 'size : A -> I', 987 | help: 'Integer I is the number of elements of aggregate A.', 988 | handlers: [ 989 | [['Aggregate'], applyToTop(x => 990 | new T.JoyInt(x.length !== undefined ? x.length : x.value.length))] 991 | ] 992 | }, 993 | 994 | /** 995 | * opcase : X [..[X Xs]..] -> [Xs] 996 | * Indexing on type of X, returns the list [Xs]. 997 | * TODO: what does this do? 998 | */ 999 | 1000 | /** 1001 | * case : X [..[X Y]..] -> Y i 1002 | * Indexing on the value of X, execute the matching Y. 1003 | * TODO: what does this do? 1004 | */ 1005 | 1006 | { 1007 | name: 'uncons', 1008 | signature: 'uncons : A -> F R', 1009 | help: 'F and R are the first and the rest of non-empty aggregate A.', 1010 | handlers: [ 1011 | [['NonEmptyAggregate'], function (stack) { 1012 | const top = stack.pop() 1013 | stack.push(top.first()) 1014 | stack.push(top.rest()) 1015 | }] 1016 | ] 1017 | }, 1018 | 1019 | { 1020 | name: 'unswons', 1021 | signature: 'unswons : A -> R F', 1022 | help: 'R and F are the rest and the first of non-empty aggregate A.', 1023 | handlers: [ 1024 | [['NonEmptyAggregate'], function (stack) { 1025 | const top = stack.pop() 1026 | stack.push(top.rest()) 1027 | stack.push(top.first()) 1028 | }] 1029 | ] 1030 | }, 1031 | 1032 | { 1033 | name: 'drop', 1034 | signature: 'drop : A N -> B', 1035 | help: 'Aggregate B is the result of deleting the first N elements of A.', 1036 | handlers: [ 1037 | [['List', 'Integer'], applyToTop2((A, N) => A.map(xs => xs.slice(N.value)))], 1038 | [['String', 'Integer'], applyToTop2((A, N) => A.map(xs => xs.slice(N.value)))], 1039 | [['Set', 'Integer'], applyToTop2((A, N) => A.drop(N.value))] 1040 | ] 1041 | }, 1042 | 1043 | { 1044 | name: 'take', 1045 | signature: 'take : A N -> B', 1046 | help: 'Aggregate B is the result of retaining just the first N elements of A.', 1047 | handlers: [ 1048 | [['List', 'Integer'], applyToTop2((A, N) => A.map(xs => xs.slice(0, N.value)))], 1049 | [['String', 'Integer'], applyToTop2((A, N) => A.map(xs => xs.slice(0, N.value)))], 1050 | [['Set', 'Integer'], applyToTop2((A, N) => A.take(N.value))] 1051 | ] 1052 | }, 1053 | 1054 | { 1055 | name: 'concat', 1056 | signature: 'concat : S T -> U', 1057 | help: 'Sequence U is the concatenation of sequences S and T.', 1058 | handlers: [ 1059 | [['List', 'List'], applyToTop2(liftA2(x => y => x.concat(y)))] 1060 | ] 1061 | }, 1062 | 1063 | { 1064 | name: 'enconcat', 1065 | signature: 'enconcat : X S T -> U', 1066 | help: ` 1067 | Sequence U is the concatenation of sequences S and T 1068 | with X inserted between S and T (== swapd cons concat) 1069 | `.trim(), 1070 | handlers: [ 1071 | [['*', 'List', 'List'], applyToTop3((x, s, t) => s.map(ss => ss.concat([x.value]).concat(t.value)))], 1072 | [['Character', 'String', 'String'], applyToTop3((x, s, t) => s.map(ss => ss.concat([x.value]).concat(t.value)))], 1073 | [['Integer', 'Set', 'Set'], applyToTop3((x, s, t) => s.union(new T.JoySet([x]).union(t)))] 1074 | ] 1075 | }, 1076 | 1077 | /** 1078 | * name : sym -> "sym" 1079 | * For operators and combinators, the string "sym" is the name of item sym, 1080 | * for literals sym the result string is its type. 1081 | */ 1082 | 1083 | { 1084 | name: 'intern', 1085 | signature: 'intern : "sym" -> sym', 1086 | help: 'Pushes the item whose name is "sym".', 1087 | handlers: [ 1088 | [['String'], applyToTop(x => new T.JoySymbol(x.value))] 1089 | ] 1090 | }, 1091 | 1092 | { 1093 | name: 'body', 1094 | signature: 'body : U -> [P]', 1095 | help: 'Quotation [P] is the body of user-defined symbol U.', 1096 | handlers: [ 1097 | [['Symbol'], applyToTop(x => opts.dictionary.get(x.value).definition)] 1098 | ] 1099 | } 1100 | ] 1101 | --------------------------------------------------------------------------------