はい。今度はScheme(処理系はGaucheを使用)でForthを実装してみました。
プログラムは5つの部分に分かれていて、
1)スタック管理
2)標準入力からプログラムを読むリーダ
3)ForthプログラムをSchemeプログラムに変換するトランスレータ
4)ワードが登録される辞書
5)実行制御
という構成になっています。
とりあえず、各部分の説明をしていきます。
1)スタック管理
;;; スタック管理
(define *data-stack* '())
(define (push val)
(set! *data-stack* (cons val *data-stack*)))
(define (pop)
(cond
((null? *data-stack*)
(raise "Stack UnderFlow."))
(else
(let ((val (car *data-stack*)))
(set! *data-stack* (cdr *data-stack*))
val))))
スタック管理は面倒くさくないグローバル変数を使ったものにしました。
関数もPUSHとPOPのみです。POPにはスタックアンダーフローがでたら例外が投げられるようになっています。
2)標準入力からプログラムを読むリーダ
;;; 標準入力から読み込み、トークンに分割する。
(define (trim str)
(begin
(set! str (regexp-replace #/^\s+/ str ""))
(set! str (regexp-replace #/\s+$/ str ""))
str))
;; シンボルに使えない文字を変換する
(define (def-word-translate str)
(begin
(set! str (regexp-replace-all #/\;/ str "edef"))
(set! str (regexp-replace-all #/\:/ str "sdef"))
(set! str (regexp-replace-all #/\./ str "dot"))
(set! str (regexp-replace-all #/\.([a-zA-Z])/ str "dot\\1"))
(set! str (regexp-replace-all #/\s+/ str " "))
str))
;; 半角スペースで分割
(define (tokenizer str)
(token->symbol (string-split (def-word-translate str) " ")))
;; トークンをシンボルと数値のリストにする
(define (token->symbol tokens)
(cond
((null? tokens) '())
((rxmatch #/[0-9\.]+/ (car tokens))
(cons (string->number (car tokens))
(token->symbol (cdr tokens))))
(else
(cons (string->symbol (car tokens))
(token->symbol (cdr tokens))))))
;; 標準入力からプログラムを読み取り、トークンにして返す
(define (reader)
(let ((line (read-line)))
(cond
((eof-object? line) '())
((string=? "" (trim line))
(reader))
(else
(tokenizer (trim line))))))
基本的には標準入力から入ってきた文字列をトークンに分割するだけなのですが、「;」や「.」がSchemeでは特別な意味を持っているので、変換してからトークンに分割しています。
3)ForthプログラムをSchemeプログラムに変換するトランスレータ
;;; ForthプログラムをSchemeに変換する
(define *program* '())
(define (translate-token token)
(cond
((number? token)
(list 'push token))
((search-dict token)
(list (get-dict token)))
(else
(raise (format #f "Undefined [~A]." token)))))
(define (define-word tokens)
(cond
((null? tokens)
(raise "Syntax Error: [;] is missing."))
((eq? 'edef (car tokens)) '())
(else
(cons (translate-token (car tokens))
(define-word (cdr tokens))))))
(define (translate tokens define-flag)
(cond
((null? tokens) '())
((and define-flag)
(if (eq? (car tokens) 'edef)
(translate (cdr tokens) #f)
(translate (cdr tokens) #t)))
((eq? (car tokens) 'sdef)
(add-dict (cadr tokens)
(append '(lambda ()) (define-word (cddr tokens))))
(translate (cdr tokens) #t))
(else
(cons (translate-token (car tokens))
(translate (cdr tokens) #f)))))
トランスレータは今はこれだけです。これに「ループ処理」や「ローカル変数」などを実装するともっと長くなりそうです。
ワード定義もここでやっていて、
(append '(lambda ()) '(define-word ...))
とやれば、手軽にワードを生成することができますね。
4)ワードが登録される辞書
;;; 辞書用プログラム
(define *dictionaly* (make-hash-table 'equal?))
;; 辞書初期化
(define (init-dict)
(begin
(hash-table-put! *dictionaly* '+
(lambda () (push (+ (pop) (pop)))))
(hash-table-put! *dictionaly* '-
(lambda () (let ((a (pop))) (push (- (pop) a)))))
(hash-table-put! *dictionaly* '*
(lambda () (let ((a (pop))) (push (* (pop) a)))))
(hash-table-put! *dictionaly* '/
(lambda () (let ((a (pop))) (push (/ (pop) a)))))
(hash-table-put! *dictionaly* 'mod
(lambda () (let ((a (pop))) (push (mod (pop) a)))))
(hash-table-put! *dictionaly* 'dot
(lambda () (print (pop))))
(hash-table-put! *dictionaly* 'dots
(lambda () (print (reverse *data-stack*))))
(hash-table-put! *dictionaly* 'exit
(lambda () (exit)))
))
;; 辞書に追加
(define (add-dict key val)
(hash-table-put! *dictionaly* key val))
;; 辞書検索
(define (search-dict key)
(hash-table-exists? *dictionaly* key))
;; 辞書から取得
(define (get-dict key)
(if (search-dict key)
(hash-table-get *dictionaly* key)
(raise (format #f "Undefined [~A]" key))))
ワード辞書もグローバル変数としました。辞書からワードを取得するとき、ワードが存在しなければ例外が投げられます。
ワードは「init-dict」関数に追加していけば使える組み込みワードが増えます。今は四則演算と表示くらいしかないですがw
5)実行制御
;;; Forthエントリポイント
;; 処理系初期化
(define (init-forth)
(begin
(load "./dictionaly")
(load "./reader")
(load "./stack")
(load "./translate")
(init-dict)))
;; ForthプログラムをSchemeに変換した結果を実行する
(define (exec-forth prog ret)
(cond
((null? prog) ret)
(else
(exec-forth (cdr prog)
(eval (car prog) (interaction-environment))))))
(init-forth)
(let loop ()
(display "> ")
(flush)
(display (exec-forth (translate (reader) #f) #f))
(newline)
(loop))
これは特に説明することもないです。
処理系の初期化関数とトランスレートしたForthプログラムの実行くらいです。
トランスレートしたプログラムはただevalしてるだけですね。
こんな感じです。
