12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LispAdvent Calendar 2020

Day 2

LibreOffice BasicでLispインタプリタ(mal)を書いた

Last updated at Posted at 2020-12-03

これは LibreOffice Advent Calendar 2020 の4日目の記事です。
(追記 2020-12-07 空いているようだったので Lisp Advent Calendar 2020 にも参加してみました)

  • シンプルな Lisp インタプリタ mal (make a lisp) を LibreOffice Basic に移植しました。
  • 実用についてはあまり考えていません。デモ・PoC っぽいつもりです。
  • できあがったばかりなのでコードは汚いです。リファクタリングなどはこれから。

できたもの

  • Optional 以外のテストはすべて通って、セルフホスト(LibreOffice Basic で書いた mal インタプリタ上で mal で書かれた mal インタプリタが動く)までできた段階。
    • テストでカバーできていない部分はいろいろ漏れがあると思います。例外まわりとか。
  • VBA互換機能は不使用
  • Ruby のコードが含まれていますが、インタプリタの本体部分を動かす分には不要です。
    • mal のテストフレームワークに乗せるために標準入出力に繋がった REPL が必要だったため、アダプタを Ruby で書きました。
  • 非効率で遅いのと、Basic でできることの制約があるため実用についてはうーんどうでしょうねという感じ。
    • まじめに実用したい場合は Java版 を使うのが良い気がします
  • advent calendar に間に合わせるために急いで作り、できあがったばかりなのでコードはまだ汚いです(大事なので何度も言いたい)。LibreOffice Basic 自体にも不慣れなので……。リファクタリングなどはこれから。

mal について

mal について簡単に。

  • kanaka/mal: mal - Make a Lisp
  • Lisp インタプリタ実装学習者向け(?)のシンプルな実装
    • たとえば Ruby 版だと全部合わせても 534行
    • シンプルとはいえ末尾呼び出しの最適化(TCO)やユーザ定義マクロ、try/catch が利用可能
  • 多数の言語に移植されている
    • 2020-12-03 現在で 86 言語
    • その中に mal 自身で書かれたセルフホスト版もある
  • 実装を11ステップに分けたガイドがあり、
    各ステップ毎のユニットテストが用意されている
  • Clojure を参考にしている

といったあたりがポイントでしょうか。

より詳しくは、たとえば下記の記事を参照してください。

実行の例

ファイルを開いて実行

※ ファイル内の Basic マクロを実行する都合上セキュリティの設定をゆるめる必要があります。
ツールバーから ツール>オプション
>LibreOffice>セキュリティ>マクロセキュリティ

サンプルとして mal-demo.fods というファイルを用意しました。 LibreOffice Calc でファイルを開いて [RUN] ボタンを押すとライフゲームが動きます。押すたびに1ターン進みます。
私の環境だと1ターンに12〜13秒かかりました :sweat_smile:

(↓Firefox だとアニメgifが再生されない場合があるようです。ファイルをクリックして画像だけ表示すると動くものが見れます。)

Peek 2020-12-04 01-05.gif

処理に時間がかかりすぎていて途中で中断したいといった場合は
ツール>マクロ>マクロの編集
実行>停止(Shift+F5)
で止められます。

矩形のテキストを書き換えて [RUN] ボタンを押すと任意の式を実行することができます。
以下 2点、この方法で実行する場合の留意点です。不便ですが、ちょっとしたお試し用ということで簡易な作りで済ませています。

  • 単一の式のみ実行可
  • 1回の実行が終わると環境がリセットされる(変数や関数を定義しても次に [RUN] ボタンを押して実行する時には消えている)

複数の式は実行できませんが、下記のように (do ... ) で囲むことで1つの式として実行できます。

(do
  (def! a 1)
  (def! b 2)
  (println (+ a b))
)

以下、ためしに何かコピペして動かしてみたいという人のためのサンプルコードです。

;; 四則演算
(/ (- (+ 515 (* -87 311)) 296) 27)

;; 変数の定義と参照
(do
  (def! x 123)
  x
  )

;; フィボナッチ関数の定義と実行
(do
  (def! fib (fn* [N] (if (= N 0) 1 (if (= N 1) 1 (+ (fib (- N 1)) (fib (- N 2)))))))
  (fib 6)
  )

;; if
(if true 7 8)

;; read
(read-string "(+ 1 2)")
;; read + eval
(eval (read-string "(+ 1 2)"))

;; throw / try / catch
(try*
  (throw "something wrong")
  (catch* exc (prn "exc is:" exc))
  )

;; 外部ファイルを読んで表示
(println (slurp "sample.mal"))

;; 外部ファイルをロードし、その中で定義されている関数を使う
(do
  (load-file "example/mylib.mal")
  (my-func)
  )

もっといろんなコード例を見たいという方は mal のテストコード を参照してください。

ターミナルで REPL を実行

(2020-12-24 追記) Docker を利用し、ターミナルで REPL を実行できるようにしました。
参考: DockerでLibreOffice Basicマクロを実行する

まず docker ブランチにスイッチしてイメージをビルドします。

git checkout docker
./tasks.sh docker-build

ビルドできたら下記のコマンドで REPL を動かせます。終了するときは Ctrl-C を押してください。

./tasks.sh docker-repl

LibreOffice 向けの組み込み関数

せっかく LibreOffice Basic で作ってますから LibreOffice の操作したいですよね、ということでいくつか適当な組み込み関数を用意してみました。

  • cell-get / cell-set
    • セルの内容の読み書き(Calc用)
  • get-ci-max / get-ri-max
    • シートの列・行の最大インデックスの取得(Calc用)
  • msgbox / wait
    • LibreOffice Basic の MsgBox, Wait に対応
  • file-write
  • execute-dispatch

これらを使ったサンプルです。

(let*
  [
    sheet-name "Sheet1"
    numseq (fn* [from to] ; from <= x < to
                 (if (<= to from)
                     ()
                   (cons from (numseq (+ from 1) to))))
    join (fn* [delim xs]
              (cond
               (= (count xs) 0) ""
               (= (count xs) 1) (str (first xs))
               "else" (str (first xs) delim (join delim (rest xs)))))
    ci-max (get-ci-max sheet-name)
    ri-max (get-ri-max sheet-name)
    ris (numseq 0 ri-max)
    cis (numseq 0 ci-max)
    pr-json (fn* [val]
                 (if (nil? val)
                     "null"
                   (pr-str val)))
    row-to-line (fn* [ri]
                     (str "[ "
                          (join ", "
                                (map
                                 (fn* [ci] (pr-json (cell-get sheet-name ci ri)))
                                 cis))
                          " ]"))
    text (join "\n" (map row-to-line ris))
  ]
  (file-write "array_table.txt" (str text "\n"))
)

シート Sheet1 の内容を次のような JSON の配列っぽいフォーマットでファイルに出力します。

[ 123, null, null, null, null ]
[ null, null, null, "■", null ]
[ "A3", null, null, null, "■" ]
[ "A4", null, "■", "■", "■" ]

参考: LibreOffice Calc Basic fun!!!: データが入力されている最終行・最終列を求める


execute-dispatch のサンプル。「マクロの記録」で生成されたコードで目にするアレです。

(do
  (execute-dispatch "GoToCell" "ToPoint" "B7")
  (execute-dispatch "EnterString" "StringName" "FDSA")
  )

実装面のメモ

外部エディタでの編集

Basic の IDE ではなく Emacs で外部のファイルを編集し、それらのファイルをテンプレートに埋め込んで fods ファイルを生成する形で作業を進めました。

ロガー

単にファイルに追記していくだけのもの。素朴だけどあると便利というかないとつらい。
基本的にはこのログを見て、ヘッドレス実行だと見れない・見にくい情報がある場合やステップ実行したい場合に Basic IDE のデバッガを併用するという感じでデバッグしてました。

例外

例外機構を持たない言語で書かれた mal 実装のうち、自分が読めそうなものということで C言語版をチラ見したり。
本来の処理が埋もれて見づらくなるのが嫌、ログ出力の追加など後から一括して変更できるようにしておきたい、といった理由により Ruby 側の前処理でコメントを置換することにしました。

Sub foo
  例外が発生する可能性のある処理
  ' CHECK_ERROR

  後続の処理
End Sub

↓ コメント行 ' CHECK_ERROR を置換

Sub Foo
  例外が発生する可能性のある処理
  ' ----------------
  If mal_error_exists() Then
    ' デバッグ用にログを出したり
    Exit Sub ' 後続の処理を実行せずに呼び出し元に戻る
  End If
  ' ----------------

  後続の処理
End Sub

GoTo をうまく使うと Basic っぽくてよかったかもしれません(?)。

関数のディスパッチ

  • LibreOffice Basic には高階関数がないけど、どうする?

  • 名前(関数名)で振り分ける

    • 関数名と処理本体の紐づけをいつどこでやるか、という問題
  • 2種類の関数

    • fn* で作った関数
      • env と処理本体を持つが名前を持たない
      • 名前との紐付けは def! によって行われる
    • 本体なしの関数オブジェクト
      • env と名前を持つが処理本体を持たない
      • core にある組み込みの関数はこっち
      • こっちは実行時に名前を見て振り分け

その他細かいの

  • TCO のループが回るときに env と ast が変化してしまうので、EVAL の引数だけ明示的に ByVal で値渡しする必要がある
    • 最初参照渡ししていてデバッグがちょっと大変だった
  • env に id を持たせて、env と関数をダンプした時に表示
    • デバッグ時にどの関数がどの env を参照しているのか分からないとつらい
  • Ruby と LibreOffice Basic 間の通信
    • とりあえずファイルをポーリングして、それっぽく動いたのでそのまま進めた(もっと良い方法があると思います)
  • map/連想配列/辞書型 的なデータ構造
    • ググると CreateObject("Scripting.Dictionary") を使う方法が出てくるが、これは Linux だと動かない
    • 仕方ないので配列を使って自前実装していたが、com.sun.star.container.EnumerableMap が使えると知り、これを使って書き換えたら(ライフゲーム全体の計測で)3倍くらい速くなった

感想など

  • 自分としては mal の実装自体が目的だったので満足
  • 高階関数のない言語での実装、TCO、例外機構については具体的にどう実装すればよいか自分にとっては自明ではなかったため、勉強になりました。やってよかった。
  • このくらいの実装量で十分立派な言語のインタプリタが作れてしまうので、あらためて Lisp はおもしろい言語だなーと思いました。
  • mal はいいぞ

環境

  • Ubuntu Linux 18.04
  • LibreOffice 6.0.7.3

せっかくなので去年のその後の話

去年の advent calendar で LibreOffice Drawのodgファイルから図形の情報を抜き出して使う という記事を書きましたが、これを使ってその後下記のようなものを作りました。

リレー式論理回路シミュレータを自作して1bit CPUまで動かした
image.png
20200412074010.gif

回路図を LibreOffice Draw で描いて、それをプログラムから読んで動かしています。
こっちはちゃんと実用になりました。よかったですね。


Advent Calendar 14日目にこういうのも書きました。ご興味あればこちらもどうぞ:

他に LibreOffice 関連で書いたもの

12
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?