SATySFiコード中で整数を16進数で書きたい

>   これは「[SATySFi Advent Calendar 2019](https://adventar.org/calendars/3929)」の5日目の記事です。
>   （4日目は [monaqa さん](https://qiita.com/monaqa/items/8bd74248c1cfbc62bd64) です。）
>   （6日目は [amutake さん](http://amutake.hatenablog.com/entry/2019/12/06/103839) です。）


できた！！

```text:hex.satyg
let hex str =
  let len = string-length str in
  let dval digit = match digit with
    | `0` -> 0 | `1` -> 1 | `2` -> 2 | `3` -> 3 | `4` -> 4
    | `5` -> 5 | `6` -> 6 | `7` -> 7 | `8` -> 8 | `9` -> 9 
    | `A` -> 10 | `B` -> 11 | `C` -> 12
    | `D` -> 13 | `E` -> 14 | `F` -> 15
    | `a` -> 10 | `b` -> 11 | `c` -> 12
    | `d` -> 13 | `e` -> 14 | `f` -> 15
    | _ -> abort-with-message `bad number format` in
  let-rec iter k a =
    if k < len then
      iter (k + 1) (a * 16 + (dval (string-sub str k 1)))
    else a in
  if len == 0 then abort-with-message `bad number format`
  else iter 0 0
```

さっそく使ってみましょう！

```text:test-1.saty
@require: stdjareport
%↓さっきのhex.satygを読み込む
@import: hex

let-inline \show-int n  = embed-string (arabic n)
in
document (|
  title = {\SATySFi;コード中で整数を16進数で書きたい};
  author = {某ZR};
|) '<
  +chapter {hex函数を使ってみる} <
    +p {\show-int(hex `2603`);}  % 0x2603 = 9731
    +p {\show-int(hex `1F986`);} % 0x1F986 = 129414
  >
>
```

![image-hexms-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/983f669a-de7b-79e1-fd38-59482d09a5c1.png)

完璧！！:smiley:

## （追記）最初にお詫び

以下の話は「SATySFiは16進数表記をサポートしない」という前提で書かれていますが、これは私の単純な<font color="red">勘違い</font>であり、実際にはSATySFiは普通に`0x`で始まる16進数表記をサポートしています（[参照](https://twitter.com/bd_gfngfn/status/1202614014506762240)）。ただし実は「16進数表記の実装」自体は本題ではないので、以下での話は「仮に未サポートだった」と思って読んでください:bow:

## ちゃんと説明しろ

※本記事の内容はSATySFiのmaster版を前提とします。（最新リリース版のv0.0.3ではない）

（現状では）SATySFiのコードにおいて整数値は10進表記（`129414`など）で書くことになっていて、他のプログラム言語とは異なり16進表記（`1F986`など）はサポートされていません。（←<font color="red">勘違い</font>）

もちろんSATySFiは「組版を目的とする」言語であるため、16進数が必要となる場面は少ないでしょう。しかし、例えばUnicodeの符号値は16進数で指定するのが普通なので、16進数表記の需要が全くないわけではないはずです。

hexパッケージは以下の函数`hex`を（グローバルに）定義します。

* `hex`: string → int  
hex *str* は *str* が16進表現として表す整数を表す。A～Fの文字は大文字でも小文字でもよい。符号はサポートしない。*str* の書式が不正の場合は**実行時エラーになる**。

*hex*を利用したもう少し実用的（？）な例として、Unicodeの符号値を16進数で指定する例を挙げます。

```text:test-2.saty
@require: stdjareport
%↓hexパッケージを利用する
@import: hex

%% \u(uc) : [int] inline-cmd
% 整数 uc をUnicode符号値とする文字を出力する.
let-inline \u uc = embed-string (string-unexplode [uc])

%% +wagahai(n) : [int] block-cmd
% "吾輩は猫である。"の段落をn回出力する.
let-block ctx +wagahai n =
  % 評価時に端末にメッセージを表示する
  let () = display-message `+wagahai is called` in
  let ib-w = read-block ctx '< +p{吾輩は猫である。} > in
  let-rec iter | 0 ib = ib
               | n ib = iter (n - 1) (ib +++ ib-w) in
    iter n block-nil

in
document (|
  title = {\SATySFi;コード中で整数を16進数で書きたい};
  author = {某ZR};
|) '<
  +chapter {「吾輩は猫である」と3回言ってみる} <
    +wagahai(3);
  >
  +chapter {「吾輩は猫である」の作者は？} <
    +p {森\u(hex `9DD7`);外（違う）}
  >
>
```

![image-hexms-2b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/66a9abab-dfa0-16ec-4769-59e49de43543.png)

やっぱり完璧:smiley:

## 本当に完璧なのか？

**さて、ここからが本題です。**

このhexパッケージは、確かに「16進表記を解釈する」機能としては完璧なのですが、ひとつ大きな問題があります。それは「引数の文字列の書式が不正である場合に**実行時に**エラーが発生する」ということです。

何が問題であるかを、先ほどのUnicode符号値を読む例を用いて確かめてみましょう。test-2.satyのコードの一部を次のように変えてみました。

* 16進表記`9DD7`の中の`D`を`Ð`(U+00D0)に“書き間違えて”みる。`Ð`は正当な16進数字でないので当然*hex*でエラーが発生する。
* 「吾輩は猫である」を3回ではなく20000回（870ページ分）出力する。

```text:test-3.saty
@require: stdjareport
%↓hexパッケージを利用する
@import: hex

let-inline \u uc = embed-string (string-unexplode [uc])

let-block ctx +wagahai n =
  % 評価時に端末にメッセージを表示する
  let () = display-message `+wagahai is called` in
  let ib-w = read-block ctx '< +p{吾輩は猫である。} > in
  let-rec iter | 0 ib = ib
               | n ib = iter (n - 1) (ib +++ ib-w) in
    iter n block-nil

in
document (|
  title = {\SATySFi;コード中で整数を16進数で書きたい};
  author = {某ZR};
|) '<
  +chapter {「吾輩は猫である」と2万回言ってみる} <
    +wagahai(20000); % 3→20000に変更
  >
  +chapter {「吾輩は猫である」の作者は？} <
    %              ↓マチガッテル 
    +p {森\u(hex `9ÐÐ7`);外（違う）}
  >
>
```

このマチガッテル文書をコンパイルしてみましょう。

```console
$ satysfi test-3.saty
 ---- ---- ---- ----
  target file: 'test-3.pdf'
...(略)...
 ---- ---- ---- ----
  evaluating texts ...
+wagahai is called
! [Error during Evaluation] bad number format
```

おっと、「`bad number format`」のエラー[^abort]が発生しました。それは予定通りなのですが、ここで重要なのは、このエラーが出る直前に十数秒くらい**端末表示が止まっている**ことです。この停止の原因は「吾輩は猫である」2万回が処理された**後に**実行時エラーが発生したからだと推測されます。

[^abort]: 「エラーを出力して処理を打ち切る」ために、新しいプリミティヴ函数の`abort-with-message`を利用しています。

実は、この推測が正しいことはエラーの直前にある端末出力を見れば確認できます。

```console
+wagahai is called
```

この端末出力は`+wagahai`命令の実装コードの先頭行にある`display-message`によるものです。つまり、`+wagahai(20000);`の処理は完了していて、その**後に**エラーが発生したわけです。

## 後からエラーが出る、それは大問題

hexパッケージを作ったそもそもの動機は「SATySFiで16進の整数表記を可能にすること」でした。普通のプログラミング言語で`9ÐÐ7`のような不正な16進の整数表記があれば「文法エラー」として即座に実行は中断される[^interpreter]はずです。それと比べると、870ページ分の文書データを処理して初めてエラーが出るというのは随分不便なことです。

[^interpreter]: インタプリタ処理系の場合。コンパイラの場合はそもそもコンパイルが通らないので実行ができない。

『The SATySFibook』でLaTeX<del>をディスって</del>の問題点を指摘している節（3ページ）に以下のような記述があります。

> エラー報告は不親切なだけでなく提示されるまでに時間を要することも多い。やはり「組版処理中に初めて不整合が生じた」ときにエラーが報告されるのが要因で、例えば500ページの文書をLaTeXで組む際に490ページ目にあたる部分に不適格な記述を書いてしまった場合は490ページ分の組版処理の後にエラーが提示されるため、ユーザは入力ミスの存在を知らされるまで短めに見積もっても数十秒から数分程度は待つことになる。

そうです。数百ページの文章を処理した後でエラーが起こるというのは、まさに**アレアレなLaTeXの世界**を象徴する事象なのです。“LaTeXを倒すため”に生まれてきたSATySFiとしては何とも許しがたいことです。

やっぱり、今のhexパッケージは完璧からは程遠いものでした。

## 完璧なhexのために必要なもの

このアレアレな“LaTeX的事象”を解決する術はあるのでしょうか。実はこれに関する発表が、先日催された「[Regex Festa]」で行われたようです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">発表は一発芸なんですが、やってることはSATySFiに多段階計算が導入されたモチベーションそのもの（DSLの正当性を静的に（stage 0で）保証したい）なので曲芸ではないです。むしろ非自明で最も簡単なmotivatingなexampleって言っていいんじゃないかくらいの勢い。</p>&mdash; κeen (@blackenedgold) <a href="https://twitter.com/blackenedgold/status/1185201117837193217?ref_src=twsrc%5Etfw">October 18, 2019</a></blockquote>

[Regex Festa]: https://opt.connpass.com/event/140566/

* [その正規表現エンジン、インタプリタで満足(サティスファイ)してる？！]（κeen氏）

[その正規表現エンジン、インタプリタで満足(サティスファイ)してる？！]: https://keens.github.io/slide/sonoseikihyougenenjin_intapuritademanzokushiteru__/

どうやら「**多段階計算**」とかいうやつを使うと“LaTeX的事象”が解決できるようです。

## SATySFiの多段階計算

では、その多段階計算とは一体どんなものなのでしょうか。さっそく当該の発表資料を見てみましょう。

![imagekeen-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/b64315eb-6538-5167-e2db-dd5a2af1cd3f.png)


……なるほど、**わからん**:cry:

[SATySFi Wiki]にgfn氏自身による解説があるので、それも参照してみましょう。

* [SATySFiに多段階計算を入れる構想]（SATySFi Wiki）

[SATySFi Wiki]: https://github.com/gfngfn/SATySFi/wiki/SATySFi-Wiki
[SATySFiに多段階計算を入れる構想]: https://github.com/gfngfn/SATySFi/wiki/SATySFi%E3%81%AB%E5%A4%9A%E6%AE%B5%E9%9A%8E%E8%A8%88%E7%AE%97%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B%E6%A7%8B%E6%83%B3

![image-satysfimsgfn-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/8196a0da-02eb-3b83-46ef-c744af38a314.png)

なるほど、**サッパリわからん**:sob:

## それでも頑張ってみる

絶望しかないわけですが、それでも[κeen氏の実装コード]を穴の開くほど見つめたり、時には情熱のSATySFiダンスを踊ったりしながら、結局「全面的に**直観に頼る**」という最終手段:muscle:に訴えて「多段階計算版のhexパッケージ」（hex-msパッケージ）をつくってみました。

[κeen氏の実装コード]: https://github.com/KeenS/satysfi-rejit

```text:hex-ms.satyh
@stage: 0

let hex str =
  let len = string-length str in
  let dval digit = match digit with
    | `0` -> &0 | `1` -> &1 | `2` -> &2 | `3` -> &3 | `4` -> &4
    | `5` -> &5 | `6` -> &6 | `7` -> &7 | `8` -> &8 | `9` -> &9 
    | `A` -> &10 | `B` -> &11 | `C` -> &12
    | `D` -> &13 | `E` -> &14 | `F` -> &15
    | `a` -> &10 | `b` -> &11 | `c` -> &12
    | `d` -> &13 | `e` -> &14 | `f` -> &15
    | _ -> abort-with-message `bad number format` in
  let-rec iter k a =
    if k < len then
      iter (k + 1) &(~a * 16 + ~(dval (string-sub str k 1)))
    else a in
  if len == 0 then abort-with-message `bad number format`
  else iter 0 &0
```

多段階計算を使った函数を実際に使う場合、典型的には函数適用の結果に`~`を適用した形を書くことになるようです。つまり、``hex `…` ``の代わりに``~(hex `…`)``と書く必要があります。

```text:test-4.saty
@require: stdjareport
%↓多段階計算版のhex-msを読み込む
@import: hex-ms

let-inline \show-int n  = embed-string (arabic n)
in
document (|
  title = {\SATySFi;の整数を16進数にしたい};
  author = {某ZR};
|) '<
  +chapter {多段階なhexを使ってみる} <
    % "~(hex …)" という形で呼び出す
    +p {\show-int(~(hex `2603`));}  % 0x2603 = 9731
    +p {\show-int(~(hex `1F986`));} % 0x1F986 = 129414
  >
>
```

恐る恐るコンパイルしてみると……。

![image-hexms-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/69c35f29-6d83-3b60-3bcd-b1bfef7dbc72.png)

もしかして：できた？:smiley:

## マチガッテルやつを試してみる

多段階計算版のhex-msパッケージができたようなフインキも若干感じるので、さっそくhex-msを使って「870ページ分処理した後にマチガッテル文書」を試してみましょう！

```text:test-5.saty
@require: stdjareport
%↓多段階計算版のhex-msを読み込む
@import: hex-ms

let-inline \u uc = embed-string (string-unexplode [uc])

let-block ctx +wagahai n =
  % 評価時に端末にメッセージを表示する
  let () = display-message `+wagahai is called` in
  let ib-w = read-block ctx '< +p{吾輩は猫である。} > in
  let-rec iter | 0 ib = ib
               | n ib = iter (n - 1) (ib +++ ib-w) in
    iter n block-nil

in
document (|
  title = {\SATySFi;の整数を16進数にしたい};
  author = {某ZR};
|) '<
  +chapter {「吾輩は猫である」と2万回言ってみる} <
    +wagahai(20000);
  >
  +chapter {「吾輩は猫である」の作者は？} <
    %                ↓マチガッテル 
    +p {森\u(~(hex `9ÐÐ7`));外（違う）}
  >
>
```

このファイルをコンパイルしてみると……。

```console
 ---- ---- ---- ----
  target file: 'test-5.pdf'
...(略)...
  preprocessing 'stdjareport.satyh' ...
  preprocessing 'test-5.saty' ...
! [Error during Evaluation] bad number format
```

今度は`+wagahai is called`が表示されずに（つまり「我輩は猫である」2万回を処理する**前に**）すぐにエラーが出て終了しました。ユーザに優しいSATySFiの世界が帰ってきたわけです！:smiley:

## まとめ

**SATySFiの多段階計算はスゴイ**、だけど、なにもわからない:sob: