cumalis-lisp

A Scheme implementation written in Typescript.

Usage no npm install needed!

<script type="module">
  import cumalisLisp from 'https://cdn.skypack.dev/cumalis-lisp';
</script>

README

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

$ 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 or Blockly.
    • Note: Cumalis Lisp is expected to be the backend of "Cumalis" 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

CodeSandbox Template

How to use as a module

Basic usage

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

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

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, "$quot;);
  const revived: LISP.Suspend = fromReferentialJSON(json, "$quot;);

  // 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.

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

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.

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 that can reproduce the bug.

Pull requests

Pull requests are accepted on Github.

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 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