gaucheをgeiserへ (final)

"The Boy Who Harnmessed the Wind" なんて本を読んだ。えっ、お前が英語の本なんてすらすら読めるのか! はい、すんません、邦題だと『風をつかまえた少年』って本です。

広大なアフリカの大地にある小国のマラウイと言う所で、全くの自力で風力発電をやってのけた少年の物語。

干ばつ被害で家がとっても貧乏。学費が払えずにまともに学校にも行けないような環境。そんな中で、少年は図書館で一冊の本に出会います。大きな風車で発電してる写真が表紙を飾るエネルギーの本。

電気が作れれば、ポンプで水をくみ上げ畑を灌漑出来る。夜も明るく出来る。貧乏から抜け出せるんじゃなかろうか。

必死で物理の本を読む。電気の勉強。材料はゴミ捨て場から調達。家の中はごみだらけ。村でも、あの子は狂ってるって後ろ指をさされる。でも、少年はめげない。

ねじ回し用のドライバーも無し。自転車のスポークを叩いて削って、ドライバーを作る有様。無いないづくしの環境。

そして、風車は完成。小さな電灯が灯った時の喜びと言ったら。。。

村の人もこの偉業にびっくり。やがて、この風車が、国の役人に知れる事となり、そこから彼は更に大きな風を捕まえた。

なんだか、オイラーの昔を思い出しちゃったぞ。ゴミ捨て場に行って、打ち捨てられたTVやらラジオを拾ってくる。そこかから使えそうな真空管やら抵抗、コンデンサー、トランスを取り出す。12BYA7(だったかな)と言う、水平偏向のドライバーの球は、CQ誌でも絶賛された、使い易い真空管だった。

このお宝を求めて、あちこち彷徨ったな。そして、時々同業者のラジオ少年とも鉢合わせ。色々と教えてもらったり、互いの家に遊びに行ったり。貧しかったけど、楽しい日々でしたよ。

sentinel

前回から間を置いてしまったけど、またgeiserにgaucheを組み込むのにチャレンジしてる。 r7rsは不得手なので、そっちに行かないように設定。(海の物とも山の物とも知れないので、詳細は割愛)

goshのプロンプトが出て来るんで、そこから1って一番簡単な入力をすると、暫くして、終了してしまう。何のメッセージも出てこないので、調べようが無い。つらつらとコードを眺めていたら、 geiser-repl.el/geoser-repl--sentinelなんてのに気づいた。番兵さん。ここにdebug-on-entryして、呼び出された瞬間を捉える。

