├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── core-tests.lisp
├── core.lisp
├── docs
├── 99-problems.org
└── post.txt
├── lisp.js
├── package.json
├── repl.js
├── rpl.js
├── server
├── 404.html
└── server.js
├── test
├── index.html
├── qHint.js
└── tests.js
├── thoughts.org
├── tpl.js
└── tut.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 | .DS_Store
106 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Thanks for contributing to ToriLisp! To get started:
2 |
3 | 1. Fork the repo, do work in a feature branch. Please include acceptance tests.
4 | 2. Add relevant, passing tests to core-test.lisp and test/tests.js
5 | 3. Issue a pull request.
6 |
7 | If you have any questions, open an issue.
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Fogus and contributors
4 | Copyright (c) 2013-2014 Mary Rose Cook and contributors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ToriLisp
2 |
3 | An ersatz LISP for little birds.
4 |
5 | An experiment in writing languages in JavaScript.
6 |
7 | A code painting.
8 |
9 | An alternative to solving jigsaw puzzles.
10 |
11 | ## Introduction
12 |
13 | Run the ToriLisp REPL in a command shell with:
14 |
15 | node repl.js core.lisp core-tests.lisp
16 |
17 | ToriLisp (TL) programs consists of expressions. The simplest expressions
18 | are things like numbers and strings, which evaluate to themselves.
19 |
20 | 鳥> 9
21 | 9
22 |
23 | 鳥> "quack"
24 | 'quack'
25 |
26 | A more extensive walk-through of the language is given in the tut.txt
27 | file in this repository.
28 |
29 | ## Notes
30 |
31 | The seeds of ToriLisp come from Mary Rose Cook's lovely
32 | [Little Lisp](https://github.com/maryrosecook/littlelisp) and takes
33 | the MIT license from it.
34 |
35 | At the moment symbols are encoded as strings containing a single quote
36 | followed by the lexematic representation of the symbol. This encoding
37 | may change and should not be relied on to remain stable.
38 |
39 | ## References
40 |
41 | - [Arc tutorial](http://www.arclanguage.org/tut.txt)
42 | - [Equal Rights for Functional Objects or, The More Things Change, The More They Are the Same](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.23.9999) by Henry Baker
43 | - [LISP 1.5 Programmer's Manual](http://www.softwarepreservation.org/projects/LISP/book/LISP%201.5%20Programmers%20Manual.pdf/view) by McCarthy, et al.
44 | - [Little Lisp](https://github.com/maryrosecook/littlelisp) by Mary Rose Cook
45 | - [Misp Chronicles, The](https://web.archive.org/web/20111109113907/http://cubiclemuses.com/cm/blog/2007/misp_final.html?showcomments=yes) by William Taysom
46 | - [ML for the Working Programmer](https://www.amazon.com/ML-Working-Programmer-2nd-Paulson/dp/052156543X/?tag=fogus-20) by L.C. Paulson
47 |
--------------------------------------------------------------------------------
/core-tests.lisp:
--------------------------------------------------------------------------------
1 | (def mylen
2 | (λ (xs)
3 | (if (no xs)
4 | 0
5 | (+ 1 (mylen (rest xs))))))
6 |
7 | (check {(mylen nil)} is? 0 "Testing that mylen of nil is 0")
8 | (check {(mylen '(a b c))} is? 3 "Testing that mylen of a list is 3")
9 | (check {(mylen [1 2 3])} is? 3 "Testing that mylen of a list literal is 3")
10 |
11 | (def translate
12 | (λ (sym)
13 | (cond
14 | (is? sym 'apple) 'mela
15 | (is? sym 'onion) 'cipolla
16 | #t 'che?)))
17 |
18 | (check {(translate 'apple)} is? 'mela "Testing that translate works.")
19 | (check {(translate 'syzygy)} is? 'che? "Testing that translate works.")
20 |
21 |
22 | (check {(map (+ 10) '(1 2 3))} eqv? [ 11, 12, 13 ] "Testing that a curried function works with map.")
23 | (check {(map (comp first rest) '((a b) (c d) (e f)))} eqv? [ 'b 'd 'f ] "Testing that a composed function works with map.")
24 | (check {(map (comp not odd?) '(1 2 3 4 5 6))} eqv? [ #f #t #f #t #f #t ] "Testing that a composed function works with map and not.")
25 |
26 | (check {(filter odd? '(1 2 3 4 5 6 7))} eqv? [1 3 5 7] "Testing filter.")
27 | (check {(remove odd? '(1 2 3 4 5 6 7))} eqv? [2 4 6] "Testing remove.")
28 |
29 |
30 | (def airports (hash))
31 | (check {airports} eqv? (hash) "Testing empty mapness")
32 | (def airports2 (set airports "Boston" 'bos))
33 | (check {airports} eqv? (hash) "Testing that map is unchanged after set")
34 |
35 | (def codes (hash "Boston" 'bos "San Francisco" 'sfo "Paris" 'cdg))
36 | (check {(map codes '("Paris" "Boston" "San Francisco"))} eqv? ['cdg 'bos 'sfo])
37 |
38 | (check {(when {#f} {42})} eqv? [] "testing false when")
39 | (check {(when {undefined} {42})} eqv? [] "testing undefined when")
40 | (check {(when {nil} {42})} eqv? [] "testing nil when")
41 | (check {(when {#t} {42})} is? 42 "testing true when")
42 | (check {(when {1} {42})} is? 42 "testing truthy when")
43 |
44 | (def avar (ref 1))
45 | (check {(swap! avar + 1)} is? 2 "testing swap return with function and arg")
46 | (check {(snap avar)} is? 2 "testing swap worked")
47 | (check {(swap! avar (+ 1))} is? 3 "testing swap return with curried function")
48 | (check {(snap avar)} is? 3 "testing swap worked")
49 | (check {(cas! avar 3 (+ 1))} is? 4 "testing cas return with curried function")
50 | (check {(snap avar)} is? 4 "testing cas worked")
51 |
52 | (def bvar (ref 1 pos?))
53 | (check {(swap! bvar + 1)} is? 2 "testing swap return with function and arg")
54 | (check {(snap bvar)} is? 2 "testing swap worked")
55 | (check {(swap! bvar + -10)} is? undefined "testing swap return with function and arg")
56 | (check {(snap bvar)} is? 2 "testing swap failed, after validation failure")
57 |
58 | (def hypo
59 | (λ (a b)
60 | (math/sqrt (+ (* a a) (* b b)))))
61 |
62 | (check {(hypo 3 4)} is? 5 "test math/sqrt")
63 | (check {(hypo 0 0)} is? 0 "test math/sqrt")
64 |
--------------------------------------------------------------------------------
/core.lisp:
--------------------------------------------------------------------------------
1 | (def id {thing | thing}
2 | "Returns whatever it's given, unchanged.")
3 |
4 | (def dec {n | (- n 1)})
5 | (def inc (+ 1))
6 |
7 | (def always {e | {e}})
8 |
9 | (def map
10 | (λ (fn list)
11 | (if (no list)
12 | list
13 | (cons (fn (first list))
14 | (map fn (rest list))))))
15 |
16 | (def reduce
17 | (λ (fn init list)
18 | (if (no list)
19 | init
20 | (reduce fn
21 | (fn init (first list))
22 | (rest list)))))
23 |
24 | (def foldr
25 | (λ (fn init list)
26 | (if (no list)
27 | init
28 | (fn (first list)
29 | (foldr fn init (rest list))))))
30 |
31 | (def filter
32 | (λ (fn list)
33 | (if (no list)
34 | list
35 | (if (fn (first list))
36 | (cons (first list) (filter fn (rest list)))
37 | (filter fn (rest list))))))
38 |
39 | (def remove
40 | (λ (fn list)
41 | (if (no list)
42 | list
43 | (if (fn (first list))
44 | (remove fn (rest list))
45 | (cons (first list) (remove fn (rest list)))))))
46 |
47 | (def append
48 | (λ (l r)
49 | (if (no l)
50 | r
51 | (cons (first l) (append (rest l) r)))))
52 |
53 | (def reverse
54 | (λ (list)
55 | (reduce {acc x | (cons x acc)} nil list)))
56 |
57 | (def member
58 | (λ (test? list)
59 | (cond
60 | (no list) undefined
61 | (test? (first list)) (first list)
62 | #t (member test? (rest list))))
63 | "Checks a list given a test function and returns the first element that matches.")
64 |
65 | (def union
66 | (λ (l r)
67 | (cond
68 | (no l) r
69 | (member (eqv? (first l)) r) (union (rest l) r)
70 | #t (cons (first l) (union (rest l) r))))
71 | "Set union of two lists.")
72 |
73 | (def intersection
74 | (λ (l r)
75 | (cond
76 | (no l) nil
77 | (member (eqv? (first l)) r) (cons (first l) (intersection (rest l) r))
78 | #t (intersection (rest l) r)))
79 | "Set intersection of two lists.")
80 |
81 | (def second (comp first rest))
82 | (def third (-> rest rest first))
83 |
84 | (def not
85 | (λ (x)
86 | (cond
87 | x false
88 | #t #t)))
89 |
90 | (def when
91 | (λ (condition body)
92 | (if (condition)
93 | (body)
94 | nil))
95 | "Takes two functions and returns the result of the second when the first returns a truthy value.")
96 |
97 | (def abs {n | (if (< n 0) (- 0 n) n)})
98 |
99 | (def repeat
100 | (λ (times body!)
101 | (if (<= times 0)
102 | nil
103 | (do
104 | (body!)
105 | (repeat (dec times) body!))))
106 | "Takes a number and a function and executes the functions the given number of times.")
107 |
108 | (def take
109 | (λ (n list)
110 | (if (<= n 0)
111 | nil
112 | (cons (first list)
113 | (take (dec n) (rest list)))))
114 | "Returns the first n number of elements from a given list.")
115 |
116 | (def drop
117 | (λ (n list)
118 | (if (<= n 0)
119 | list
120 | (drop (dec n) (rest list))))
121 | "Removes the first n number of elements from a given list.")
122 |
123 | (def split
124 | (λ (n list)
125 | (cons (take n list)
126 | (cons (drop n list) nil))))
127 |
128 | (def null? {x | (is? x js/null)})
129 |
130 | (def zero? {n | (is? n 0)})
131 | (def pos? {n | (> n 0)})
132 | (def neg? {n | (< n 0)})
133 |
134 | (def odd? {n | (is? (mod n 2) 1)})
135 | (def even? (comp not odd?))
136 |
137 | (def math/logn {n base | (/ (math/log n) (math/log base))})
138 |
139 | (def math/pow {x y | (math/exp (* y (math/log x)))})
140 |
141 | (def math/sec {x | (/ 1 (math/cos x))})
142 | (def math/csc {x | (/ 1 (math/sine x))})
143 | (def math/tan {x | (/ (math/sine x) (math/cos x))})
144 | (def math/cot {x | (/ (math/cos x) (math/sine x))})
145 |
146 |
147 |
148 | (def Y
149 | (λ (f)
150 | ({x | (f (x x))}
151 | {x | (f (x x))})))
152 |
153 | (def fib0
154 | (λ (f)
155 | (λ (n)
156 | (cond (is? n 0) 1
157 | (is? n 1) 1
158 | #t (+ (f (- n 1))
159 | (f (- n 2)))))))
160 |
161 |
--------------------------------------------------------------------------------
/docs/99-problems.org:
--------------------------------------------------------------------------------
1 | #+TITLE: 99 ToriLisp Problems
2 | #+AUTHOR: Fogus
3 | #+LANGUAGE: en
4 |
5 | * 99 Problems
6 |
7 | This document is an implementation of the [[https://sites.google.com/site/prologsite/prolog-problems][99 Prolog Problems]] for ToriLisp.
8 |
9 | ** TODO 1.01 Find the last element of a list.
10 |
11 | #+name: p1_01
12 | #+begin_src lisp :tangle 99-problems.lisp :noweb yes
13 |
14 | #+end_src
15 |
16 | ** TODO 1.02 Find the last but one element of a list.
17 |
18 | #+name: p1_02
19 | #+begin_src lisp :tangle 99-problems.lisp :noweb yes
20 |
21 | #+end_src
22 |
--------------------------------------------------------------------------------
/docs/post.txt:
--------------------------------------------------------------------------------
1 | -*- mode: markdown -*-
2 |
3 | When deciding to work on a side-project three factors are needed to transition from fancy to application: goal, motivation, and time.
4 |
5 | Time is usually the biggest sticking point for me personally but with COVID most of what I may have spent my time on this year was cancelled. However, motivation was still a huge sticking point until I came across a couple of projects that helped propel me forward. First, I spent some time earlier this year combing over [Mary Rose Cook](https://twitter.com/maryrosecook)'s lovely [Little Lisp](https://github.com/maryrosecook/littlelisp) interpreter code. Given what I knew about Mary's previous projects it was no surprise that the Little Lisp implementation was simple and elegant. However, what I wasn't prepared for was that hacking on the interpreter turned out to be straight-forward and addictive. However, it wasn't until I re-discovered [William Taysom](https://twitter.com/wtaysom)'s old Scheme-like language [Misp](https://web.archive.org/web/20090625142941/http://moonbase.rydia.net/mental/blog/programming/misp-is-a-lisp) that I had a form for the interpreter in mind. At the time of William's original blog posts about Misp I was drawn to his passion and enjoyed the implementations of the language that he posted.[^dustbin] Around the same time I found Paul Graham's original Arc tutorial [tut.txt](http://www.arclanguage.org/tut.txt) and used it extensively to guide me in what to implement next.[^readme] All discussion about Arc aside, I definitely appreciate the clarity and layout of the Arc tutorial and found it a great outline for a little language.
6 |
7 | Finally, as some of you might know I [dabbled in functional programming in JavaScript](https://www.amazon.com/gp/product/B00D624AQO?tag=fogus-20) and even went so far as to create a couple of libraries pushing the edges of fp in js; namely [Lemonad](https://github.com/fogus/lemonad) and [underscore-contrib](https://github.com/documentcloud/underscore-contrib). Some of the ideas in these libraries found their way into my own interpreter which ultimately pushed my code away from Misp, Little Lisp, and Arc into ...something else -- that something else I'm calling **Tori Lisp -- an ersatz LISP for little birds**.
8 |
9 |
ToriLisp = Litle Lisp + Misp + tut.txt + Lemonad + underscore-contrib
10 |
11 | Here's a very small sample of the language:
12 |
13 | 鳥> (let (x 3 y 4)
14 | (+ (* x 2) (* y 2)))
15 | ;;=> 14
16 |
17 | (def map
18 | (λ (fn list)
19 | (if (no list)
20 | list
21 | (cons (fn (first list))
22 | (map fn (rest list))))))
23 |
24 | 鳥> (map (+ 10) '(1 2 3))
25 | ;;=> [ 11, 12, 13 ]
26 |
27 | 鳥> ({x y | (/ (+ x y) 2)} 2 4)
28 | ;;=> 3
29 |
30 | 鳥> (len "abc")
31 | ;;=> 3
32 | 鳥> (len {a | a})
33 | ;;=> 1
34 | 鳥> (len +)
35 | ;;=> 2
36 |
37 | 鳥> (read "(push [1 2] 'foo)")
38 | ;;=> [ "'push", [ "'list", 1, 2 ], [ "'quote", "'foo" ] ]
39 | 鳥> (eval (read "(push [1 2] 'foo)"))
40 | ;;=> [ "'foo", 1, 2 ]
41 |
42 | If I wanted to present a list of features then the following list would work:[^mac]
43 |
44 | - Functional
45 | - Immutable access to JavaScript arrays and hash-maps.
46 | - Function auto-currying
47 | - Function introspection
48 | - Bottoms-out at JavaScript structures and functions
49 | - ML-like Refs
50 | - Lightweight syntax for common language forms
51 |
52 | If you're interested in checking out the language then the [ToriLisp Github repository](https://github.com/fogus/tori-lisp) has a quick start, test suite, and its own tut.txt.
53 |
54 | [^dustbin]: Though the implementations did not make the Internet Archive it seems. I reached out to Mr. Taysom years ago and he was kind enough to send me the code but I hesitate to share it publicly without
55 | his approval.
56 |
57 | [^readme]: Consider this a form of [README-driven language development](https://tom.preston-werner.com/2010/08/23/readme-driven-development.html).
58 |
59 | [^mac]: Currently ToriLisp doesn't have macros though not because it would have been terribly difficult to add. Instead, I wanted to start with functional literals and auto-currying and see how far it could take me. I may add them at a later date but only time will tell.
60 |
--------------------------------------------------------------------------------
/lisp.js:
--------------------------------------------------------------------------------
1 | ;(function(exports) {
2 | var sym = (token) => "'" + token;
3 |
4 | var CRLF = sym("crlf");
5 | var DOC_KEY = sym("__docstring__");
6 | var PARENT_KEY = sym("_PARENT");
7 |
8 | var _array = (arr, from) => Array.prototype.slice.call(arr, from || 0);
9 | var _list = (...args) => _array(args);
10 |
11 | var existy = (val) => (val != null);
12 |
13 | var truthy = function(val) {
14 | if (val === "") return true;
15 |
16 | return (val !== false) && existy(val) && (val.length !== 0);
17 | }
18 |
19 | var toS = function(obj) {
20 | if (is_seq({}, obj)) {
21 | return "[" + obj.map(toS) + "]";
22 | }
23 | else if (is_symbol({}, obj)) {
24 | return obj.substring(1);
25 | }
26 | else {
27 | return "" + obj;
28 | }
29 | }
30 |
31 | /** Function meta utilities **/
32 |
33 | var garner_name = function(fn) {
34 | if (fn.name != undefined) return fn.name;
35 |
36 | var ret = fn.toString();
37 | ret = ret.substr('function '.length);
38 | ret = ret.substr(0, ret.indexOf('('));
39 | return ret;
40 | };
41 |
42 | var garner_params = function(fn) {
43 | return (fn + '')
44 | .replace(/[/][/].*$/mg,'') // strip single-line comments
45 | .replace(/\s+/g, '') // strip white space
46 | .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments
47 | .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters
48 | .replace(/=[^,]+/g, '') // strip any ES6 defaults
49 | .split(',').filter(Boolean); // split & filter [""]
50 | }
51 |
52 | /** Ostrich functional equality **/
53 |
54 | var _egal = function(l, r) {
55 | if (Array.isArray(l) && Array.isArray(r)) {
56 | if (l.length !== r.length) return false
57 | }
58 |
59 | const keys = Object.keys(l)
60 | for (let i = 0; i < keys.length; i++) {
61 | const key = keys[i]
62 | if (Array.isArray(l[key])) {
63 | if (Array.isArray(r[key])) {
64 | if (!_eqvp(l[key], r[key])) return false
65 |
66 | continue
67 | }
68 |
69 | return false
70 | }
71 |
72 | if (l[key] instanceof Date) {
73 | if (r[key] instanceof Date) {
74 | if (l[key].valueOf() !== r[key].valueOf()) return false
75 | continue
76 | }
77 |
78 | return false
79 | }
80 |
81 | if (l[key] instanceof RegExp) {
82 | if (r[key] instanceof RegExp) {
83 | if (l[key].toString() !== r[key].toString()) return false
84 | continue
85 | }
86 |
87 | return false
88 | }
89 |
90 | if (typeof l[key] === 'object') {
91 | if (typeof r[key] === 'object' && !Array.isArray(r[key])) {
92 | if (!_compare(l[key], r[key])) return false
93 | continue
94 | }
95 |
96 | return false
97 | }
98 |
99 | if (l[key] !== r[key]) return false
100 | }
101 |
102 | return true
103 | }
104 |
105 | /* Function implementation */
106 |
107 | /** builds a ToriLisp function from a JavaScript function and
108 | augments it with useful metadata **/
109 | var procedure = function(env, params, body) {
110 | var fn = function() {
111 | var args = arguments;
112 | var context = params.reduce(function(ctx, param, index) {
113 | ctx[param] = args[index];
114 | return ctx;
115 | }, {});
116 |
117 | context[PARENT_KEY] = env;
118 | return _eval(context, body);
119 | }
120 |
121 | fn.body = body;
122 | fn.params = params;
123 |
124 | return fn;
125 | }
126 |
127 | /** ToriLisp functions are curried by default whenever they receive
128 | fewer arguments than expected.**/
129 | var auto_curry = (function () {
130 | var curry = function curry(fn /* variadic number of args */) {
131 | var args = _array(arguments, 1);
132 | return function curried() {
133 | return fn.apply(this, args.concat(_array(arguments)));
134 | };
135 | };
136 |
137 | return function auto_curry(fn, expectedArgs) {
138 | var params = fn.params || garner_params(fn);
139 | var body = fn.body || fn;
140 |
141 | expectedArgs = expectedArgs || fn.length;
142 | var ret = function curried() {
143 | var remaining = expectedArgs - arguments.length;
144 |
145 | if (arguments.length < expectedArgs) {
146 | if (remaining > 0) {
147 | return auto_curry(curry.apply(this, [fn].concat(_array(arguments))), expectedArgs - arguments.length);
148 | }
149 | else {
150 | return curry.apply(this, [fn].concat(_array(arguments)));
151 | }
152 | }
153 | else if (arguments.length > expectedArgs) {
154 | throw new Error("Too many arguments to function: expected " + expectedArgs + ", got " + arguments.length);
155 | }
156 | else {
157 | return fn.apply(this, arguments);
158 | }
159 | };
160 |
161 | if (body.name != undefined) ret.name = body.name;
162 | ret.body = body;
163 | ret.params = params; // TODO: calculate the remaining args and only return those.
164 | return ret;
165 | };
166 | }());
167 |
168 | /* Core functions */
169 |
170 | var _apply = auto_curry(function(fn, args) {
171 | return fn.apply(null, args);
172 | }, 2);
173 |
174 | /** identity, equivalence, comparison, and existence **/
175 |
176 | var _isp = auto_curry(function is(l, r) {
177 | return l === r;
178 | }, 2);
179 |
180 | var _eqvp = auto_curry(function(l, r) {
181 | if (l && r && ((Array.isArray(l) && Array.isArray(r)) || (typeof l === 'object' && typeof r === 'object')) && (l.length === r.length)) {
182 | return _egal(l, r)
183 | }
184 |
185 | return l === r;
186 | }, 2);
187 |
188 | var comparator = function(pred) {
189 | return function(l, r) {
190 | if (pred(l, r)) return -1;
191 |
192 | return 1;
193 | };
194 | }
195 |
196 | var _no = function(thing) {
197 | if (existy(thing) && existy(thing.length)) {
198 | return thing.length === 0;
199 | }
200 |
201 | if (is_bool(null, thing)) return !thing;
202 |
203 | return false;
204 | };
205 |
206 | /** list functions **/
207 |
208 | var _first = function(seq) { return seq[0] };
209 | var _second = function(seq) { return seq[1] };
210 | var _rest = function(seq) { return seq.slice(1) };
211 | var _head = function(seq) { return [seq[0]]; };
212 | var _last = function(seq) { return seq[seq.length - 1]; };
213 | var _cons = auto_curry(function(elem, seq) {
214 | return [elem].concat(seq);
215 | }, 2);
216 |
217 | var _part = auto_curry(function(n, array) {
218 | var i, j;
219 | var res = [];
220 |
221 | for (i = 0, j = array.length; i < j; i += n) {
222 | res.push(array.slice(i, i+n));
223 | }
224 |
225 | return res;
226 | }, 2);
227 |
228 | /** basic math **/
229 |
230 | var _plus = auto_curry(function(l, r) {
231 | return l + r;
232 | }, 2);
233 |
234 | var _minus = auto_curry(function(l, r) {
235 | return l - r;
236 | }, 2);
237 |
238 | var _div = auto_curry(function(l, r) {
239 | return l / r;
240 | }, 2);
241 |
242 | var _mult = auto_curry(function(l, r) {
243 | return l * r;
244 | }, 2);
245 |
246 | var _mod = auto_curry(function(l, r) {
247 | return l % r;
248 | }, 2);
249 |
250 | /** comparators **/
251 |
252 | var _lt = auto_curry(function(l, r) {
253 | return l < r;
254 | }, 2);
255 |
256 | var _gt = auto_curry(function(l, r) {
257 | return l > r;
258 | }, 2);
259 |
260 | var _lte = auto_curry(function(l, r) {
261 | return l <= r;
262 | }, 2);
263 |
264 | var _gte = auto_curry(function(l, r) {
265 | return l >= r;
266 | }, 2);
267 |
268 | var _oddp = function(n) { return (n % 2) > 0 };
269 | var _evenp = function(n) { return (n % 2) === 0 };
270 |
271 | /** lengthness **/
272 |
273 | var _len = function(thing) {
274 | if (existy(thing)) {
275 | return thing.length || thing.size || thing.params.length;
276 | }
277 |
278 | return undefined;
279 | }
280 |
281 | var _sort = auto_curry(function(pred, ary) {
282 | var ret = ary.slice(0).sort(comparator(pred));
283 | return ret;
284 | }, 2);
285 |
286 | var _str = function() {
287 | return Array.from(arguments).map(e => toS(e)).join("");
288 | }
289 |
290 | /** I/O functions **/
291 |
292 | var _out = function(to) {
293 | var rest = [].slice.call(arguments, 1);
294 | var ret = true;
295 |
296 | for (var elem of rest) {
297 | if(elem.valueOf() == CRLF) {
298 | ret = to.call(this, "\n");
299 | }
300 | else {
301 | ret = to.call(this, toS(elem));
302 | }
303 | }
304 |
305 | return ret;
306 | }
307 |
308 | /** combinators **/
309 |
310 | var _comp = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
311 | var _juxt = (...fns) => x => fns.map((f) => f(x));
312 | var _pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
313 |
314 | var _flip = function(fn) {
315 | return function(first, second) {
316 | var rest = [].slice.call(arguments, 2)
317 | return fn.apply(null, [second, first].concat(rest))
318 | }
319 | }
320 |
321 | /** stack manipulation **/
322 |
323 | var _push = auto_curry(function(ary, elem) {
324 | return _cons(elem, ary);
325 | }, 2);
326 |
327 | var _pop = function(ary) {
328 | return _rest(ary);
329 | };
330 |
331 | /** hash maps **/
332 |
333 | var _hash = function(...elems) {
334 | var pairs = _part(2, elems);
335 | var last = _last(pairs);
336 |
337 | if (!existy(last)) return new Map();
338 | if (last.length === 1) throw new Error("hash expects an even number of arguments");
339 |
340 | return new Map(pairs);
341 | }
342 |
343 | var _set = function(target, key, value) {
344 | var copy = new Map(target);
345 | copy.set(key, value);
346 | return copy;
347 | }
348 |
349 | var _get = function(target, key) {
350 | return target.get(key);
351 | }
352 |
353 | var _keys = (hash) => Array.from(hash.keys());
354 | var _vals = (hash) => Array.from(hash.values());
355 | var _pairs = (hash) => Array.from(hash.entries());
356 |
357 | /** meta functions **/
358 |
359 | var _body = function(fn) {
360 | return _rest(fn.body);
361 | }
362 |
363 | var _params = function(fn) {
364 | return fn.params;
365 | }
366 |
367 | /** Refs **/
368 |
369 | var Ref = function(init, validator) {
370 | if (validator && !validator(init))
371 | throw new Error("Attempted to set invalid value " + init + " in ref initialization.");
372 |
373 | this._value = init;
374 | this._validator = validator;
375 | };
376 |
377 | Ref.prototype.setVal = function setValue(newVal) {
378 | var validate = this._validator;
379 | var oldVal = this._value;
380 |
381 | if (validate) {
382 | if (!validate(newVal)) {
383 | //throw new Error("Attempted to set invalid value " + newVal);
384 | return undefined;
385 | }
386 | }
387 |
388 | this._value = newVal;
389 | return this._value;
390 | };
391 |
392 | Ref.prototype.swap = function swap(fun /* , args... */) {
393 | var args = Array.prototype.slice.call(arguments);
394 | return this.setVal(fun.apply(this, _cons(this._value, _rest(args))));
395 | };
396 |
397 | Ref.prototype.compareAndSwap = function compareAndSwap(oldVal, f) {
398 | if (_eqvp(this._value, oldVal)) {
399 | this.setVal(f(this._value));
400 | return this._value;
401 | }
402 | else {
403 | return void 0;
404 | }
405 | };
406 |
407 | var _ref = function(val, checker) {
408 | return new Ref(val, checker);
409 | };
410 |
411 | var _swap = function(ref) {
412 | var args = Array.prototype.slice.call(arguments);
413 | return ref.swap.apply(ref, _rest(args));
414 | };
415 |
416 | var _cas = function(ref) {
417 | var args = Array.prototype.slice.call(arguments);
418 | return ref.compareAndSwap.apply(ref, _rest(args));
419 | };
420 |
421 | var _snap = function(ref) {
422 | return ref._value;
423 | }
424 |
425 | /** test functions **/
426 |
427 | var _check = function(assertion, checker, expect, msg) {
428 | var res = assertion();
429 | console.assert(checker(res, expect), msg + ": %s", " UNEXPECTED: " + toS(res));
430 | };
431 |
432 | /** Math core **/
433 |
434 | var _sine = function(n) {
435 | return Math.sin(n);
436 | };
437 |
438 | var _cos = function(n) {
439 | return Math.cos(n);
440 | };
441 |
442 | var _atan = function(n) {
443 | return Math.atan(n);
444 | };
445 |
446 | var _log = function(n) {
447 | return Math.log(n);
448 | };
449 |
450 | var _exp = function(n) {
451 | return Math.exp(n);
452 | };
453 |
454 | var _sqrt = function(n) {
455 | return Math.sqrt(n);
456 | };
457 |
458 | /* Internals */
459 |
460 | /** bindings utils **/
461 |
462 | var garner_bindings = function(env, binds) {
463 | if (is_seq(env, binds)) {
464 | return binds;
465 | }
466 | else {
467 | var keys = [];
468 | for (var key in binds) {
469 | keys.push(key);
470 | }
471 |
472 | return keys.map(index => binds[index]);
473 | }
474 | }
475 |
476 | var doify = function(form) {
477 | return _cons("'do", form);
478 | }
479 |
480 | /** environment utils **/
481 |
482 | var lookup = function(env, id) {
483 | if (id in env) {
484 | return env[id];
485 | } else if (PARENT_KEY in env) {
486 | return lookup(env[PARENT_KEY], id);
487 | }
488 | throw new Error(id + " not set in " + Object.keys(env));
489 | };
490 |
491 | /** special forms **/
492 |
493 | var SPECIAL_FORMS = {
494 | "'quote": (_, form) => _second(form),
495 |
496 | "'do": function(env, form) {
497 | var ret = null;
498 | var body = _rest(form);
499 |
500 | for (var i = 0; i < body.length; i++) {
501 | ret = _eval(env, body[i]);
502 | }
503 |
504 | return ret;
505 | },
506 |
507 | "'if": (env, form) => (truthy(_eval(env, form[1])) ? _eval(env, form[2]) : _eval(env, form[3])),
508 |
509 | "'cond": function(env, form) {
510 | var pairs = _part(2, _rest(form));
511 | if (pairs[pairs.length - 1].length === 1) throw new Error("cond expects an even number of condition pairs");
512 |
513 | for (var i = 0; i < pairs.length; i++) {
514 | var elem = pairs[i];
515 | var condition = _eval(env, elem[0]);
516 |
517 | if (truthy(condition)) return _eval(env, elem[1]);
518 | };
519 |
520 | return null;
521 | },
522 |
523 | "'let": function(env, form) {
524 | var binds = _part(2, form[1]);
525 |
526 | var scope = binds.reduce(function (acc, pair) {
527 | acc[pair[0]] = _eval(env, pair[1]);
528 | return acc;
529 | }, {"'_PARENT": env});
530 |
531 | var body = form.slice(2);
532 | var ret = undefined;
533 |
534 | for (var i = 0; i < body.length; i++) {
535 | ret = _eval(scope, body[i]);
536 | }
537 |
538 | return ret;
539 | },
540 |
541 | "'def": function(env, form) {
542 | var bind = _rest(form);
543 | var name = _first(bind);
544 |
545 | if (!is_symbol(env, name)) throw new Error("Non-symbol found in LHS of `def` form: " + name);
546 |
547 | var val = _eval(env, _second(bind));
548 |
549 | if (is_fun(env, val)) {
550 | if (_len(bind) === 3) {
551 | if (is_string({}, _last(bind))) {
552 | val[DOC_KEY] = _last(bind);
553 | }
554 | }
555 |
556 | if (val.name == undefined) {
557 | val.name = name;
558 | }
559 | }
560 |
561 | CORE[name] = val;
562 | return val;
563 | },
564 |
565 | "'λ": function(env, form) {
566 | var params = garner_bindings(env, _second(form));
567 | var body = doify(_rest(_rest(form)));
568 |
569 | if (params.length < 2) return procedure(env, params, body);
570 |
571 | return auto_curry(procedure(env, params, body), params.length);
572 | },
573 |
574 | "'and": function(env, form) {
575 | var args = _rest(form);
576 | var ret = true;
577 |
578 | for (var i=0; i < args.length; i++) {
579 | ret = _eval(env, args[i]);
580 | if (!truthy(ret)) return ret;
581 | }
582 |
583 | return ret;
584 | },
585 |
586 | "'or": function(env, form) {
587 | var args = _rest(form);
588 | var ret = false;
589 |
590 | for (var i=0; i < args.length; i++) {
591 | ret = _eval(env, args[i]);
592 | if (truthy(ret)) return ret;
593 | }
594 |
595 | return ret;
596 | }
597 | };
598 |
599 | var toString = Object.prototype.toString;
600 |
601 | /** type predicates **/
602 |
603 | var garner_type = function(obj) {
604 | return toString.call(obj);
605 | };
606 |
607 | var is_number = function (env, form) {
608 | return garner_type(form) == "[object Number]";
609 | }
610 |
611 | var is_string = function(env, form) {
612 | return garner_type(form) == "[object String]";
613 | }
614 |
615 | var is_bool = function(env, form) {
616 | return form === true || form === false || (garner_type(form) == "[object Boolean]");
617 | }
618 |
619 | var is_seq = function(env, form) {
620 | return Array.isArray(form);
621 | }
622 |
623 | var is_hash = function(env, form) {
624 | return garner_type(form) == "[object Map]";
625 | }
626 |
627 | var is_fun = function(env, form) {
628 | return garner_type(form) == "[object Function]";
629 | }
630 |
631 | var is_symbol = function (env, form) {
632 | if (is_string(env, form)) {
633 | return form.charAt(0) == "'";
634 | }
635 | else {
636 | return false;
637 | }
638 | }
639 |
640 | var is_self_evaluating = function (env, form) {
641 | return is_number(env, form)
642 | || is_string(env, form)
643 | || is_bool(env, form);
644 | }
645 |
646 | /** EVIL **/
647 |
648 | var evlis = function(env, form) {
649 | var op = _first(form);
650 |
651 | if ((form.length > 0) && (op in SPECIAL_FORMS)) {
652 | return SPECIAL_FORMS[op](env, form);
653 | }
654 | else if (!is_symbol(env,op) && is_string(env, op)) {
655 | var args = _rest(form);
656 |
657 | if (args.length === 1) {
658 | return op.charAt(_second(form));
659 | }
660 |
661 | return op.slice.apply(op, args);
662 | }
663 | else {
664 | var callable = _eval(env, op);
665 |
666 | if (is_fun(env, callable)) {
667 | var args = form.slice(1).map(e => _eval(env, e));
668 | return callable.apply(undefined, args);
669 | }
670 | else if (is_hash(env, callable)) {
671 | var args = form.slice(1).map(e => _eval(env, e));
672 | return Map.prototype.get.apply(callable, args);
673 | }
674 | else {
675 | throw new Error("Non-function found in head of array: " + op);
676 | }
677 | }
678 | }
679 |
680 | var _eval = function(env, form) {
681 | if (env === undefined) return _eval(CORE, form);
682 |
683 | var type = garner_type(form);
684 |
685 | if (is_symbol(env, form)) {
686 | return lookup(env, form);
687 | }
688 | else if (is_self_evaluating(env, form)) {
689 | return form;
690 | }
691 | else if (is_seq(env, form)) {
692 | return evlis(env, form);
693 | }
694 | }
695 |
696 | /** READER
697 | tokenization is borrowed from MRC's littlelisp as it's a clear way to
698 | simplify the token stream for the reader.
699 | **/
700 |
701 | var tokenize = function(input) {
702 | return input.split('"')
703 | .map(function(x, i) {
704 | if (i % 2 === 0) { // not in string
705 | return x.replace(/\(/g, ' ( ')
706 | .replace(/\)/g, ' ) ')
707 | .replace(/\{/g, ' { ')
708 | .replace(/\}/g, ' } ')
709 | .replace(/\[/g, ' [ ')
710 | .replace(/\]/g, ' ] ')
711 | .replace(/\|/g, ' | ')
712 | .replace(/\'/g, " ' ");
713 | } else { // in string
714 | return x.replace(/ /g, "!whitespace!");
715 | }
716 | })
717 | .join('"')
718 | .trim()
719 | .split(/\s+/)
720 | .map(function(x) {
721 | return x.replace(/!whitespace!/g, " ");
722 | });
723 | };
724 |
725 | var mangle = function(token) {
726 | if (!isNaN(parseFloat(token))) {
727 | return parseFloat(token);
728 | }
729 | else if (token[0] === '"' && token.slice(-1) === '"') {
730 | return token.slice(1, -1);
731 | }
732 | else if (token == "#t") {
733 | return true;
734 | }
735 | else if (token == "#f") {
736 | return false;
737 | }
738 | else {
739 | return sym(token);
740 | }
741 | }
742 |
743 | var Rdr = function(context = {}) {
744 | this.raw = null;
745 | this.index = 0;
746 | this.length = 0;
747 | this.sexpr = [];
748 | this.SPECIAL = ['(', ')', '{', '}', '[', ']'];
749 | this.CONTEXT = context;
750 | };
751 |
752 | Rdr.prototype.read_sexpr = function(src=null) {
753 | if (src) {
754 | this.raw = tokenize(src);
755 | this.length = this.raw.length;
756 | this.index = 0;
757 | }
758 |
759 | var token = this.read_token();
760 | var expr = null;
761 |
762 | if ((token === ')') || (token === '}') || (token === ']')) {
763 | throw new Error("Unexpected closing bracket '" + token + "'");
764 | }
765 |
766 | if (token === '(') {
767 | expr = [];
768 |
769 | token = this.read_token();
770 |
771 | while (token !== ')') {
772 | if (token === '(') {
773 | this.prev();
774 | expr.push(this.read_sexpr());
775 | }
776 | else if (token === null) {
777 | throw new Error("Invalid end of s-expression!");
778 | }
779 | else {
780 | this.prev();
781 | expr.push(this.read_sexpr());
782 | }
783 |
784 | token = this.read_token();
785 | }
786 |
787 | return expr;
788 | }
789 | if (token === '[') {
790 | expr = ["'list"];
791 |
792 | token = this.read_token();
793 |
794 | while (token !== ']') {
795 | if ((token === '[') || (token === '(')) {
796 | this.prev();
797 | expr.push(this.read_sexpr());
798 | }
799 | else if (token === null) {
800 | throw new Error("Invalid end of list literal!");
801 | }
802 | else {
803 | this.prev();
804 | expr.push(this.read_sexpr());
805 | }
806 |
807 | token = this.read_token();
808 | }
809 |
810 | return expr;
811 | }
812 | else if (token === "'") {
813 | expr = ["'quote"];
814 | sexpr = this.read_sexpr();
815 | expr.push(sexpr);
816 | return expr;
817 | }
818 | else if (token === '{') {
819 | var fn = ["'λ"];
820 | var params = [];
821 | var expr = params;
822 | var body = [];
823 | var hasParams = false;
824 |
825 | token = this.read_token();
826 |
827 | while (token !== '}') {
828 | if (token === '(') {
829 | this.prev();
830 | expr.push(this.read_sexpr());
831 | }
832 | else if (token === null) {
833 | throw new Error("Invalid end of s-expression!");
834 | }
835 | else if (token === "'|") {
836 | hasParams = true;
837 | expr = body;
838 | }
839 | else {
840 | this.prev();
841 | expr.push(this.read_sexpr());
842 | }
843 |
844 | token = this.read_token();
845 | }
846 |
847 | if (hasParams) {
848 | fn.push(params);
849 | return fn.concat(body);
850 | }
851 | else {
852 | fn.push([]);
853 | return fn.concat(params);
854 | }
855 | }
856 |
857 | return token;
858 | }
859 |
860 | Rdr.prototype.read_token = function() {
861 | if (this.index >= this.length) return null;
862 |
863 | var ret = null;
864 |
865 | if (this.SPECIAL.includes(this.current())) {
866 | ret = this.current();
867 | this.next();
868 | return ret;
869 | }
870 | else if(is_string(this.CONTEXT, this.current())) {
871 | ret = this.current();
872 | this.next();
873 |
874 | if (ret === "'") return "'";
875 |
876 | return mangle(ret);
877 | }
878 |
879 | return null;
880 | }
881 |
882 | Rdr.prototype.current = function() {
883 | return this.raw[this.index];
884 | }
885 |
886 | Rdr.prototype.next = function() {
887 | return this.index += 1;
888 | }
889 |
890 | Rdr.prototype.prev = function() {
891 | return this.index -= 1;
892 | }
893 |
894 | var _read = function(s) {
895 | var rdr = new Rdr();
896 | return rdr.read_sexpr(s);
897 | };
898 |
899 | /* ToriLisp global environment */
900 |
901 | var CORE = {
902 | "'apply": _apply,
903 | "'first": _first,
904 | "'rest": _rest,
905 | "'head": _head,
906 | "'last": _last,
907 | "'cons": _cons,
908 | "'part": _part,
909 | "'meta/body" : _body,
910 | "'meta/params": _params,
911 | "'read": _read,
912 | "'eval": _flip(_eval),
913 | "'nil": [],
914 | "'true": true,
915 | "'false": false,
916 | "'undefined": undefined,
917 | "'js/null": null,
918 | "'out": _out,
919 | "'": ((typeof process !== 'undefined') ? process.stdout.write.bind(process.stdout) : console.log),
920 | "'crlf": CRLF,
921 | "'+": _plus,
922 | "'-": _minus,
923 | "'*": _mult,
924 | "'/": _div,
925 | "'mod": _mod,
926 | "'<": _lt,
927 | "'>": _gt,
928 | "'<=": _lte,
929 | "'>=": _gte,
930 | "'even?": _evenp,
931 | "'odd?": _oddp,
932 | "'len": _len,
933 | "'no": _no,
934 | "'is?": _isp,
935 | "'eqv?": _eqvp,
936 | "'comp": _comp,
937 | "'juxt": _juxt,
938 | "'->": _pipe,
939 | "'hash": _hash,
940 | "'set": _set,
941 | "'get": _get,
942 | "'keys": _keys,
943 | "'vals": _vals,
944 | "'pairs": _pairs,
945 | "'str": _str,
946 | "'push": _push,
947 | "'pop": _pop,
948 | "'sort": _sort,
949 | "'doc": (obj) => obj[DOC_KEY],
950 | "'type": garner_type,
951 | "'list": _list,
952 | "'check": _check,
953 | "'ref": _ref,
954 | "'swap!": _swap,
955 | "'cas!": _cas,
956 | "'snap": _snap,
957 | "'math/PI": Math.PI,
958 | "'math/E": Math.E,
959 | "'math/sine": _sine,
960 | "'math/cos": _cos,
961 | "'math/atan": _atan,
962 | "'math/log": _log,
963 | "'math/exp": _exp,
964 | "'math/sqrt": _sqrt
965 | };
966 |
967 | exports.lisp = {
968 | VERSION: "0.5.6",
969 | read: _read,
970 | evil: (str) => _eval(CORE, _read(str)),
971 | Rdr: Rdr,
972 | core: CORE,
973 | };
974 | })(typeof exports === 'undefined' ? this : exports);
975 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "torilisp",
3 | "description": "an ersatz lisp for little birds",
4 | "author": "fogus",
5 | "version": "0.0.1",
6 | "dependencies": {
7 | "immutable": "4.0.0-rc.9"
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/repl.js:
--------------------------------------------------------------------------------
1 | /** REPL == Read, Eval, Print, Loop **/
2 |
3 | const repl = require('repl');
4 | const fs = require('fs');
5 | const util = require('util');
6 |
7 | var 鳥 = require("./lisp").lisp;
8 | var rdr = new 鳥.Rdr(鳥.core);
9 | const writer = (obj) => ";;=> " + util.inspect(obj, writer.options);
10 | writer.options = { ...util.inspect.defaultOptions, showProxy: true, colors: true };
11 |
12 | console.log("Starting tori-lisp v" + 鳥.VERSION + "...");
13 | process.argv.slice(2).forEach(function(infile) {
14 | var src = fs.readFileSync(infile, "utf8");
15 | console.log("...loading " + infile);
16 | 鳥.evil("(do " + src + ")");
17 | });
18 | console.log("done\n");
19 |
20 | repl.start({
21 | prompt: "鳥> ",
22 | eval: function(cmd, context, filename, callback) {
23 | var ret = 鳥.evil(cmd);
24 | callback(null, ret);
25 | },
26 | writer: writer
27 | });
28 |
29 |
--------------------------------------------------------------------------------
/rpl.js:
--------------------------------------------------------------------------------
1 | /** RPL == Read, Print, Loop **/
2 |
3 | const repl = require('repl');
4 | const fs = require('fs');
5 | const util = require('util');
6 |
7 | var 鳥 = require("./lisp").lisp;
8 | var rdr = new 鳥.Rdr();
9 |
10 | const writer = (obj) => ";;=> " + util.inspect(obj, writer.options);
11 | writer.options = { ...util.inspect.defaultOptions, showProxy: true, colors: true };
12 |
13 | console.log("Starting tori-lisp reader v" + 鳥.VERSION + "...");
14 |
15 | repl.start({
16 | prompt: "鳥r> ",
17 | eval: function(cmd, context, filename, callback) {
18 | var ret = rdr.read_sexpr(cmd, { word:/\w+/, whitespace:/\s+/, punctuation:/[^\w\s]/ }, 'invalid');
19 | callback(null, ret);
20 | },
21 | writer: writer
22 | });
23 |
24 |
--------------------------------------------------------------------------------
/server/404.html:
--------------------------------------------------------------------------------
1 | page not found...
2 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var fs = require('fs');
3 | var path = require('path');
4 |
5 | function noParams(filename) {
6 | var paramStart = filename.indexOf("?");
7 |
8 | if (paramStart > -1) {
9 | return filename.slice(0, paramStart);
10 | }
11 |
12 | return filename;
13 | }
14 |
15 | http.createServer(function (request, response) {
16 | console.log('request ', request.url);
17 |
18 | var filePath = '.' + noParams(request.url);
19 | if (filePath == './') {
20 | filePath = './index.html';
21 | }
22 |
23 | var extname = noParams(String(path.extname(filePath)).toLowerCase());
24 |
25 | var mimeTypes = {
26 | '.html': 'text/html',
27 | '.js': 'text/javascript',
28 | '.css': 'text/css',
29 | '.json': 'application/json',
30 | '.png': 'image/png',
31 | '.jpg': 'image/jpg',
32 | '.gif': 'image/gif',
33 | '.svg': 'image/svg+xml',
34 | '.wav': 'audio/wav',
35 | '.mp4': 'video/mp4',
36 | '.woff': 'application/font-woff',
37 | '.ttf': 'application/font-ttf',
38 | '.eot': 'application/vnd.ms-fontobject',
39 | '.otf': 'application/font-otf',
40 | '.wasm': 'application/wasm'
41 | };
42 |
43 | var contentType = mimeTypes[extname] || 'application/octet-stream';
44 |
45 | fs.readFile(filePath, function(error, content) {
46 | if (error) {
47 | if(error.code == 'ENOENT') {
48 | fs.readFile('./404.html', function(error, content) {
49 | response.writeHead(404, { 'Content-Type': 'text/html' });
50 | response.end(content, 'utf-8');
51 | });
52 | }
53 | else {
54 | response.writeHead(500);
55 | response.end('Sorry, check with the site admin for error: '+error.code+' ..\n');
56 | }
57 | }
58 | else {
59 | response.writeHead(200, { 'Content-Type': contentType });
60 | response.end(content, 'utf-8');
61 | }
62 | });
63 |
64 | }).listen(8125);
65 | console.log('Server running at http://127.0.0.1:8125/');
66 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tori-lisp tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/qHint.js:
--------------------------------------------------------------------------------
1 | (function(window) {
2 | // modified version of XHR script by PPK, http://www.quirksmode.org/js/xmlhttp.html
3 | function sendRequest(url,callback) {
4 | var req = createXMLHTTPObject();
5 | if (!req) return;
6 | var method = "GET";
7 | req.open(method,url,true);
8 | req.setRequestHeader('User-Agent','XMLHTTP/1.0');
9 | req.onreadystatechange = function () {
10 | if (req.readyState != 4) return;
11 | if (req.status != 200 && req.status != 304) {
12 | alert("HTTP error " + req.status + " occured.");
13 | return;
14 | }
15 | callback(req);
16 | };
17 |
18 | if (req.readyState == 4) return;
19 | req.send();
20 | }
21 |
22 | var XMLHttpFactories = [
23 | function () { return new XMLHttpRequest(); },
24 | function () { return new ActiveXObject("Msxml2.XMLHTTP"); },
25 | function () { return new ActiveXObject("Msxml3.XMLHTTP"); },
26 | function () { return new ActiveXObject("Microsoft.XMLHTTP"); }
27 | ];
28 |
29 | function createXMLHTTPObject() {
30 | for (var i = 0; i < XMLHttpFactories.length; i++) {
31 | try {
32 | return XMLHttpFactories[i]();
33 | } catch (e) {}
34 | }
35 | return false;
36 | }
37 |
38 | // the qHint function
39 | window.qHint = function (name, sourceFile, options) {
40 | function validateFile(source) {
41 | var i, len, err,
42 | result = JSHINT(source, options);
43 |
44 | ok(result);
45 |
46 | if (result) {
47 | return;
48 | }
49 |
50 | for (i = 0, len = JSHINT.errors.length; i < len; i++) {
51 | err = JSHINT.errors[i];
52 | if (!err) {
53 | continue;
54 | }
55 |
56 | ok(false, err.reason + " on line " + err.line + ", character " + err.character);
57 | }
58 | }
59 |
60 | return asyncTest(name, function() {
61 | sendRequest(sourceFile, function(source) {
62 | start();
63 | validateFile(source.responseText);
64 | });
65 | });
66 | };
67 | })(window, undefined);
68 |
--------------------------------------------------------------------------------
/test/tests.js:
--------------------------------------------------------------------------------
1 | QUnit.test( "read forms", function(assert) {
2 | assert.deepEqual(lisp.read("25"), 25, "int number");
3 | assert.deepEqual(lisp.read("0.5"), 0.5, "decimal number");
4 | assert.deepEqual(lisp.read("\"foo\""), 'foo', "string");
5 | assert.deepEqual(lisp.read("a"), "'a", "symbols");
6 | assert.deepEqual(lisp.read("()"), [], "empty array");
7 | assert.deepEqual(lisp.read("[]"), ["'list"], "empty list literal");
8 | assert.deepEqual(lisp.read("(a)"), ["'a"], "array");
9 | assert.deepEqual(lisp.read("[a]"), ["'list", "'a"], "list literal");
10 | assert.deepEqual(lisp.read("(+ 1 2)"), ["'+", 1, 2], "array");
11 | assert.deepEqual(lisp.read("(λ (a b) (+ a b))"), ["'λ", ["'a", "'b"], ["'+", "'a", "'b"]], "lambda");
12 | assert.deepEqual(lisp.read("{a b | (+ a b)}"), ["'λ", ["'a", "'b"], ["'+", "'a", "'b"]], "function literal form");
13 | assert.deepEqual(lisp.read("'a"), ["'quote", "'a"], "quoted symbol");
14 | assert.deepEqual(lisp.read("'(a)"), ["'quote", ["'a"]], "quoted array");
15 | assert.deepEqual(lisp.read("'(a b (c (d)))"), ["'quote", ["'a", "'b", ["'c", ["'d"]]]], "quoted nested array");
16 | });
17 |
18 | QUnit.test( "math", function(assert) {
19 | assert.deepEqual(lisp.evil("(+ 1 2)"), 3, "simple add");
20 | assert.deepEqual(lisp.evil("(+ (+ 1 2) (+ 3 (+ 4 5)))"), 15, "nested add");
21 | assert.throws(() => lisp.evil("(+ 1 2 3)"), Error, "wrong add args count");
22 | assert.deepEqual(lisp.evil("(- 1 2)"), -1, "simple sub");
23 | assert.deepEqual(lisp.evil("(* 100 2)"), 200, "simple mult");
24 | assert.deepEqual(lisp.evil("(/ 1 2)"), 0.5, "simple div");
25 | });
26 |
27 | QUnit.test( "function literals", function(assert) {
28 | assert.equal(lisp.evil("({x y | (/ (+ x y) 2)} 2 4)"), 3);
29 | });
30 |
31 | QUnit.test( "curried functions", function(assert) {
32 | var curriedAdd = lisp.evil("(+ 1)");
33 |
34 | assert.ok((typeof curriedAdd) === 'function');
35 | assert.equal(curriedAdd(2), 3);
36 |
37 | var avg = lisp.evil("(def average {x y | (/ (+ x y) 2)})");
38 | assert.ok((typeof avg) === 'function');
39 | assert.equal(avg(2, 4), 3);
40 |
41 | var curriedAvg = avg(2);
42 | assert.ok((typeof curriedAvg) === 'function');
43 | assert.equal(curriedAvg(4), 3);
44 | });
45 |
46 | QUnit.test( "function meta", function(assert) {
47 | var avg = lisp.evil("(def average {x y | (/ (+ x y) 2)})");
48 | assert.ok((typeof avg) === 'function');
49 |
50 | assert.deepEqual(lisp.evil("(meta/body average)"), [ [ "'/", [ "'+", "'x", "'y" ], 2 ] ], "function body");
51 | assert.deepEqual(lisp.evil("(meta/params average)"), [ "'x", "'y" ], "function params");
52 | });
53 |
54 | QUnit.test( "def", function(assert) {
55 | assert.ok(lisp.evil("(def foo 13)"));
56 | assert.equal(lisp.evil("foo"), 13);
57 |
58 | assert.ok(lisp.evil("(def x '(a b))"));
59 | assert.ok(lisp.evil("(cons 'f x)"));
60 | assert.deepEqual(lisp.evil("x"), ["'a", "'b"], "list unchanged");
61 | });
62 |
63 | QUnit.test( "quote", function(assert) {
64 | assert.equal(lisp.evil("'foo"), "'foo");
65 | });
66 |
67 | QUnit.test( "lists", function(assert) {
68 | assert.deepEqual(lisp.evil("(cons 'a nil)"), ["'a"]);
69 | assert.deepEqual(lisp.evil("(first '(a b c))"), "'a");
70 | assert.deepEqual(lisp.evil("(rest '(a b c))"), ["'b", "'c"]);
71 | assert.deepEqual(lisp.evil("(head '(a b c))"), ["'a"]);
72 | assert.deepEqual(lisp.evil("(list 'a 1 \"foo\" '(b))"), [ "'a", 1, 'foo', [ "'b" ] ]);
73 | assert.deepEqual(lisp.evil("['a 1 \"foo\" ['b]]"), [ "'a", 1, 'foo', [ "'b" ] ]);
74 |
75 | assert.deepEqual(lisp.evil("(part 2 [1 2 3 4 5 6])"), [ [1, 2], [3, 4], [5, 6] ]);
76 | assert.deepEqual(lisp.evil("(part 2 [1 2 3 4 5])"), [ [1, 2], [3, 4], [5] ]);
77 | assert.deepEqual(lisp.evil("(part 3 [1 2 3 4 5 6])"), [ [1, 2, 3], [4, 5, 6] ]);
78 | assert.deepEqual(lisp.evil("(part 3 [1 2 3 4])"), [ [1, 2, 3], [4] ]);
79 |
80 | assert.deepEqual(lisp.evil("(sort < '(2 9 3 7 5 1))"), [ 1, 2, 3, 5, 7, 9 ], "sorting a list <");
81 | assert.deepEqual(lisp.evil("(sort {l r | (< (len l) (len r))} '(\"orange\" \"pea\" \"apricot\" \"apple\"))"), [ 'pea', 'apple', 'orange', 'apricot' ], "sorting a list w/ a custon function");
82 | assert.deepEqual(lisp.evil("(sort {l r | (< (len l) (len r))} '(\"aa\" \"bb\" \"cc\"))"), [ 'aa', 'bb', 'cc' ], "stable sorting");
83 | });
84 |
85 | QUnit.test( "strings", function(assert) {
86 | assert.equal(lisp.evil("(\"foo\" 0)"), 'f', "one arg");
87 | assert.equal(lisp.evil("(\"foo\" 0 2)"), 'fo', "two args");
88 | assert.equal(lisp.evil("(str 99 \" bottles of \" 'bee ['r])"), '99 bottles of bee[r]');
89 | });
90 |
91 | QUnit.test( "let", function(assert) {
92 | assert.equal(lisp.evil("(let (x 1) (+ x (* x 2)))"), 3, "let w/ one bind");
93 | assert.equal(lisp.evil("(let (x 1) (+ x (* x 2)) x)"), 1, "let w/ one bind but multiple body forms");
94 | assert.equal(lisp.evil("(let (x 3 y 4) (+ (* x 2) (* y 2)))"), 14, "let w/ two binds");
95 | });
96 |
97 | QUnit.test( "if", function(assert) {
98 | assert.equal(lisp.evil("(if (odd? 1) 'a 'b)"), "'a");
99 | assert.equal(lisp.evil("(if (even? 1) 'a 'b)"), "'b");
100 | assert.equal(lisp.evil("(if nil 'a 'b)"), "'b");
101 | assert.equal(lisp.evil("(if undefined 'a 'b)"), "'b");
102 | assert.equal(lisp.evil("(if #t 'a 'b)"), "'a");
103 | assert.equal(lisp.evil("(if true 'a 'b)"), "'a");
104 | assert.equal(lisp.evil("(if #f 'a 'b)"), "'b");
105 | assert.equal(lisp.evil("(if false 'a 'b)"), "'b");
106 | assert.equal(lisp.evil("(if [1 2 3] 'a 'b)"), "'a");
107 | assert.equal(lisp.evil("(if 1 'a 'b)"), "'a");
108 | assert.equal(lisp.evil("(if 0 'a 'b)"), "'a");
109 | assert.equal(lisp.evil("(if -1 'a 'b)"), "'a");
110 | assert.equal(lisp.evil("(if (hash 'a 1) 'a 'b)"), "'a");
111 | assert.equal(lisp.evil("(if \"a\" 'a 'b)"), "'a");
112 | assert.equal(lisp.evil("(if 'truthy 'a 'b)"), "'a");
113 | assert.equal(lisp.evil("(if #t (do 1 2)))"), 2);
114 | assert.equal(lisp.evil("(if #t 1)"), 1);
115 | assert.equal(lisp.evil("(if #f 1)"), undefined);
116 | assert.equal(lisp.evil("(if \"\" 'a 'b)"), "'a");
117 | assert.throws(() => lisp.evil("(if doesnetexist 'a 'b)"), Error, "non-bound var");
118 | });
119 |
120 | QUnit.test( "len", function(assert) {
121 | assert.equal(lisp.evil("(len [1 2 3])"), 3);
122 | assert.equal(lisp.evil("(len \"abc\")"), 3);
123 | assert.equal(lisp.evil("(len {a | a})"), 1);
124 | assert.equal(lisp.evil("(len {})"), 0);
125 | assert.equal(lisp.evil("(len +)"), 2);
126 | });
127 |
128 | QUnit.test( "is?", function(assert) {
129 | assert.ok(lisp.evil("(is? 'a 'a)"));
130 | assert.ok(lisp.evil("(is? \"foo\" \"foo\")"));
131 | assert.ok(lisp.evil("(let (x ['a]) (is? x x))"));
132 | assert.ok(lisp.evil("(let (x 'c) (or (is? x 'a) (is? x 'b) (is? x 'c)))"));
133 | assert.notOk(lisp.evil("(is? ['a] ['a])"));
134 | });
135 |
136 | QUnit.test( "eqv?", function(assert) {
137 | assert.ok(lisp.evil("(eqv? ['a] ['a])"));
138 | });
139 |
140 | QUnit.test( "comp, juxt, ->", function(assert) {
141 | assert.deepEqual(lisp.evil("((comp first rest) '(1 2 3))"), 2);
142 | assert.deepEqual(lisp.evil("((juxt first rest) '(1 2 3))"), [ 1, [ 2, 3 ] ]);
143 | });
144 |
145 | QUnit.test( "hash maps", function(assert) {
146 | assert.deepEqual(lisp.evil("(hash 'x 1 'y 2)"), new Map([["'x", 1], ["'y", 2]]));
147 | assert.deepEqual(lisp.evil("(let (m (hash 'x 1 'y 2)) (get m 'x))"), 1);
148 | assert.deepEqual(lisp.evil("(let (m (hash 'x 1 'y 2)) (get m 'a))"), undefined);
149 | assert.deepEqual(lisp.evil("(let (m (hash 'x 1 'y 2)) (m 'x))"), 1);
150 |
151 | lisp.evil("(def codes (hash \"Boston\" 'bos \"San Francisco\" 'sfo \"Paris\" 'cdg))");
152 | assert.deepEqual(lisp.evil("(keys codes)"), [ 'Boston', 'San Francisco', 'Paris' ]);
153 | assert.deepEqual(lisp.evil("(pairs codes)"), [ [ 'Boston', "'bos" ], [ 'San Francisco', "'sfo" ], [ 'Paris', "'cdg" ] ]);
154 | });
155 |
156 | QUnit.test( "lists as stacks", function(assert) {
157 | lisp.evil("(def x '(c a b))");
158 | assert.deepEqual(lisp.evil("(pop x)"), [ "'a", "'b" ]);
159 | assert.deepEqual(lisp.evil("x"), [ "'c", "'a", "'b" ]);
160 | assert.deepEqual(lisp.evil("(push x 'f)"), [ "'f", "'c", "'a", "'b" ]);
161 | assert.deepEqual(lisp.evil("x"), [ "'c", "'a", "'b" ]);
162 | });
163 |
164 | QUnit.test( "apply", function(assert) {
165 | assert.equal(lisp.evil("(apply + '(1 2))"), 3);
166 |
167 | var p1 = lisp.evil("(apply + '(1))");
168 | assert.ok((typeof p1) === 'function');
169 |
170 | assert.equal(p1(2), 3);
171 | assert.equal(lisp.evil("((apply + '(1)) 2)"), 3);
172 | });
173 |
--------------------------------------------------------------------------------
/thoughts.org:
--------------------------------------------------------------------------------
1 | * Goals
2 |
3 | - To create a [[https://web.archive.org/web/20111109113907/http://cubiclemuses.com/cm/blog/2007/misp_final.html?showcomments=yes][Misp-like]] language hosted on JavaScript
4 | - Forms bottom-out in JavaScript data
5 | - Auto-currying
6 | - Lightweight syntactic forms
7 |
8 | * TODOs
9 | ** DONE Create currying machinery and wire into namespace
10 | ** DONE Implement ~{a | ...}~ reader that creates a ~["'fn" ["'a"] ...]~ structure.
11 | ** DONE Create a simple test framework to test core.lisp implementations.
12 | ** DONE Explore ML refs
13 | *** DONE Implement ~ref~, ~swap!~, ~cas!~, and ~snap~
14 | *** DONE Test implementation
15 | *** DONE Add ref discussion to tut.txt (w/ validator)
16 | *** DONE Add counter closure example and tests.
17 | ** DONE Migrate appropriate functions to ~() =>~ form.
18 | ** DONE Add commentary to lisp.js
19 | ** DONE Write blog post (credit Mary RC, [[http://www.arclanguage.org/tut.txt][pg]], and Tay), mention lemonad
20 | ** DONE Expose `part` as curried function
21 | ** TODO Implement John D Cook's minimal math lib https://www.johndcook.com/blog/2021/01/05/bootstrapping-math-library/
22 | ** TODO Port environments to Maps
23 | ** TODO microKanren
24 | ** TODO Explore Symbols instead of encoded strings for symbols
25 | ** TODO Explore pulling in more of lemonad
26 | ** TODO Explore Scheme-48 modules
27 | ** TODO Explore "tables" and the queries around them
28 | ** TODO Explore in-browser REPL, listener, editor, etc., a la XLISP-STAT
29 | ** TODO Implement ~eval~
30 |
--------------------------------------------------------------------------------
/tpl.js:
--------------------------------------------------------------------------------
1 | /** TPL == Tokenize, Print, Loop **/
2 |
3 | const repl = require('repl');
4 | const fs = require('fs');
5 | const util = require('util');
6 |
7 | var 鳥 = require("./lisp").lisp;
8 |
9 | const writer = (obj) => ";;=> " + util.inspect(obj, writer.options);
10 | writer.options = { ...util.inspect.defaultOptions, showProxy: true, colors: true };
11 |
12 | console.log("Starting tori-lisp tokenizer v" + 鳥.VERSION + "...");
13 |
14 | repl.start({
15 | prompt: "鳥t> ",
16 | eval: function(cmd, context, filename, callback) {
17 | var ret = {"1" : 鳥.tokenize1(cmd, { word:/\w+/, whitespace:/\s+/, punctuation:/[^\w\s]/ }, 'invalid'),
18 | "2" : 鳥.tokenize2(cmd, { word:/\w+/, whitespace:/\s+/, punctuation:/[^\w\s]/ }, 'invalid')};
19 | callback(null, ret);
20 | },
21 | writer: writer
22 | });
23 |
24 |
--------------------------------------------------------------------------------
/tut.txt:
--------------------------------------------------------------------------------
1 | -*- mode: markdown -*-
2 |
3 | *This is a brief tutorial on Tori-Lisp, an ersatz Lisp for little
4 | birds. A passing familiarity with Lisp and Lisp-like languages is
5 | presumed.*
6 |
7 | TL programs consist of expressions. The simplest expressions are
8 | things like numbers and strings, which evaluate to themselves.
9 |
10 | 鳥> 25
11 | ;;=> 25
12 |
13 | 鳥> "foo"
14 | ;;=> 'foo'
15 |
16 | There's an important detail about TL that bears mentioning at this
17 | time. That is, the language is hosted on the JavaScript programming
18 | language and thus "bottoms-out" at JavaScript data-types and
19 | structures. Therefore, the TL console will always display its
20 | results as JavaScript types and indeed, the evaluation engine of
21 | the language operates on and with JavaScript types and functions.
22 |
23 | TL is like most Lisp-like languages in that the list form
24 | represents a function call:
25 |
26 | 鳥> (+ 1 2)
27 | ;;=> 3
28 |
29 | Since expression and evaluation are both defined recursively,
30 | programs can be as complex as you want:
31 |
32 | 鳥> (+ (+ 1 2) (+ 3 (+ 4 5)))
33 | ;;=> 15
34 |
35 | Unlike many Lisps, TL functions always expect a fixed number of
36 | arguments. For example, the `+` function shown above expects to
37 | receive 2 arguments and if it receives too many then an error
38 | occurs:
39 |
40 | 鳥> (+ 1 2 3)
41 | ;;=> Error: Too many arguments to function: expected 2, got 3
42 |
43 | However, if you supply fewer arguments than a function expects then
44 | a new function will be returned:
45 |
46 | 鳥> (+ 1)
47 | ;;=> [Function: curried]
48 |
49 | Functions in TL are "curried" by default. This means that when a
50 | function is called it might return another function that expects to
51 | receive the remainder of its expected arguments. Once all of the
52 | expected arguments are received the function is executed and a
53 | result returned.
54 |
55 | 鳥> ((+ 1) 2)
56 | ;;=> 3
57 |
58 | Lisp dialects like TL have a data type most languages don't:
59 | symbols. We've already seen one: `+` is a symbol. Symbols don't
60 | evaluate to themselves the way numbers and strings do. They return
61 | whatever value they've been assigned.
62 |
63 | If we give `foo` the value 13 in the global scope, it will return
64 | 13 when evaluated:
65 |
66 | 鳥> (def foo 13)
67 | ;;=> 13
68 | 鳥> foo
69 | ;;=> 13
70 |
71 | You can turn off evaluation by putting a single quote character
72 | before an expression. So `'foo` returns the JavaScript equivalent
73 | to a TL symbol `"'foo"`.
74 |
75 | 鳥> 'foo
76 | ;;=> "'foo"
77 |
78 | Particularly observant readers may be wondering how we got away
79 | with using `foo` as the first argument to `def`. If the arguments
80 | are evaluated left to right, why didn't this cause an error when
81 | `foo` was evaluated? There are some operators that violate the usual
82 | evaluation rule, and `def` is one of them. Its first argument isn't
83 | evaluated.
84 |
85 | 鳥> (+ 1 2)
86 | ;;=> 3
87 | 鳥> '(+ 1 2)
88 | ;;=> ["'+", 1, 2]
89 |
90 | The first expression returns the number 3. The second, because it
91 | was quoted, returns a JS array consisting of the string `"'+` and the
92 | numbers 1 and 2. The use of the pattern `"'symbol"` is the way that
93 | symbols are encoded in TL.
94 |
95 | You can build up TL lists with `cons`, which returns a list with a new
96 | element on the front:
97 |
98 | 鳥> (cons 'f '(a b))
99 | ;;=> [ "'f", "'a", "'b" ]
100 |
101 | It doesn't change the original list:
102 |
103 | 鳥> (def x '(a b))
104 | ;=> [ "'a", "'b" ]
105 |
106 | 鳥> (cons 'f x)
107 | ;;=> [ "'f", "'a", "'b" ]
108 |
109 | 鳥> x
110 | ;=> [ "'a", "'b" ]
111 |
112 | The empty list is represented by the symbol `nil`, which is defined
113 | to evaluate to itself. So to make a list of one element you say:
114 |
115 | 鳥> (cons 'a nil)
116 | ;;=> [ "'a" ]
117 |
118 | You can take lists apart with car and cdr, which return the first
119 | element and everything but the first element respectively:
120 |
121 | 鳥> (first '(a b c))
122 | ;;=> "'a"
123 |
124 | 鳥> (rest '(a b c))
125 | ;;=> ["'b", "'c"]
126 |
127 | Additionally, TL provides a `head` function to take apart a list by
128 | returning a list of the first element:
129 |
130 | 鳥> (head '(a b c))
131 | ;;=> [ "'a" ]
132 |
133 | To create a list with many elements you can use the `list` function:
134 |
135 | 鳥> (list 'a 1 "foo" '(b))
136 | ;;=> [ "'a", 1, 'foo', [ "'b" ] ]
137 |
138 | However, TL also provides a list literal syntax using elements placed
139 | between square brackets:
140 |
141 | 鳥> ['a 1 "foo" ['b]]
142 | ;;=> [ "'a", 1, 'foo', [ "'b" ] ]
143 |
144 | We've already seen some functions: `+`, `cons`, `first`, `rest`, and `head`.
145 | You can define new ones with def, which takes a symbol to use as the name, a
146 | list of symbols representing the parameters, and then zero or more expressions
147 | called the body. When the function is called, those expressions will be
148 | evaluated in order with the symbols in the body temporarily set ("bound") to
149 | the corresponding argument. Whatever the last expression returns will be
150 | returned as the value of the call.
151 |
152 | Here's a function that takes two numbers and returns their average:
153 |
154 | 鳥> (def average {x y | (/ (+ x y) 2)})
155 | ;;=> [Function: curried]
156 | 鳥> (average 2 4)
157 | ;;=> 3
158 |
159 | From the definition above you'll notice that TL uses a literal syntax for
160 | functions of the form `{args | body}`. This is sugar for the more conventional
161 | `(λ (x y) (/ (+ x y) 2))` form. In either case, the body of the function consists
162 | of one expression, (/ (+ x y) 2). It's common for functions to consist of one
163 | expression; in purely functional code (code with no side-effects) they always do.
164 |
165 | What's the strange-looking object returned as the value of the `def`
166 | expression? That's what a function looks like. In TL, as in most
167 | Lisps, functions are a data type, just like numbers or strings.
168 |
169 | Indeed, user-defined functions can be inspected at runtime:
170 |
171 | 鳥> (meta/body average)
172 | ;;=> [ [ "'/", [ "'+", "'x", "'y" ], 2 ] ]
173 |
174 | 鳥> (meta/params average)
175 | ;;=> [ "'x", "'y" ]
176 |
177 | This capability will come in handy later in the tutorial but for now we can
178 | proceed.
179 |
180 | Of course you can use a literal function wherever you could use
181 | a symbol whose value is one, e.g.
182 |
183 | 鳥> ({x y | (/ (+ x y) 2)} 2 4)
184 | ;;=> 3
185 |
186 | This expression has three elements, `{x y | (/ (+ x y) 2)}`, which
187 | yields a function that returns averages, and the numbers 2 and 4.
188 | So when you evaluate all three expressions and pass the values of
189 | the second and third to the value of the first, you pass 2 and 4
190 | to a function that returns averages, and the result is 3.
191 |
192 | There's one thing you can't do with functions that you can do with
193 | data types like symbols and strings: you can't print them out in a
194 | way that could be read back in. The reason is that the function
195 | could be a closure; displaying closures is a tricky problem.
196 |
197 | In TL, data structures can be used wherever functions are, and
198 | they behave as functions from indices to whatever's stored there.
199 | So to get the first element of a string you say:
200 |
201 | 鳥> ("foo" 0)
202 | ;;=> 'f'
203 |
204 | Or to get a slice of the string you can use:
205 |
206 | 鳥> ("foo" 0 2)
207 | ;;=> 'fo'
208 |
209 | There is one commonly used operator for establishing temporary
210 | variables, namely `let`.
211 |
212 | 鳥> (let (x 1)
213 | (+ x (* x 2)))
214 | ;;=> 3
215 |
216 | To bind multiple variables, just add more pairs to the `let`.
217 |
218 | 鳥> (let (x 3 y 4)
219 | (+ (* x 2) (* y 2)))
220 | ;;=> 14
221 |
222 | So far we've only had things printed out implicity as a result of
223 | evaluating them. The standard way to print things out in the middle
224 | of evaluation is with `out`. It takes multiple arguments, the first
225 | being the "port" to print to and the rest that print in order.
226 |
227 | Here's a variant of average that tells us what its arguments were:
228 |
229 | 鳥> (def average
230 | {x y | (do (out "my args were: " x " and " y crlf)
231 | (/ (+ x y) 2))})
232 | ;;=> [Function: curried]
233 |
234 | 鳥> (average 100 200)
235 | my args were: 100 and 200
236 | ;;=> 150
237 |
238 | A couple of points to note about the call to `out` is that the first
239 | argument is the "port" that the rest of the arguments are written to.
240 | In the case of the call above, the argument `` means that the
241 | print happens to the console (i.e. stdout). Also, the argument `crlf`
242 | indicates that a newline should be printed.
243 |
244 | The standard conditional operator is `if`. Like `def`, it doesn't
245 | evaluate all of its arguments. When given three arguments, it evaluates
246 | the first, and if that returns `true`, it returns the value of the
247 | second, otherwise the value of the third:
248 |
249 | 鳥> (if (odd? 1) 'a 'b)
250 | ;;=> "'a"
251 | 鳥> (if (even? 1) 'a 'b)
252 | ;;=> "'b"
253 |
254 | Returning `true` means returning anything except `nil`. `nil` is
255 | conventionally used to represent falsity as well as the empty list.
256 |
257 | 鳥> (if nil 'a 'b)
258 | ;;=> "'b"
259 |
260 | 鳥> nil
261 | ;;=> []
262 |
263 | The symbol `#t` is often used to represent truth, but any value other
264 | than `nil`, `false`, or `undefined` would serve just as well.
265 |
266 | 鳥> (odd? 1)
267 | ;;=> true
268 |
269 | 鳥> (odd? 2)
270 | ;;=> false
271 |
272 | If the third argument to `if` is missing the result is `undefined`.
273 |
274 | 鳥> (if #f 1)
275 | ;;=> undefined
276 |
277 | Each argument to if is a single expression, so if you want to do
278 | multiple things depending on the result of a test, combine them
279 | into one expression with `do`.
280 |
281 | 鳥> (do (out "hello" crlf)
282 | (+ 2 3))
283 | hello
284 | ;;=> 5
285 |
286 | If you just want several expressions to be evaluated when some
287 | condition is true, you could say
288 |
289 | 鳥> (if #t
290 | (do 1
291 | 2))
292 | ;;=> 2
293 |
294 | As shown, the last expression in the `do` block is the return value of
295 | the block itself.
296 |
297 | 鳥> (and nil
298 | (out "you'll never see this"))
299 | ;;=> []
300 |
301 | The negation operator is called `no`, a name that also works when
302 | talking about `nil` as the empty list. Here's a function to return
303 | the length of a list:
304 |
305 | 鳥> (def mylen
306 | (λ (xs)
307 | (if (no xs)
308 | 0
309 | (+ 1 (mylen (rest xs))))))
310 | ;;=> [Function: fn]
311 |
312 | If the list is `nil` the function will immediately return 0. Otherwise
313 | it returns 1 more than the length of the `rest` of the list.
314 |
315 | 鳥> (mylen nil)
316 | ;;=> 0
317 | 鳥> (mylen '(a b c))
318 | ;;=> 3
319 |
320 | I called it `mylen` because there's already a function called `len` for
321 | this. You're welcome to redefine TL functions, but redefining
322 | `len` this way might break code that depended on it, because `len` works
323 | on more than lists like strings and even functions.
324 |
325 | 鳥> (len "abc")
326 | ;;=> 3
327 | 鳥> (len {a | a})
328 | ;;=> 1
329 | 鳥> (len +)
330 | ;;=> 2
331 |
332 | The standard comparison operator is `is?`, which returns `true` if its
333 | arguments are identical or, if lexemes (i.e. strings and symbols), have the
334 | same characters.
335 |
336 | 鳥> (is? 'a 'a)
337 | ;;=> true
338 | 鳥> (is? "foo" "foo")
339 | ;;=> true
340 | 鳥> (let (x ['a]) (is? x x))
341 | ;;=> true
342 | 鳥> (is? ['a] ['a])
343 | ;;=> false
344 |
345 | Note that it returns false for two lists with the same elements.
346 | There's another operator for that, `eqv?` (from equivalent).
347 |
348 | 鳥> (eqv? ['a] ['a])
349 | ;;=> true
350 |
351 | If you want to test whether something is one of several alternatives,
352 | you could use `or`.
353 |
354 | 鳥> (let (x 'c)
355 | (or (is? x 'a) (is? x 'b) (is? x 'c)))
356 | ;;=> true
357 |
358 | The `cond` operator takes alternating expression pairs and returns
359 | the value of the expression after the first that returns a truthy
360 | value.
361 |
362 | 鳥> (def translate
363 | (λ (sym)
364 | (cond
365 | (is? sym 'apple) 'mela
366 | (is? sym 'onion) 'cipolla
367 | #t 'che?)))
368 | ;;=> [Function: fn]
369 |
370 | 鳥> (translate 'apple)
371 | ;;=> "'mela"
372 | 鳥> (translate 'syzygy)
373 | ;;=> "'che?"
374 |
375 | The `map` function takes a function and a list and returns the
376 | result of applying the function to successive elements.
377 |
378 | 鳥> (map (+ 10) '(1 2 3))
379 | ;;=> [ 11, 12, 13 ]
380 |
381 | Removing variables is a particularly good way to make programs
382 | shorter. An unnecessary variable increases the conceptual load of
383 | a program by more than just what it adds to the length.
384 |
385 | You can compose functions using the `comp` function. Composed
386 | functions are convenient as arguments.
387 |
388 | 鳥> (map (comp first rest) '((a b) (c d) (e f)))
389 | ;;=> [ "'b", "'d", "'f" ]
390 |
391 | You can also negate a function by calling `not` on its result.
392 |
393 | 鳥> (map (comp not odd?) '(1 2 3 4 5 6))
394 | ;;=> [ false, true, false, true, false, true ]
395 |
396 | There's another function siliar to `comp` that takes a number of
397 | functions and applies them in sequence, building a list of each
398 | of the results called `juxt`.
399 |
400 | 鳥> ((juxt first rest) '(1 2 3))
401 | ;;=> [ 1, [ 2, 3 ] ]
402 |
403 | There are a number of functions like `map` that apply functions to
404 | successive elements of a sequence. The most commonly used is `filter`,
405 | which returns the elements satisfying some test.
406 |
407 | 鳥> (filter odd? '(1 2 3 4 5 6 7))
408 | ;;=> [ 1, 3, 5, 7 ]
409 |
410 | Another includes `remove`, which does the opposite of `filter`.
411 |
412 | 鳥> (remove odd? '(1 2 3 4 5 6))
413 | ;;=> [ 2, 4, 6 ]
414 |
415 | Lists can be used to represent a wide variety of data structures,
416 | but if you want to store key/value pairs efficiently, TL also has
417 | hash tables.
418 |
419 | 鳥> (def airports (hash))
420 | ;;=> Map {}
421 | 鳥> (def airports2 (set airports "Boston" 'bos))
422 | ;;=> Map { 'Boston' => "'bos" }
423 | 鳥> airports
424 | ;;=> Map {}
425 | 鳥> airports2
426 | ;;=> Map { 'Boston' => "'bos" }
427 |
428 | If you want to create a hash table filled with values, you can use
429 | the `hash` function passing alternating key/value pairs which returns the
430 | corresponding hash table.
431 |
432 | 鳥> (hash 'x 1 'y 2)
433 | ;;=> Map { "'x" => 1, "'y" => 2 }
434 |
435 | Retrieving items from a hash map is done via the `get` function.
436 |
437 | 鳥> (let (m (hash 'x 1 'y 2))
438 | (get m 'x))
439 | ;;=> 1
440 | 鳥> (let (m (hash 'x 1 'y 2))
441 | (get m 'a))
442 | ;;=> undefined
443 |
444 | Like strings, hash tables can be used wherever functions are.
445 |
446 | 鳥> (let (m (hash 'x 1 'y 2))
447 | (m 'x))
448 | ;;=> 1
449 |
450 | 鳥> (def codes (hash "Boston" 'bos "San Francisco" 'sfo "Paris" 'cdg))
451 | ;;=> Map {'Boston' => "'bos", 'San Francisco' => "'sfo", 'Paris' => "'cdg" }
452 |
453 | 鳥> (map codes '("Paris" "Boston" "San Francisco"))
454 | ;;=> [ "'cdg", "'bos", "'sfo" ]
455 |
456 | The function `keys` returns the keys in a hash table, `vals` returns
457 | the values, and `pairs` returns an array of the key/value pairs in the
458 | hash.
459 |
460 | 鳥> (keys codes)
461 | ;;=> [ 'Boston', 'San Francisco', 'Paris' ]
462 | 鳥> (vals codes)
463 | ;;=> [ "'bos", "'sfo", "'cdg" ]
464 | 鳥> (pairs codes)
465 | ;;=> [ [ 'Boston', "'bos" ], [ 'San Francisco', "'sfo" ], [ 'Paris', "'cdg" ] ]
466 |
467 |
468 | The most general way to build strings is `str`, which takes any number of
469 | arguments and mushes them into a string:
470 |
471 | 鳥> (str 99 " bottles of " 'bee ['r])
472 | ;;=> '99 bottles of bee[r]'
473 |
474 | The `push` and `pop` operators treat lists as stacks, pushing a new
475 | element on the front and popping one off respectively but in a
476 | non-destructive way.
477 |
478 | 鳥> (def x '(c a b))
479 | ;;=> [ "'c", "'a", "'b" ]
480 | 鳥> (pop x)
481 | ;;=> [ "'a", "'b" ]
482 | 鳥> x
483 | ;;=> [ "'c", "'a", "'b" ]
484 | 鳥> (push x 'f)
485 | ;;=> [ "'f", "'c", "'a", "'b" ]
486 | 鳥> x
487 | ;;=> [ "'c", "'a", "'b" ]
488 |
489 | While managing stacks non-destructively is a powerful technique, there
490 | are circumstances where you might want to manipulate a stack in
491 | place. While lists cannot be changed by the TL mechanisms, there is
492 | a way to model imperative change via the `ref` type. A `ref` is
493 | analogous to a bucket that holds a value. While the value in the
494 | bucket does not itself change, we can change what's in the bucket
495 | by applying functions to it and storing the result in the bucket
496 | instead.
497 |
498 | 鳥> (def stack (ref '(c a b)))
499 | ;;=> Ref { _value: [ "'c", "'a", "'b" ], _validator: undefined }
500 |
501 | The `ref` named `stack` now holds the list that you'd like to
502 | manipulate. However, trying to pop the `ref` directly will not
503 | work. Instead, TL provides a couple of functions that will
504 | manipulate the contents of the `ref` -- namely `swap!` and `cas!`.
505 | The `swap!` function takes the ref that you want to change, a
506 | function to apply to the value in the `ref` and any additional
507 | arguments that the function needs to perform its action: [^1]
508 |
509 | 鳥> (swap! stack pop)
510 | ;;=> [ "'a", "'b" ]
511 | 鳥> (swap! stack push 'f)
512 | ;;=> [ "'f", "'a", "'b" ]
513 | 鳥> stack
514 | ;;=> Ref { _value: [ "'f", "'a", "'b" ], _validator: undefined }
515 |
516 | The `cas!` function works similarly except that it takes an
517 | additional value that denotes the value that you expect should
518 | be in the `ref` before the given function is applied. If the
519 | expected value does not match the actual value then the change
520 | fails.
521 |
522 | 鳥> (cas! stack '(f a b) pop)
523 | ;;=> [ "'a", "'b" ]
524 | 鳥> stack
525 | ;;=> Ref { _value: [ "'a", "'b" ], _validator: undefined }
526 |
527 | To get the value out of a `ref` the function `snap` is used. [^2]
528 |
529 | 鳥> (snap stack)
530 | ;;=> [ "'a", "'b" ]
531 |
532 | The `sort` function returns a copy of a sequence sorted according to
533 | the function given as the first argument.
534 |
535 | 鳥> (sort < '(2 9 3 7 5 1))
536 | ;;=> [ 1, 2, 3, 5, 7, 9 ]
537 |
538 | In practice the things one needs to sort are rarely just lists of
539 | numbers. More often you'll need to sort things according to some
540 | property other than their value, e.g.
541 |
542 | 鳥> (sort {l r | (< (len l) (len r))} '("orange" "pea" "apricot" "apple"))
543 | ;;=> [ 'pea', 'apple', 'orange', 'apricot' ]
544 |
545 | TL's sort is stable, meaning the relative positions of elements
546 | judged equal by the comparison function won't change:
547 |
548 | 鳥> (sort {l r | (< (len l) (len r))} '("aa" "bb" "cc"))
549 | ;;=> [ 'aa', 'bb', 'cc' ]
550 |
551 | To supply a list of arguments to a function, use `apply`:
552 |
553 | 鳥> (apply + '(1 2))
554 | ;;=> 3
555 |
556 | As with all functions, supplying fewer arguments than a function
557 | expects using `apply` will return a curried function.
558 |
559 | 鳥> (apply + '(1))
560 | ;;=> { [Function: curried] body: [Function: curried], params: [] }
561 | 鳥> ((apply + '(1)) 2)
562 | ;;=> 3
563 |
564 | Let's take a moment to talk about macros as most Lisps have them.
565 | Macros are basically functions that generate code. The code generated
566 | by macros is basically fed back into the Lisp interpreter and executed.
567 | This feature allows programmers to create custom syntaxtic structures
568 | that bottom out at Lisp primitives. Currently, TL doesn't have macros
569 | but via the use of currying and light-weight function syntax you can
570 | create interesting pseudo-syntax structures.
571 |
572 | The function `when` is a good example of a TL pseudo-syntax that is
573 | typically implemented as a macro in most Lisps.
574 |
575 | 鳥> (when {1} {
576 | (out "hello" crlf)
577 | 2
578 | })
579 | hello
580 | ;;=> 2
581 |
582 | The implementation of `when` is pretty simple:
583 |
584 | (def when
585 | (λ (condition body)
586 | (if (condition)
587 | (body)
588 | nil)))
589 |
590 | The use of the bracketed function syntax allows the deferal of the
591 | body of `when` until the conditional part evaluates as truthy.
592 |
593 | 鳥> (when {#f} {
594 | (out "hello" crlf)
595 | 2
596 | })
597 | ;;=> []
598 |
599 | We now know enough TL to read the definitions of some of the
600 | predefined functions. Here are a few of the simpler ones.
601 |
602 | (def dec {n | (- n 1)})
603 | (def inc (+ 1))
604 |
605 | (def second (comp first rest))
606 | (def third (-> rest rest first))
607 |
608 | (def map
609 | (λ (fn list)
610 | (if (no list)
611 | list
612 | (cons (fn (first list))
613 | (map fn (rest list))))))
614 |
615 | (def reverse
616 | (λ (list)
617 | (reduce {acc x | (cons x acc)} nil list)))
618 |
619 | (def reduce
620 | (λ (fn init list)
621 | (if (no list)
622 | init
623 | (reduce fn
624 | (fn init (first list))
625 | (rest list)))))
626 |
627 | (def foldr
628 | (λ (fn init list)
629 | (if (no list)
630 | init
631 | (fn (first list)
632 | (foldr fn init (rest list))))))
633 |
634 | (def filter
635 | (λ (fn list)
636 | (if (no list)
637 | list
638 | (if (fn (first list))
639 | (cons (first list) (filter fn (rest list)))
640 | (filter fn (rest list))))))
641 |
642 | These definitions are taken from core.lisp. As its name suggests,
643 | reading that file is a good way to learn more about both TL and
644 | functional programming techniques. Nothing in it is used before it's
645 | defined; it is an exercise in building the part of the language
646 | written in TL up from the "axioms" defined in lisp.js. I hoped this
647 | would yield a simple language. The definitions in core.lisp are also
648 | an experiment in another way. They are the language spec.
649 |
650 | It may sound rather dubious to say that the only spec for something
651 | is its implementation. It sounds like the sort of thing one might
652 | say about C++, or the Common Lisp loop macro. But that's also how
653 | math works. If the implementation is sufficiently abstract, it
654 | starts to be a good idea to make specification and implementation
655 | identical.
656 |
657 | I agree with Abelson and Sussman that programs should be written
658 | primarily for people to read rather than machines to execute. The
659 | Lisp defined as a model of computation in McCarthy's original paper
660 | was. It seems worth trying to preserve this as you grow Lisp into
661 | a language for everyday use.
662 |
663 | Notes
664 | =====
665 |
666 | [^1]: The `ref` type is how one would implement something like the
667 | ubitquitous counter.
668 |
669 | (let (counter (ref 0))
670 | (def new-id {(swap! counter (+ 1))})
671 | (def reset-id {(swap! counter {0})}))
672 |
673 | Its use being:
674 |
675 | 鳥> (new-id)
676 | ;;=> 1
677 | 鳥> (new-id)
678 | ;;=> 2
679 | 鳥> (reset-id)
680 | ;;=> 0
681 |
682 | [^2]: The `ref` function also accepts an optional *validation* function
683 | that verifies that every change to the `ref` adheres to the function.
684 | If the value in the `ref` falses a validation check then the value
685 | `undefined` is returned and the value in the `ref` remains unchanged.
686 |
687 | 鳥> (def bad (ref 1 pos?))
688 | ;;=> Ref { _value: 1, _validator: [Function: function...] }
689 | 鳥> (swap! bad + -10)
690 | ;;=> undefined
691 | 鳥> (snap bad)
692 | ;;=> 1
693 |
694 |
695 |
696 |
697 |
698 | 鳥>
699 | 鳥>
700 | 鳥>
701 | 鳥>
702 | 鳥>
703 | 鳥>
704 | 鳥>
705 | 鳥>
706 |
707 |
--------------------------------------------------------------------------------