標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

引き続き、ISLisp 検証パズルを進めてる

引き続き、ISLisp の検証プログラム対応を続けている

NG は多々あるんだけど、まずは、全ての検証式の処理以前に途中で止まってしまう問題をクリアしないといけない。でないと合格率も算出できない

だが、ここまで進んでいると、想像もしなかった ISLisp 仕様が登場してきて、なかなかおもしろくなってきた


(tagbody
  tag1
  (tagbody
   tag2
   (unwind-protect 
       (go tag1) ;;; tag2 invalid
     (go tag2))))

当初、これが無限ループではなく、<control-error> にならなきゃいけない理由が分からなかった

答えとしては、try〜finally 的な処理を行う (unwind-protect FORM CLEANUP-FORM)CLEANUP-FORM 部分では大域脱出的なことをしてはいけないから…らしい。上の場合だと (go tag2)<control-error> になるということのようだ。


(defun if ()) という式があって、<program-error> にしなければいけないというのは分かるんだが、なぜか (with-handler) で補足できず、検証プログラム自体が NG 判定する前に止まってしまう

> NG: (+ (defdynamic *dynamic-2* 2)) -> *dynamic-2* [#<Error> <error>]
> NG: (defun) -> #<Error> <error> [#<Error> <program-error>]
> NG: (defun foo) -> #<Error> <error> [#<Error> <program-error>]
> NG: (defun nil nil) -> #<Error> <domain-error> [nil]
> NG: (defun t nil) -> #<Error> <domain-error> [t]
too many arguments
        at if
        at tp-eval
        at setq
        at tp-$error-prim
        at tp-$error
        at case
        at while
        at let
        at for
        at with-open-input-file
        at with-standard-input
        at tp-main
        at while
        at let
        at for
        at let
        at tp-all
Makefile:89: recipe for target 'verify' failed
make: *** [verify] Error 1
exit status 2

しばらく悩んだが、(if) の再定義ができてしまって、引数ゼロ個の関数になってしまっていて、それが検証プログラム本体の動作も変えてしまったせいらしい。

うける!


(defun foo-5 () 1)
(flet ((foo-5 ()  (foo-5))) (foo-5))

無限ループになってしまった。でも、これは何が悪いか分かる。

(flet) の定義するローカル関数はローカル関数自身からは見えないはずなので、無限ループになるはずがない。

が、末尾呼び出し最適化を施す際に、関数に自分の名前が分かるようにしてしまったんだよなー。 これは ISLisp の仕様が分からん系ではなくて、純粋に不具合だった。


だいたいのケースは

  • 無限ループになる
  • 例外を補足できない状態になる

のどちらかみたい。もうさすがに検証プログラムの仕組み自体を実行できないケースはないかな…【油断】

自作Lispで、ようやくISLisp の検証プログラムが少し動くようになってきた

自作Lispで、ようやく ISLisp の検証プログラムが少し動くようになってきた。

このプログラム、テストケースを動かすための土台(tp.lsp)そのものがテクニックが駆使された難しいコードで、自作Lisp ではそれをまず処理できずにつまづいていた。テストケースにあるコードを直接実行する分には動作しているように見えるのに、検証プログラムでは NG というケースも多々あった。まず

  • パーサーが ',、シングルクォート(quote)の直後にカンマ(unquote)があるパターンに対応していなかった
(setq form `(eval ',form)))

という式が評価できていなかった。結果として (defglobal) など宣言系のコマンドが全てスルーされる結果になっていた。

  • ((lambda (x) (+ x x)) 4) などという式が Valid

lambda の結果は関数オブジェクトだから funcall 経由でないと呼び出せないものとばかり思っていた。実際に OK!Lisp や IRIS で試したところ、両者とも問題なく動作した。

$ ISLisp.exe
> ISLisp  Version 0.80 (1999/02/25)
>
ISLisp>((lambda (x) (+ x x)) 4)
8
ISLisp>(funcall (lambda (x) (+ x x)) 4)
8
$ iris.exe
Iris ISLisp Interpreter Commit HEAD on go1.22.3
Copyright 2017 islisp-dev All Rights Reserved.
>>> ((lambda (x) (+ x x)) 4)
8
>>> (funcall (lambda (x) (+ x x)) 4)
8

例外対応として、cons の評価部分で、car がシンボルであれば対応する関数を、関数オブジェクトならば funcall 相当の処理を呼び出すようにした。

( が、なぜか結果はあってるのに異常終了判定されてしまう問題に数時間悩まされるはめに。原因は今まで、式がconsであるかの確認/変換処理を Go の型アサーションでやってたのを、専用の型変換関数を使うようにしたせいだった。型があっていない時に自動的に Lisp の例外ハンドラーが呼び出され、異常終了フラグがセットされてしまったのだった。以下、不具合を再現させる必要最小限のコードを示す。ここで ERROR=nil と表示されなくてはいけないのが ERROR=t と表示されてしまう…。どんだけマニアックなコードやねん )

(defglobal *tp-error-flag* nil)
(defun tp-error-handler (condition)
  (setq *tp-error-flag* t)
  (format t "ERROR-HANDLE: ~S~%" condition)
  (throw 'tp-error condition))

(defmacro tp-eval-form (&rest form)
  `(catch 'tp-error
     (with-handler #'tp-error-handler ,@form)))

(defglobal form '((lambda (x) (+ x x)) 4))
(defglobal source (list 'tp-eval-form form))
(format (standard-output) "SOURCE=~S~%" source)
(format (standard-output) "RESULT=~S~%" (eval source))
;(format (standard-output) "RESULT=~S~%" (tp-eval-form form))
(format (standard-output) "ERROR=~S~%" *tp-error-flag*)
(format (standard-output) "EXPAND=~S~%" (macroexpand (list 'tp-eval-form form)))

ISLisp の検証プログラムとは

未来の自分のために書いておこう

検証プログラムは islisp.org の Downloadページ からダウンロードできる。

$ curl -O http://islisp.org/program/Verify.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 76761  100 76761    0     0   657k      0 --:--:-- --:--:-- --:--:--  669k

$ unzip Verify.zip
Archive:  Verify.zip
  inflating: readme.txt
  inflating: tp-ipa.zip

$ unzip tp-ipa.zip
Archive:  tp-ipa.zip
   creating: tp-ipa/
   creating: tp-ipa/data/
  inflating: tp-ipa/data/array.lsp
   creating: __MACOSX/
   creating: __MACOSX/tp-ipa/
   creating: __MACOSX/tp-ipa/data/
  inflating: __MACOSX/tp-ipa/data/._array.lsp
  :

$ cd tp-ipa

$ gmnlisp -e "(load \"tp.lsp\") (tp-all)"

> TP File : data/formeval.lsp
> NG: (function) -> #<Correct number of arguments] [#<Wrong number of arguments>]
> NG: (function nil nil) -> #<Correct number of arguments] [#<Wrong number of arguments>]
> NG: (funcall (function -) 3) -> 3 [-3]
> NG: (funcall (function -) 3) -> 3 [-3]
> NG: (function . (+ . 1)) -> #<Error> <error> [#<Error> <error>]
    :
  • 検証プログラムは (eval OBJ)(load "FILENAME") という命令があることが前提になっている。それらってISLisp の規格にないと思うんだけど、いいんですかね (まぁ、そもそも沖電気さんが OK!Lisp のために作った検証プログラムだしな)

  • テストを動かすプログラムは tp.lsp のみで、テストケースは data/*.lsp に格納されている

  • デフォルトでは失敗したケースしか表示されないが、(tp-all) のところを (tp-all 'verbose) とかに変えると成功ケースも表示されるようになる。

  • デフォルトでは全ケースのテストを行うようになっているが、(tp-all)(tp "data/formeval.lsp" 'verbose) などにすることで、特定のテストファイルだけをテストすることができる

  • data/ 以下にあるテストケースは、そのまま動くLisp のプログラムのように見えるが、実はS式で書かれたデータにすぎない。基本は (式 期待値 equalなどの評価関数名) のリストとなっている。$eval というシンボルも散見されるが、命令ではなく、マークにすぎない(tp.lsp の方でそれを解釈して、cond で処理を分岐させている)

それにしても、またニーズがない書き物をしてしまったよ

Go の "flag" パッケージのヘルプ表示部分を差し替える

flag パッケージのヘルプ表示は -h オプションを指定された時や、引数ゼロの時に自動で表示される。

だが、issue 対応している中、その自由度が結構きつくて、ユーザの提案に対応できない事態が発生した。

-flagname XXXXX
    UUUUUUUUUUUU (default: DDDDD)
  • この時 XXXXX の内容は独立した引数で与えるのではなく、UUUUUUUUUUUU の中に `XXXXX` といった形で含んでいなくてはいけない。存在しなかった場合、型名になる
    • XXXXX が単語ではなく、[always|auto|never] といった選択肢を書きたい場合,同じフレーズが二回ヘルプに表示されてしまうのは不都合
  • デフォルト値がゼロ値の時、(default: DDDDD) が表示されない
    • デフォルト値の表示はユーザに任せた方がいいんじゃね?
  • 廃止オプションなど、Usage として表示させたくない場合に対応できない

必ずしも提案を採用する必要はなかったけれども、廃止オプションに関しては (obsolute) とか表示して回避するのはみっともないなーと思ったので、ヘルプ表示をさしかえることにした。

  • 改造点
    • flag.String や flag.Bool のusage引数で引数の名前として解釈する部分は `name` ではなく、name\vusage とする。name は本文では表示しない
    • usage引数が "" の時は、ヘルプに表示しない
var (
    flagOneLineScript = flag.String("e", "", "code\vexecute code")
    flagColor         = flag.String("color", "always", "[always|never] (default: always)\vcolorize the output")
    flagCompile       = flag.String("compile", "", "executable-name\vcompile executable with <script>.lua embedded; script is not executed")
    flagDebug         = flag.Bool("D", false, "print debug information")
    _                 = flag.Bool("nologo", false, "")
)

func main(){
    ; 略
    flag.Usage = newUsage
    flag.Parse()
    ; 略
}

func newUsage() {
    // 見出しを表示する
    w := flag.CommandLine.Output()
    fmt.Fprintf(w, "Expect-lua %s-windows-%s with %s\n",
        version, runtime.GOARCH, runtime.Version())
    fmt.Fprintf(w, "Usage of %s:\n", os.Args[0])

    // 各オプションの説明を出す
    newPrintDefaults(flag.CommandLine)
}

func newPrintDefaults(fs *flag.FlagSet) {
    fs.VisitAll(func(f *flag.Flag) {
        // 各フラグの内容が f で与えられるので、
        // 出したい内容を io.Writer の fs.Output に出力する
        if f.Usage == "" {
            return // for obsolute flag
        }
        var b strings.Builder

        fmt.Fprintf(&b, "  -%s", f.Name)

        var detail string
        var usage string
        var ok bool

        if detail, usage, ok = strings.Cut(f.Usage, "\v"); ok {
            fmt.Fprintf(&b, " %s", detail)
        } else {
            usage = f.Usage
        }
        if b.Len() <= 4 {
            b.WriteByte('\t')
        } else {
            b.WriteString("\n    \t")
        }
        b.WriteString(strings.ReplaceAll(usage, "\n", "\n    \t"))
        fmt.Fprintln(fs.Output(), b.String())
    })
}
$ .\expect.exe
Expect-lua v0.12.0-14-g5641909-windows-amd64 with go1.20.14
Usage of .\expect.exe:
  -D    print debug information
  -color [always|never] (default: always)
        colorize the output
  -compile executable-name
        compile executable with <script>.lua embedded; script is not executed
  -e code
        execute code

よし!

最近の jj の動向 (v0.17〜v0.19)

Git互換 DVCS jj の最近の動向

次の v0.20 あたりに反映されそうな気がするので、それまでは v0.17.1 でがんばりましょう

Windows11の開発者向け仮想環境イメージのダウンロードと日本語化

Windows デベロッパーセンターより、各種仮想化ソフトウェア向けのイメージをダウンロードする

VirtualBox 向けのイメージをダウンロード。中味は OVF ファイルを圧縮した ZIP ファイルだった。

$ unzip WinDev2404Eval.VirtualBox.zip
Archive:  WinDev2404Eval.VirtualBox.zip
  inflating: WinDev2404Eval.ova

$ dir WinDev2404Eval.*
 ドライブ C のボリューム ラベルがありません。
 ボリューム シリアル番号は 0229-7A5A です

 C:\Users\hymkor\Downloads のディレクトリ

2024/04/16 火  21:45    23,424,313,856 WinDev2404Eval.ova
2024/07/07 日  00:03    23,201,160,423 WinDev2404Eval.VirtualBox.zip
               2 個のファイル      46,625,474,279 バイト
               0 個のディレクトリ  445,992,783,872 バイトの空き領域
  • イメージをダウンロードしたら、チェックサム(SHA256)を確認しておく
$ certutil -hashfile .\WinDev2404Eval.VirtualBox.zip SHA256
SHA256 hash of .\WinDev2404Eval.VirtualBox.zip:
d1270e7ddfb7b343d81bc29d55972cd8ca00417c863ffd9fd3adde75ba829cf2
CertUtil: -hashfile command completed successfully.
  • VirtualBox を起動したら、ovf ファイルをインポートする
    メニュー → ファイル(F) → 仮想アプライアンスのインポート ( すごく時間がかかる )

  • 最初に起動する前に念のためスナップショットをとる

    • メニュー→ 仮想マシン(M) → ツール → スナップショット(S)
    • なぜか最初は見えにくい場所にある。一度でもスナップショットを取ると、メニュー直下に「スナップショット(S)」が現れる
  • 初回起動後、ブートが終わるまで すごく時間がかかる。 終わるまで、ウインドウサイズを変更しない
    ( 画面が真っ暗のまま更新されなくなる場合がある )

  • 起動したら、まず時刻と地域を行う

    • 田 → Setting → Time & language
      • Time zone → (UTC +09:00) Osaka, Sapporo, Tokyo
      • Language & region → Preferred languages [Add a language]
        → 日本語 Japanese → Next
        → Set as my Windows display language にチェック → Install
        → 各種コンポーネントのダウンロードで、またかなり時間がかかる ( たぶんギガ単位 )
        → 反映するために Sign out ボタンを押下
  • 仮想マシンのユーザアカウントは未設定状態なので、デスクトップからはパスワードなしでログインできる

  • 日本語化したタイミングで、二回目のスナップショットをとっておく

References

コマンドラインフィルターを使って nyagos の文法を手直しする

以前もコマンドラインフィルターについて次のような文書を書いたことがあるのですが、説明がほとんどなかったので、改めて解説文書を書いてみました。

できれば、ゼロベースの説明としたかったところですが、やはりLua製ということもあり、Lua の知識が必要となります。

シングルクォートを使いたい

上の記事中、nyagos 上で jq 用のワンライナーを書こうとしたのですが、シングルクォーテーションが引用符として使えないので、\" がたくさん登場して、かなり見辛いことになりました。

jq -r ". | map(\"#\"+(.number|tostring)+\" [\"+ .user.login + \"](\"+ .user.html_url + \")\") | join(\"\r\n\")" issues.json

PowerShell だとシングルクォートも引用符として使えるのでこういうことはないのですが、nyagos はDOS由来の書式とUNIX由来の書式の共存を意図したシェルなので、そういう互換性や慣習が衝突する部分については導入を避けてきました。そのせいもあって、nyagos はかなり保守的なイメージがあると思います。

しかしながら、あきらめる必要はありません。nyagos には「コマンドラインフィルター」という強力な機能があります。

コマンドラインフィルターとは

Lua 関数を使って、ユーザの入力を自動的に加工する機能です。

具体的には、nyagos.filter というフックに、引数1個、戻り値1個の関数を登録します。 コマンドラインの内容が引数として与えられるので、それを加工して return すれば OK です。

サンプル

local orgfilter = nyagos.filter

nyagos.filter = function(cmdline)
    cmdline = string.gsub(cmdline,"@date",os.date())
    if orgfilter then
        cmdline = orgfilter(cmdline)
    end
    return cmdline
end

コマンドラインの中の @date というテキストがあったら、それを日付に置換するという簡単なサンプルです。nyagos.filter には他のフィルターが既に登録されていたりしますので、置換を実行した後、それらを呼び出すのを忘れないようにしてください。 ( 忘れると、逆クォートが効かなくなったりします )

これを~/.nyagos にコピペしたあと、nyagos で実験してみます。

$ echo @date
30 Jun 24 22:09 JST

期待どおり動作しているようです。

シングルクォーテーションを使えるようにする

イメージとしては次のとおりにユーザが入力するテキストを置換すれば、/bin/sh 互換とはいかないまでも用は足りそうです 1

  • " で囲まれていない ' は、" に置換する
  • ' で囲まれている " は、\" に置換する
ユーザが入力するコード 内部的に置き変わるコード 備考
'(print "ahaha")' "(print \"ahaha\")" 新書式
"(print \"ahaha\")" "(print \"ahaha\")" そのまま※1
"(print 'ahaha')" "(print 'ahaha')" そのまま※2

※ 1 今までの書式もそのまま使えないといけないので
※ 2 一部のシングルクォーテーションを引用符に使える言語向け(lua,goawkなど)

これをコードにしたものが次のスクリプトです。

sq2dq.lua

local function sq2dq(source)
    local sq = false
    local dq = false
    return (string.gsub(source,"[\"']",function(c)
        if c == "'" then
            if not dq then
                sq = not sq
                return '"'
            end
        end
        if c == '"' then
            if sq then
                return [[\"]]
            end
            dq = not dq
        end
        return c
    end))
end

if nyagos then
    local oldfilter = nyagos.filter
    nyagos.filter = function(s)
        s = sq2dq(s)
        if oldfilter then
            s = oldfilter(s)
        end
        return s
    end
else
    assert(sq2dq([['(print "ahaha")']])   == [["(print \"ahaha\")"]])
    assert(sq2dq([["(print \"ahaha\")"]]) == [["(print \"ahaha\")"]])
    assert(sq2dq([["(print 'ahaha')"]])   == [["(print 'ahaha')"]]  )
end

スクリプトの後半にはテストコードも書いておきました。

識別子の nyagos は nyagos の中にいる時だけしかセットされないので、not nyagos な時のブロックは直接、普通の lua.exe から実行する時だけ動かすコードなどを書くことができます。ここにテストコードを書いておけばよいでしょう。

[[〜]]lua の文字列表現の一つです。今回はテスト用の文字列の中に "' も使いますんで、こういう時にこそ使う表現ですね。

これが意図通り動作するか…全部 .nyagos に埋め込むと行数が膨れて見辛くなるので、sq2dq.lua という別ファイルに書いて、.nyagos から include するようにしましょう。

package.path = package.path ..  ";(sq2dq.luaの置き場所のディレクトリ)\\?.lua"
require "sq2dq"

nyagos を起動して実験です。

$ gawk 'BEGIN{ print "ahaha" }'
ahaha

$ gmnlisp -e '(format (standard-output) "~S~%" (cons 1 2))'
(1 . 2)

最初に問題視したワンライナーも見易くなりました。

$ jq -r '. | map("#"+(.number|tostring)+" ["+ .user.login + "]("+ .user.html_url + ")") | join("\r\n")' issues.json
  :出力結果は人のアカウント情報などがあるのでちょっと省略

  1. 本当はシングルクォーテーション内のシェル変数は置換しない、ダブルクォーテーション内の変数はシェル置換するというルールがあります。そこまで対応するのはたいへんなので、今回は見送ります。

curl と jq で、GitHub レポジトリの Contributor list を作る

(1) issue の起案者の一覧を GitHub からダウンロード

curl "https://api.github.com/repos/USERNAME/REPOSITORY/issues?state=all" > issues.json
  • Open/Close 問わずに取得したいので、URL の ?state=all を追加する
  • そのままだと1ページ目だけしか出ない。2ページ名を取得するには &page=2 を URL に追加する

(2) JSON から issue 番号とユーザ名、ユーザページ名を抽出

cat issues.json | jq -r ". | map(\"#\"+(.number|tostring)+\" [\"+ .user.login + \"](\"+ .user.html_url + \")\") | join(\"\r\n\")"

#NN [@USERNAME](https://github.com/USERNAME) という行形式で取得できる

  • 基本的にはパイプライン言語なので、入力はパイプライン記号 | の左に書く
  • . はよく分からないが、カレントオプジェクトとでも言えばよさそう
  • map は PerlRuby の map に似ている。ここでは issue の配列を、起案ユーザについての情報をまとめた文字列の配列に変換する
  • 変換した文字列の配列を join で連結して1文字列にする
  • そのままだと "..\r\n..." といった形で表示されるので、 -r オプションをつけて、生文字列化して表示される

(追記) Windows CMD.EXE ではコード中の \" がきついので ' を使えるようにする

single2double.exe というツールを作った。

cat issues.json | single2double.exe jq -r ". | map('#'+(.number|tostring)+' ['+ .user.login + ']('+ .user.html_url + ')') | join('\r\n')"

引数中に含まれる'\" に変えて、第一引数のコマンドを呼び出しているだけ

(追記) PowerShell だと、UNIX同様の引用符が使えますねぇ

Get-Content issues.json | jq -r '. | map("#"+(.number|tostring)+" ["+ .user.login + "]("+ .user.html_url + ")") | join("\r\n")'

ただし、PowerShell は外部コマンドとのパイプラインのやりとりをANSI(MBCS)とみなす。今回はANK文字だけなので問題ないが、リダイレクト、もしくは jq から直接読み込ませた方がよさそうだ。

jq -r '. | map("#"+(.number|tostring)+" ["+ .user.login + "]("+ .user.html_url + ")") | join("\r\n")' issues.json

(追記) jq 完璧に理解した!

長いワンライナーjq はスクリプト化した方がいいね!(あたりまえ)

issues.jq

[
    .[] |
    select(.labels[] | select(.name == "bug" or .name == "enhancement")) |
    "#"+(.number|tostring)+" ["+ .user.login + "]("+ .user.html_url + ")"
] | join("\r\n")

これを jq -rf issues.jq issues.json で呼び出す

  • -fスクリプトファイルを指定するオプション
  • issues のうち、ラベルに bugenhancement がついているものだけを抽出するようにした
    • 最初、改造方法がまったく検討つかなかったが、ChatGTP に『jq で JSON 文字列「[ { "labels": [ { "name":"値",...} ] } ]」の中から "値”が bug もしくは enhanced のものを CSV 化するコードはどう書けばいいでしょうか』と質問して、その回答を元に改造した。見当がつかない課題に対して、とっかかりを得るのにとても便利だ
  • 「展開された複数の値」と配列を今までゴッチャに考えていた。
    • .[] は配列を中味の値を複数の値に展開する
      • issues のトップは配列値なので、まずは展開した方が処理しやすい
    • | で、展開された値をひとつひとつ個別に処理できる
      • . がカレントの値
      • .NAME がカレントの値のフィールド
    • [ 複数の値 ] で、展開された複数の値をまた1配列に戻すことができる

(追記) nyagos でもUNIX同様の引用符が使えるようにするアドオン書いた

Referneces