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