cumalis-lisp
Version:
A Scheme implementation written in Typescript.
264 lines (212 loc) • 9.24 kB
Markdown
# Cumalis Lisp
Cumalis Lisp is a stack-based implementation of R7RS Scheme Language
in Typescript.
Can be used as a library in web browsers or Node.js environment.
## Installation
```bash
$ yarn add cumalis-lisp
# or
$ npm install cumalis-lisp
```
## Features
- Almost full implementation of R7RS (small) except complex/fraction numbers, including:
* call-with-current-continuation (call/cc).
* guard / with-exception-handler / raise / raise-continuable.
* dynamic-wind.
* define-library / import.
* make-parameter / parameterize.
* define-record-type.
* let-values / values.
* syntax-rules with nested patterns.
* quasiquote.
* nested multiline comments.
* datum tags.
* etc.
- Standard libraries in R7RS (small) except (scheme complex) are implemented.
* (scheme base) -- imported by default.
* (scheme read)
* (scheme write)
* (scheme promise)
* (scheme time)
* (scheme inexact)
* (scheme case-lambda)
* (scheme char)
* (scheme cxr)
* (scheme process-context)
* (scheme file)
* (scheme eval)
* (scheme repl)
* (scheme load)
* (scheme r5rs)
- Proper tail recursion. (tail call optimization)
- Javascript interfaces
* Adding built-in syntaxes, procedures, libraries.
* Able to write expressions as Javascript Arrays and evaluate.
* Able to contain Javascript objects in AST.
- All objects, AST, and call-frames consist of pure JSON objects.
* Continuations can be serialized to JSON strings. (Circular references need to be resolved.)
* Simple JSON serializer/deserializer utility is bundled. (toReferentialJSON / fromReferentialJSON)
- No dependency.
With these features, The following application fields can be considered.
- Macro system for online applications. (Programs can be built as simple Javascript objects.)
- Mobile agent systems. (Send running application via network and continue to run on another machine.)
- Games that need to save the running status.
- Work-flow systems.
- Backend of Web-based visual programming environment, like [scratch-blocks](https://github.com/LLK/scratch-blocks) or [Blockly](https://developers.google.com/blockly).
* Note: Cumalis Lisp is expected to be the backend of ["Cumalis"](https://cumalis.net/) in the next major version.
- etc.
Note: Implementations of Scheme are not required to implement fraction and complex numbers. See section
"6.2.3. Implementation restrictions" on R7RS.
## Web REPL & Sandbox
[Cumalis Lisp Web REPL](https://trb-a.github.io/cumalis-lisp/web-repl.html)
[CodeSandbox Template](https://codesandbox.io/s/using-cumalis-lisp-xvmjc?file=/src/index.ts)
## How to use as a module
### Basic usage
```typescript
import { Interpreter, toJS } from "cumalis-lisp";
const itrp = new Interpreter(); // Create interpreter.
const ret = itrp.eval(`
(define (fib n)
(if (<= n 2)
1
(+ (fib (- n 1)) (fib (- n 2)))))
(fib 10)
`); // Evaluate S-expression.
const num = toJS(ret); // returns 55.
```
### Defining built-in procedures / built-in macros
```typescript
import { Interpreter, is, create, toJS, fromJS, defineBuiltInProcedure } from "cumalis-lisp";
const itrp = new Interpreter(); // Create interpreter.
const helloProc = defineBuiltInProcedure("hello", [ // Define procedure
{ name: "obj" }
], function ({obj}) {
if (!is.Object(obj)) {
throw new Error("Not a object");
}
console.log(`Hello ${toJS(obj)}`);
return create.Number(42);
});
const hello2Proc = defineBuiltInProcedure("hello2", [ // Define macro
{ name: "obj" }
], function ({obj}) {
if (!is.Object(obj)) {
throw new Error("Not a object");
}
return fromJS(["string-append", `"HELLO "`, obj]); // Write LISP as JS array.
}, true); // <-- this "true" indicates macro.
itrp.setBuiltInProcedure(helloProc); // Set the procedure to the interpreter.
itrp.setBuiltInProcedure(hello2Proc); // Set the procedure to the interpreter.
console.log(toJS(itrp.eval(`(hello "world")`))); // => 42
console.log(toJS(itrp.eval(`(hello2 "WORLD")`))); // => HELLO WORLD
```
### Suspend / serialize / deserialize / resume
```Typescript
import {
Interpreter, LISP, create, toJS,
SuspendEnvelope, isSuspendEnvelope, suspendValueFromEnvelope,
toReferentialJSON, fromReferentialJSON,
} from "cumalis-lisp";
const itrp = new Interpreter(); // Create interpreter.
// Suspend
let suspend: SuspendEnvelope | null = null;
try {
itrp.eval(`(+ 11 (suspend "SUSPEND HERE"))`);
} catch (e) {
if (isSuspendEnvelope(e)) {
suspend = e;
} else {
throw e;
}
}
if (suspend) {
console.log(toJS(suspendValueFromEnvelope(suspend))); // => "SUSPEND HERE"
// Serialize/Deserialize
const json = toReferentialJSON(suspend, "$$$");
const revived: LISP.Suspend = fromReferentialJSON(json, "$$$");
// Resume
const ret = itrp.resume(revived, create.Number(31));
console.log(toJS(ret)); // => 42
}
```
### Using files
To handle files in Cumalis Lisp on Node.js, "fs" object must be passed to the interpreter
as a constructor's option when you create a Interpreter instance.
```Typescript
import { Interpreter } from "cumalis-lisp";
import fs from "fs";
const itrp = new Interpreter({fs}); // <= set "fs" object as option.
itrp.eval(`
(import (scheme file))
(if (file-exists? "some-file.txt")
(with-input-from-file "some-file.txt"
(define x (read-line))
...
(with-output-to-file "some-other-file.txt"
(lambda ()
(write-char #\a)
(write-string "ABC")
(newline)
...
(delete-file "some-file.txt")
`);
```
Note: If you want to serialize / deserialize suspended continuations, open files status (seek position, open/close status, etc) can't be recovered when you deserialize / resume. It will cause unexpected behaviour. Be sure to close files before suspend / serialization.
## R7RS Specification
[Revised7 Report on the Algorithmic Language Scheme](https://github.com/johnwcowan/r7rs-spec/blob/errata/spec/r7rs.pdf)
## Limitations
### About number
* Only integer and real number is supported. Complex / fraction number is not implemented.
* Standard library (scheme complex) is not implemented.
* 1.0 and 1 is same value. (like Javascript's number primitive).
* "exact" means Number.isSafeInteger is true in Javascript.
- "exact" procedure trys to convert float numbers to safe-integer. It raises an error if it fails.
- "inexact" procedure does nothing than returning the given value.
* In S-expressions, hexadecimal, octal, binary literals can't have digits.
### About syntax-rules
* Patterns with vectors are not supported.
### About procedure call
* Procedure call must be a proper list. The last cdr of procedure call will be ignored.
* Defining syntax-rules pattern to call procedure with improper list raises syntax-error.
### About environment
* (scheme base) library is imported by default. Importing "(scheme base)" is just ignored.
* (scheme base) is imported by default even if (environment) (null-environment) (scheme-report-environment 5).
* (environment) doesn't make immutable bindings.
### About library
* "import" doesn't make immutable bindings.
### About string
* (eqv? "aaa" "aaa") returns #t. (like Javascript's "aaa" === "aaa" returns true).
### About file library
* char-ready? u8-ready? raise errors for file ports. Because Node.js doesn't seem to have any ftell(3) equivalent.
* read-char read-line etc. may block until complete reading.
## Notes
- include / include-cli (or load etc.) always read files from the path relative to the current working directory.
- toReferentialJSON / fromReferentialJSON don't respect "toJSON" property of class instances. If you want to include class instances in serialization, please consider other serializers like js-yaml, etc.
- exit / emergency-exit does't do process.exit() but throws an Envelope object that isExitEnvelope() returns true, so that users can catch it and perform proper finalizations.
## Contributing
Contributions are welcome.
### Bug reports
Bugs are tracked in the project's [issue tracker](https://github.com/trb-a/cumalis-lisp/issues).
Please read Limitations on this README before you submit.
Please include informaton:
* Expected outcome
* Actual outcome.
* Your running environment. At least:
- Cumalis Lisp's version.
- Node.js version or/and browser name and version.
- Operating system's name and version.
* If possible, URL of [CodeSandbox](https://codesandbox.io/) that can reproduce the bug.
### Pull requests
Pull requests are accepted on [Github](https://github.com/trb-a/cumalis-lisp/pulls).
## TODOs / Future plans
- Better documentation (especially Javascript interfaces more).
- REPL for Node.js.
- Expose all items in create, forms, part of functions and LISP that can be called directly for usability.
- Review the parameter names of functions. (to match R7RS)
- Add JSDocs.
- Add async/await feature to handle Javascript's async functions.
- Add some built-in library to handle <js> objects.
- It will be nice if there are Regular expressions(SRFI-115), Hashtables(SRFI-69), Handling date and time(SRFI-19), Sorting lists and vectors, etc.
- Fraction/Complex might be implemented using Fraction.js and Complex.js.
## LICENSE
MIT