├── .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 |
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 |
--------------------------------------------------------------------------------