LibreOffice BasicでLispインタプリタ（mal）を書いた

これは [LibreOffice Advent Calendar 2020](https://adventar.org/calendars/5258) の4日目の記事です。
（追記 2020-12-07 空いているようだったので [Lisp Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lisp) にも参加してみました）

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

<!--
  - 末尾再帰の最適化やユーザ定義マクロ、try/catch などが利用可能。
-->

<!-- ■■■■■■■■ -->
# できたもの

https://github.com/sonota88/mal/tree/lirbreoffice_advent_calendar_2020/impls/libreoffice-basic

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

<!--
が、LibreOffice 本体だけあれば Basic が動くので、つまり SDK が不要です。ここはメリットといえばメリットでしょうか……強いていえば、というレベルですが。

- 遅いです。高速化の余地はあると思いますが、ベースが Basic なので厳しい気がします。
// - mal に PR 出すのはちょっと先になりそう
// ruby部分: ここはもうちょっと薄くできそう。
// 実用面でのメリットを強いて挙げるなら、〜
-->


<!-- ■■■■■■■■ -->
# mal について

mal について簡単に。

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

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

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

- [72のプログラミング言語に移植された埋め込み可能なLISP言語の紹介 (複数言語を使った開発プロジェクトで同じコード／ロジックを再利用できます。JavaScript編) - Qiita](https://qiita.com/javacommons/items/58b355c00cefb208e982)
- [Make a Lisp で Lisp 処理系を学んでつくる (with Crystal) - はやくプログラムになりたい](https://rhysd.hatenablog.com/entry/2015/06/11/212141)




<!-- ■■■■■■■■ -->
# 実行の例

## ファイルを開いて実行

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

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

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

![Peek 2020-12-04 01-05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/55e36965-a606-e447-9e44-d2b279edb306.gif)

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


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

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



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

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

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

```clojure
;; 四則演算
(/ (- (+ 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 のテストコード](https://github.com/kanaka/mal/tree/master/impls/tests) を参照してください。

<!--
;; 外部ファイルのロード
(do
  (load-file "mylib.mal")
  (sample-func 123) ; mylib.mal で定義されている関数を使用
  )

// ここではデモとして矩形のテキストを読んで実行する例について説明していますが、
// 要はインタプリタに mal のコードを文字列として渡せばよいので、セルに書かれたコードを読んだり、外部のファイルから読んだりといったこともできるでしょう。

;; セルの値の読み書き: Sheet1 のセル B3 の値をインクリメント
(cell-set "Sheet1" 1 2
  (+ 1 (cell-get "Sheet1" 1 2)))
-->

<!--

\## CLI で動かす

下記の Docker イメージを使うと手軽に試せます……といいたかったのですが、コンテナ内で `libreoffice foo.fods "vnd.sun.star.script:〜"` の形で Basic マクロを実行できず今回は諦めました。

というわけで Ubuntu 18.04 でしか動作確認していませんが、 Ruby があれば下記でターミナルで REPL を使えます。

```
./tasks.sh repl
```

ただし、環境依存な部分があるため Windows や Mac では動かないかもしれません。
Ubuntu 18.04 でしか動作確認していません。

TODO 実行した様子
-->

## ターミナルで REPL を実行

（2020-12-24 追記） Docker を利用し、ターミナルで REPL を実行できるようにしました。
参考: [DockerでLibreOffice Basicマクロを実行する](https://qiita.com/sonota88/items/1deb7dc3cb37d0484a5e)

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

```sh
git checkout docker
./tasks.sh docker-build
```

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

```sh
./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

<!--
自分で独自に組み込み関数を追加したい場合はこれらの実装箇所（Core）を参考にしてください。
-->

----

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

```clojure
(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!!!: データが入力されている最終行・最終列を求める](http://calibreblo.blogspot.com/2011/04/blog-post_26.html)

----

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

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


# 実装面のメモ

## 外部エディタでの編集

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

## ロガー

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



## 例外

<!--
LibreOffice Basic だと何行か書かないといけなくて鬱陶しく
-->

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

<!--
こういうのは手作業でがんばっても得るものが少ないのでなるべく横着します。
あまり意味はない
-->

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

  後続の処理
End Sub
```

↓ コメント行 `' CHECK_ERROR` を置換

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

  後続の処理
End Sub
```

GoTo をうまく使うと Basic っぽくてよかったかもしれません（？）。


## 関数のディスパッチ

- LibreOffice Basic には高階関数がないけど、どうする？
- 名前（関数名）で振り分ける
  - 関数名と処理本体の紐づけをいつどこでやるか、という問題

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

<!--
- 結局どこかで関数名で振り分けて dispatch するしかない
-->

## その他細かいの

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



<!--
これが最初正しく実装できていなくて、ログとにらめっこしてなんで動かないのか調べたりして大変でした
-->



<!--
Basic → Ruby / Ruby → Basic の双方向ともファイルです。
かっこよくはないですが、単純なので他の言語で同様のことをしたいときにも応用しやすいんじゃないかと思います。
-->

# 感想など

<!--
高階関数のない言語での実装、TCO、例外機構については今まで具体的にどう実装するかよく知らずブラックボックスな部分だったので勉強になりました。やってみてよかった。
-->

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


# 環境

- Ubuntu Linux 18.04
- LibreOffice 6.0.7.3


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

去年の advent calendar で [LibreOffice Drawのodgファイルから図形の情報を抜き出して使う](https://memo88.hatenablog.com/entry/2019/12/03/003517) という記事を書きましたが、これを使ってその後下記のようなものを作りました。

[リレー式論理回路シミュレータを自作して1bit CPUまで動かした](https://memo88.hatenablog.com/entry/2020/05/03/132253)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/6f97c93c-5171-2636-5590-c932c94b96de.png)
![20200412074010.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/f8077376-8fab-fce3-162d-e4bfe22555b1.gif)


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

<!--

\# TODO 雑多

- execute-dispatch
    - サンプル: セルに値を書き込む
- interop libo calc
    - msgbox
    - wait
    - cell-get/cell-set
- disclaimer: エラーメッセージは分かりやすくない
    - エラーになった場合の対処がしんどい
- RUN で実行したものが止まらない場合
    - マクロの編集から
- 組み込み関数の追加
- 感想
    - 例外まわり、TCO、マクロ、高階関数がない言語でどうするか … これらについてはどう具体的に実装するか知らなかったので、今回書いてみて勉強になった
- get-ci-max / get-ri-max
    - サンプル追加
- 個人的には mal の実装自体が目的だったので満足
- Lisp は処理系が手軽に実装できて楽しいと今回改めて思いました
- env id ... デバッグ用

-->

----

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

- [素朴な自作言語のコンパイラをLibreOffice Basicに移植した](https://qiita.com/sonota88/items/31c127c42a9722892aa8)

# 他に LibreOffice 関連で書いたもの

https://memo88.hatenablog.com/archive/category/LibreOffice