Debugger entered--entering a function:
* geiser-repl--sentinel(#<process Gauche REPL> "segmentation fault\n")
  accept-process-output(#<process Gauche REPL> 1.0)
    :
  geiser-con--send-string/wait((t (:filter . comint-output-filter) (:tq ((nil "$
  geiser-eval--send/wait((:eval (:ge completions "1")) nil nil)
    con = (t (:filter . comint-output-filter) (:tq ((nil "\\(\ngosh> \\)" ((:id$
    str = "(geiser:eval '#f '(geiser:completions \"1\"))"
    cont = geiser-eval--set-sync-retort
    timeout = nil
    sbuf = nil
     :

セグフォって言ってるよ。どんな物を喰わせたかは、履歴に残ってた。

久しぶりにgdb登場

これから暫く、goshと格闘だな。再現実験。

(base) sakae@debian:tmp$ \gosh -I .
gosh> (use geiser)
gosh> (geiser:eval '#f '(geiser:completions "1"))
Segmentation fault

これはもう、検視するしかないかな。まて、coreを吐かせて調べるより、gdbでsentinelしろよ。なんと言っても、gdbはその方面に長けていますから。

(base) sakae@debian:tmp$ gdb -q gosh
Reading symbols from gosh...done.
(gdb) r -I .
Starting program: /usr/local/bin/gosh -I .
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff73c7700 (LWP 1602)]
gosh> (use geiser)
gosh> (geiser:eval '#f '(geiser:completions "1"))

Thread 1 "gosh" received signal SIGSEGV, Segmentation fault.
0x00007ffff7b3e2f2 in GC_clear_stack_inner (arg=arg@entry=0x0,
    limit=0x7fffff7fe080 <error: Cannot access memory at address 0x7fffff7fe080>) at extra/../misc.c:311
311           BZERO((/* no volatile */ void *)dummy, sizeof(dummy));

番兵が起動してきました。早速、履歴を調べます。

(gdb) bt
#0  0x00007ffff7b3e2f2 in GC_clear_stack_inner (arg=arg@entry=0x0,
    limit=0x7fffff7fe080 <error: Cannot access memory at address 0x7fffff7fe080>) at extra/../misc.c:311
#1  0x00007ffff7b3e328 in GC_clear_stack_inner (arg=arg@entry=0x0,
    limit=<optimized out>) at extra/../misc.c:313
#2  0x00007ffff7b3e328 in GC_clear_stack_inner (arg=arg@entry=0x0,
    limit=<optimized out>) at extra/../misc.c:313
     :
#1422 0x00007ffff7a271ff in Scm_Error (
    msg=0x7ffff7b53ce2 "unbound variable: %S") at error.c:669
#1423 0x00007ffff7a17f4a in run_loop () at ./vminsn.scm:841
#1424 0x00007ffff7a1f364 in user_eval_inner (program=<optimized out>,
    codevec=codevec@entry=0x7fffff837bb0) at vm.c:1497
#1425 0x00007ffff7a20142 in apply_rec (vm=0x7ffff73cac00, vm=0x7ffff73cac00,
    nargs=<optimized out>, proc=0x7ffff6933180) at vm.c:1590
#1426 Scm_ApplyRec (proc=0x7ffff6933180, args=0x20b) at vm.c:1610
#1427 0x00007ffff7a20656 in Scm_VMThrowException (raise_flags=1,
    exception=0x7ffff5b94680, vm=0x7ffff73cac00) at vm.c:2202
     :
#53876 0x00007ffff7a1f364 in user_eval_inner (program=<optimized out>, codevec=codevec@entry=0x7fffffffe0f0) at vm.c:1497
#53877 0x00007ffff7a20142 in apply_rec (vm=0x7ffff73cac00, vm=0x7ffff73cac00, nargs=<optimized out>, proc=0x7ffff68ca1e0) at vm.c:1590
#53878 Scm_ApplyRec (proc=0x7ffff68ca1e0, args=args@entry=0x20b) at vm.c:1610
#53879 0x00007ffff7a201ff in safe_eval_wrap (kind=1, arg0=0xb, args=0xb, cstr=0x5555555582be "(read-eval-print-loop)", env=0x7ffff7d77dc0 <userModule>, result=0x0) at vm.c:1736
#53880 0x0000555555557948 in enter_repl () at main.c:647
#53881 0x0000555555556a1d in main (ac=<optimized out>, av=<optimized out>) at main.c:783

ふむ、gaucheのエラー処理の所でループしてしまい、メモリーを食いつぶして しまった って所ですかね。最後はGCまで呼んでメモリー確保にやっきになってるって図ですかな。

素人が無謀な使い方をしてgaucheを落としてしまった? gaucheの機微に触れる所(秘孔)を突かれて自滅した。さて、どちらでしょうか?

分解主義

上の評価は、無謀過ぎると思うので、分解してみる。肝になる部分を、そろりそろりと実行。

gosh> (geiser:completions "1")
*** ERROR: unbound variable: environment-symbols
Stack Trace:
_______________________________________
  0  (environment-symbols (interaction-environment))
        at "./geiser.scm":48
  1  (environment-symbols (interaction-environment))
        at "./geiser.scm":48
  2  (map write-to-string (environment-symbols (interaction-enviro ...
        at "./geiser.scm":48
  3  (filter (lambda (el) (string-prefix? prefix el)) (map write-t ...
        at "./geiser.scm":46
  4  (eval expr env)
        at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269

今度は、真面目に止まってくれた。

    43    (define (geiser:completions prefix . rest)
    44      rest
    45      (sort string-ci<?
    46            (filter (lambda (el)
    47                      (string-prefix? prefix el))
    48                    (map write-to-string (environment-symbols (interaction-environment))))))

chezに特有の手続きだな。マニュアルを引く。

(environment-symbols env )                                      procedure
returns: a list of symbols
libraries: (chezscheme)

This procedure returns a list of symbols representing the identifiers bound 
in environment env . It is primarily useful in building the list of symbols 
to be copied from one environment to another.

ほぼ同じ場所に、aprposの説明も有った。同類項と見做してしまえ。(雑だけど、先にすらすら進むのがHacker道と心得る)

うんと単純化。aproposの結果をオイラーは既に持っているぞ。前回やったrlwrap + gauche の時に作った、.gosh_completionsと言うファイル。これ、一行に一関数名が書かれた4000行ぐらいのファイルだ。

これを読み込んで、リストにしちゃえば良いだろう。この方法だと、静的になっちゃうけど、まずは動くのが先ですから。こういう手法は、GAFAでも採用してるね。ぐぐる、あぷる、フェースブック、あまぞんとかね。取り合えず動くものを先に出した方が勝ち組。

ファイルを一気読みして、それを行で分割。rubyとかだと1行で書けたかな。gaucheの場合はどうよ? 自分で考えていても使う部品が浮かんでこない。こういう時は、shiroさんの脳汁がにじみ出てるマニュアルを参照するに限る。

一本になったhtmlファイルをブラウザーに喰わせて、そこで検索。ぐぐるに聞くより良い方法。検索語は何にする? そーさね、linesぐらいでいいんでねぇ。

Function: stream-lines stream

    {util.stream} Splits stream where its element equals to #\n, and returns 
    a stream of splitted streams.

    (stream->list
     (stream-map stream->string
                 (stream-lines (string->stream "abc\ndef\nghi"))))
     ⇒ ("abc" "def" "ghi")

これしか引っかからなかった。ファイルの内容が一気読み出来ていれば、これが使えるけど、ちと大仰過ぎると思う。本来の要求なんて頻出するパターンだから、もっとスマートに出来ると思うぞ。

こういう時はネット(?)に当たらずに、古典を開くに限る。幸い『プログラミングGauche』なんて本を持ってるからね。port->xxxが便利に使えるよってさ。

6.22.7.4 入力ユーティリティ手続き

Function: port->string port
Function: port->list reader port
Function: port->string-list port
Function: port->sexp-list port

これを使うには、portを手に入れれば、いいんだな。

(define (file-to-list file)
  (let* ((ii (open-input-file file))
         (rv (port->list read-line ii)))
    (close-port ii)
    rv))

(define symdb '())

(define (envionment-symbols)
  (when (null? symdb)
    (set! symdb (file-to-list "/home/sakae/.gosh_completions")))
  symdb)

本来なら、より簡単な port->string-list を使うのが正解だけど、引数が余分にある方を、なんとなく使ってみた。そして、geiser:completionsが呼ばれる度に、ファイルから読んでいたんでは、DISKの寿命が縮まってしまうので、キャッシュするようにした。

上記のfile-to-list関数は、下記のようにも書ける。こちらの方が通っぽいな。pythonでは、やり方は ひとつ ってのが厳格に守られているようだけど、gaucheでは、便利なら色々な方法で書けるよ。これもそれも、裏方にmacroが有るからだな。

(define (file-to-list file)
  (call-with-input-file file
    (lambda (in)
      (port->list read-line in))))

really?

gosh> ,d port->list
#<closure (port->list . _)> is an instance of class <procedure>
Defined at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/portutil.scm":47
slots:
  required  : 2
   :
gosh> ,d port->string-list
#<closure (port->string-list . _)> is an instance of class <procedure>
Defined at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/portutil.scm":56
slots:
  required  : 1
   :

こんな風にして、ソースの在処が確認出来るんで、原本に当たってみる。

(define (port->list reader port)
  (with-port-locking port
    (^[]
      (let loop ([obj (reader port)]
                 [result '()])
        (if (eof-object? obj)
          (reverse! result)
          (loop (reader port) (cons obj result)))))))

(define (port->string-list port) (port->list (cut read-line <> #t) port))

マクロになってるかと思ったら違った。その代わりにcutと言うマクロで穴埋めしてた。

(cut cons (+ a 1) <>)  ≡ (lambda (x2) (cons (+ a 1) x2))
(cut list 1 <> 3 <> 5) ≡ (lambda (x2 x4) (list 1 x2 3 x4 5))
(cut list)             ≡ (lambda () (list))
(cut list 1 <> 3 <...>)
   ≡ (lambda (x2 . xs) (apply list 1 x2 3 xs))
(cut <> a b)           ≡ (lambda (f) (f a b))

;; Usage
(map (cut * 2 <>) '(1 2 3 4))
(for-each (cut write <> port) exprs)

cutの例。便利なら、それでいいじゃん精神が発露されてます。使いこなせるかが問題だけどね。

機械学習の勧め

こうして何とかgeiser:completionsをやっつけて、geiser:evalにかかったら、相変わらずセグフォ。chezに有ってgaucheに無い関数をだまかして、取り合えず走るようにした。

で、先にchezの挙動を確認しておく。きっと先輩と同じ口調で答えられるようになれば、emacs君も反応してくれるだろう。(と、淡い期待を込めて)

正しい例。

(base) sakae@debian:chez$ scheme
Chez Scheme Version 9.5.3
Copyright 1984-2019 Cisco Systems, Inc.

> (import (geiser))
> (geiser:completions "1")
()
> (geiser:completions "input")
("input-port-ready?" "input-port?")
> (geiser:eval '#f '(geiser:completions "1"))
((result "()\n") (output . ""))
> (geiser:eval '#f '(geiser:completions "input"))
((result "(\"input-port-ready?\" \"input-port?\")\n") (output . ""))

悪い例。

(base) sakae@debian:tmp$ gosh
gosh> (use geiser)
gosh> (geiser:completions "1")
()
gosh> (geiser:eval '#f '(geiser:completions "1"))
((result "") (output . "") (error (key . "#<error \"wrong number of arguments for \">")))
#<undef>
gosh> (geiser:completions "input")
("input-file-port?" "input-port-open?" "input-port?" "input-serializer?"
 "input-string-port?" "input-virtual-port?")
gosh> (geiser:eval '#f '(geiser:completions "input"))
((result "") (output . "") (error (key . "#<error \"wrong number of arguments for \">")))
#<undef>

これって、正に機械学習だよね。機械学習と言っても、教師有り学習と教師無し学習の2種類がある。今回は、教師有りの方ね。だって、何も無い所から作り上げるって無理に決まってますから。

で、教師有り学習の手順だと、教師の結果と推定の結果の差分から、エラーを無くす方向に調整(これをフィードフォワードとか言うんだったな)。調整したやつで確認(雑に言うと、バックプロパゲーション)。これをエラーが無くなるまで繰り返せばOK。

今はこのフェーズを回る訳ね。

エラーは続くよどこまでも

上のエラーは、geiser:eval中で定義してる手続き内のevalにおいて、第二引数が抜けていた為だった。目を皿にして調べてやっと見つけた。けど、上の不具合結果中に表われる、errorってTAGを冷静に信じれば、もっと早く辿りつけただろう。

そして、次なるエラーは、repl内で補完が働きそうな語句を入力しようとした時に、mini-bufferに表われた。(defineを補完しようとして、defまで入力)

Assertion failed: (cl-every (function stringp) candidates)

同様な警告は、*Messages* にも載ってた。

(New file)
Starting Geiser REPL ... [3 times]
Gauche REPL up and running!
Company: An error occurred in auto-begin
Assertion failed: (cl-every (function stringp) candidates)
Company: An error occurred in auto-begin
Assertion failed: (cl-every (function stringp) candidates)

ちゃんとemacsが捕まえてくれる、筋が良いエラーなんですかね。それは良いとして、本当のエラー現場を押さえたい。どうする? 散歩中に思いついた。得意のM-x d-o-e cl-every

Debugger entered--entering a function:
* cl-every(stringp (define define+ define-cgen-literal define-cise-expr define-$
  company--preprocess-candidates((define define+ define-cgen-literal define-cis$
  company-calculate-candidates("defi" nil)
  company--begin-new()
  company--perform()
  company-auto-begin()
  company-idle-begin(#<buffer * Gauche REPL *> #<window 4 on * Gauche REPL *> 4$
  apply(company-idle-begin (#<buffer * Gauche REPL *> #<window 4 on * Gauche RE$
  timer-event-handler([t 24038 57769 909128 nil company-idle-begin (#<buffer * $

補完の候補に挙がっているものが、全て文字列か試験してるんだな。所が、候補はシンボルになってたんで、チェックに引っかかったとな。思い当たる節があるぞ。そう、geiser:evalを修正した時だ。オリジナルでは、結果を作り上げる手続き内が下記のようになってた。

                             `((result ,(with-output-to-string
                                          (lambda ()
                                            (pretty-print result-mid))))
                               (output . "")))))

このpretty-printがgaucheには無かったんで、pprintに変更したんだった。これって人間が閲覧する為の手続きだ。プログラム上のやり取りには、機械可読(再現できる)にしないといけない。writeに変更。これで、補完が出来るようになった。

でも、一つ疑問。この部分がwith-output-to-stringで囲まれている事。これが働いて、シンボルも強制的に文字列に変換されてもよさそうなもの。何か勘違いしてるかな? まあ、勝てば官軍、動くのが正義ですから。

次なるエラー

replなbufferで、C-c C-l して、編集buffer上のファイルをロードしようとすると、第三の画面が出て来て、エラーを告げていた。詳しく見ようとすると直ぐに消失してしまったので、*Messages*を参照した。そこには

Loading /tmp/aa.scm ...
*** ERROR: unbound variable: maybe-compile-file
Stack Trace:
_______________________________________
  0  (maybe-compile-file filename output-filename)
        at "/home/sakae/.emacs.d/elpa/geiser-20191025.650/scheme/gauche/geiser/\
geiser.scm":31
  1  (maybe-compile-file filename output-filename)
        at "/home/sakae/.emacs.d/elpa/geiser-20191025.650/scheme/gauche/geiser/\
geiser.scm":31
  2  (eval expr env)
        at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269

chezはロード時にcompileしてsoファイルを作り、それを取り込んでいるのね。gaucheにはそんな機能は無いので、指定されたやつを単にloadするようにした。

エラーはエラーか?

次は、エラーの挙動確認。repl上で

gosh> (/ 3 0)
*** ERROR: attempt to calculate a division by zero
Stack Trace:
_______________________________________
  0  (/ 3 0)
        at "(standard input)":1
  1  (eval expr env)
        at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269
gosh>

正しい挙動だ。では、編集buffer上で、C-x C-e は、どうかな?

何も応答無し。通常は、評価結果がmini-bufferに、=> 3 なんて風に出てくるんで、気にいらない答えは返さないって挙動になってるのかな? chezで実験すると、同じ挙動になった。

call/cc

geiser:evalの中で継続が使われていた。構えてしまったぞ。しかも、他のオイラーが使った事が無いような関数の中で。

16. 継続の中の、4. Scheme の継続 を読んで、納得した。大域脱出なのね。

geiser:evalの中の該当部分

         (result (call/cc
                  (lambda (k)
                    (with-exception-handler
                        (lambda (e)
                          (k (gen-result e #t)))
                      (lambda ()
                        (call-with-values
                            (lambda ()
                              (body))
                          (lambda (x . y)
                            (if (null? y)
                                (k (gen-result x #f))
                                (k (gen-result (cons x y) #f)))))))))))
    (write result)

resultは、let*節の一部。call/ccを使った関数だ。それを呼び出しているのは、最後の所にある(write result)だ。素直に読めば、結果を出力して下さいだな。結果を作るのは、resultって言う関数って塩梅。

emacs側から渡ってきた評価依頼内容は(body)にまとめてあるんだけど、ひょっとしたら評価に失敗してエラーを返すかも知れない。又は評価結果が多値になるかも知れないって言う、得体不明な物。これらをまとめて扱いたい。エラーも正しい結果も統一的に扱いたい。

そんな多くの要求をgen-resultに一任させちゃった。失敗/成功の区別はgen-resultの第二引数に載せる。第一引数は、失敗の場合その原因を載せる。成功の場合で多値の場合は、多値をconsしたもの。単値の場合はそのまま。

gen-resultが生成したものを、一気にresult関数の結果として返したい(出口は3ヶ所)。で、それぞれのgen-resultの頭にkを置いて、結果を持って、resultを脱出させてる。 分かってしまえば、どうって事無かったね。

なぜSchemeにはreturnが無いのか、これを読むと、継続(の統一理論)が良く分かる。

まとめと成果の公表

今回の移植で、大体満足出来る所まで漕ぎつけた。補完機能、評価機能、ロード機能、そしてreplが有るから好きにしてくれ。

racketなんかだと、eldocと言うんか知らないけど、引数の簡単な説明がmini-bufferに出て来たりするけど、オイラーはそんなの見ないし目障りなんで、実装する気無し。せいぜいその場でinfoが引ければ良い。これは既にemacsの手続きとして実装してあるんで、全く困る事は無い。

若し追加するとしても、moduleの切り替えとかだな。まあ、自分でモジュールを開発する事も無いだろうから、やらなくてもいいか。

ああ、追加するなら補完の種を自動生成させる、主活動領域のuserモジュール上で定義された手続きを補完の種にダイナミックに追加する、ぐらいだな。

geiserはr7rsをターゲットにしてるな。chezもそれに倣っていた。chezの住処をリノベーションして旧人でも住めるようになるまで一苦労だった。今後の事を考えると、r7rsに対処しておく事が、一番に利がありそうだな。

と言う事で、ここら辺で終了って事にしますかね。

今回の開発で大活躍した、emacsのdebug。ヒットした時、渡って来た引数が確認出来る。呼び出し履歴が一望出来る。ソースがその場で確認出来る。cコマンドでcontinuすると、出口で再び止まって、評価結果を確認出来る。高機能なtraceとしても使える。これを使えるようになったのが、一番の成果かな。

後は、emacs側から渡って来るコマンドを、単独のgosh上で評価すると言う、開発方法を実践したって事かな。API単位で問題を分離するとも言える。

最後に、下記から成果をDL出来るようにしときます。詳細は同梱の資料を参照してください。

Download

geiser_gauche_set.tgz