This is a small interpreter of a subset of Scheme in about 800 lines of TypeScript 4.6 (Deno 1.20). It implements almost the same language as
- little-scheme-in-crystal
- little-scheme-in-cs
- little-scheme-in-dart
- little-scheme-in-go
- little-scheme-in-java
- little-scheme-in-kotlin
- little-scheme-in-lisp
- little-scheme-in-php
- little-scheme-in-python
- little-scheme-in-ruby
and their meta-circular interpreter, little-scheme. As a Scheme implementation, it optimizes tail calls and handles first-class continuations properly.
As from v2.0, it runs not on Node.js, but on Deno.
Before v2.0, two callbacks, stdInOnData
and stdInOnEnd
, were defined and used on Node.js as follows:
process.stdin.setEncoding('utf8');
process.stdin.on('data', stdInOnData);
process.stdin.on('end', stdInOnEnd);
Now, readLine
is defined on Deno as follows:
const decoder = new TextDecoder(); // from utf-8 bytes
const buf = new Uint8Array(8000);
readLine = async function(): Promise<string | null> {
const n = await Deno.stdin.read(buf) as (number | null);
if (n === null)
return null; // End-Of-File
return decoder.decode(buf.subarray(0, n));
};
You can run this interpreter also on web browsers by giving appropriate values to
readStringFrom
, write
and readLine
.
Refer to head and tail of scm.ts
for these functions.
The file example/index.html shows a simple example.
The following is an excerpt from it:
let readLine_resolve;
readLine = () => new Promise((resolve, _reject) => {
readLine_resolve = resolve;
});
ta.addEventListener("keyup", (event) => {
if (event.key === "Enter") {
let line = ta.value.substring(current_position);
readLine_resolve(line);
}
});
$ deno --version
deno 1.20.1 (release, aarch64-apple-darwin)
v8 10.0.139.6
typescript 4.6.2
$ deno bundle scm.ts scm.js
Check file:///Users/suzuki/tmp/little-scheme-in-typescript/scm.ts
Bundle file:///Users/suzuki/tmp/little-scheme-in-typescript/scm.ts
Emit "scm.js" (19.08KB)
$ deno run scm.js
> (+ 5 6)
11
> (cons 'a (cons 'b 'c))
(a b . c)
> (list
| 1
| 2
| 3
| )
(1 2 3)
>
Or just use example/scm.js
, which I provided by bundling scm.ts
and arith.ts
in the same way as above.
$ deno run example/scm.js
> (+ 7.8 9)
16.8
>
Press EOF (e.g. Control-D) to exit the session.
> Goodbye
$
You can also open example/index.html
with a modern web browser to
run scm.js
.
$ open example/index.html
It is also presented here.
You can run node scm.js
with a Scheme script.
Examples are found in
little-scheme;
download it at ..
and you can try the following:
$ deno run --allow-read scm.js ../little-scheme/examples/yin-yang-puzzle.scm
*
**
***
****
*****
******
*******
********
*********
^C
$ deno run --allow-read scm.js ../little-scheme/examples/amb.scm
((1 A) (1 B) (1 C) (2 A) (2 B) (2 C) (3 A) (3 B) (3 C))
$ deno run --allow-read scm.js ../little-scheme/examples/nqueens.scm
((5 3 1 6 4 2) (4 1 5 2 6 3) (3 6 2 5 1 4) (2 4 6 1 3 5))
$ deno run --allow-read scm.js ../little-scheme/scm.scm < ../little-scheme/examples/nqueens.scm
((5 3 1 6 4 2) (4 1 5 2 6 3) (3 6 2 5 1 4) (2 4 6 1 3 5))
$
Press INTR (e.g. Control-C) to terminate the yin-yang-puzzle.
Put a "-
" after the script in the command line to begin a session
after running the script.
$ deno run --allow-read scm.js ../little-scheme/examples/fib90.scm -
2880067194370816120
> (globals)
(globals error number? = < * - + apply call/cc symbol? eof-object? read newline display list not
null? pair? eq? cons cdr car fibonacci)
> (fibonacci 16)
987
> (fibonacci 1000)
434665576869374564356885276750406258025646605173717804024817290895365554179490518904038798400792
551692959225930803226347752096896232398733224711616429964409065331879382989696499285160037044761
37795166849228875
>
Scheme Expression | Internal Representation |
---|---|
numbers 1 , 2.3 |
bigint or number |
#t |
true |
#f |
false |
strings "hello, world" |
string |
symbols a , + |
class Sym |
() |
null |
pairs (1 . 2) , (x y z) |
class Cell |
closures (lambda (x) (+ x 1)) |
class Closure |
built-in procedures car , cdr |
class Intrinsic |
continuations | class Continuation |
- Integers are represented by
bigint
, which is supported by TypeScipt 3.2 and later, Node.js 10.4 and later, Firefox 68 and later etc. On the platforms that do not supportbigint
(e.g. Safari 13.0), integers are represented bynumber
automatically. SeetryToParse
inarith.ts
.
The implementation is similar to those of little-scheme-in-dart and little-scheme-in-cs.
-
v [variable reference]
-
(e0 e1...) [procedure call]
-
(
quote
e)
'
e [transformed into (quote
e) when read] -
(
if
e1 e2 e3)
(if
e1 e2) -
(
begin
e...) -
(
lambda
(v...) e...) -
(
set!
v e) -
(
define
v e)
For simplicity, this Scheme treats (define
v e) as an expression type.
(car lst) |
(display x) |
(+ n1 n2) |
(cdr lst) |
(newline ) |
(- n1 n2) |
(cons x y) |
(read ) |
(* n1 n2) |
(eq? x y) |
(eof-object? x) |
(< n1 n2) |
(pair? x) |
(symbol? x) |
(= n1 n2) |
(null? x) |
(call/cc fun) |
(number? x) |
(not x) |
(apply fun arg) |
(globals ) |
(list x ...) |
(error reason arg) |
-
(error
reason arg)
throws an error with the message "Error:
reason:
arg". It is based on SRFI-23. -
(globals)
returns a list of keys of the global environment. It is not in the standard.
See GlobalEnv
in scm.ts
for the implementation of the procedures
except call/cc
and apply
.
call/cc
and apply
are implemented particularly at
applyFunction
in scm.ts
.
I hope this serves as a handy model of how to write a Scheme interpreter in TypeScript/JavaScript.