(全体継続, マルチショット)
[1] eta の基本的な機能
[2] eta の実行プロセス
[3] eta の特徴的な実装
call/cc
の実装[4] まとめ
定義: https://github.com/abap34/eta/blob/main/devdocs/grammar.md
load
, do
;; 平方根をニュートン法で求める
(define (sqrt-newton x)
(define (improve guess) (/ (+ guess (/ x guess)) 2))
(define (good-enough? guess) (< (abs (- (* guess guess) x)) 0.0001))
(define (try guess)
(if (good-enough? guess) guess
(try (improve guess))))
(try 3.4))
(display (sqrt-newton 2))
基本的な Scheme の機能をサポート.
(例) 再帰
(define (fact n)
(if (= n 1)
1
(* n (fact (- n 1)))))
(display "5! = ")
(display (fact 5))
(例) named-let
(define (fib n)
(let loop ((i n) (a 0) (b 1))
(if (= i 0)
a
(loop (- i 1) b (+ a b)))))
(display (fib 10))
eta 自身で書かれた stdlib も整備
io.eta
: 入出力logical.eta
: 論理演算math.eta
: 数学関数list.eta
: リスト操作higher-order.eta
: 高階関数vector.eta
: ベクトル操作examples/alife.eta
... ライフゲームの pulsar パターンを描画するプログラム
より (その実装が) 発展的な機能もサポート
call/cc
, 実装も含めてあとのセクションで
let*
, letrec
, cond
などは Desugar を使って実現 (後述)1. REPL (Read-Eval-Print Loop) ─ (eta
コマンドで起動)
2. スクリプト実行 ─ (eta --script <filanem>
で filename
の中身を実行)
eta
)同じ環境を使い回して
入力・評価・出力を繰り返す
複数行入力
便利コマンド
詳細は [3] eta の特徴的な実装
eta --script <filename>
)スクリプトを読み込んで実行する.
例えば cond
は のようなマクロで
実現できる.
⇩
stdlib にこのマクロを入れてしまってサポートしてしまうのもあり
; cond マクロの実装例 (racket)
(define-syntax cond
(syntax-rules (else)
((cond)
#f)
((cond (else expr ...))
(begin expr ...))
((cond (test expr ...) rest ...)
(if test
(begin expr ...)
(cond rest ...)))))
(cond
((= 1 2) (display "1 is not equal to 2"))
((= 1 1) (display "1 is equal to 1"))
((= 2 3) (display "2 is not equal to 3")))
マクロを使うには
マクロを実装する必要がある
⇩
最初から衛生的マクロをサポートするつもりだった
⇩
マクロの完成を待つと本格的なプログラムが書けるまで時間がかかりそう
⇩
とりあえず Desugar を使って実装しておいたもの
文字列を Token
のリストに変換.
(struct Token (typ val loc))
desc | example | |
---|---|---|
typ |
トークンの種類 | 'IntToken , 'IdToken |
val |
トークンの値 | x , 42 , "hello" , let , + |
loc |
トークンの位置 (後述) | example.eta:(1, 2) - (3, 4) |
Token
のリストを Expr
(eval
しやすい用の head
がついたリスト) に変換
(struct Expr (head args loc))
desc | example | |
---|---|---|
head |
ノードの種類 | 'ConstHead , 'IfHead |
args |
式の引数 (head に応じたリスト) |
(list name value) |
loc |
式の位置 (後述) | example.eta:(1, 2) - (3, 4) |
; parse.rkt#parse-begin
; Begin ::= (begin Exp*)
(define parse-begin
(map-parser
(sequence
(label "LParen" lparen)
(label "begin" (keyword "begin"))
(label "Exp*" (zero-or-more (parser-ref parse-exp)))
(label "RParen" rparen))
(lambda (result)
(let ([args (third result)]
[loc (create-span-location
(loc (first result))
(loc (last result)))])
(make-begin loc args)))))
Expr
を Desugar して単純な形の Expr
に変換.
and
の Desugar処理 ((and a b c)
→ (if a (if b c #f) #f)
); desugar.rkt#desugar-and-expressions
(define (desugar-and-expressions args loc)
(make-ifthenelse loc
(desugar (first args))
(if (null? (rest (rest args)))
(desugar (second args))
(desugar-and-expressions (rest args) loc))
(make-const loc 'BoolConstNode #f)))
Desugar により Expr
の head
は限られたもののみになる.
これらの評価だけ書けば OK.
; interp.rkt#eval-expr
(define (eval-expr expr env k stack #:tail? [tail? #f])
(let ([head (Expr-head expr)])
(cond
[(equal? head 'ConstHead) (eval-const expr env k stack)]
[(equal? head 'IdHead) (eval-var expr env k stack)]
[(equal? head 'AppHead) (eval-app expr env k stack #:tail? tail?)]
[(equal? head 'LambdaHead) (eval-lambda expr env k stack)]
[(equal? head 'QuoteHead) (eval-quote expr env k stack)]
[(equal? head 'DefineHead) (eval-define expr env k stack)]
[(equal? head 'IfHead) (eval-if expr env k stack #:tail? tail?)]
[(equal? head 'SetHead) (eval-set! expr env k stack)]
[(equal? head 'BodyHead) (eval-body expr env k stack #:tail? tail?)]
[(equal? head 'CallCCHead) (eval-call/cc expr env k stack #:tail? tail?)]
[(equal? head 'LoadHead) (eval-load expr env k stack #:tail? tail?)])))
Evaluate における主要なデータ構造
Env
(環境) (eval/env.rkt
)CallFrame
, CallStack
(コールスタック) (eval/stack-frame.rkt
)RuntimeValue
(実行時の値) (eval/runtime-value.rkt
)(Env frame parent)
親への参照を持つ ハッシュテーブル.
親の環境との差分を保持.
(struct Env (frame parent))
(define (env-lookup env name)
(let loop ([e env])
(cond
[(not e) (make-runtime-error (format "Undefined variable: ~a" name))]
[(hash-has-key? (Env-frame e) name) (hash-ref (Env-frame e) name)]
[else (loop (Env-parent e))])))
CallFrame
, CallStack
CallFrame
desc | |
---|---|
proc |
呼び出した手続き |
args |
実引数 |
loc |
call-cite の位置 |
parent |
親のコールフレーム |
tail? |
末尾呼び出しかどうか |
CallStack
... CallFrame
のリスト
エラー発生時に
StackTrace
に変換
; error.rkt#StackTraceEntry
(struct StackTraceEntry (func-name location))
; stack-frame.rkt#call-stack->stack-trace
(define (call-stack->stack-trace stack)
(if (call-stack-empty? stack)
'()
(map (lambda (frame)
(let ([proc (CallFrame-proc frame)]
[loc (CallFrame-loc frame)])
(make-stack-trace-entry
(extract-function-name proc) loc)))
(CallStack-frames stack))))
RuntimeValue
インタプリタ側で見えている値は
全て RuntimeValue
として管理.
タグ付きで 実行時 にいろいろ検査
(struct RuntimeValue (tag value))
desc | example | |
---|---|---|
tag |
値の種類を表すタグ | 'IntTag , 'StringTag |
value |
実際の値を表す racket のオブジェクト | 42 , "hello" |
(define (RuntimeValueTag? tag)
(or (equal? tag 'IntTag)
(equal? tag 'FloatTag)
(equal? tag 'StringTag)
(equal? tag 'BooleanTag)
(equal? tag 'NilValueTag)
(equal? tag 'PairTag)
(equal? tag 'SymbolTag)
(equal? tag 'EtaBuiltinTag)
(equal? tag 'EtaClosureTag)
(equal? tag 'EtaStructTag)
(equal? tag 'VectorTag)
(equal? tag 'VoidTag)
(equal? tag 'UndefinedTag)
(equal? tag 'EtaContinuationTag)))
AST を直接評価する方式.
例) 識別子の評価
; interp.rkt#eval-var
(define (eval-var expr env k stack)
(let* ([var-args (Expr-args expr)]
[name (first var-args)]
[result (env-lookup env name)])
(if (EtaError? result)
(k (localize-error-location result (Expr-loc expr)) stack)
(k result stack))))
call/cc
の実装eval-*
系の基本的なシグネチャ:
(define (eval-<head> expr env k stack #:tail? [tail? #f])
...
(let ([<args> (Expr-args expr)])
...
(k <result> stack)))
expr
: 評価する部分式env
: 環境stack
: コールスタックtail?
: 末尾呼び出しかどうかk
: 継続call/cc
の実装 CPS (Continuation-Passing Style) で評価を実装
call/cc
の実装すごく簡単に ※ (簡略化 ver)
(define (eval-call/cc expr env k stack tail)
(let ([proc-expr (first (Expr-args expr))]
[loc (Expr-loc expr)])
(eval-expr
proc-expr
env
(lambda (proc-value new-stack)
(if (RuntimeError? proc-value)
(k proc-value new-stack)
(let ([reified-cont (make-continuation k new-stack)])
(apply-proc proc-value (list refied-cont) env k new-stack)
[else
(k (make-error) new-stack)]))))
stack)))
各評価で末尾かをチェック
例) eval-body-of-body
; interp.rkt#eval-body-of-body
(define (eval-body-of-body expr-list env k stack #:tail? [tail? #f])
(if (null? expr-list)
(k ... stack)
(let loop ([exprs expr-list]
...)
(if (null? exprs)
(k (first result) current-stack)
(let ([is-last-expr? (null? (rest exprs))]) ; <- 末尾かチェック
(eval-expr ... new-stack)))
current-stack
#:tail? (and tail? is-last-expr?)))))))
▶︎ 末尾呼び出しなら CallStack に新しく Frame を積まずに置換する
; stack-frame.rkt#call-stack-push
(define (call-stack-push stack frame)
(cond
[(>= (CallStack-current-depth stack) (MAX-STACK-DEPTH))
(make-runtime-error "Stack overflow: maximum stack depth exceeded")]
[(and (CallFrame-tail? frame) (not (call-stack-empty? stack)))
(CallStack (cons frame (cdr (CallStack-frames stack)))
(CallStack-current-depth stack))]
[else
(CallStack (cons frame (CallStack-frames stack))
(+ 1 (CallStack-current-depth stack)))]))
以下のようなプログラムを実行すると...
(set-max-stack-depth! 90)
(define (fact-with-tco n)
(define (fact-iter n acc)
(if (= n 1)
acc
(fact-iter (- n 1) (* n acc))))
(fact-iter n 1))
(define (fact-without-tco n)
(if (= n 1)
1
(* n (fact-without-tco (- n 1)))))
エラー報告に謎のこだわり
エラーは 3つに分類 (ref: error.rkt)
error
)どれも
を持つ. (+ RuntimeError
は stack-trace
も持つ)
(struct RuntimeError EtaError (stack-trace))
(struct TokenizeError EtaError ())
(struct ParseError EtaError ())
eta では ほぼ全ての
Token
Expr
*Error
が Location
を持つように実装されている!
; location.rkt#Location
(struct Location (file sline scol eline ecol))
※ 例外的に、eval
関数によって作られる Token
, Expr
などはダミーの位置情報を保持しています。
Location
を付与(define (make-token typ val sline scol eline ecol)
(Token typ val (Location file sline scol eline ecol)))
(define (read-symbol pos line col)
...
(let* ((end (loop pos))
...(make-token token-type lexeme
line col line (+ col (- end pos)))
end line (+ col (- end pos)))))
; ^^^ 位置情報を付与
Location
を追跡; App ::= (Exp Exp*)
(define parse-app
(map-parser
(sequence
(label "LParen" lparen)
(label "Exp" (parser-ref parse-exp))
(label "Exp*" (zero-or-more (parser-ref parse-exp)))
(label "RParen" rparen))
(lambda (result)
(let ([op (second result)]
[args (third result)]
[loc (create-span-location
(loc (first result))
(loc (last result)))])
(make-app loc op args)))))
...
▶︎ 位置情報付きで raise!
; interp.rkt#eval-app
...
(k (make-runtime-error
(format
"Application of non-function: ~a"
(RuntimeValue-tag operator-value))
(Expr-loc operator-expr))
op-stack)
...
RuntimeError
に スタックトレースを付与
... エラー時に stack が見えている eval-*
がつけてあげる
; interp.rkt#collect-stack-trace-for-error
(define (collect-stack-trace-for-error error stack)
(if (RuntimeError? error)
(add-stack-trace-to-error
error (call-stack->stack-trace stack))
error))
同様の仕組み... localize-error-location
いくらか機能をつけた REPL を実装
multiline input
各種コマンド
ときのみ入力を終了する
; input-reader.rkt
(struct StringState (in-string? escaped?))
(struct ParseState (balance string-state))
(define (input-needs-continuation? input [initial-state (ParseState 0 (StringState #f #f))])
(let* ([final-state (count-bracket-balance input initial-state)]
[balance (ParseState-balance final-state)]
[str-state (ParseState-string-state final-state)]
[in-string? (StringState-in-string? str-state)])
(or (> balance 0) ; Unclosed brackets
in-string? ; Unclosed string
)))
