├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── docs ├── klisp.gif ├── nightvale.gif └── term.png ├── lib ├── klisp.klisp └── math.klisp ├── nightvale.service ├── src ├── cli.ink ├── core.ink ├── klisp.ink ├── nightvale.ink └── tests.ink ├── static ├── css │ ├── main.css │ └── paper.min.css ├── index.html └── js │ ├── auto-render.katex.min.js │ ├── katex.min.js │ ├── main.js │ ├── markus.js │ └── torus.min.js ├── test ├── 000.klisp ├── 001.klisp ├── 002.klisp ├── 003.klisp ├── 004.klisp ├── 005.klisp ├── 006.klisp ├── 007.klisp ├── 008.klisp ├── 009.klisp ├── collatz.klisp ├── eval.klisp └── tco.klisp ├── util ├── ink-linux └── klisp.vim └── vendor ├── auth.ink ├── http.ink ├── mime.ink ├── percent.ink ├── route.ink ├── std.ink ├── str.ink └── suite.ink /.gitignore: -------------------------------------------------------------------------------- 1 | db/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Linus Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: ci 2 | 3 | # run main binary 4 | run: 5 | ink ./src/cli.ink test/000.klisp 6 | ink ./src/cli.ink test/001.klisp 7 | ink ./src/cli.ink test/002.klisp 8 | ink ./src/cli.ink test/003.klisp 9 | ink ./src/cli.ink test/004.klisp 10 | ink ./src/cli.ink test/005.klisp 11 | ink ./src/cli.ink test/006.klisp 12 | ink ./src/cli.ink test/007.klisp 13 | ink ./src/cli.ink test/008.klisp 14 | ink ./src/cli.ink test/009.klisp 15 | ink ./src/cli.ink test/eval.klisp 16 | 17 | # run as repl 18 | repl: 19 | rlwrap ink ./src/cli.ink 20 | 21 | # run nightvale server on an auto-restart loop 22 | serve: 23 | until ink ./src/cli.ink --port 7900; do echo 'Re-starting Nightvale...'; done 24 | 25 | # run all tests under test/ 26 | check: run 27 | ink ./src/tests.ink 28 | t: check 29 | 30 | fmt: 31 | inkfmt fix src/*.ink 32 | f: fmt 33 | 34 | fmt-check: 35 | inkfmt src/*.ink 36 | fk: fmt-check 37 | 38 | configure: 39 | cp util/klisp.vim ~/.vim/syntax/klisp.vim 40 | 41 | install: 42 | sudo echo '#!/bin/sh' > /usr/local/bin/klisp 43 | sudo echo rlwrap `pwd`/src/cli.ink '$$*' >> /usr/local/bin/klisp 44 | sudo chmod +x /usr/local/bin/klisp 45 | 46 | # run by CI, uses vendored Ink binary 47 | ci: 48 | ./util/ink-linux ./src/cli.ink test/000.klisp 49 | ./util/ink-linux ./src/cli.ink test/001.klisp 50 | ./util/ink-linux ./src/cli.ink test/002.klisp 51 | ./util/ink-linux ./src/cli.ink test/003.klisp 52 | ./util/ink-linux ./src/cli.ink test/004.klisp 53 | ./util/ink-linux ./src/cli.ink test/005.klisp 54 | ./util/ink-linux ./src/cli.ink test/006.klisp 55 | ./util/ink-linux ./src/cli.ink test/007.klisp 56 | ./util/ink-linux ./src/cli.ink test/008.klisp 57 | ./util/ink-linux ./src/cli.ink test/009.klisp 58 | ./util/ink-linux ./src/cli.ink test/eval.klisp 59 | ./util/ink-linux ./src/tests.ink 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Klisp 🐍 2 | 3 | [![Build Status](https://travis-ci.com/thesephist/klisp.svg?branch=main)](https://travis-ci.com/thesephist/klisp) 4 | 5 | **Klisp** is a very minimal **LISP** written in about 200 lines of [In**K**](https://dotink.co). It's primarily a pedagogical project -- I made it to understand Lisp better. (That's another way of saying: don't use it for serious things.) Ink's semantics are already quite lispy, so Klisp builds on Ink's semantics and adds an S-expression grammar and a repl, a true read-eval-print loop. 6 | 7 | ![Examples in a Klisp repl](docs/term.png) 8 | 9 | Syntactically, Klisp borrows from Scheme and Clojure, but tries to be as simple as possible without losing power. You can find some working examples of Klisp code in... 10 | 11 | - [The core library, `klisp.klisp`](lib/klisp.klisp) 12 | - [A simple prime sieve](test/003.klisp) 13 | - [Integration test cases](test/) 14 | 15 | For example, a factorial is easily defined as the product of a range of integers. 16 | 17 | ```lisp 18 | (defn fact (n) 19 | (prod (range 1 (inc n) 1))) 20 | (fact 10) ; => 3628800 21 | ``` 22 | 23 | And the Fibonacci sequence is recursively defined. 24 | 25 | ```lisp 26 | (defn fib (n) 27 | (if (< n 2) 28 | 1 29 | (+ (fib (dec n)) 30 | (fib (dec (dec n)))))) 31 | (fib 10) ; => 89 32 | (map (seq 20) fib) 33 | ; => (1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765) 34 | ``` 35 | 36 | ## About Klisp 37 | 38 | Klisp is a true lisp-1, with a minimal core of 6 special forms (`quote`, `do`, `def`, `if`, `fn`, `macro`) operating on S-expressions. The reader, evaluator, and printer in Klisp are all implemented as [single Ink functions](src/klisp.ink) operating directly on S-expressions and strings. 39 | 40 | - **quote** takes its argument list as a Klisp list and returns it. `(quote (a b c)) => (a b c)` 41 | - **do** evaluates all forms in its arguments and returns the result of the last form. `(do (+ 1 2) (* 3 4)) => 12` 42 | - **def** takes a name and a value, and binds the value to the name in the current environment ("scope"). Only function bodies create new scopes. `(do (def x 3) (* x x)) => 9` 43 | - **if** returns either a consequent or an alternative depending on a condition. `(if (= 1 2) 'A' 'B') => 'B'` 44 | - **fn** creates a lambda expression (a function). It takes a list of arguments and a function body. It behaves like `lambda` in scheme. 45 | ```lisp 46 | (do 47 | (def double 48 | (fn (n) (* 2 n))) 49 | (double 12)) ; => 24 50 | ``` 51 | - **macro** is like `fn`, but defines a macro instead of a normal function. For example, the `list` macro is implemented in this way. There is an accompanying builtin function `expand`, which expands a list representing a macro expression without evaluating it. Use `expand-all` to apply this recursively to an expression. 52 | 53 | These special forms, along with a small set of builtin functions like arithmetic operators and `car` / `cdr` / `cons`, are provided in the default environment. Every other language feature, including short-circuiting binary operators like `and` and `or` and fundamental forms like `let` and `list`, is implemented in the userspace in `lib/klisp.klisp` as functions or macros. 54 | 55 | ### Usage 56 | 57 | If you have [Ink](https://dotink.co) installed in your `$PATH`, the simplest way to try Klisp is to clone the repository and run `make repl`, which will start a read-eval-print loop. You can also run `./src/cli.ink .klisp` to run the interpreter over a source file, like `test/000.klisp`. 58 | 59 | To run the repl, you'll also want `rlwrap` installed for the best experience. `rlwrap` provides a line-editable input to the repl. 60 | 61 | ```sh 62 | $ git clone https://github.com/thesephist/klisp 63 | $ cd klisp 64 | $ make repl 65 | 66 | rlwrap ink ./src/cli.ink 67 | Klisp interpreter v0.1. 68 | > (+ 1 2 3) 69 | 6 70 | > (defn add1 (n) (+ n 1)) 71 | (fn (n) (+ n 1)) 72 | > (add1 41) 73 | 42 74 | > ; start typing... 75 | ``` 76 | 77 | Klisp is composed of two Ink source files, `src/klisp.ink` and `src/cli.ink`, and one library file, `lib/klisp.klisp`. Klisp also depends on the `std` and `str` libraries in `vendor/`. As long as these 5 files are accessible in those directories, running `ink src/cli.ink` will start the interpreter. 78 | 79 | ### Non-goals 80 | 81 | Klisp has two significant flaws that were non-goals for this project. 82 | 83 | 1. Klisp's current interpreter **does not handle errors very well**. This was mostly an educational project, and I don't think I'll end up writing much Klisp code, so preferred concision over error recovery in the interpreter. If I do end up writing lots of Klisp, I might come back and add better error handling in the interpreter. As it stands today, syntactic or semantic errors in Klisp code will crash the interpreter. 84 | 2. Klisp is **not fast**. Actually, it's quite slow. That's because it's written in a dynamic, interpreted language itself. The Ink interpreter I've been testing Klisp with is a [tree-walk interpreter written in Go](https://github.com/thesephist/ink), which is itself comparable to Python on a good day. Although faster, alternative interpreters like [Vanta, written in Go](https://github.com/thesephist/vanta), are being worked on, Klisp isn't designed to be fast, just an educational prototype mostly for myself. 85 | 86 | ## Implementation 87 | 88 | As noted above, Klisp's semantics match Ink's closely, because Ink is semantically already lispy, with primitive values and one complex type (a dictionary/associative array, which is used to implement lists in Klisp). 89 | 90 | Most Klisp values, except for the symbol (atom), the list, and the function, are implemented transparently in the interpreter as Ink values. For example, the number 42 in Klisp is also just the number `42` within the Interpreter -- no boxing or wrapper types. 91 | 92 | **Symbols** in Klisp are implemented as strings, which are variable-length byte arrays. To differentiate symbols from strings, symbols are prefixed with the null character `char(0)`. 93 | 94 | **Lists** are implemented with cons cells, and cons cells are implemented with a list of length 2 (`[_, _]`) in the underlying Ink code. 95 | 96 | **Functions and macros** are implemented using an indirection using an object in the interpreter that wraps a function taking Klisp values as arguments. You can read more about this design in [the interpreter source](https://github.com/thesephist/klisp/blob/main/src/klisp.ink#L188). 97 | 98 | The rest of the ~200 lines of the interpreter core in `src/klisp.ink` are well commented and written to be legible. If you're curious about Klisp's inner workings, the source code is a great starting point. If you have questions, feel free to [open an issue](https://github.com/thesephist/klisp/issues). 99 | -------------------------------------------------------------------------------- /docs/klisp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/klisp/44863053d659fe238477567cb891b19997cfe96a/docs/klisp.gif -------------------------------------------------------------------------------- /docs/nightvale.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/klisp/44863053d659fe238477567cb891b19997cfe96a/docs/nightvale.gif -------------------------------------------------------------------------------- /docs/term.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/klisp/44863053d659fe238477567cb891b19997cfe96a/docs/term.png -------------------------------------------------------------------------------- /lib/klisp.klisp: -------------------------------------------------------------------------------- 1 | ; core library 2 | 3 | ; cons cell shorthands 4 | (def caar 5 | (fn (x) (car (car x)))) 6 | (def cadr 7 | (fn (x) (car (cdr x)))) 8 | (def cdar 9 | (fn (x) (cdr (car x)))) 10 | (def cddr 11 | (fn (x) (cdr (cdr x)))) 12 | 13 | ; lazy-evaluating boolean combinators 14 | (def ! 15 | (fn (x) 16 | (if x false true))) 17 | (def & 18 | (macro (terms) 19 | (if (= terms ()) 20 | ,true 21 | (cons ,if 22 | (cons (car terms) 23 | (cons (cons ,& (cdr terms)) 24 | (cons ,false ()))))))) 25 | 26 | (def | 27 | (macro (terms) 28 | (if (= terms ()) 29 | ,false 30 | (cons ,if 31 | (cons (car terms) 32 | (cons ,true 33 | (cons (cons ,| (cdr terms)) 34 | ()))))))) 35 | (def ^ 36 | (macro (terms) 37 | (cons ,! 38 | (cons (cons ,= terms) 39 | ())))) 40 | (def eq? =) 41 | (def not !) 42 | (def and &) 43 | (def or |) 44 | (def != ^) 45 | (def xor ^) 46 | (def neq? ^) 47 | 48 | ; type assertions 49 | (def nil? 50 | (fn (x) (= x ()))) 51 | (def zero? 52 | (fn (x) (= x 0))) 53 | (def number? 54 | (fn (x) (= (type x) 'number'))) 55 | (def boolean? 56 | (fn (x) (= (type x) 'boolean'))) 57 | (def string? 58 | (fn (x) (= (type x) 'string'))) 59 | (def symbol? 60 | (fn (x) (= (type x) 'symbol'))) 61 | (def function? 62 | (fn (x) (= (type x) 'function'))) 63 | (def list? 64 | (fn (x) (= (type x) 'list'))) 65 | (def pair? 66 | (fn (x) 67 | (& (list? x) 68 | (= (size x) 2)))) 69 | 70 | ; identity 71 | (def id 72 | (fn (x) x)) 73 | 74 | (def gensym 75 | (fn () 76 | (-> (rand) 77 | (* 100000000) 78 | floor 79 | number->string 80 | ((fn (s) (+ 'sym' s))) 81 | string->symbol))) 82 | 83 | ; basic math 84 | (def neg 85 | (fn (n) (- 0 n))) 86 | (def neg? 87 | (fn (n) (< n 0))) 88 | (def abs 89 | (fn (n) 90 | (if (neg? n) 91 | (neg n) 92 | n))) 93 | (def sign 94 | (fn (n) 95 | (if (neg? n) 96 | (neg 1) 97 | 1))) 98 | (def sqrt 99 | (fn (n) (# n 0.5))) 100 | (def even? 101 | (fn (n) (zero? (% n 2)))) 102 | (def odd? 103 | (fn (n) (! (even? n)))) 104 | (def >= 105 | (fn (a b) (! (< a b)))) 106 | (def <= 107 | (fn (a b) (! (> a b)))) 108 | (def inc 109 | (fn (n) (+ n 1))) 110 | (def dec 111 | (fn (n) (- n 1))) 112 | (def sum 113 | (fn (ns) (reduce ns + 0))) 114 | (def prod 115 | (fn (ns) (reduce ns * 1))) 116 | (def times 117 | ; repeat x, n times in a list 118 | (fn (n x) 119 | (map (range 0 n 1) 120 | (fn () x)))) 121 | 122 | ; macros 123 | (def when 124 | (macro (terms) 125 | (list ,if (car terms) (cadr terms) ()))) 126 | 127 | (def unless 128 | (macro (terms) 129 | (list ,if (car terms) () (cadr terms)))) 130 | 131 | (def let 132 | (macro (terms) 133 | (do 134 | (def decl (car terms)) 135 | (def declname (car decl)) 136 | (def declval (cadr decl)) 137 | (def body (cadr terms)) 138 | (list 139 | (list ,fn (list declname) body) 140 | declval)))) 141 | 142 | (def list 143 | (macro (items) 144 | ((def -list 145 | (fn (items) 146 | (if (nil? items) 147 | () 148 | (cons ,cons 149 | (cons (car items) 150 | (cons (-list (cdr items)) 151 | ())))))) 152 | items))) 153 | 154 | (def quasiquote 155 | (macro (terms) 156 | (cons 157 | ,list 158 | (map (car terms) 159 | (fn (term) 160 | (if (list? term) 161 | (if (= ,unquote (car term)) 162 | (cadr term) 163 | (list ,quasiquote term)) 164 | (list ,quote term))))))) 165 | 166 | (def do-times 167 | (macro (terms) 168 | (cons ,do 169 | (times (car terms) (list (cadr terms)))))) 170 | 171 | ; (while condition . body) 172 | (def while 173 | (macro (terms) 174 | (do 175 | (def cndn (car terms)) 176 | (def body (cdr terms)) 177 | (def -while-f (gensym)) 178 | (quasiquote 179 | ((def (unquote -while-f) 180 | (fn () 181 | (if (unquote cndn) 182 | (do 183 | (unquote (cons ,do body)) 184 | ((unquote -while-f))) 185 | ())))))))) 186 | 187 | ; shorthand for defining functions in scope 188 | (def defn 189 | (macro (terms) 190 | (quasiquote 191 | (def (unquote (car terms)) 192 | (fn (unquote (cadr terms)) 193 | (unquote (car (cddr terms)))))))) 194 | 195 | ; (cond (pred body) (pred body) (default-body)) 196 | (def cond 197 | (macro (terms) 198 | ((def -cond 199 | (fn (terms) 200 | (if (nil? terms) 201 | () 202 | (if (nil? (cdar terms)) 203 | (caar terms) 204 | (quasiquote 205 | (if (unquote (car (car terms))) 206 | (unquote (cadr (car terms))) 207 | (unquote (-cond (cdr terms))))))))) 208 | terms))) 209 | 210 | ; (match val (tag body) (tag body) (default-body)) 211 | (def match 212 | (macro (terms) 213 | (do 214 | (def -match-val (gensym)) 215 | (def -match 216 | (fn (terms) 217 | (if (nil? terms) 218 | () 219 | (if (nil? (cdar terms)) 220 | (caar terms) 221 | (quasiquote 222 | (if (= (unquote -match-val) (unquote (car (car terms)))) 223 | (unquote (cadr (car terms))) 224 | (unquote (-match (cdr terms))))))))) 225 | (quasiquote 226 | (let ((unquote -match-val) (unquote (car terms))) 227 | (unquote (-match (cdr terms)))))))) 228 | 229 | ; thread-first 230 | (def -> 231 | (macro (terms) 232 | (do 233 | (def apply-partials 234 | (fn (partials expr) 235 | (if (nil? partials) 236 | expr 237 | (if (symbol? (car partials)) 238 | (list (car partials) 239 | (apply-partials (cdr partials) expr)) 240 | (cons (caar partials) 241 | (cons (apply-partials (cdr partials) expr) 242 | (cdar partials))))))) 243 | (apply-partials (reverse (cdr terms)) 244 | (car terms))))) 245 | 246 | ; thread-last 247 | (def ->> 248 | (macro (terms) 249 | (do 250 | (def apply-partials 251 | (fn (partials expr) 252 | (if (nil? partials) 253 | expr 254 | (if (symbol? (car partials)) 255 | (list (car partials) 256 | (apply-partials (cdr partials) expr)) 257 | (append (car partials) 258 | (apply-partials (cdr partials) expr)))))) 259 | (apply-partials (reverse (cdr terms)) 260 | (car terms))))) 261 | 262 | ; partial application 263 | (def partial 264 | (macro (terms) 265 | (let (-partial-arg (gensym)) 266 | (list ,fn 267 | (cons -partial-arg ()) 268 | (map (car terms) 269 | (fn (x) 270 | (if (= x ,_) -partial-arg x))))))) 271 | 272 | ; macro expansion functions and macros 273 | (def macroexpand 274 | (macro (terms) 275 | (quasiquote (expand (quote (unquote (car terms))))))) 276 | 277 | (def expand-all 278 | (fn (expr) 279 | (if (list? expr) 280 | (let (expanded (expand expr)) 281 | (if (list? expanded) 282 | (map expanded expand-all) 283 | expanded)) 284 | expr))) 285 | 286 | (def macroexpand-all 287 | (macro (terms) 288 | (quasiquote (expand-all (quote (unquote (car terms))))))) 289 | 290 | ; list methods 291 | (def nth 292 | (fn (xs i) 293 | (if (zero? i) 294 | (car xs) 295 | (nth (cdr xs) (dec i))))) 296 | 297 | (def nth? 298 | (fn (xs i x) 299 | (if (zero? i) 300 | (= (car xs) x) 301 | (nth? (cdr xs) (dec i) x)))) 302 | 303 | (def last 304 | (fn (xs) 305 | (if (nil? xs) 306 | () 307 | (if (nil? (cdr xs)) 308 | (car xs) 309 | (last (cdr xs)))))) 310 | 311 | (def index 312 | (fn (xs x) 313 | (do 314 | (def index-from 315 | (fn (xs x rest) 316 | (if (nil? xs) 317 | (neg 1) 318 | (if (= (car xs) x) 319 | rest 320 | (index-from (cdr xs) x (inc rest)))))) 321 | (index-from xs x 0)))) 322 | 323 | (def find 324 | (fn (xs f?) 325 | (if (nil? xs) 326 | () 327 | (if (f? (car xs)) 328 | (car xs) 329 | (find (cdr xs) f?))))) 330 | 331 | (def some? 332 | (fn (xs) 333 | (if (nil? xs) 334 | false 335 | (if (car xs) 336 | true 337 | (some? (cdr xs)))))) 338 | 339 | (def every? 340 | (fn (xs) 341 | (if (nil? xs) 342 | true 343 | (if (car xs) 344 | (every? (cdr xs)) 345 | false)))) 346 | 347 | (def min 348 | (fn (xs) 349 | (if (nil? xs) 350 | () 351 | (reduce xs 352 | (fn (a b) 353 | (if (< a b) a b)) 354 | (car xs))))) 355 | 356 | (def max 357 | (fn (xs) 358 | (if (nil? xs) 359 | () 360 | (reduce xs 361 | (fn (a b) 362 | (if (< a b) b a)) 363 | (car xs))))) 364 | 365 | (def contains? 366 | (fn (xs x) 367 | (<= 0 (index xs x)))) 368 | 369 | ; O(n^2) behavior with linked lists 370 | (def append 371 | (fn (xs el) 372 | (if (nil? xs) 373 | (list el) 374 | (cons (car xs) 375 | (append (cdr xs) el))))) 376 | 377 | (def join 378 | (fn (xs ys) 379 | (if (nil? xs) 380 | ys 381 | (cons (car xs) 382 | (if (nil? (cdr xs)) 383 | ys 384 | (join (cdr xs) ys)))))) 385 | 386 | (def range 387 | (fn (start end step) 388 | ; intentionally avoiding then when macro for efficiency 389 | (if (< start end) 390 | (cons start 391 | (range (+ start step) end step)) 392 | ()))) 393 | 394 | (def seq 395 | (fn (n) (range 0 n 1))) 396 | 397 | (def nat 398 | (fn (n) (range 1 (inc n) 1))) 399 | 400 | (def reverse 401 | (fn (x) 402 | (if (nil? x) 403 | x 404 | (append (reverse (cdr x)) 405 | (car x))))) 406 | 407 | (def map 408 | (fn (xs f) 409 | (if (nil? xs) 410 | () 411 | (cons (f (car xs)) 412 | (map (cdr xs) f))))) 413 | 414 | (def map-deep 415 | (fn (xs f) 416 | (map xs (fn (x) 417 | (if (list? x) 418 | (map-deep x f) 419 | (f x)))))) 420 | 421 | (def reduce 422 | (fn (xs f acc) 423 | (if (nil? xs) 424 | acc 425 | (reduce (cdr xs) f (f acc (car xs)))))) 426 | 427 | (def filter 428 | (fn (xs f) 429 | (if (nil? xs) 430 | () 431 | (if (f (car xs)) 432 | (cons (car xs) 433 | (filter (cdr xs) f)) 434 | (filter (cdr xs) f))))) 435 | 436 | (def each 437 | (fn (xs f) 438 | (if (nil? xs) 439 | () 440 | (do 441 | (f (car xs)) 442 | (each (cdr xs) f))))) 443 | 444 | (def size 445 | (fn (xs) 446 | (if (nil? xs) 447 | 0 448 | (inc (size (cdr xs)))))) 449 | 450 | (def zip-with 451 | (fn (xs ys f) 452 | (if (| (nil? xs) (nil? ys)) 453 | () 454 | (cons (f (car xs) (car ys)) 455 | (zip-with (cdr xs) (cdr ys) f))))) 456 | 457 | (def zip 458 | (fn (xs ys) 459 | (zip-with xs ys list))) 460 | 461 | (def take 462 | (fn (xs n) 463 | (if (| (nil? xs) (zero? n)) 464 | () 465 | (cons (car xs) 466 | (take (cdr xs) (dec n)))))) 467 | 468 | (def drop 469 | (fn (xs n) 470 | (if (| (nil? xs) (zero? n)) 471 | xs 472 | (drop (cdr xs) (dec n))))) 473 | 474 | (def flatten 475 | (fn (xs) 476 | (reduce xs join ()))) 477 | 478 | (def partition 479 | (fn (xs n) 480 | (if (nil? xs) 481 | () 482 | (cons (take xs n) 483 | (partition (drop xs n) n))))) 484 | 485 | ; string functions 486 | (def cat 487 | (fn (xs joiner) 488 | (if (nil? xs) 489 | '' 490 | (do 491 | (def cat-onto 492 | (fn (xs prefix) 493 | (if (nil? xs) 494 | prefix 495 | (cat-onto (cdr xs) 496 | (+ prefix joiner (car xs)))))) 497 | (cat-onto (cdr xs) (car xs)))))) 498 | 499 | (defn char-at (s i) 500 | (gets s i (inc i))) 501 | 502 | ; composites: persistent immutable associative array 503 | ; 504 | ; comps store key-value pairs in a list as 505 | ; ((key . value) (key . value) (key . value)) for O(n) lookup and O(1) insert. 506 | ; Each entry is a single cons cell rather than a list to make value lookup a bit 507 | ; more efficient. 508 | (def comp 509 | (macro (terms) 510 | (do 511 | (def -comp 512 | (fn (items) 513 | (if (nil? items) 514 | () 515 | (list ,cons 516 | (list ,cons (car items) (cadr items)) 517 | (-comp (cddr items)))))) 518 | (-comp terms)))) 519 | 520 | ; recursive value lookup by key 521 | (def getc 522 | (fn (cp k) 523 | (if (nil? cp) 524 | () 525 | (if (= k (caar cp)) 526 | (cdar cp) 527 | (getc (cdr cp) k))))) 528 | 529 | ; comps are immutable, and new values are set by adding new entries 530 | ; to the head of the comp's underlying list. setc does not modify the 531 | ; given comp and returns a new comp with the new key, value set. 532 | (def setc 533 | (fn (cp k v) 534 | (cons (cons k v) cp))) 535 | 536 | ; get just the comp keys 537 | (def keys 538 | (fn (cp) 539 | (map cp car))) 540 | 541 | ; get just the comp values 542 | (def values 543 | (fn (cp) 544 | (map cp cdr))) 545 | 546 | ; utilities 547 | (def println 548 | (macro (terms) 549 | ; we expand the macro manually here 550 | ; because println should be as fast as possible 551 | (cons ,do 552 | (cons (cons ,print terms) 553 | (cons ,(print (char 10)) 554 | ()))))) 555 | 556 | (def comment 557 | ; add "(comment val)" to an expr head 558 | ; to substitute the expr with "val" 559 | (macro (terms) (car terms))) 560 | 561 | (def log-runtime 562 | ; prints runtime (finish - start) of an expression 563 | (macro (terms) 564 | (let (-val (gensym)) 565 | (quasiquote 566 | (do 567 | (def start (time)) 568 | (def (unquote -val) (unquote (cadr terms))) 569 | (println (+ 'Runtime for ' (unquote (car terms)) ':') 570 | (number->string (* 1000 (- (time) start))) 571 | 'ms') 572 | (unquote -val)))))) 573 | 574 | -------------------------------------------------------------------------------- /lib/math.klisp: -------------------------------------------------------------------------------- 1 | ; math library 2 | ; depends on klisp.klisp 3 | 4 | ; Euclid's GCD algorithm 5 | (defn gcd (a b) 6 | ; prereq: a < b 7 | (do 8 | (defn sub (a b) 9 | (if (zero? a) 10 | b 11 | (sub (% b a) a))) 12 | (def a (abs a)) 13 | (def b (abs b)) 14 | (if (> a b) 15 | (sub b a) 16 | (sub a b)))) 17 | 18 | ; LCM using GCD 19 | (defn lcm (a b) 20 | (* a (/ b (gcd a b)))) 21 | 22 | (defn factor? (n c) 23 | (zero? (% n c))) 24 | 25 | ; prime filter 26 | (defn prime? (n) 27 | (if (< n 2) 28 | false 29 | (do 30 | (def max (inc (floor (sqrt n)))) 31 | (defn sub (i) 32 | (if (= i max) 33 | true 34 | (if (factor? n i) 35 | false 36 | (sub (inc i))))) 37 | (sub 2)))) 38 | 39 | ; prime factorize natural number 40 | (defn prime-factors (n) 41 | (do 42 | (defn sub (pfs m pf) 43 | (if (= m 1) 44 | pfs 45 | (if (factor? m pf) 46 | (sub (cons pf pfs) 47 | (/ m pf) 48 | pf) 49 | (sub pfs 50 | m 51 | (inc pf))))) 52 | (reverse (sub () n 2)))) 53 | 54 | ; naive factorize 55 | (defn factors (n) 56 | (let (first-half (-> (nat (floor (sqrt n))) 57 | (filter (partial (factor? n _))))) 58 | (cond 59 | ((nil? first-half) first-half) 60 | ((nil? (cdr first-half)) first-half) 61 | (true 62 | (join first-half 63 | (let (rev-first-half (reverse first-half)) 64 | (if (= (car rev-first-half) 65 | (/ n (car rev-first-half))) 66 | (cdr (map rev-first-half (partial (/ n _)))) 67 | (map rev-first-half (partial (/ n _)))))))))) 68 | 69 | (defn randi (max) 70 | (floor (* (rand) max))) 71 | 72 | (defn mean (xs) 73 | (/ (sum xs) (size xs))) 74 | (def avg mean) 75 | 76 | (defn geomean (xs) 77 | (# (prod xs) (/ 1 (size xs)))) 78 | 79 | -------------------------------------------------------------------------------- /nightvale.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=nightvale notebook server (klisp) 3 | ConditionPathExists=/home/nightvale-user/go/bin/ink 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | User=nightvale-user 9 | LimitNOFILE=1024 10 | PermissionsStartOnly=true 11 | 12 | Restart=on-failure 13 | RestartSec=100ms 14 | StartLimitIntervalSec=60 15 | 16 | WorkingDirectory=/home/nightvale-user/klisp 17 | ExecStart=/home/nightvale-user/go/bin/ink ./src/cli.ink --port 7900 18 | 19 | # make sure log directory exists and owned by syslog 20 | PermissionsStartOnly=true 21 | ExecStartPre=/bin/mkdir -p /var/log/nightvale 22 | ExecStartPre=/bin/chown syslog:adm /var/log/nightvale 23 | ExecStartPre=/bin/chmod 755 /var/log/nightvale 24 | StandardOutput=syslog 25 | StandardError=syslog 26 | SyslogIdentifier=nightvale 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | -------------------------------------------------------------------------------- /src/cli.ink: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ink 2 | 3 | ` Klisp CLI ` 4 | 5 | std := load('../vendor/std') 6 | 7 | log := std.log 8 | f := std.format 9 | scan := std.scan 10 | clone := std.clone 11 | slice := std.slice 12 | cat := std.cat 13 | map := std.map 14 | readFile := std.readFile 15 | writeFile := std.writeFile 16 | 17 | klisp := load('klisp') 18 | 19 | read := klisp.read 20 | eval := klisp.eval 21 | print := klisp.print 22 | symbol := klisp.symbol 23 | setenv := klisp.setenv 24 | makeNative := klisp.makeNative 25 | reduceL := klisp.reduceL 26 | Env := klisp.Env 27 | 28 | core := load('core') 29 | 30 | withLibs := core.withLibs 31 | withCore := core.withCore 32 | 33 | nightvale := load('nightvale') 34 | 35 | safeEval := nightvale.evalAtMostSteps 36 | 37 | Version := '0.1' 38 | ` after some testing, considering Go/Ink's stack limit, 39 | this seemed like a reasonable number for a web repl ` 40 | MaxWebReplSteps := 100000 41 | 42 | Args := args() 43 | Args.2 :: { 44 | '--port' -> number(Args.3) :: { 45 | () -> log(f('{{0}} is not a number', [Args.3])) 46 | _ -> withCore(env => ( 47 | http := load('../vendor/http') 48 | mime := load('../vendor/mime') 49 | percent := load('../vendor/percent') 50 | 51 | mimeForPath := mime.forPath 52 | pctDecode := percent.decode 53 | 54 | server := (http.new)() 55 | MethodNotAllowed := {status: 405, body: 'method not allowed'} 56 | 57 | addRoute := server.addRoute 58 | addRoute('/eval', params => (req, end) => req.method :: { 59 | 'POST' -> end({ 60 | status: 200 61 | headers: {'Content-Type': 'text/plain'} 62 | body: ( 63 | stdout := '' 64 | localEnv := clone(env) 65 | printResp := s => stdout.len(stdout) := s 66 | setenv(localEnv, symbol('print'), makeNative(L => printResp(reduceL( 67 | L.1, (a, b) => a + ' ' + (type(b) :: { 68 | 'string' -> b 69 | _ -> print(b) 70 | }) 71 | type(L.0) :: { 72 | 'string' -> L.0 73 | _ -> print(L.0) 74 | } 75 | )))) 76 | 77 | out := print(safeEval(read(req.body), localEnv, MaxWebReplSteps)) 78 | log(f('(eval {{0}}) => {{1}}', [req.body, out])) 79 | stdout + out 80 | ) 81 | }) 82 | _ -> end(MethodNotAllowed) 83 | }) 84 | addRoute('/doc/*docID', params => (req, end) => req.method :: { 85 | 'GET' -> ( 86 | dbPath := 'db/' + params.docID 87 | readFile(dbPath, file => file :: { 88 | () -> end({status: 404, body: 'doc not found'}) 89 | _ -> end({ 90 | status: 200 91 | headers: {'Content-Type': 'application/json'} 92 | body: file 93 | }) 94 | }) 95 | ) 96 | 'PUT' -> ( 97 | dbPath := 'db/' + params.docID 98 | readFile(dbPath, file => file :: { 99 | () -> end({status: 404, body: 'doc not found'}) 100 | _ -> writeFile(dbPath, req.body, r => r :: { 101 | true -> end({ 102 | status: 200 103 | headers: {'Content-Type': 'text/plain'} 104 | body: '1' 105 | }) 106 | _ -> end({ 107 | status: 500 108 | body: 'error saving doc' 109 | }) 110 | }) 111 | }) 112 | ) 113 | 'POST' -> ( 114 | dbPath := 'db/' + params.docID 115 | readFile(dbPath, file => file :: { 116 | () -> writeFile(dbPath, req.body, r => r :: { 117 | true -> end({ 118 | status: 200 119 | headers: {'Content-Type': 'text/plain'} 120 | body: '1' 121 | }) 122 | _ -> end({ 123 | status: 500 124 | body: 'error saving doc' 125 | }) 126 | }) 127 | _ -> end({ 128 | status: 409 129 | headers: {'Content-Type': 'text/plain'} 130 | body: 'conflict' 131 | }) 132 | }) 133 | ) 134 | 'DELETE' -> ( 135 | dbPath := 'db/' + params.docID 136 | delete(dbPath, evt => evt.type :: { 137 | 'end' -> end({ 138 | status: 204 139 | body: '' 140 | }) 141 | 'error' -> end({ 142 | status: 500 143 | body: 'error deleting doc' 144 | }) 145 | }) 146 | ) 147 | _ -> end(MethodNotAllowed) 148 | }) 149 | addRoute('/doc/', params => (req, end) => req.method :: { 150 | 'GET' -> dir('db', evt => evt.type :: { 151 | 'error' -> end({ 152 | status: 500 153 | body: 'error reading database' 154 | }) 155 | _ -> end({ 156 | status: 200 157 | headers: {'Content-Type': 'text/plain'} 158 | body: cat(map(evt.data, entry => entry.name), char(10)) 159 | }) 160 | }) 161 | _ -> end(MethodNotAllowed) 162 | }) 163 | 164 | serveStatic := path => (req, end) => req.method :: { 165 | 'GET' -> ( 166 | staticPath := 'static/' + path 167 | readFile(staticPath, file => file :: { 168 | () -> end({status: 404, body: 'file not found'}) 169 | _ -> end({ 170 | status: 200 171 | headers: {'Content-Type': mimeForPath(staticPath)} 172 | body: file 173 | }) 174 | }) 175 | ) 176 | _ -> end(MethodNotAllowed) 177 | } 178 | addRoute('/static/*staticPath', params => serveStatic(params.staticPath)) 179 | addRoute('/', params => serveStatic('index.html')) 180 | 181 | (server.start)(number(Args.3)) 182 | )) 183 | } 184 | ` initialize environment and start ` 185 | _ -> withCore(env => ( 186 | paths := slice(args(), 2, len(args())) 187 | path := paths.0 :: { 188 | () -> ( 189 | log(f('Klisp interpreter v{{0}}.', [Version])) 190 | (sub := env => ( 191 | out('> ') 192 | scan(line => line :: { 193 | () -> log('EOF.') 194 | _ -> ( 195 | log(print(eval(read(line), env))) 196 | sub(env) 197 | ) 198 | }) 199 | ))(env) 200 | ) 201 | _ -> readFile(path, file => file :: { 202 | () -> log(f('error: could not read {{0}}', [path])) 203 | _ -> eval(read(file), env) 204 | }) 205 | } 206 | )) 207 | } 208 | 209 | -------------------------------------------------------------------------------- /src/core.ink: -------------------------------------------------------------------------------- 1 | ` helpers for loading Klisp libraries ` 2 | 3 | std := load('../vendor/std') 4 | 5 | clone := std.clone 6 | readFile := std.readFile 7 | 8 | klisp := load('klisp') 9 | 10 | read := klisp.read 11 | eval := klisp.eval 12 | Env := klisp.Env 13 | 14 | ` bootstrapping function to boot up an environment with given libraries ` 15 | withLibs := (libs, cb) => (sub := (i, env) => i :: { 16 | len(libs) -> cb(env) 17 | _ -> readFile(libs.(i), file => ( 18 | eval(read(file), env) 19 | sub(i + 1, env) 20 | )) 21 | })(0, clone(Env)) 22 | 23 | ` boot up an environment with core libraries ` 24 | withCore := cb => withLibs([ 25 | './lib/klisp.klisp' 26 | './lib/math.klisp' 27 | ], cb) 28 | 29 | -------------------------------------------------------------------------------- /src/klisp.ink: -------------------------------------------------------------------------------- 1 | ` Klisp: a LISP written in Ink ` 2 | 3 | std := load('../vendor/std') 4 | str := load('../vendor/str') 5 | 6 | log := std.log 7 | slice := std.slice 8 | cat := std.cat 9 | map := std.map 10 | every := std.every 11 | 12 | digit? := str.digit? 13 | replace := str.replace 14 | trim := str.trim 15 | 16 | Newline := char(10) 17 | Tab := char(9) 18 | NUL := char(0) 19 | 20 | ` helper function. Like std.reduce, but traverses a list of sexprs ` 21 | reduceL := (L, f, init) => (sub := (acc, node) => node :: { 22 | () -> acc 23 | _ -> sub(f(acc, node.0), node.1) 24 | })(init, L) 25 | 26 | ` turns a name into a Klisp symbol (prefixed with a ,) ` 27 | symbol := s => NUL + s 28 | 29 | ` takes a string, reports whether the string is a Klisp 30 | symbol (starts with a NUL byte) or not ` 31 | symbol? := s => s.0 = NUL 32 | 33 | ` memoized symbols (optimization) ` 34 | Quote := symbol('quote') 35 | Do := symbol('do') 36 | Def := symbol('def') 37 | If := symbol('if') 38 | Fn := symbol('fn') 39 | Macro := symbol('macro') 40 | Expand := symbol('expand') 41 | 42 | ` reader object constructor, 43 | state containing a cursor through a string ` 44 | reader := s => ( 45 | data := {s: s, i: 0} 46 | peek := () => s.(data.i) 47 | next := () => ( 48 | c := peek() 49 | data.i := data.i + 1 50 | c 51 | ) 52 | nextSpan := () => ( 53 | (sub := acc => peek() :: { 54 | () -> acc 55 | ' ' -> acc 56 | Newline -> acc 57 | Tab -> acc 58 | '(' -> acc 59 | ')' -> acc 60 | _ -> sub(acc + next()) 61 | })('') 62 | ) 63 | ff := () => ( 64 | (sub := () => peek() :: { 65 | ' ' -> (next(), sub()) 66 | Newline -> (next(), sub()) 67 | Tab -> (next(), sub()) 68 | ` ignore / ff through comments ` 69 | ';' -> (sub := () => next() :: { 70 | () -> () 71 | Newline -> ff() 72 | _ -> sub() 73 | })() 74 | _ -> () 75 | })() 76 | ) 77 | 78 | data.peek := peek 79 | data.next := next 80 | data.nextSpan := nextSpan 81 | data.ff := ff 82 | ) 83 | 84 | ` read takes a string and returns a List representing the input sexpr ` 85 | read := s => ( 86 | r := reader(trim(trim(s, ' '), Newline)) 87 | 88 | peek := r.peek 89 | next := r.next 90 | nextSpan := r.nextSpan 91 | ff := r.ff 92 | 93 | ` ff through possible comments at start ` 94 | ff() 95 | 96 | parse := () => c := peek() :: { 97 | () -> () ` EOF ` 98 | ')' -> () ` halt parsing ` 99 | ',' -> ( 100 | next() 101 | ff() 102 | [Quote, [parse(), ()]] 103 | ) 104 | '\'' -> ( 105 | next() 106 | (sub := acc => peek() :: { 107 | () -> acc 108 | '\\' -> (next(), sub(acc + next())) 109 | '\'' -> (next(), ff(), acc) 110 | _ -> sub(acc + next()) 111 | })('') 112 | ) 113 | '(' -> ( 114 | next() 115 | ff() 116 | (sub := (acc, tail) => peek() :: { 117 | () -> acc 118 | ')' -> (next(), acc) 119 | '.' -> ( 120 | next() 121 | ff() 122 | cons := parse() 123 | ff() 124 | acc := (acc :: { 125 | () -> cons 126 | _ -> (tail.1 := cons, acc) 127 | }) 128 | sub(acc, cons) 129 | ) 130 | _ -> ( 131 | cons := [parse(), ()] 132 | ff() 133 | acc := (acc :: { 134 | () -> cons 135 | _ -> (tail.1 := cons, acc) 136 | }) 137 | sub(acc, cons) 138 | ) 139 | })((), ()) 140 | ) 141 | _ -> ( 142 | span := nextSpan() 143 | ff() 144 | every(map(span, c => digit?(c) | c = '.')) :: { 145 | true -> number(span) 146 | _ -> symbol(span) 147 | } 148 | ) 149 | } 150 | 151 | term := [parse(), ()] 152 | prog := [Do, term] 153 | (sub := tail => peek() :: { 154 | () -> prog 155 | ')' -> prog 156 | _ -> ( 157 | term := [parse(), ()] 158 | tail.1 := term 159 | ff() 160 | sub(term) 161 | ) 162 | })(term) 163 | ) 164 | 165 | ` Sentinel value to be used in environments to denote a null () value ` 166 | KlispNull := rand() 167 | 168 | ` helper to query an environment (scope) for a name. 169 | getenv traverses the environment hierarchy ` 170 | getenv := (env, name) => v := env.(name) :: { 171 | () -> e := env.'-env' :: { 172 | () -> () 173 | _ -> getenv(e, name) 174 | } 175 | KlispNull -> () 176 | _ -> v 177 | } 178 | 179 | ` helper to set a name to a variable in an environment. 180 | pairs with getenv and abstracts over the KlispNull impl detail ` 181 | setenv := (env, name, v) => v :: { 182 | () -> env.(name) := KlispNull 183 | _ -> env.(name) := v 184 | } 185 | 186 | ` Klisp has two kinds of values represented as "functions". 187 | 188 | 1. Functions, defined by (fn ...). Klisp functions take finite 189 | arguments evaluated eagerly in-scope. 190 | 2. Macros, defined by (macro ...) Klisp macros take a sexpr List 191 | containing the arguments and returns some new piece of syntax. 192 | Arguments are not evaluated before call. 193 | 194 | In Klisp, fns and macros are represented as size-3 arrays to differentiate 195 | from 2-element arrays that represent sexprs. The first value reports 196 | whether the function is a macro or a normal function, and the second value 197 | is the actual function. The third is optional, and contains the sexpr that 198 | defines the fn or macro. This is used to pretty-print the definition. ` 199 | makeFn := (f, L) => [false, f, L] 200 | makeMacro := (f, L) => [true, f, L] 201 | makeNative := f => makeFn(f, ()) 202 | 203 | ` the evaluator ` 204 | eval := (L, env) => type(L) :: { 205 | 'composite' -> L.0 :: { 206 | Quote -> L.'1'.0 207 | Def -> ( 208 | name := L.'1'.0 209 | val := eval(L.'1'.'1'.0, env) 210 | setenv(env, name, val) 211 | val 212 | ) 213 | Do -> (sub := form => form.1 :: { 214 | () -> eval(form.0, env) 215 | _ -> ( 216 | eval(form.0, env) 217 | sub(form.1) 218 | ) 219 | })(L.1) 220 | If -> ( 221 | cond := L.'1'.0 222 | conseq := L.'1'.'1'.0 223 | altern := L.'1'.'1'.'1'.0 224 | eval(cond, env) :: { 225 | true -> eval(conseq, env) 226 | _ -> eval(altern, env) 227 | } 228 | ) 229 | Fn -> ( 230 | params := L.'1'.0 231 | body := L.'1'.'1'.0 232 | makeFn(args => eval( 233 | body 234 | (sub := (envc, params, args) => params = () | args = () :: { 235 | true -> envc 236 | _ -> ( 237 | setenv(envc, params.0, args.0) 238 | sub(envc, params.1, args.1) 239 | ) 240 | })({'-env': env}, params, args) 241 | ), L) 242 | ) 243 | Macro -> ( 244 | params := L.'1'.0 245 | body := L.'1'.'1'.0 246 | makeMacro(args => eval( 247 | body 248 | (sub := (envc, params, args) => params = () | args = () :: { 249 | true -> envc 250 | _ -> ( 251 | setenv(envc, params.0, args.0) 252 | sub(envc, params.1, args.1) 253 | ) 254 | ` NOTE: all arguments to a macro are passed as the first parameter ` 255 | })({'-env': env}, params, [args, ()]) 256 | ), L) 257 | ) 258 | Expand -> expr := eval(L.'1'.0, env) :: { 259 | () -> expr 260 | _ -> funcStub := eval(expr.0, env) :: { 261 | [_, _, _] -> funcStub.0 :: { 262 | true -> eval(funcStub.1, env)(expr.1) 263 | _ -> expr 264 | } 265 | _ -> expr 266 | } 267 | } 268 | _ -> ( 269 | argcs := L.1 270 | funcStub := eval(L.0, env) 271 | func := eval(funcStub.1, env) 272 | 273 | ` funcStub.0 reports whether a function is a macro ` 274 | funcStub.0 :: { 275 | true -> eval(func(argcs), env) 276 | _ -> ( 277 | reduceL(argcs, (head, x) => ( 278 | cons := [eval(x, env)] 279 | head.1 := cons 280 | cons 281 | ), args := []) 282 | func(args.1) 283 | ) 284 | } 285 | ) 286 | } 287 | 'string' -> symbol?(L) :: { 288 | true -> getenv(env, L) 289 | _ -> L 290 | } 291 | _ -> L 292 | } 293 | 294 | ` the default environment contains core constants and functions ` 295 | Env := { 296 | ` constants and fundamental forms ` 297 | symbol('true'): true 298 | symbol('false'): false 299 | symbol('car'): makeNative(L => L.'0'.0) 300 | symbol('cdr'): makeNative(L => L.'0'.1) 301 | symbol('cons'): makeNative(L => [L.0, L.'1'.0]) 302 | symbol('len'): makeNative(L => type(L.0) :: { 303 | 'string' -> symbol?(L.0) :: { 304 | true -> len(L.0) - 1 305 | _ -> len(L.0) 306 | } 307 | _ -> 0 308 | }) 309 | ` (gets s a b) returns slice of string s between 310 | indexes [a, b). For characters out of bounds, it returns '' ` 311 | symbol('gets'): makeNative(L => type(L.0) :: { 312 | 'string' -> slice(L.0, L.'1'.0, L.'1'.'1'.0) 313 | _ -> '' 314 | }) 315 | ` (sets! s a t) overwrites bytes in s with bytes from t 316 | starting at index a. sets! does not grow s if out of bounds 317 | due to an interpreter design limitation. It returns the new s. ` 318 | symbol('sets!'): makeNative(L => type(L.0) :: { 319 | 'string' -> ( 320 | s := L.0 321 | idx := L.'1'.0 322 | s.(idx) := slice(L.'1'.'1'.0, 0, len(s) - idx) 323 | ) 324 | _ -> '' 325 | }) 326 | 327 | ` direct ports of monotonic Ink functions ` 328 | symbol('char'): makeNative(L => char(L.0)) 329 | symbol('point'): makeNative(L => point(L.0)) 330 | symbol('sin'): makeNative(L => sin(L.0)) 331 | symbol('cos'): makeNative(L => cos(L.0)) 332 | symbol('floor'): makeNative(L => floor(L.0)) 333 | symbol('rand'): makeNative(rand) 334 | symbol('time'): makeNative(time) 335 | 336 | ` arithmetic and logical operators ` 337 | symbol('='): makeNative(L => every(reduceL(L.1, (acc, x) => acc.len(acc) := L.0 = x, []))) 338 | symbol('<'): makeNative(L => L.0 < L.'1'.0) 339 | symbol('>'): makeNative(L => L.0 > L.'1'.0) 340 | symbol('+'): makeNative(L => reduceL(L.1, (a, b) => a + b, L.0)) 341 | symbol('-'): makeNative(L => reduceL(L.1, (a, b) => a - b, L.0)) 342 | symbol('*'): makeNative(L => reduceL(L.1, (a, b) => a * b, L.0)) 343 | symbol('#'): makeNative(L => reduceL(L.1, (a, b) => pow(a, b), L.0)) 344 | symbol('/'): makeNative(L => reduceL(L.1, (a, b) => a / b, L.0)) 345 | symbol('%'): makeNative(L => reduceL(L.1, (a, b) => a % b, L.0)) 346 | 347 | ` types and conversions ` 348 | symbol('type'): makeNative(L => L.0 :: { 349 | [_, _, _] -> 'function' 350 | [_, _] -> 'list' 351 | _ -> ty := type(L.0) :: { 352 | 'string' -> symbol?(L.0) :: { 353 | true -> 'symbol' 354 | _ -> ty 355 | } 356 | _ -> ty 357 | } 358 | }) 359 | symbol('string->number'): makeNative(L => ( 360 | operand := L.0 361 | type(operand) :: { 362 | 'string' -> every(map(operand, c => digit?(c) | c = '.')) :: { 363 | true -> len(operand) :: { 364 | 0 -> 0 365 | _ -> number(operand) 366 | } 367 | _ -> 0 368 | } 369 | _ -> 0 370 | } 371 | )) 372 | symbol('number->string'): makeNative(L => string(L.0)) 373 | symbol('string->symbol'): makeNative(L => symbol(L.0)) 374 | symbol('symbol->string'): makeNative(L => symbol?(L.0) :: { 375 | true -> slice(L.0, 1, len(L.0)) 376 | _ -> L.0 377 | }) 378 | 379 | ` I/O and system ` 380 | symbol('print'): makeNative(L => out(reduceL( 381 | L.1, (a, b) => a + ' ' + (type(b) :: { 382 | 'string' -> b 383 | _ -> print(b) 384 | }) 385 | type(L.0) :: { 386 | 'string' -> L.0 387 | _ -> print(L.0) 388 | } 389 | ))) 390 | } 391 | 392 | ` the printer 393 | print prints a value as sexprs, preferring lists and falling back to (a . b) ` 394 | print := L => L :: { 395 | [_, _] -> ( 396 | list := (sub := (term, acc) => term :: { 397 | [_, [_, _]] -> ( 398 | acc.len(acc) := print(term.0) 399 | sub(term.1, acc) 400 | ) 401 | [_, ()] -> acc.len(acc) := print(term.0) 402 | [_, _] -> ( 403 | acc.len(acc) := print(term.0) 404 | acc.len(acc) := '.' 405 | sub(term.1, acc) 406 | ) 407 | _ -> acc.len(acc) := print(term) 408 | })(L, []) 409 | '(' + cat(list, ' ') + ')' 410 | ) 411 | [_, _, _] -> L.2 :: { 412 | () -> '(function)' 413 | _ -> print(L.2) 414 | } 415 | _ -> type(L) :: { 416 | 'string' -> symbol?(L) :: { 417 | true -> slice(L, 1, len(L)) 418 | _ -> '\'' + replace(replace(L, '\\', '\\\\'), '\'', '\\\'') + '\'' 419 | } 420 | _ -> string(L) 421 | } 422 | } 423 | 424 | -------------------------------------------------------------------------------- /src/nightvale.ink: -------------------------------------------------------------------------------- 1 | ` utilities for the Nightvale interface ` 2 | 3 | klisp := load('klisp') 4 | 5 | Quote := klisp.Quote 6 | Def := klisp.Def 7 | Do := klisp.Do 8 | If := klisp.If 9 | Fn := klisp.Fn 10 | Macro := klisp.Macro 11 | 12 | reduceL := klisp.reduceL 13 | symbol? := klisp.symbol? 14 | getenv := klisp.getenv 15 | setenv := klisp.setenv 16 | makeFn := klisp.makeFn 17 | makeMacro := klisp.makeMacro 18 | 19 | decrementer := n => ( 20 | S := [n] 21 | () => ( 22 | n := S.'0' - 1 23 | S.'0' := n 24 | n 25 | ) 26 | ) 27 | 28 | ` a version of klisp's eval() that bails/errors if the interpreter 29 | evaluates more than some maximum number of forms, to avoid the interpreter 30 | getting stuck in infinite loops unrecoverably. 31 | 32 | Yes, this is not the ideal solution -- the "correct" solution would be to 33 | send a timeout signal to a separate thread, or something like that. Unfortunately, 34 | Ink doesn't have the right primitives to multithread like that, so we do this instead. ` 35 | safeEval := (L, env, dec) => dec() :: { 36 | 0 -> () 37 | _ -> type(L) :: { 38 | 'composite' -> L.0 :: { 39 | Quote -> L.'1'.0 40 | Def -> ( 41 | name := L.'1'.0 42 | val := safeEval(L.'1'.'1'.0, env, dec) 43 | setenv(env, name, val) 44 | val 45 | ) 46 | Do -> (sub := form => form.1 :: { 47 | () -> safeEval(form.0, env, dec) 48 | _ -> ( 49 | safeEval(form.0, env, dec) 50 | sub(form.1) 51 | ) 52 | })(L.1) 53 | If -> ( 54 | cond := L.'1'.0 55 | conseq := L.'1'.'1'.0 56 | altern := L.'1'.'1'.'1'.0 57 | safeEval(cond, env, dec) :: { 58 | true -> safeEval(conseq, env, dec) 59 | _ -> safeEval(altern, env, dec) 60 | } 61 | ) 62 | Fn -> ( 63 | params := L.'1'.0 64 | body := L.'1'.'1'.0 65 | makeFn(args => safeEval( 66 | body 67 | (sub := (envc, params, args) => params = () | args = () :: { 68 | true -> envc 69 | _ -> ( 70 | setenv(envc, params.0, args.0) 71 | sub(envc, params.1, args.1) 72 | ) 73 | })({'-env': env}, params, args) 74 | dec 75 | ), L) 76 | ) 77 | Macro -> ( 78 | params := L.'1'.0 79 | body := L.'1'.'1'.0 80 | makeMacro(args => safeEval( 81 | body 82 | (sub := (envc, params, args) => params = () | args = () :: { 83 | true -> envc 84 | _ -> ( 85 | setenv(envc, params.0, args.0) 86 | sub(envc, params.1, args.1) 87 | ) 88 | ` NOTE: all arguments to a macro are passed as the first parameter ` 89 | })({'-env': env}, params, [args, ()]) 90 | dec 91 | ), L) 92 | ) 93 | _ -> ( 94 | argcs := L.1 95 | funcStub := safeEval(L.0, env, dec) 96 | func := safeEval(funcStub.1, env, dec) 97 | 98 | ` funcStub.0 reports whether a function is a macro ` 99 | funcStub.0 :: { 100 | true -> safeEval(func(argcs), env, dec) 101 | _ -> ( 102 | reduceL(argcs, (head, x) => ( 103 | cons := [safeEval(x, env, dec)] 104 | head.1 := cons 105 | cons 106 | ), args := []) 107 | func(args.1) 108 | ) 109 | } 110 | ) 111 | } 112 | 'string' -> symbol?(L) :: { 113 | true -> getenv(env, L) 114 | _ -> L 115 | } 116 | _ -> L 117 | } 118 | } 119 | 120 | evalAtMostSteps := (L, env, steps) => safeEval(L, env, decrementer(steps)) 121 | 122 | -------------------------------------------------------------------------------- /src/tests.ink: -------------------------------------------------------------------------------- 1 | std := load('../vendor/std') 2 | 3 | log := std.log 4 | f := std.format 5 | each := std.each 6 | map := std.map 7 | reduce := std.reduce 8 | flatten := std.flatten 9 | 10 | klisp := load('klisp') 11 | 12 | symbol := klisp.symbol 13 | read := klisp.read 14 | eval := klisp.eval 15 | print := klisp.print 16 | Env := klisp.Env 17 | 18 | core := load('core') 19 | 20 | withLibs := core.withLibs 21 | withCore := core.withCore 22 | 23 | Newline := char(10) 24 | logf := (s, x) => log(f(s, x)) 25 | 26 | s := (load('../vendor/suite').suite)( 27 | 'Klisp language and standard library' 28 | ) 29 | 30 | ` short helper functions on the suite ` 31 | m := s.mark 32 | t := s.test 33 | 34 | ` used for both read and print tests ` 35 | SyntaxTests := [ 36 | ['freestanding number' 37 | '240', 240] 38 | ['freestanding symbol' 39 | 'test-word', symbol('test-word')] 40 | ['freestanding null' 41 | '()', ()] 42 | ['single symbol form' 43 | '(x)', [symbol('x'), ()]] 44 | ['cons of symbols' 45 | '(x . y)', [symbol('x'), symbol('y')]] 46 | ['escaped strings' 47 | '(+ \'Hello\' \' World\\\'s!\')', [symbol('+'), ['Hello', [' World\'s!', ()]]]] 48 | ['list of symbols' 49 | '(a b c)', [symbol('a'), [symbol('b'), [symbol('c'), ()]]]] 50 | ['list of symbols in cons' 51 | '(x . (y . ()))', [symbol('x'), [symbol('y'), ()]]] 52 | ['list numbers' 53 | '(+ 1 23 45.6)', [symbol('+'), [1, [23, [45.6, ()]]]]] 54 | ['nested lists' 55 | '((x) (y)( z))', [[symbol('x'), ()], [[symbol('y'), ()], [[symbol('z'), ()], ()]]]] 56 | [ 57 | 'Multiline whitespaces' 58 | '(a( b ' + Newline + 'c )d e )' 59 | [symbol('a'), [[symbol('b'), [symbol('c'), ()]], [symbol('d'), [symbol('e'), ()]]]] 60 | ] 61 | [ 62 | 'Comments ignored by reader' 63 | '(a b ; (more sexprs)' + Newline + '; (x (y . z))' + 64 | Newline + '; (e f g)' + Newline + 'c)' 65 | [symbol('a'), [symbol('b'), [symbol('c'), ()]]] 66 | ] 67 | [ 68 | 'Lack of trailing parentheses' 69 | '(+ 1 (* 2 3)' 70 | [symbol('+'), [1, [[symbol('*'), [2, [3, ()]]], ()]]] 71 | ] 72 | [ 73 | 'Too many trailing parentheses' 74 | '(+ 1 (* 2 3))))' 75 | [symbol('+'), [1, [[symbol('*'), [2, [3, ()]]], ()]]] 76 | ] 77 | ] 78 | 79 | ` used for eval tests ` 80 | EvalTests := [ 81 | ['boolean equalities' 82 | '(& (= 3 3) (= 1 2))', false] 83 | ['variadic boolean relations' 84 | '(list (& true true true) 85 | (& false true false) 86 | (| false false true) 87 | (| false false false) 88 | (^ true true false) 89 | (^ false false false))', [true, [false, [true, [false, [true, [false, ()]]]]]]] 90 | ['variadic addition' 91 | '(+ 1 2 3 4 5)', 15] 92 | ['basic arithmetic ops with single arguments' 93 | '(+ (+ 1) (- 2) (* 3) (/ 4) (# 5) (% 6))', 21] 94 | ['variadic addition, cons' 95 | '(+ . (1 2 3 4 5 6))', 21] 96 | ['mixed arithmetic' 97 | '(- 100 (/ (* 10 10) 5) 40)', 40] 98 | ['string operations' 99 | '(+ \'Hello\' \' World\\\'s!\')', 'Hello World\'s!'] 100 | ['simple if form' 101 | '(if false 2 . (3))', 3] 102 | ['complex if form' 103 | '(if (& (= 3 3) 104 | (= 1 2)) 105 | (+ 1 2 3) 106 | (* 4 5 6))', 120] 107 | ['anonymous fns' 108 | '((fn () \'result\') 100)', 'result'] 109 | ['fn form returns a function' 110 | '(type (fn (x) (- 0 x)))', 'function'] 111 | ['anonymous fns with argument' 112 | '((fn (x) (+ 1 x)) 2)', 3] 113 | ['anonymous fns with arguments' 114 | '((fn (x y z) (* x y z)) 2 3 10)', 60] 115 | ['string->number' 116 | '(+ (string->number \'3.14\') 117 | (string->number \'20\') 118 | (string->number (+ \'10\' \'0\')))', 123.14] 119 | ['string->number on invalid strings = 0' 120 | '(+ (string->number \'broken\') 121 | (string->number \'0.234h\') 122 | (string->number \'\'))', 0] 123 | ['number->string' 124 | '(+ (number->string 1.23) 125 | (number->string 0.333333333) 126 | (number->string 40) 127 | (number->string (- 100 90)))', '1.230.3333333334010'] 128 | ['string->symbol' 129 | '(cons (string->symbol \'quote\') (string->symbol (+ \'hel\' \'lo\'))', [symbol('quote'), symbol('hello')]] 130 | ['symbol->string' 131 | '(cons (symbol->string ,quote) (symbol->string (quote hello)))', ['quote', 'hello']] 132 | ['define form', [ 133 | '(def a 647)' 134 | '(+ a a)' 135 | '(def doot (fn () (- 1000 (+ a a))))' 136 | '(doot)' 137 | ], ~294] 138 | ['define form returns its operand' 139 | '(def some-data (quote (my 1 2 3)))', [symbol('my'), [1, [2, [3, ()]]]]] 140 | ['aliased fns with define forms', [ 141 | '(def add +)' 142 | '(def mul *)' 143 | '(mul (add 1 2 3) (mul 10 10))' 144 | ], 600] 145 | ['fibonacci generator', [ 146 | '(def fib 147 | (fn (n) 148 | (if (< n 2) 149 | 1 150 | (+ (fib (- n 1)) (fib (- n 2))))))' 151 | '(fib 10)' 152 | ], 89] 153 | ['literal quote' 154 | '(quote (123 . 456))', [123, 456]] 155 | ['shorthand quote' 156 | ',(10 20 30 40 50)', [10, [20, [30, [40, [50, ()]]]]]] 157 | ['shorthand quote on symbols and atoms' 158 | '(list ,abc ,123 ,\'def\' ,())', [symbol('abc'), [123, ['def', [(), ()]]]]] 159 | ['double quote' 160 | '(quote (quote 1 2 3))', [symbol('quote'), [1, [2, [3, ()]]]]] 161 | ['car of quote' 162 | '(car (quote (quote 1 2 3)))', symbol('quote')] 163 | ['cdr of quote' 164 | '(cdr (quote (quote 1 2 3)))', [1, [2, [3, ()]]]] 165 | ['cons of quote' 166 | '(cons 100 (quote (200 300 400)))', [100, [200, [300, [400, ()]]]]] 167 | ['map over list', [ 168 | '(def map 169 | (fn (xs f) 170 | (if (= xs ()) 171 | () 172 | (cons (f (car xs)) 173 | (map (cdr xs) f)))))' 174 | '(def square (fn (x) (* x x)))' 175 | '(def nums ,(1 2 3 4 5))' 176 | '(map nums square)' 177 | ], [1, [4, [9, [16, [25, ()]]]]]] 178 | ['list macro', [ 179 | '(def square (fn (x) (* x x)))' 180 | '(list 1 2 3 4 (+ 2 3) (list 1 2 3))' 181 | ], [1, [2, [3, [4, [5, [[1, [2, [3, ()]]], ()]]]]]]] 182 | ['let macro' 183 | '(let (a 10) (let (b 20) (* a b)))', 200] 184 | ['let macro with shadowing' 185 | '(let (a 5) (let (b 20) (let (a 10) (* a b))))', 200] 186 | ['sum, size, average' 187 | '(avg (quote (100 200 300 500)))', 275] 188 | [ 189 | 'builtin fn type' 190 | '(+ (type 0) (type \'hi\') (type ,hi) (type true) (type type) (type ()) (type ,(0)))' 191 | 'numberstringsymbolbooleanfunction()list' 192 | ] 193 | ['builtin len on string' 194 | '(len \'hello\')', 5] 195 | ['builtin len on symbol' 196 | '(len ,hello)', 5] 197 | ['builtin len on invalid value' 198 | '(len 3)', 0] 199 | ['gets in bounds' 200 | '(gets \'hello world\' 4 8)', 'o wo'] 201 | ['gets partial out of bounds' 202 | '(gets \'hello world\' 4 100)', 'o world'] 203 | ['gets completely out of bounds' 204 | '(gets \'hello world\' 50 100)', ''] 205 | ['sets!', [ 206 | '(def s \'hello world\')' 207 | '(sets! s 6 \'klisp\')' 208 | 's' 209 | ], 'hello klisp'] 210 | ['sets! returns mutated string', [ 211 | '(def s \'hello world\')' 212 | '(sets! s 6 \'klisp\')' 213 | ], 'hello klisp'] 214 | ['sets! does not grow underlying string', [ 215 | '(def s \'hello world\')' 216 | '(sets! s 6 \'klispers everywhere\')' 217 | 's' 218 | ], 'hello klisp'] 219 | ['builtin char' 220 | '(+ (char 10) (char 13))', char(10) + char(13)] 221 | [ 222 | 'builtin point' 223 | '(+ (point \'A\') (point \'B\') (point \'Zte\'))' 224 | point('A') + point('B') + point('Zte') 225 | ] 226 | ] 227 | 228 | ` run tests with core libraries loaded ` 229 | withCore(env => ( 230 | m('read') 231 | ( 232 | each(SyntaxTests, term => ( 233 | msg := term.0 234 | line := term.1 235 | sexpr := term.2 236 | t(msg, (read(line).1).0, sexpr) 237 | )) 238 | ) 239 | 240 | m('eval') 241 | ( 242 | each(EvalTests, testEval := term => ( 243 | msg := term.0 244 | prog := term.1 245 | val := term.2 246 | type(prog) :: { 247 | 'string' -> t(msg, eval(read(prog), env), val) 248 | 'composite' -> reduce(prog, (env, term, i) => ( 249 | i :: { 250 | len(prog) - 1 -> t(msg, eval(read(term), env), val) 251 | _ -> eval(read(term), env) 252 | } 253 | env 254 | ), env) 255 | _ -> log(f('error: invalid eval test {{0}}', [prog])) 256 | } 257 | )) 258 | ) 259 | 260 | m('print') 261 | ( 262 | each(SyntaxTests, term => ( 263 | msg := term.0 264 | line := term.1 265 | sexpr := term.2 266 | ` because syntax in SyntaxTests is not normalized, 267 | we read twice here to normalize ` 268 | t(msg, read(print((read(line).1).0)), read(line)) 269 | )) 270 | ` use EvalTests to test print, since they're more complex, 271 | but we need to first expand out multiline tests. ` 272 | EvalTestLines := flatten(map(EvalTests, term => type(term.1) :: { 273 | 'string' -> [term.1] 274 | _ -> term.1 275 | })) 276 | each(EvalTestLines, term => ( 277 | msg := term 278 | line := term 279 | t(msg, read(print((read(line).1).0)), read(line)) 280 | )) 281 | ) 282 | 283 | ` end test suite, print result ` 284 | (s.end)() 285 | )) 286 | 287 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | --c-bg: #f8f8f8; 4 | --font-sans: 'IBM Plex Sans', Helvetica, sans-serif; 5 | --font-mono: 'IBM Plex Mono', 'Menlo', monospace; 6 | --paper-accent: #222; 7 | 8 | background: var(--c-bg); 9 | font-size: 18px; 10 | margin: 0; 11 | } 12 | 13 | body, 14 | textarea, 15 | input, 16 | button { 17 | font-size: 1em; 18 | font-family: var(--font-sans); 19 | } 20 | 21 | h1, 22 | h2, 23 | h3, 24 | p { 25 | font-weight: normal; 26 | min-height: 1em; 27 | } 28 | 29 | button { 30 | cursor: pointer; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | } 36 | 37 | code, 38 | pre { 39 | font-family: var(--font-mono); 40 | } 41 | 42 | textarea { 43 | display: block; 44 | } 45 | 46 | textarea:focus { 47 | outline: none; 48 | } 49 | 50 | .mobile { 51 | display: none; 52 | } 53 | 54 | /* LAYOUT */ 55 | 56 | header, 57 | main, 58 | footer { 59 | max-width: 840px; 60 | width: calc(100% - 2em); 61 | margin: 32px auto 64px auto; 62 | line-height: 1.5em; 63 | } 64 | 65 | /* HEADER, FOOTER */ 66 | 67 | header, 68 | footer { 69 | display: flex; 70 | flex-direction: row; 71 | align-items: center; 72 | justify-content: space-between; 73 | position: relative; 74 | } 75 | 76 | .left, 77 | .right { 78 | flex-grow: 0; 79 | flex-shrink: 1; 80 | white-space: nowrap; 81 | } 82 | 83 | .center { 84 | flex-grow: 1; 85 | flex-shrink: 1; 86 | height: 1px; 87 | width: 0; 88 | background: #888; 89 | margin: 0 1em; 90 | } 91 | 92 | header a { 93 | text-decoration: none; 94 | } 95 | 96 | header a:hover { 97 | opacity: .6; 98 | } 99 | 100 | header .left { 101 | /* in case of really long doc names */ 102 | overflow: hidden; 103 | text-overflow: ellipsis; 104 | } 105 | 106 | .docID { 107 | color: #888; 108 | overflow: hidden; 109 | text-overflow: ellipsis; 110 | cursor: pointer; 111 | } 112 | 113 | .docID:hover { 114 | text-decoration: underline; 115 | } 116 | 117 | nav { 118 | display: flex; 119 | flex-direction: row; 120 | align-items: center; 121 | } 122 | 123 | nav a { 124 | display: block; 125 | margin-left: .8em; 126 | } 127 | 128 | nav a:first-child { 129 | margin-left: 0; 130 | } 131 | 132 | footer .right { 133 | font-style: italic; 134 | } 135 | 136 | @keyframes syncing-slide { 137 | 0% { 138 | transform: scaleX(1) translateX(-100%); 139 | } 140 | 50% { 141 | transform: translateX(0); 142 | } 143 | 100% { 144 | transform: scaleX(1) translateX(100%); 145 | } 146 | } 147 | 148 | .syncing { 149 | position: fixed; 150 | top: 0; 151 | left: 0; 152 | right: 0; 153 | width: 100%; 154 | display: block; 155 | background: var(--paper-accent); 156 | height: 2px; 157 | z-index: 10; 158 | 159 | animation: syncing-slide linear 1s infinite; 160 | animation-direction: alternate; 161 | } 162 | 163 | /* BLOCK */ 164 | 165 | .block { 166 | margin: 1.5em 0; 167 | width: 100%; 168 | position: relative; 169 | line-height: 1.5em; 170 | border-radius: 2px; 171 | padding: 0; 172 | min-height: 40px; 173 | } 174 | 175 | .block-text:hover { 176 | cursor: pointer; 177 | } 178 | 179 | .block-buttons { 180 | position: absolute; 181 | top: 4px; 182 | right: 4px; 183 | transition: opacity 0.15s; 184 | opacity: 0; 185 | pointer-events: none; 186 | font-size: 16px; 187 | border-radius: 6px; 188 | z-index: 10; 189 | } 190 | 191 | .block:hover .block-buttons { 192 | opacity: 1; 193 | pointer-events: all; 194 | } 195 | 196 | .runButton.paper { 197 | position: absolute; 198 | right: 8px; 199 | bottom: 8px; 200 | background: #fff; 201 | border-radius: 6px; 202 | font-size: .8em; 203 | } 204 | 205 | .block-code { 206 | border-radius: 6px; 207 | overflow: hidden; 208 | transition: opacity .3s; 209 | } 210 | 211 | .block-code.evaling { 212 | opacity: .5; 213 | } 214 | 215 | .block-code-result { 216 | padding: .8em .5em; 217 | background: #f0f0f0; 218 | white-space: pre-wrap; 219 | overflow-wrap: break-word; 220 | box-sizing: border-box; 221 | } 222 | 223 | .textarea-code, 224 | .textarea-text, 225 | .p-spacer { 226 | padding: .8em .5em; 227 | border: 0; 228 | resize: none; 229 | box-sizing: border-box; 230 | width: 100%; 231 | } 232 | 233 | .textarea-code, 234 | .block-code-result, 235 | .block-code-editor .p-spacer { 236 | width: 100%; 237 | font-family: var(--font-mono); 238 | line-height: 1.5em; 239 | } 240 | 241 | .textarea-text { 242 | line-height: 1.5em; 243 | } 244 | 245 | .block-text-editor, 246 | .block-code-editor { 247 | position: relative; 248 | } 249 | 250 | .p-spacer { 251 | white-space: pre-wrap; 252 | visibility: hidden; 253 | } 254 | 255 | .p-spacer.padded { 256 | padding-bottom: 2.3em; /* 1.5em line + .8em padding */ 257 | } 258 | 259 | .block-text-editor .textarea-text, 260 | .block-code-editor .textarea-code { 261 | position: absolute; 262 | top: 0; 263 | left: 0; 264 | right: 0; 265 | bottom: 0; 266 | height: 100%; 267 | } 268 | 269 | .block-buttons button { 270 | border: 0; 271 | background: #eee; 272 | font-family: var(--font-mono); 273 | margin: 4px; 274 | } 275 | 276 | .block-buttons button:hover { 277 | background: #ddd; 278 | border-radius: 4px; 279 | } 280 | 281 | .new-block-button { 282 | border: 0; 283 | background: 0; 284 | text-decoration: underline; 285 | margin: 0; 286 | padding: 0; 287 | } 288 | 289 | /* STATIC PAGES */ 290 | 291 | .inputRow { 292 | display: flex; 293 | flex-direction: row; 294 | align-items: center; 295 | width: 100%; 296 | justify-content: flex-start; 297 | } 298 | 299 | .inputRow input { 300 | margin-right: .8em; 301 | width: 20em; 302 | min-width: 0; 303 | flex-grow: 0; 304 | flex-shrink: 1; 305 | -webkit-appearance: none; 306 | } 307 | 308 | .inputRow button { 309 | flex-grow: 0; 310 | flex-shrink: 0; 311 | } 312 | 313 | /* DOC LIST */ 314 | 315 | .doc-list { 316 | padding-left: 0; 317 | } 318 | 319 | .doc-list-li { 320 | list-style: none; 321 | line-height: 1.5em; 322 | margin-bottom: .3em; 323 | } 324 | 325 | /* MESSAGE ALERTS */ 326 | 327 | .message { 328 | width: calc(100% - 1.5em); 329 | position: fixed; 330 | top: 1em; 331 | left: 50%; 332 | transform: translate(-50%, 0); 333 | max-width: 320px; 334 | line-height: 1.5em; 335 | } 336 | 337 | .message-buttons { 338 | display: flex; 339 | flex-direction: row; 340 | align-items: center; 341 | justify-content: flex-end; 342 | margin-top: .5em; 343 | width: 100%; 344 | } 345 | 346 | .message-buttons button { 347 | margin-left: .5em; 348 | } 349 | 350 | @media only screen and (max-width: 550px) { 351 | html, 352 | body { 353 | font-size: 16px; 354 | } 355 | .mobile { 356 | display: initial; 357 | } 358 | .desktop { 359 | display: none; 360 | } 361 | } 362 | 363 | -------------------------------------------------------------------------------- /static/css/paper.min.css: -------------------------------------------------------------------------------- 1 | body{--paper-accent:#3819e4;--paper-foreground:#000;--paper-background:#fff;--paper-border-width:4px}.paper{display:block;color:var(--paper-foreground);background:var(--paper-background);box-shadow:0 5px 12px -2px rgba(0,0,0,.4);transform:none;box-sizing:border-box;border:0;outline:none;padding:.5em 1em;border-radius:0;text-decoration:none}.paper.inline{display:inline-block;padding:.1em .5em}.paper.wrap{padding:0}.paper.movable{cursor:pointer;transition:transform .15s,box-shadow .15s;font-weight:700}.paper.colored{color:var(--paper-accent)}.paper.movable:focus,.paper.movable:hover{transform:translateY(-2px);box-shadow:0 8px 22px -1px rgba(0,0,0,.24)}.paper.movable:focus{outline:1px solid var(--paper-accent)}.paper.movable:active{transform:translateY(1px);box-shadow:0 2px 4px 0 rgba(0,0,0,.5);color:var(--paper-foreground)}.paper.movable.colored:active{color:var(--paper-accent)}.paper.accent{background:var(--paper-accent)}.paper.accent,.paper.movable.accent:active{color:var(--paper-background)}.paper.paper-border-left{border-left:var(--paper-border-width) solid var(--paper-accent)}.paper.paper-border-right{border-right:var(--paper-border-width) solid var(--paper-accent)}.paper.paper-border-top{border-top:var(--paper-border-width) solid var(--paper-accent)}.paper.paper-border-bottom{border-bottom:var(--paper-border-width) solid var(--paper-accent)}.paper.accent.paper-border-left{border-left:var(--paper-border-width) solid var(--paper-background)}.paper.accent.paper-border-right{border-right:var(--paper-border-width) solid var(--paper-background)}.paper.accent.paper-border-top{border-top:var(--paper-border-width) solid var(--paper-background)}.paper.accent.paper-border-bottom{border-bottom:var(--paper-border-width) solid var(--paper-background)} 2 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Nightvale :: Klisp docs 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /static/js/auto-render.katex.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),a=function(e,t,r){for(var n=r,o=0,a=e.length;n Debounce coalesces multiple calls to the same function in a short 16 | // period of time into one call, by cancelling subsequent calls within 17 | // a given timeframe. 18 | const debounce = (fn, delayMillis) => { 19 | let lastRun = 0; 20 | let to = null; 21 | return (...args) => { 22 | clearTimeout(to); 23 | const now = Date.now(); 24 | const dfn = () => { 25 | lastRun = now; 26 | fn(...args); 27 | } 28 | to = setTimeout(dfn, delayMillis); 29 | } 30 | } 31 | 32 | function message(msg) { 33 | function close() { 34 | document.body.removeChild(node); 35 | } 36 | 37 | const node = render(null, null, jdom`
38 | ${msg} 39 |
40 | 41 |
42 |
`); 43 | document.body.appendChild(node); 44 | } 45 | 46 | function confirm(msg, ifOk) { 47 | function close() { 48 | document.body.removeChild(node); 49 | } 50 | 51 | const node = render(null, null, jdom`
52 | ${msg} 53 |
54 | 55 | 59 |
60 |
`); 61 | document.body.appendChild(node); 62 | } 63 | 64 | class Para extends Record { 65 | childIndexes() { 66 | const childIndex = (this.get('index') + this.get('next')) / 2; 67 | const childNext = this.get('next'); 68 | this.update({next: childIndex}); 69 | 70 | return { 71 | index: childIndex, 72 | next: childNext, 73 | } 74 | } 75 | } 76 | 77 | class Doc extends StoreOf(Para) { 78 | get comparator() { 79 | return block => block.get('index'); 80 | } 81 | } 82 | 83 | function remoteEval(expr) { 84 | return fetch('/eval', { 85 | method: 'POST', 86 | body: expr, 87 | }).then(resp => { 88 | if (resp.status === 200) { 89 | return resp.text(); 90 | } else { 91 | return Promise.resolve('eval-error'); 92 | } 93 | }); 94 | } 95 | 96 | // Borrowed from thesephist/sandbox, an event handler that does some light 97 | // parentheses matching / deletion on certain events. Used by Component Block. 98 | function handleEditorKeystroke(evt) { 99 | switch (evt.key) { 100 | case 'Tab': { 101 | evt.preventDefault(); 102 | const idx = evt.target.selectionStart; 103 | if (idx !== null) { 104 | const input = evt.target.value; 105 | const front = input.substr(0, idx); 106 | const back = input.substr(idx); 107 | evt.target.value = front + ' ' + back; 108 | evt.target.setSelectionRange(idx + 4, idx + 4); 109 | } 110 | break; 111 | } 112 | case '(': { 113 | evt.preventDefault(); 114 | const input = evt.target.value; 115 | const {selectionStart, selectionEnd} = evt.target; 116 | const front = input.substr(0, selectionStart); 117 | const back = input.substr(selectionEnd); 118 | if (selectionStart !== null) { 119 | evt.target.value = front + '()' + back; 120 | const cursorPos = selectionStart + 1; 121 | evt.target.setSelectionRange(cursorPos, cursorPos); 122 | } 123 | break; 124 | } 125 | case ')': { 126 | const idx = evt.target.selectionStart; 127 | if (idx !== null) { 128 | const input = evt.target.value; 129 | if (input[idx] === ')') { 130 | evt.preventDefault(); 131 | evt.target.setSelectionRange(idx + 1, idx + 1); 132 | } 133 | } 134 | break; 135 | } 136 | case '\'': { 137 | evt.preventDefault(); 138 | const idx = evt.target.selectionStart; 139 | if (idx !== null) { 140 | const input = evt.target.value; 141 | if (input[idx] === '\'') { 142 | evt.preventDefault(); 143 | evt.target.setSelectionRange(idx + 1, idx + 1); 144 | } else { 145 | const front = input.substr(0, idx); 146 | const back = input.substr(idx); 147 | // if trying to escape this quote, do not add a pair 148 | if (input[idx - 1] === '\'') { 149 | evt.target.value = front + '\'' + back; 150 | } else { 151 | evt.target.value = front + '\'\'' + back; 152 | } 153 | evt.target.setSelectionRange(idx + 1, idx + 1); 154 | } 155 | } 156 | break; 157 | } 158 | case 'Backspace': { 159 | // Backspace on a opening pair should take its closing sibling wth it 160 | const idx = evt.target.selectionStart; 161 | if (idx !== null) { 162 | const input = evt.target.value; 163 | if ((input[idx - 1] === '(' && input[idx] === ')') 164 | || (input[idx - 1] === '\'' && input[idx] === '\'')) { 165 | evt.preventDefault(); 166 | const front = input.substr(0, idx - 1); 167 | const back = input.substr(idx + 1); 168 | evt.target.value = front + back; 169 | evt.target.setSelectionRange(idx - 1, idx - 1); 170 | } 171 | } 172 | break; 173 | } 174 | } 175 | } 176 | 177 | class Block extends Component { 178 | init(para, remover, {addTextBlock, addCodeBlock}) { 179 | this.editing = false; 180 | 181 | this.evaling = false; 182 | this.evaled = null; 183 | 184 | this.remover = remover; 185 | this.addTextBlock = () => addTextBlock(this.record.childIndexes()); 186 | this.addCodeBlock = () => addCodeBlock(this.record.childIndexes()); 187 | this.startEditing = this.startEditing.bind(this); 188 | 189 | this.bind(para, this.render.bind(this)); 190 | 191 | // if code block, exec code on first load 192 | if (this.record.get('type') == BLOCK.CODE && this.evaled == null) { 193 | this.eval(); 194 | } 195 | } 196 | async eval(evt) { 197 | this.evaling = true; 198 | this.render(); 199 | 200 | try { 201 | this.evaled = await remoteEval(evt ? evt.target.value : this.record.get('text')); 202 | } catch (e) { 203 | this.evaled = 'eval-error'; 204 | } 205 | this.evaling = false; 206 | this.render(); 207 | } 208 | startEditing(evt) { 209 | // sometimes we click on the block to navigate to a link 210 | if (evt.target.tagName.toLowerCase() == 'a') { 211 | return; 212 | } 213 | 214 | this.editing = true; 215 | this.render(); 216 | 217 | // allow exiting editing mode by clicking elsewhere on the page 218 | requestAnimationFrame(() => { 219 | const unEdit = evt => { 220 | if (!this.node.contains(evt.target)) { 221 | this.editing = false; 222 | this.render(); 223 | document.removeEventListener('click', unEdit); 224 | } 225 | } 226 | document.addEventListener('click', unEdit); 227 | }); 228 | } 229 | compose() { 230 | const {type, text} = this.record.summarize(); 231 | const buttons = jdom`
evt.stopPropagation()}> 233 | 234 | 235 | 236 |
`; 237 | 238 | if (type == BLOCK.CODE) { 239 | return jdom`
241 | 242 | ${buttons} 243 |
244 |
${text}
245 |