gaucheをgeiserへ

確かな力が身につくJavaScript「超」入門 第2版 なんて本を見ている。怖い物見たさにね。

最新式のWebPageを作る時に、役立つらしい。HTML + CSS + Javascript が主流みたいで、オイラーみたいに生HTMLを書く(いやPIKIって言うマークダウンの亜流でこのページとかを書いてるから、さすがに生ではないけど)人なんて、時代に取り残された恐竜みたいな者だ。

6章から始まる、jQueryを使ったアプリ編が、オイラーに新しい知をもたらしてくれた。避けて通れないJSONが出てきたり、最後の章では、外部のやつにお願いして、位置情報を取得したり、これサツの御用達だな。

WebAPIで、お天気おねーさんを呼び出したり。ここまで来るとアプリっぽくなるな。 Ajaxなんてのも普通に使われいるしね。

時代にやっと追いついた、感じがしますよ。続きは下記でってお勧めされたよ。

開発者による開発者のためのリソース

CSSの血がオイラーには不足してる事をひしひしと感じたので、修行してくるか。

パケット盗聴

前回の最後にやったslimeで、勝手にpipeが施設されてた。非常用の通信路として使われるわけでもなく、その存在理由が不明だった。

で、あれから考えたんだ。これはemacsの仕様ではなかろうかと。そう、make-processする時、ptyを使うかpipeにするかの選択肢が有った。どちらかを選べって事。積極的にptyを選ばないとpipeが使われるって意味の事が書いてあった。

実験してみる。make-process と make-network-processをdebug-on-entryする。そしてslimeを起動。

Debugger entered--entering a function:
* make-process(:name "inferior-lisp" :buffer #<buffer *inferior-lisp*> :command$
  apply(make-process (:name "inferior-lisp" :buffer #<buffer *inferior-lisp*> :$
  start-process("inferior-lisp" #<buffer *inferior-lisp*> "sbcl")
  apply(start-process "inferior-lisp" #<buffer *inferior-lisp*> "sbcl" nil)
  start-file-process("inferior-lisp" #<buffer *inferior-lisp*> "sbcl")
  apply(start-file-process "inferior-lisp" #<buffer *inferior-lisp*> "sbcl" nil$
  comint-exec-1("inferior-lisp" #<buffer *inferior-lisp*> "sbcl" nil)
  comint-exec(#<buffer *inferior-lisp*> "inferior-lisp" "sbcl" nil nil)
   :

最初にsbclのプロセスが起動した。親のemacsが子のsbclを誕生させたって訳だ。次にネット接続の所で引っかかってきた。

Debugger entered--entering a function:
* make-network-process(:name "SLIME Lisp" :buffer nil :host "localhost" :servic$
  open-network-stream("SLIME Lisp" nil "localhost" 38687)
  apply(open-network-stream "SLIME Lisp" nil "localhost" 38687 nil)
  slime-net-connect("localhost" 38687)
  apply(slime-net-connect "localhost" 38687 nil)
  slime-connect("localhost" 38687 iso-latin-1-unix)
   :

ローカルホストの38687番に接続しろとな。このサーバー側の番号をどうやって知る? その答えが、pipeを伝わって、sbclからemacs側に届いていた。

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
* (progn (load "/home/sakae/.emacs.d/elpa/slime-20191025.1421/swank-loader.lisp\
" :verbose t) (funcall (read-from-string "swank-loader:init") :from-emacs t) (f\
uncall (read-from-string "swank:start-server") "/tmp/slime.2775"))

; loading #P"/home/sakae/.emacs.d/elpa/slime-20191025.1421/swank-loader.lisp"
WARNING:
   redefining EMACS-INSPECT (#<SB-PCL:SYSTEM-CLASS COMMON-LISP:T>) in DEFMETHOD
;; Swank started at port: 38687.
38687
*

こういうお知らせが無いとemacsも何処に接続したらいいか分からずに困ってしまう。ちなみにsbclのlsof結果の一部(BSD系だとfstatだな)

sbcl    2820 sakae    0r  FIFO   0,12      0t0    53268 pipe
sbcl    2820 sakae    1w  FIFO   0,12      0t0    53269 pipe
sbcl    2820 sakae    2w  FIFO   0,12      0t0    53269 pipe
sbcl    2820 sakae    4u  IPv4  53271      0t0      TCP c8:38687->c8:49608 (ESTABLISHED)

c8は動かしているサーバー名ね。localhost(127.0.0.1)も名前解決してくれてるんだ。 で、ふと出来心で、tcpdumpしてパケットをモニター。(cdr '(a b c))を評価。

(base) [sakae@c8 tmp]$ sudo tcpdump -i lo -w LOGLOG port 38687

いきなりwiresharkでもいいんだけど、盗聴の現場の機器はコンパクトな方が好ましい。盗聴なんて危ない言葉を使うな。スパイっぽくタッピングとか言え。いやHackerっぽくモニターとか言え。まあ、言葉のマジックで本質は変わらんわ。 (オイラーがwiresharkでのモニター嫌いは、雑音をごっそりと拾ってしまう為です)

wiresharkにログを持って行って、Analyzeメニューのfollow -> tcp streamでまとめ。

00006b(:emacs-rex (swank:simple-completions "cd" (quote "COMMON-LISP-USER")) "COMMON-LISP-USER" :repl-thread 18)
000097(:return (:ok (("cdaaar" "cdaadr" "cdaar" "cdadar" "cdaddr" "cdadr" "cdar" "cddaar" "cddadr" "cddar" "cdddar" "cddddr" "cdddr" "cddr" "cdr") "cd")) 18)000065(:emacs-rex (swank:operator-arglist "cdaaar" "COMMON-LISP-USER") "COMMON-LISP-USER" :repl-thread 19)
000022(:return (:ok "(cdaaar LIST)") 19)00007d(:emacs-rex (swank:autodoc (quote
("cd" swank::%cursor-marker%)) :print-right-margin 80) "COMMON-LISP-USER" :repl-thread 20)
 :
00002D(:return (:ok ("(cdr ===> list <===)" t)) 28)00005d(:emacs-rex (swank-repl:listener-eval "(cdr '(a b c))
") "COMMON-LISP-USER" :repl-thread 29)
000024(:presentation-start 2 :repl-result)000024(:write-string "(B C)" :repl-result)000022(:presentation-end 2 :repl-result)000020(:write-string "
" :repl-result)000016(:return (:ok nil) 29)

emacsとsbclは、せわしなくやり取りしてるんですなあ、なんて事が知れた。手軽に出来るのがいいね! とFacebook風。

まて、そんなスパイもどきな事をしなくても、*slime-events* に、綺麗にまとめてあるぞ。 下記は、その最後の部分。

(:emacs-rex
 (swank-repl:listener-eval "(cdr '(a b c))\n")
 "COMMON-LISP-USER" :repl-thread 29)
(:presentation-start 2 :repl-result)
(:write-string "(B C)" :repl-result)
(:presentation-end 2 :repl-result)
(:write-string "\n" :repl-result)
(:return
 (:ok nil)
 29)

2012年の最初にslimvって題材で、slimeを取り上げていたな。歴史は繰り返すんです。

geiser remote connect

気をよくしたオイラーはgeiserでリモート接続して、パケットを盗聴してみる事にした。 racketには、その為の起動スクリプトが用意されてる。

(base) [sakae@c8 bin]$ ./geiser-racket.sh -n localhost -p 29825
Welcome to Racket v7.4.
Geiser server running at port 29825
>

名前解決?に時間がかかるようで、数十秒後にやっと待ち受け画面になった。後はemacs側で、M-x geiser-connect(省略形で、g-con)する。例のクダーらないS式を評価。

,geiser-eval #f #f ,apply geiser:module-completions ("")
((result "(\"acks/acks\" \"compiler/cm\" ....
 :

wiresharkのログを見ると、めちゃくちゃ補完データのやり取りが行われていた。しょうがないので、S式を除いてすっきりさせてみた。

(base) [sakae@c8 tmp]$ egrep  -v -- '^\(' log.txt
,geiser-eval #f #f ,apply geiser:module-completions ("")
racket@> ,geiser-eval #f #f ,apply geiser:completions ("c")
racket@> ,geiser-eval #f #f ,apply geiser:module-completions ("c")
racket@> ,geiser-eval #f #f ,apply geiser:autodoc ('(c c))
racket@> ,geiser-eval #f #f ,apply geiser:completions ("cd")
racket@> ,geiser-eval #f #f ,apply geiser:module-completions ("cd")
racket@> ,geiser-eval #f #f ,apply geiser:autodoc ('(cdaaar))
  :
racket@> ,geiser-eval #f #f ,apply geiser:module-completions ("c")
racket@> ,geiser-eval #f #f ,apply geiser:autodoc ('(c a cdr))
racket@> ,geiser-eval #f #f ,apply geiser:module-completions ("")
racket@> (cdr '(a b c))
'(b c)
racket@>

もう一方の雄、guileも試してみる。 guile --help すると、

  --listen[=P]   listen on a local port or a path for REPL clients;
                 if P is not given, the default is local port 37146

guileだと組み込みでネットワーク接続が出来るようになってた。

(base) [sakae@c8 tmp]$ guile --listen
GNU Guile 2.0.14
Copyright (C) 1995-2016 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.

これだとデフォの37146で待ち合わせ。オイラーの好きな番号を指定しても、バナーに何番で待ってるよとは出てこなかった。

,geiser-eval #f ge:module-completions ("(") ()
While executing meta-command:
ERROR: Unbound variable: find-module
scheme@(guile-user)> ,geiser-eval #f ge:completions ("c") ()
While executing meta-command:
ERROR: Unbound variable: find-module
scheme@(guile-user)> ,geiser-eval #f ge:module-completions ("c") ()
  :
ERROR: Unbound variable: ge:autodoc
scheme@(guile-user)> ,geiser-eval #f ge:module-completions ("(a b c))") ()
While executing meta-command:
ERROR: Unbound variable: find-module
scheme@(guile-user)> (cdr '(a b c))
$4 = (b c)
scheme@(guile-user)>

モジュールが見つからないとか散々言われた挙句に、やっと評価が完了した。

base by chez

さてさて、geiserに我gaucheを参加させて見ようと思う。多分、世界中で誰もやってない試みだから、途中で挫折するかも知れないけど。

たたき台が欲しい。各インプリメントのうちでemacs側のソースは大小同意と思う。問題は各scheme側のコード。短いのがよさそうって基準ならchezだ。既存のやつで、何処にchezと言う文字列が有るか確認したよ。

sakae@debian:geiser-20191025.650$ grep -l chez *.el
geiser-autoloads.el
geiser-chez.el
geiser.el
geiser-impl.el

geiser-chez.elは、geiser-gauche.elと言う名前でコピー。他の3つのファイルは、chezと言う語句を探して、その辺をコピーしてgaucheに書き換え。

geiser.elの例

;;; sakae
;;;###autoload
(autoload 'run-gauche "geiser-gauche" "Start a Geiser Gauche REPL." t)

;;;###autoload
(autoload 'switch-to-gauche "geiser-gauche"
  "Start a Geiser Gauche REPL, or switch to a running one." t)
;;; sakae
;;;###autoload
(autoload 'run-chez "geiser-chez" "Start a Geiser Chez REPL." t)

;;;###autoload
(autoload 'switch-to-chez "geiser-chez"
  "Start a Geiser Chez REPL, or switch to a running one." t)

schemeの下にgaucheってdirを作り、そこにgeiser.ssってchezのままのやつをgosh.scmって名前でコピー。単体で起動して、エラーにならない程度に修飾したよ。同様にgeiser-gauche.elの方も目に付く所はgauche用に書き替え。

こうしておいて起動すると

Starting Geiser REPL ...
 ;; wait about 20sec
Gauche REPL up and running!

待たされた挙句にgosh> のプロンプトが出てきた。何故待たされるかは不明。取り合えず、12とかして評価出来るか確認しよう。1を入力した所でハングアップですよ。と思っていたら、暫くして2が入力されたようなのでRET

gosh> 12
12
gosh[r7rs.user]>

いつの間にかr7rsの環境になってるよ。なんじゃこりゃ。 どうする? 10秒考えて、emacsでハングした時、制御をemacsに取り戻す方法が有る事を思い出した。

M-: して

Eval: (setq debug-on-quit t)

ハングしたと思ったら、C-g と入力。emacsのdebug画面にバックトレースが出てくる。 最初のリンクの所の、geiser-con--send-string/wait にカーソルを合わせて v する。

  geiser-con--send-string/wait((t (:filter . comint-output-filter) (:tq ((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
  geiser-eval--send/wait((:eval (:ge completions "1")) nil nil)
  (geiser-eval--retort-result (geiser-eval--send/wait code timeout buffer))
  geiser-eval--send/result((:eval (:ge completions "1")))
    :

vコマンドは、関数の引数を確認するやつだ。同関数は、geiser-connection.elにて定義されてた。

(defvar geiser-connection-timeout 30000
  "Time limit, in msecs, blocking on synchronous evaluation requests")

(defun geiser-con--send-string/wait (con str cont &optional timeout sbuf)
  (save-current-buffer
    (let ((proc (and con (geiser-con--connection-process con))))
      (unless proc (error "Geiser connection not active"))
      (let* ((req (geiser-con--send-string con str cont sbuf))
             (id (geiser-con--request-id req))
             (timeout (/ (or timeout geiser-connection-timeout) 1000.0)))
        (with-timeout (timeout (geiser-con--request-deactivate req))
            :

接続が確立してるcon目掛けて、strの内容を送り付け、返答を待つって関数だな。待ち時間は指定無しだから、リミット値の30秒までは待つって事か。

単体試験

emacs側がgaucheに送り付けるコマンドが分かったので、直接実行してみる。

(base) sakae@debian:geiser$ gosh -l ./gosh.scm
gosh> (geiser:eval '#f '(geiser:completions \"1\"))
*** UNHANDLED-SIGNAL-ERROR: unhandled signal 2 (SIGINT)
Stack Trace:
_______________________________________
  0  (read)
        at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":236
gosh>

幾ら待っても応答が無かったので、C-cで止めた。goshは待ちに入ってたって事で、上の障害と符合してる。で、マニュアルと首っ引き。

geiser.scm  gosh.scm
(base) sakae@debian:geiser$ cat gosh.scm
(use geiser)

今までgosh.scmだったのをモジュールとして扱う為、geiser.scmに改名。gosh.scmは、それを読み込むだけのものにした。下記はemacs側の起動パラメータ。

(defun geiser-gauche--parameters ()
  `("-I" ,(expand-file-name "gauche/geiser/gosh.scm" geiser-scheme-dir) "-i"))

実行してみる。

gosh> 123
123
gosh> (define (aa n ) (* n n n))
aa
gosh> (aa 5)
125

これでGache REPL側の方は、何とか動くようになった。 それじゃと思って、Scheme-buffer側で評価すると、第3の窓が表れた。

*** ERROR: unbound variable: geiser:eval
Stack Trace:
_______________________________________
  0  (geiser:eval ’#f ’(define (zz n) (* n n n)))
        at "(standard input)":2
  1  (eval expr env)
        at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269
gosh>
gosh> (find-module 'gauche)
#<module gauche>
gosh> (find-module 'geiser)
#f
gosh> (use geiser)
*** ERROR: cannot find "geiser" in ("/home/sakae/.emacs.d/elpa/geiser-20191025.\
650/scheme/gauche/geiser/gosh.scm" "/usr/local/share/gauche-0.97/site/lib" "/us\
r/local/share/gauche-0.97/0.9.8/lib")
    While compiling "(standard input)" at line 2: (use geiser)
Stack Trace:
_______________________________________
  0  (eval expr env)
        at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269
gosh> ,pwd
/home/sakae/.emacs.d/elpa/geiser-20191025.650

goshが認識してる場所は、大本の所なのね。まあ、そうなるわな。 replが普通に動き出すと、色々調べられて便利です。

good result

chezで実験。

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

> (import (geiser))

> (geiser:eval '#f '(geiser:completions \"1\"))
((result "") (output . "") (error (key . "Exception: variable \\x22;1\\x22; is not bound")))

> (geiser:eval ’#f ’(define (zz n) (* n n n)))
Exception: invalid context for definition (define (zz n) (* n n n))
Type (debug) to enter the debugger.

コンテキストが違うとか言われたけど何故?

r7rs ?

途中からr7rsのプロンプトに化けちゃう事からすると、r7rsで動いてる? ちょっと実験。

(base) sakae@debian:tmp$ cat r7rs.scm
(import (scheme base) (scheme write))
(import (srfi 1))

(display "Hello, world!\n")
                                                                                (define (foo x)                                                                   (number->string x))
                                                                                (foo 1234)
(display (iota 10))

走らせてみる。

(base) sakae@debian:tmp$ gosh -r7 -i
gosh[r7rs.user]> (load "./r7rs")
Hello, world!
(0 1 2 3 4 5 6 7 8 9)#t

swank-gauche

ちょいと疲れたので、2012年の年頭にやった swank-gauche なんてのを思い出してやってみる。こちらは大胆にもslimeでgaucheを動かしてしまおうと言う作戦。 geiserよりもslime基盤としては実績が有りますからなあ。

;; gosh
(require 'slime)
(slime-setup
 '(slime-fancy
   slime-scheme))
(setq swank-gauche-path "/home/sakae/.emacs.d/lisp")
(setq swank-gauche-gauche-source-path "/home/sakae/src/Gauche-0.9.8")
(push swank-gauche-path load-path)
(require 'swank-gauche)
(setq slime-lisp-implementations
      '((gauche ("gosh") :init gauche-init :coding-system utf-8-unix)))
(setq slime-find-buffer-package-function 'find-gauche-package)
(setq slime-complete-symbol-function 'slime-complete-symbol*)
(define-key slime-mode-map (kbd "C-c C-d H") 'gauche-ref-lookup)

走らせてみると、

(begin (add-load-path "/home/sakae/.emacs.d/lisp") (require "swank-gauche") (wi\
th-module swank-gauche (load-gauche-operator-args nil) (start-swank "/tmp/slime\
.3815")))

*** ERROR: variables of or-pattern differ in ((or :write-string :debug :debug-c\
ondition :debug-activate :debug-return :channel-send :presentation-start :prese\
ntation-end :new-package :new-features :ed :%appl ...
    While compiling "/home/sakae/.emacs.d/lisp/swank-gauche.scm" at line 306: (\
define (dispatch-event event) (log-event "dispatch-event: ~s~%" event) (match e\
vent ((:emacs-rex par ...
    While loading "/home/sakae/.emacs.d/lisp/swank-gauche.scm" at line 320
    While compiling "(standard input)" at line 1: (begin (add-load-path "/home/\
sakae/.emacs.d/lisp") (require "swank-gauche") (with-module swank-gauche ...
    While loading "(standard input)" at line 1
Stack Trace:
_______________________________________

Process inferior-lisp exited abnormally with code 70

こんな具合に見事に撃沈です。色々調べてみると、 6.8 keywordにあるように、シンボルとキーワードの扱いの統合が有ったようです。

そして、こんなのを知ったので、試してみます。

gosh> (base) [sakae@c8 lisp]$ export GAUCHE_KEYWORD_DISJOINT=1
(base) [sakae@c8 lisp]$ gosh
gosh> ,l ./swank-gauche.scm
WARNING: Unquoted keyword `:emacs-rex' in match pattern: event.  This would likely break in future versions of Gauche.  See the ``Keyword and symbol integration'' section of the manual for the details.
  :
WARNING: Unquoted keyword `:line' in match pattern: part.  This would likely break in future versions of Gauche.  See the ``Keyword and symbol integration'' section of the manual for the details.
#t
gosh>

派手に警告が出つつも、エラーは回避出来ました。これで動くかな?

Versions differ: 2.24 (slime) vs. 2012-01-06 (swank). Continue? (y or n)
  :
Connected. Hacks and glory await!

ミスマッチしてるけど続ける? 勿論。ハックして栄光を手にしよう。