0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript で Scheme インタプリタを作った話 — ブラウザ REPL・SICP 演習・デバッガまで

0
Posted at

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/

ページ URL
トップ https://nsas454.github.io/scheme.js/
機能デモ https://nsas454.github.io/scheme.js/demo.html
REPL https://nsas454.github.io/scheme.js/repl.html
SICP 演習 REPL https://nsas454.github.io/scheme.js/sicp-repl.html
デバッガ https://nsas454.github.io/scheme.js/debug.html

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.jsdist/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 で:

  1. npm run builddist/ を生成
  2. HTML と dist/ をサイト用にまとめる
  3. 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 が書ける」処理系を目指してきました。今回の更新で:

  1. GitHub Pages — インストール不要のオンラインデモ
  2. SICP 演習 REPL — 32 問の演習を章ごとに実行
  3. ステップデバッガ — 評価過程の可視化
  4. JS 相互運用jsdot など自然な記法
  5. npm パッケージ / CLI — Node.js への組み込み

を揃えました。

ぜひデモを触ってみてください。

Issue や PR も歓迎です。


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?