generated at
make a lisp 2週目
make a lispをやるで一通り実際に動かしながら確認したが、ちゃんと自分で全部書かなかったのでやり直す

全体で1400行もない


mjsでの実行のさせかた確認しつつ

ブラウザで動作させたほうが楽しいのでそれの用意と
あとテストだな

ブラウザ上でとりあえずreplっぽい動きは用意できた
mochaでテスト用意

step0
とにかく文字を返す

step1
正規表現をまるっとけずる
const re = /[\s,]*([^\s]*)/g;
数字を整数と小数読む
あとは Symbol.for('hoge') とした

malのテスト見るとdefferable(遅延可能)とそうでないのが分かりやすい

ここで必要なのはリスト
評価はせずにリストとしてparseできるようにしよう

diff
- const re = /[\s,]*([^\s]*)/g; + const re = /[\s,]*([()]|[^\s()]*)/g;

カッコ込でちゃんとパースするだけで正規表現クソ大変だった
「または」の順序
[\s,]* 先頭トリム? でもカンマの意味が分からん
ちがうわ、無視する部分か
( a , b) のとき、 ( , \s(a) , \s,\s(b) こういう感じでマッチさせるんだ

/A(B)/ というグループで分けて、Bにマッチさせたいやつを和集合で渡して、無視したいのをAにかけばいいのか


後回しにしたもの
nil, true, false
string literal
エラーのテスト
閉じカッコなし
文字列の閉じなし
クオート
'
quote
quasiquote,
unquote ~
splice-unquote ~@
キーワード
ベクタ
ハッシュマップ
コメント
@ / deref

optional
metadata ^
nonalphanumerice characters
コメント後のいろんな文字列




step2

リストの評価

js
const eval_ast = (ast, env) => { if (typeof ast === "symbol") { if (ast in env) { return env[ast]; } else { throw Error(`'${Symbol.keyFor(ast)}' not found`); } } else if (ast instanceof Array) { return ast.map((x) => EVAL(x, env)); } else { return ast; } }; const EVAL = (ast, env) => { if (!isList(ast)) { return eval_ast(ast, env); } if (ast.length === 0) { return ast; } // evaluate function const [f, ...args] = eval_ast(ast, env); return f(...args); };
こんなかんじで評価する
リスト内はそのままmapで評価すれば再帰する
symbolはそのまま

step2の遅延可能なの
配列, hashmapの内側評価




step3

envを外側に出す
new_env
letとか関数の代入をやる
(fn* (a b) (+ a b))
環境を複製して、呼び出し時に渡されたexprをa,bでenvにセットする
なのでまだ関数定義作ってないのにnew_envの中身を先に実装してしまったなmiyamonz
env_get
env_set

EVALでのenvへの副作用
def!
!はなんだろ
副作用あるやつとかにつけるのか

let*
まあ*無しでもいいかな

js
case "def": return env_set(env, a1, EVAL(a2, env)); case "let": const let_env = new_env(env); for (let i = 0; i < a1.length; i += 2) { env_set(let_env, a1[i], EVAL(a1[i + 1], let_env)); } return EVAL(a2, let_env);
こんな感じ
defは現在のenvにset
letは新規envを作って評価
let_envはここで使用されて消える

defferable
letの定義部分をvectorでも可能にする
(let [z 1] z)
letの評価部分もvectorを許す
(let (a 5 b 6) [a b])

miyamonz
副作用のあるなしとかはmetadataとして渡せば良いはず(必要なら)

step4

list関数
list
(list) => ()
list?
empty?
count
if
conditinals
=
これは再帰的にやらないといけない
coreに書いた
配列(list)の再帰だけやった
mapの再帰も後で足す
>, <, >=, <=
これは普通にcoreにそのまま実装


fn*

クロージャ

do
ただ単に順番にevalする
prn
これはいいや

step4 のdeffarable
文字の比較、
variable length argument
((fn* (& more) (count more)) 1 2 3)
多分既に動くと思うが


---


step5
tail call optimizatoin
なんかコードが汚くなるからやだな
まあやった
たしかにでかい再帰が実行できた
EVALの関数定義のとこ (fn args exp)
これを、mal上の関数オブジェクトとして定義して末尾呼出最適化できるようにしてある
const fn = () => {} で空っぽにしても動いた



step6 ファイルとか

ファイルを呼んだりするのに文字列使うので、ここまで放置してきた文字列リテラルをやる

文字列リテラル
" これあたりreadでちゃんとparseする
js
const re = /[\s,]*([()]| [^\s,()]*)/g; const re = /[\s,]*([()]|"(?:\\.|[^\\"])*"?|[^\s,()]*)/g;
/"(?:\\.|[^\\"])*"?/
"hoge" をちゃんとparseする
?: カッコのグループをキャプチャしない
Wコロン内の文字はキャプチャしなくていい
\\. メタ文字の認識
\s とか
[^\\"] \ " 以外をキャプチャ(文字列リテラル内の文字)



txt
"ho\"ge" [ '"ho\\"ge"' ] "hoge\n" [ '"hoge\\n"' ]

ダブルクオートも含めて文字列として投げる
バックスラッシュも文字列
正規表現でパースした結果
バックスラッシュも文字列としてパースするので、表示状はエスケープされる

print側
js
} else if (typeof obj === "string") { const str = obj .replace(/\\/g, String.raw`\\`) .replace(/"/g, String.raw`\"`) .replace(/\n/g, String.raw`\n`); return `"${str}"`; }

エスケープが必要な文字を見つけたら、エスケープした上で表示する
regex上でエスケープが必要であるかどうかとか、
String.rawは完全にそのまま


文字列を扱う上で必要なことは、
読み込み時はメタ文字を解決しつつ、
表示時にはそれを戻す

しかし、メタ文字の解決はmalでは改行しかやってないな まあいいや

コメントアウト
diff
- const re = /[\s,]*([()]|"(?:\\.|[^\\"])*"?| [^\s,() ]*)/g; + const re = /[\s,]*([()]|"(?:\\.|[^\\"])*"?|;.*|[^\s,();]*)/g;

右の除外文字に ; を入れる
js
while ((match = re.exec(str)[1]) != "") { if (match[0] === ";") { continue; } results.push(match); }
このcontinueで無視できる
これだけでもいけるのだが、コメント部分をまとめてtokenにするために ;.* もパースするようにしてある
malがそうなっている
テストのexpectedがコメントに書いてあるからこれ使ってるのかな?


eval
バグが発生して非常に悩まされた
new_env時に親のenvを固定していたのがだめだった
const e = {...outer}

動的に参照するように Object.create(outer) して解決

(let () (do (eval (read-string "(def aa 1)")) aa) )
これのevalで、大本のreplのenvにセットされるはずなのだが、let時に上記の {...outer} してしまっているとこの変化が見えないのでaaがnot foundになってしまう
let時点でのenvが固定されてしまう

これ、今は理解できてるけど後で読み返して意味わかるか?
多分大丈夫だと思う



step6
load-file
(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))'
こうなっとる
slurp
外部ファイルをそのまま文字列としてとってくる
malにおいては、nodeならreadFileSync, ブラウザならhttp request

しかしload-fileも後回しでいいか
deferrableではないのだが


atomとderef
どちらもcoreで定義
Atomはそういうクラスを作ってる
js
class Atom { constructor(val) { ths.val = val } }
こんだけ
derefはatom => atom.valするだけ

resetやswap
swapはオモロイ
(swap! atm + 3) とかができるね

難しいところはないな


step6のdefferable
@ derefのショートハンド
cljs
(def! atm (atom 5)) @atm ;=> 5

コメント内の特殊な記号とか



step7

cons
(a,b) => [a, ...b] するだけ
concat
(...a) => a.reduce( (x,y) => x.concat(y), [])
[...x, ...y] でもいい
quote
unquote
splice-unquote

= のsymbolの等価
=自体は実装済み
テスト内でstr使うものがあったので、遅延していたpr-strとstrを実装した
pr-strとstrのprint_readablyの意味がちょっとわかった
正確には追ってない
前に文字列リテラルまわり整えた時やったはずなんだが
jsで実装する場合のString.raw大事


defferable
' quote
` quasiquote
~ unquote
~@ splice-unquote

readerの正規表現に追加して、対応する変換を書いて
js
case `'`: reader.next(); return [Symbol.for("quote"), read_form(reader)]; case "`": reader.next(); return [Symbol.for("quasiquote"), read_form(reader)]; case "~": reader.next(); return [Symbol.for("unquote"), read_form(reader)]; case "~@": reader.next(); return [Symbol.for("splice-unquote"), read_form(reader)];
こんだけ


ベクタでの動作
これは後回しにしよっと


step8 macro

defmacro
macroexpand


defferable
non-macroな関数
not
これだけやった
nth
first
rest
cond macro
これthrowはいってるからstep8にあるのは間違いやな。step9のあとにやる


step9

throw
try, catch

ここでcondを実装
nth
first
rest
が必要だったので入れた

builtin
symbol?
nil?
true?
false?
apply
map


deferrable
sequential

dissoc
assocって実装したっけ?
ハッシュマップ





stepA

deferrable
meta
with-meta
string?
number?
fn?
macro?
conj
seq



残っている関数がいくつかあるものの、だいたいできた気がする
750Lくらいか。内容が濃いのはあるがおもったより書いてなかったな

以上の実装内容の依存関係をグラフで表示してみたい


formとは、最初の要素が特別なコマンド(関数またはマクロ)であるようなリストのこと