JavaScript で Scheme インタプリタを作った話 — ブラウザ REPL・SICP 演習・デバッガまで
はじめに
「プログラミング言語の仕組みを理解したい」「SICP(計算機プログラムの構造と解釈)をブラウザで試したい」「Lisp マクロや call/cc を実際に動かしてみたい」——そんなとき、手元ですぐ動く Scheme 処理系があると便利です。
本記事では、JavaScript 単体で動作する Scheme インタプリタ scheme.js について紹介します。npm パッケージ名は scheme-js、MIT ライセンスで公開しています。
ブラウザ・Node.js の両方で動き、最近は次のような機能も揃えました。
- GitHub Pages でオンラインデモ公開
- SICP 演習専用 REPL(章ごとに 32 問の演習を実行)
- ステップ実行デバッガ(評価過程を追跡)
-
JavaScript 相互運用(
jsdotなどの糖衣構文) - R5RS / R7RS 準拠の拡張
インストール不要で試せるので、まずはデモ URL から触ってみてください。
オンラインデモ: https://nsas454.github.io/scheme.js/
scheme.js とは
scheme.js は、純粋な JavaScript で書かれた Scheme インタプリタです。2014 年頃から開発が続いており、最近はモジュール分割・テスト整備・ブラウザ UI の充実を進めています。
設計上のこだわり
1. ファーストクラスの継続(call/cc)
継続は CPS(Continuation-Passing Style) で実装し、トランポリンで実行します。これにより:
- 捕捉した継続を変数に保存して何度でも再呼び出しできる
- 深い再帰でもスタックオーバーフローしにくい
;; 継続を変数に保存して再利用
(call/cc
(lambda (k)
(set! my-k k)
42))
(my-k 99) ; => 99
2. 真のクロージャーとマクロ
- 字句スコープに基づくクロージャー
-
define-macro(Lisp 風マクロ) -
define-syntax/syntax-rules(衛生的マクロ、エリプシス...対応)
(define-macro (when test body)
(list 'if test body 0))
(when (eq? 1 1) 42) ; => 42
3. 数値タワー
多倍長整数・有理数・浮動小数・複素数をサポートします。
(* 1000000000000 1000000000000) ; 正確な整数
(+ 1/3 1/6) ; => 1/2
(* 3+4i 1+2i) ; 複素数演算
4. 本物の cons セル
実行時のリストは Pair オブジェクトで表現されます。set-car! / set-cdr! による破壊的更新、ドット対、循環リストにも対応しています。
アーキテクチャ概要
ソースは src/ 以下にモジュール分割され、scripts/build.js で dist/schemInp.js に結合されます。
ソース文字列
→ Tokenizer / parse (parser.js)
→ AST (JavaScript 配列)
→ seval (CPS, evaluator.js)
↓ Bounce チェーン
→ trampoline (core.js) で反復実行
→ 結果値
| モジュール | 役割 |
|---|---|
parser.js |
字句解析・S 式パース |
evaluator.js |
CPS 評価器・マクロ展開 |
env.js |
環境・クロージャー |
continuations.js |
call/cc, dynamic-wind
|
numbers.js |
数値タワー |
js_interop.js |
JavaScript 連携 |
debugger.js |
ステップ実行・トレース |
runtime.js |
公開 API・REPL |
興味がある方はリポジトリの docs/ARCHITECTURE.md も参照してください。
使い方
npm / Node.js
npm install scheme-js
const {
scheme, scheme_run, repr,
setGlobal, fromScheme, getGlobal
} = require('scheme-js');
console.log(scheme('(+ 1 2 3)')); // => 6
// JavaScript オブジェクトを Scheme へ
setGlobal('config', { retries: 3 });
scheme('(jsdot config retries)'); // => 3
// Scheme 手続きを JavaScript 関数として
scheme('(define (double x) (* x 2))');
const double = fromScheme(getGlobal('double'));
double(21); // => 42
CLI
npm install -g scheme-js
scheme-js examples/hello.scm
scheme-js -e "(display (+ 1 2))"
scheme-js # 対話 REPL
ブラウザ — <script type="text/scheme">
HTML に Scheme を直接書けます。
<script src="dist/r7rs_large.js"></script>
<script src="dist/schemInp.js"></script>
<script type="text/scheme">
(display (+ 1 2 3))
(define (fact n)
(if (= n 0) 1 (* n (fact (- n 1)))))
(display (fact 10))
</script>
ブラウザ — REPL UI
<div id="repl"></div>
<script>
scheme_repl_ui(document.getElementById('repl'));
</script>
scheme_repl_eval(code) で 1 式ずつ評価する API もあります。
今回作ったもの① — SICP 演習 REPL
SICP を学ぶとき、「教科書の演習をすぐ試したい」というニーズに応えるため、章ごとに演習を選んで実行できる専用 REPL を作りました。
URL: https://nsas454.github.io/scheme.js/sicp-repl.html
収録内容
| 章 | 演習数 | 例 |
|---|---|---|
| 第1章 手続きによる抽象化 | 13 問 | 1.1 式の評価、1.7 平方根、1.43 repeated
|
| 第2章 データによる抽象化 | 8 問 | 2.1 有理数、2.18 reverse、2.40 unique-pairs
|
| 第3章 変異可能データ | 7 問 | 3.1 口座、3.3 デジタル回路 |
| 第4章 メタ言語的抽象 | 2 問 | 4.1 eval、4.4 quasiquote |
| 第5章 計算機による抽象 | 2 問 | 5.2 階乗、5.7 フィボナッチ |
| 合計 | 32 問 |
本の全演習を網羅しているわけではなく、このインタプリタで動く代表例を章ごとに選んでいます。各演習は自己完結型のコードになっており、エディタで編集してから実行できます。
URL パラメータで直接開くこともできます。
https://nsas454.github.io/scheme.js/sicp-repl.html?ch=1&ex=1.7
演習 1.7(ニュートン法による平方根)の例
(define (square x) (* x x))
(define (average x y) (/ (+ x y) 2))
(define (improve guess x)
(average guess (/ x guess)))
(define (good-enough? guess x)
(< (abs (- (square guess) x)) 0.001))
(define (sqrt-iter guess x)
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x) x)))
(define (sqrt x) (sqrt-iter 1.0 x))
(display (sqrt 2))
左のサイドバーで章を選び、演習をクリック → 実行 ボタンで REPL に送って評価、という流れです。
今回作ったもの② — ステップ実行デバッガ
「この式がいつ評価されたか」「環境には何が束縛されているか」を学習者が追えるよう、CPS 評価器にフックしたデバッガを実装しました。
URL: https://nsas454.github.io/scheme.js/debug.html
- F10 — 1 ステップ進む
- F5 — 続行
- 評価イベント(
eval/return/apply)を一覧表示 - 停止時の環境(変数束縛)を表示
JavaScript API からも使えます。
const { scheme_debug_start } = require('scheme-js');
const sess = scheme_debug_start('(+ 1 2)');
sess.start();
console.log(sess.currentEvent); // { phase: 'eval', source: '(+ 1 2)', ... }
sess.step();
sess.continue();
SICP の「環境モデル」を学ぶとき、実行の流れを目で追えるのがポイントです。
今回作ったもの③ — JavaScript 相互運用
Node.js やブラウザの API を Scheme から自然に触れるよう、相互運用レイヤを強化しました。
糖衣構文
;; 従来
(js-call (js-ref (js-global) "Math") "abs" -3)
;; 現在
(jsdot (js-ref js-window "Math") abs -3) ; => 3
(jslog "hello" 42) ; console.log
(jsdot! (jsnew Date 0) getFullYear) ; new Date(0).getFullYear()
| マクロ / 手続き | 用途 |
|---|---|
jsdot |
プロパティ参照・メソッド呼び出し |
jsdot! |
引数なしメソッド |
jslog |
console.log |
jsnew |
コンストラクタ呼び出し |
js-window |
グローバルオブジェクト |
JavaScript から Scheme へ
setGlobal('user', { name: 'Alice', score: 100 });
scheme('(jsdot user name)'); // => "Alice"
今回作ったもの④ — GitHub Pages 公開
デモ・REPL・SICP REPL・デバッガを GitHub Actions で自動デプロイしています。
master ブランチへの push で:
-
npm run buildでdist/を生成 - HTML と
dist/をサイト用にまとめる - GitHub Pages へデプロイ
ローカルでビルドする場合:
git clone https://github.com/nsas454/scheme.js.git
cd scheme.js
npm install
npm test # ビルド + 全テスト
python3 -m http.server 8000
# http://localhost:8000 で各 HTML を開く
R5RS / R7RS 対応
R5RS 系(抜粋)
-
syntax-rules衛生的マクロ - 数値タワー(多倍長整数・有理数・複素数)
- I/O ポート(文字列ポート、Node.js ではファイルポート)
-
dynamic-wind+ 継続 - 内部
defineの R5RS 準拠(letrec脱糖)
R7RS 系(抜粋)
-
define-library/import/export -
case-lambda,define-values,let-values -
guard/raise,define-record-type - ハッシュテーブル、ソート、バイトベクタ、ストリーム など(large 拡張)
テストは npm test で一括実行でき、R5RS / R7RS / JS 連携 / デバッガ / SICP 演習の計 80 件以上がパスする状態です。
こんな用途に向いています
- SICP を読みながらブラウザで演習を試す
- マクロ・継続・クロージャーの挙動を REPL で確認する
- 評価器の内部をデバッガでステップ追跡する
- JavaScript プロジェクトに Scheme を埋め込む(npm パッケージ)
- 言語処理系の勉強(パーサ・CPS 評価器・トランポリンの実例)
まとめ
scheme.js は「ブラウザを開けばすぐ Scheme が書ける」処理系を目指してきました。今回の更新で:
- GitHub Pages — インストール不要のオンラインデモ
- SICP 演習 REPL — 32 問の演習を章ごとに実行
- ステップデバッガ — 評価過程の可視化
-
JS 相互運用 —
jsdotなど自然な記法 - npm パッケージ / CLI — Node.js への組み込み
を揃えました。
ぜひデモを触ってみてください。
- リポジトリ: https://github.com/nsas454/scheme.js
- オンラインデモ: https://nsas454.github.io/scheme.js/
- SICP REPL: https://nsas454.github.io/scheme.js/sicp-repl.html
Issue や PR も歓迎です。