SATySFiでトンボと裁ち落としを設定するパッケージを作った話

これは「SATySFi Advent Calendar 2020」の6日目の記事です。

5日目はmonaqaさんによる「SATySFi で湯婆婆」でした。

6日目はabenoriさんのSATySFiで可換図式です。


「ネタがないなら今年作ったパッケージについて語ろう」ということで、自作パッケージを宣伝する記事第二弾です。

今回はSATySFiでトンボと裁ち落としを簡単に付けるためのパッケージを紹介したいと思います。

作ったものについて

クラスファイルの中を少し変更するだけでトンボや裁ち落としを付けることができ、さらに裁ち落としのサイズやトンボを付けるか付けないかなどを自由に弄ることができるような機能を提供するパッケージを作りました。

パッケージのリポジトリは"puripuri2100/satysfi-tombo"にあります

実際に使ってみるとこのようになります(実際には色はつきません)。

f:id:puripuri2100:20201212110250j:plain

薄い灰色部分の本文の周りに暗い縁で3mm幅の裁ち落としが付けられ、さらにトンボもついているのがわかると思います。

使い方

インストールは

opam install satysfi-tombo
satyrographos install

によってインストールできます。 提供されるパッケージファイルはtombo/tombo.satyhで、モジュール名はTomboです。

クラスファイル内での使用を想定しており、一般の文書作成者が触ることは想定していません(クラスファイルから作る場合は触るでしょうが)。

クラスファイルにおいてdocument型を生成するために使われるpage-breakプリミティブをTombo.page-break関数に置き換え、context型の引数を最初に追加で与えるだけで、生成されるPDFに裁ち落としとトンボが付いてきます。

具体的には、普通クラスファイルの中で

page-break page pagecontf pagepartsf bb-main

のようにしてdocument型を作っている部分を

Tombo.tombo-page-break ctx-doc page pagecontf pagepartsf bb-main

のように変更するだけです。

二段組にも対応していて、

Tombo.tombo-page-break-two-column ctx-doc page len f pagecontf pagepartsf
bb-main

とするだけで出来ます。

オプション引数で裁ち落としのサイズとトンボのための余白の大きさを変えることができます。

Tombo.tombo-page-break ?:(Some(<裁ち落としの長さ:length>)) ?:(Some(<余白の大きさ:length>)) ctx-doc page ...

一番目が裁ち落としの長さで、二番目が余白の大きさです。

どちらもlength optionで与えます。一番目にNoneを与えると裁ち落としが設定されなくなり、二番目にNoneを与えるとトンボが付けられなくなります。

作った動機

私用で印刷所に入稿するPDFデータをSATySFiで作ることにしたため、必要になると思い作成しました。

「進捗大陸」というSATySFiで同人誌を作成しているサークルでは手動でページサイズを3mmずつ増やしていたりするらしいですが(参考:進捗大陸6「SATySFiで技術同人誌を作ろう」 by amutake)、そこでも指摘されている通り、PDFを配布したいときなどの裁ち落とし等を設定しない場合との切り替えが面倒であったりするので、そこら辺の面倒を無くしたい気持ちが結構ありました。

実装方法

page-breakには

  • ページサイズ
  • ページ番号を受け取って本文の高さと本文の開始座標を返す関数
  • ページ番号を受け取って
    • ヘッダーの中身
    • ヘッダーの開始位置の座標
    • フッターの中身
    • フッターの開始位置の座標

を返す関数

を与えます。

そこで、この3つの値に対して

  • ページサイズを裁ち落としとトンボの余白大きくした値を設定しなおす
  • 本文・ヘッダー・フッターの開始位置の座標をそれぞれ大きくする余白分ずらす

という操作を行います。

これで裁ち落としの設定ができました

あとはトンボのグラフィックを用意し、それを描画させるだけです。

グラフィックを用意しても、それが評価されないと出力されないので、

inline-graphics 0pt 0pt 0pt (fun _ -> tombo-gr)

のようにして大きさの無いinline-boxesを作り、これをblock-boxesに変換した後にヘッダーの下などに付けておくようにします。

こうすることで全てのページでトンボを描画する関数が評価され、トンボが出力されることになります。

もっと詳しく知りたい人はコードを読んでください。

課題

現状SATySFiはPDFの書き出しでMediaBoxしか設定できません(これはSATySFiがPDF書き出しに使用しているcamlpdfを弄れば実現できるかもしれません)。

そのため、「裁ち落とし」や「トンボ」と言ってもBleedBoxやTrimBoxが設定されているわけではないため、ある意味「飾り」と言っても過言ではない状況になっています(裁ち落としに関しては実用性はそこそこありますが)。

将来SATySFiがMediaBox以外も書き出せるようになったらここに対応したいと強く思っています。

また、page-breakをラップして実装しているため、graphicsの座標指定で絶対座標を使っている場合は大きくズレてしまうという欠点も抱えています。

さいごに

(様々な都合で)実用できるか微妙なところのSATySFi-tomboですが、どうやら自分が使う予定である用事では「トンボは付けず、3mmの裁ち落としと塗り足しを設定すること」という印刷所からの指定への対応としては使えそうです(3mmである前提で印刷所が作業をするのでTrimBoxなどを設定してなくても大丈夫、とのことでした)。

入稿の設定によってはこのパッケージが使える場面はあると思いますので、使っていただけると嬉しいです。

SATySFiでルビを使う~SATySFi-rubyパッケージ~

これは「SATySFi Advent Calendar 2020」の1日目の記事です。

2日目はmatsud224さんによる「Satyrographos Package Index の紹介」です。

とうとう始まりましたSATySFi Advent Calendar 2020。

楽しんでいきましょう!


今回はSATySFiでルビ(振り仮名)付きのテキストを出力する機能を提供するパッケージである、「SATySFi-ruby」を紹介(宣伝)します。

このパッケージは2020/12/01時点でヴァージョンが0.1.2と、まだまだ改善の余地のあるモノではありますが、ルビを表示するという一定の機能を有しています。

また、SATySFiの基本的な機能のみを使っているため、SATySFi for Windowsなどの「古いSATySFi」でも動きます。

satyrographos-repoに既に登録されているため、satyrographosとopamがインストールされている状態で

opam update

opam install satysfi-ruby && satyrographos install

とするだけでインストールできます。

SATySFiでルビ出力をするパッケージは今までに存在していないため、「パッケージとして存在する」というだけでアピールポイントにはなりますが、それ以外にも

  • 「日本語組版処理の要件」(JLREQ) にある規定にできるだけ沿ったルビの組版の実現を目指す。
  • 実際のルビ組版で必要となる空白などの処理をなるべく自動化で実現しているため、ユーザーが手で空白を打ち込んで調整する必要がない。

というポイントがあります。

では、使い方の解説などをしていきます。

パッケージの読み込み

satyrographosでインストールした場合は

@require: ruby/ruby

とするだけで読み込むことができます。

他の方法でインストールした人は、中にあるruby.satyhへの適切なパスを記述してください。

このパッケージが提供するモジュール名はRubyです。

基本編

ルビを振るためのコマンドとして\rubyを提供しています。使い方はこのようになります。

\ruby ?:[<オプション>] [<ルビ文字>] {|<親文字>|}

<ルビ文字>string listで、<親文字>inline-text listです。

先頭のオプションは省略可能です。詳しい使い方は後程説明します。

とりあえず簡単に使ってみる

親文字が1文字の場合は単純にルビと親文字を一つずつ与えるだけです。

あれは\ruby[`たか`]{|鷹|}ではなく\ruby[`うぐいす`]{|鶯|}です。

f:id:puripuri2100:20201201202043j:plain
親文字が一文字の時の使い方

親文字の上にルビが振られてますね。 ルビの方が親文字よりも長い場合も、前後に適切にスペースが入れられているのが分かると思います。

親文字が2文字以上ある場合は、親文字とルビの入力は適切に区切らないといけません。

\ruby[`こ`; `ばと`]{|小|鳩|}
\ruby[`く`; `じゃく`]{|孔|雀|}
\ruby[`しち`; `めん`; `ちょう`]{|七|面|鳥|}

f:id:puripuri2100:20201201202133j:plain
親文字が二文字の時の使い方

きちんとそれぞれの親文字の上で中央揃えになっていることがわかると思います。

これである程度のルビを振ることは出来るようになったと思います。

ルビの種類を変えてみる

実はルビにはいくつかの種類があります。

まずは「モノルビ」です。先ほどから例で見ているように、「親文字1字ごとに対応してルビを配置する」というルビの振り方です。文中に登場する単独の漢字に振りたいときに使われたりします。

f:id:puripuri2100:20201201202155j:plain
親文字が二文字の時の使い方

つぎに「グループルビ」です。これは、熟字訓などの、ルビを分割することができない場合に使われます。

f:id:puripuri2100:20201201202209j:plain
グループルビの例

最後に「熟語ルビ」です。 熟語としてひとまとまりになっているのにルビのせいで親文字の間にスペースが入ってしまうことを避けたいときなどに使われます。

f:id:puripuri2100:20201201202227j:plain
熟語ルビの例

比較として、上の例をモノルビで組んだ時の様子を表示してみます。

f:id:puripuri2100:20201201202243j:plain
比較用のモノルビ

スペースがいくつか消えていることがわかるでしょう。

勿論、これらを変更する機能があります。 オプション引数にRuby.mode Ruby.mなどのように入れると実現されます。モノルビのときにはRuby.m、グループルビの時はRuby.g、熟語ルビの時はRuby.jです。

v0.1.2の時点ではモノルビがデフォルトです。

モノルビの場合:

\ruby ?:[Ruby.mode Ruby.m] [`く`; `じゃく`]{|孔|雀|}

グループの場合:

\ruby ?:[Ruby.mode Ruby.g] [`く`; `じゃく`]{|孔|雀|}

熟語ルビの場合:

\ruby ?:[Ruby.mode Ruby.j] [`く`; `じゃく`]{|孔|雀|}

f:id:puripuri2100:20201201202749j:plain
ルビの種類ごとの出力の違い

さて、これで使い方は大体わかったと思われます。

ルビの前後への進入を調節する機能などもありますが、具体的にはドキュメントをお読みください。

実装方法(簡単に)

簡単にルビの実装方法を解説します。

実装方法に強い関心のある方などは、実際の実装を見ていただくのが良いと思います。

モノルビの場合

ルビと親文字はリストの形で既に貰っているので、これをそのまま利用します。

最初はルビと親文字をそれぞれ一つずつ組んでinline-boxes(以下ib)にします。

ここで親文字とそれに対応するルビのibの長さを評価します。

もし親文字の方が長かったらルビ文字を中央揃えになるようにスペースを入れ、ルビ文字の方が長かったら親文字が中央になるようにスペースを入れます。

これを全ての親文字とそれに対応するルビに対して行います。

このままですと「一」のように高さの低い文字があったときにルビの高さが揃わなかったり、変にルビの位置が低くなったりしてしまうので調節します。

まず、親文字の高さを比較して一番大きかった高さを記録します。 次に、contextに含まれているフォントサイズの情報とその記録を比較し、大きい方を記録します。 最後に、記録していた最大の高さと実際の親文字の高さのギャップ分スペースを入れます。

こうして親文字の高さが揃うので、line-stack-bottomというプリミティブを使って親文字とルビを連結し、さらに連結された親文字達どうしを++プリミティブを使って横に連結させることで完成です。

グループルビの場合

まずは親文字もルビも連結させた状態でibにします。

次に、出来た親文字のibとルビのibの長さを比較します。

親文字の方が長かった場合、ルビを一文字ずつ分解し、長さの差分を均等に分けたスペースを入れながら再度組みなおします。

あとはline-stack-bottomを使ってルビと親文字を上下に結合させるだけです。

親文字の方が短かった場合も、差分を基に均等に分けたスペースを入れて再度組みなおし、結合させます。

熟語ルビの場合

JLReqで説明されている方法はとても複雑なので、JIS X 4051に規定されている簡単な方を採用しています。

まず、ルビと親文字をそれぞれ組み、長さを評価します。

全ての親文字について、ルビよりも長さが長かったときはモノルビで組みなおします。

そうではなく、一文字でも親文字の方がルビよりも短かいものがあった場合は、グループルビで組みなおします。

いかがだったでしょうか?

こう書くと結構簡単に実装できたかのように見えますが、進入との兼ね合いなどを考えて実装すると結構複雑になりました。

今後実装したい機能

今後実装・改良したい機能はこのような感じです。 もし、「実装・改良してみたよ!」という方がいらっしゃいましたら、リポジトリにプルリクエストを送っていただけると嬉しいです。

  • 進入の調節が面倒なので、改良したい。
  • 規定値を変更する機能を提供したい。
  • 両側ルビや下側ルビにも対応したい。

今回、自分の作ったsatysfi-rubyパッケージを紹介してみました。使っていただけると嬉しいです。

また、今後多くのSATySFiパッケージが公開され、便利になっていくことが楽しみです。

SATySFi Conf 2020まとめ

はじめに

SATySFi Conf 2020が7/25に行われました。 参加してくださった皆さん、ありがとうございました。

さて、SATySFi Conf 2020で行われた発表の簡単なまとめを作成しました。

発表しているところを撮影した動画とスライドへのリンクをそれぞれ貼っていますので、ご利用ください。

発表者と発表タイトルは以下の通りとなっています(敬称略)。

  • puripuri2100「カスタマイズ可能なクラスファイルについて」
  • na4zagin3「Satyrographos: SATySFi向けパッケージマネージャー」
  • wasabiz「satysfi-baseについて」
  • gfn「SATySFiのこれからの課題たち」
  • setoazusa「Pandoc使いSATySFiに挑戦する」
  • hikalium「SATySFiのMarkdown機能を使って技術同人誌を書いてる話」
  • zptmtr「SATySFiの描画結果をテストする」
  • monaqa「satysfi-easytable: SATySFiで簡易な表組版

puripuri2100「カスタマイズ可能なクラスファイルについて」

動画: https://youtu.be/g39yZuvVu8w

スライド: https://speakerdeck.com/puripuri2100/kasutamaizuke-neng-nakurasuhuairunituite

SATySFiでレポートを書くために簡単にレイアウトなどを弄ることができるクラスファイルが必要である。

現在あるSATySFiのクラスファイルの内、カスタマイズ可能なものは

  • stdja*
  • exdesign
  • jlreq

の3つで、どれもレコード型を使って指定する方法を取っている。

今回新たに

  • jsonファイルからクラスファイルを自動生成する
  • unit listと破壊的代入を使ってレイアウトを変更する

という2つの方法を提案した。

jsonファイルからクラスファイルを生成するためのソフトウェアを既にRustで実装している(リポジトリ)。

{
  "page-size": "b5",
  "left-space" : "20mm",
  "right-space" : "20mm",
  "main-font" : {
    "size":"12pt",
    "cjk-name":"ipaexm",
    "cjk-ratio":"0.88",
    "cjk-correction":"0.",
    "latin-name":"lmsans",
    "latin-ratio":"1.",
    "latin-correction":"0."
  },
  "header-fun":"empty",
  "footer-fun":"empty",
  "sec-depth":2,
  "sec-name-list" : ["section", "subsection"]
}

のようなjsonファイルから、おおよそ500行程度のクラスファイルができる。

unit listを使う方法では、内部で破壊的代入を行うinline-text -> unitのような関数を用意しておき、それをまとめたリストを評価することで値を書き換え、レイアウトに反映させるという手法を取る。

document [
  Doc.title {title};
  Doc.page-size Page.b5j;
  Doc.left-space 20mm;
  Doc.right-space 20mm
] '<
  +p{text}
>

のようなコードになることを想定している。ただし、これを実現するクラスファイルはまだ作成していない。

今後は

  • formatclsの改良
  • unit listを使ったクラスファイルを作ってみる
  • 各クラスファイルユーザの感想を集めたい
  • どの手法が良いのかわからないので数を撃ちたい

ということをしていく予定である

na4zagin3「Satyrographos: SATySFi向けパッケージマネージャー」

動画: https://youtu.be/0GkovrAHL6c

スライド: https://speakerdeck.com/na4zagin3/satyrographos-satysfixiang-kepatukezimaneziya

動機

(La)TeXを使う時に以下のことに悩まされることがある

  • TeX Liveはfullでインストールするのが基本だけど、重い
  • パッケージやエンジンの更新で壊われることが多々
  • ビルドコマンドが不明
  • 使用パッケージが見つからない
  • 古いパッケージが必要なこともある

このような体験をしないために、SATySFiにパッケージマネージャが欲しい!

パッケージマネージャは

  • パッケージをリモートのリポジトリから取得し、壊れていないかを検査する
  • バージョンや依存性の解決
  • パッケージの選択・更新ができること

ビルドツールの役割を持っていることもある

satyrographos

ライブラリのビルド再現性を実現するためのパッケージマネージャ

リポジトリna4zagin3/satyrographos

SATySFi用にライブラリを準備

  1. OPAMを使った外部リポジトリからライブラリを持ってくる
  2. opamフォルダに入れられたライブラリをSATySFiのライブラリ用のディレクトリに入れる

という動作をする

satyrographos repo

SATySFiのライブラリを登録しているところ

リポジトリna4zagin3/satyrographos-repo

  • パッケージライブラリは"satysfi-*"
  • フォントライブラリは"satysfi-fonts-*"
  • クラスライブラリは"satysfi-class-*"
  • ドキュメントやテストも別ライブラリとして切り出し

という名前の付け方で登録している

スナップショットについて:

  • 相互に上手く動くパッケージやバージョンをスナップショットできる
  • ライブラリの新しいバージョンが出たらスナップショットが更新される

まとめ

  • satyrographosはビルド再現性を実現させるためのパッケージマネージャ
  • 依存性解決等はOPAMにまかせている
  • 文書ビルドやパッケージ一覧サイト生成などはまだ
  • OPAM依存から脱却したい

wasabiz「satysfi-baseについて」

動画: https://youtu.be/1cq-R0al_zY

スライド: https://docs.google.com/presentation/d/1V1cAhOfgnKyCRsy1ZRPXLQUP3O4O6D22d8FuuTmiH7Y/edit#slide=id.p

satysfi-baseのリポジトリnyuichi/satysfi-baseにある

インストールはsatyrographosを使って貰うことが前提

satysfi-baseはSATySFiの標準ライブラリを補完することを目指している

基本はデータ構造とアルゴリズムAPIを拡充するのがメインだが、組版系の機能もある(組版系のは分離させるかも)

標準ライブラリには

  • APIに一貫性が乏しい
  • 提供されている機能が少ない

という問題があるが、プリミティブを使えって新たに定義していたけば解決できる

バイテックトーキョーで同人誌を書く時にSATySFiの標準ライブラリのこれらの問題に直面し、これを解決するものを作っていた

これを切り出して公開したのがsatysfi-base

@require: base/ナントカで読み込む

インラインテキストで関数を使うためのevalコマンドなど、色々と便利である。

綺麗なAPIや統一的なインターフェースの提供を目指している。

型クラスが無いのでequal関数などは手動で名前を揃えたりしている

「Ord用の型」もあるので疑似的な型クラスは使える

正規表現の高速化など取り組む課題はあるので、v2.0.0をそのうち出す

gfn「SATySFiのこれからの課題たち」

動画: https://youtu.be/wCkWr962Ox0

スライド: https://www.slideshare.net/bd_gfngfn/satysfi-237236824

昔の話

型無しのMacrodownという言語を作る(Turing完全ということを示される)。

方針転換してMacrodwonというSATySFiに通じるML系言語に

未踏事業でバックエンドや型システムを開発

修士論文を書くために拡張される

The SATySFibookの執筆や、多段階計算の追加など色々発展する

これからについて(機能以外)

  • ライブラリ整備
  • ライブラリのドキュメントを用意したい
  • 処理系の技術的負債→リファクタリング
  • 英文ドキュメントを用意したい

これからについて(機能面)

  • モジュールシステムが弱い
    • ファンクタや定義の公開できる型の導入
    • 1ファイル1モジュール化の導入(書き方の制約)
    • directの廃止(名前空間が汚染されるので)
  • フォント設定がパッケージ依存なのをなんとかする
  • contextを拡張して、ユーザ側でもエントリを作れるようにする
  • ドキュメント生成機能
    • @docを使ってドキュメント用のコードとライブラリ用のコードを分離する
    • @doc @require: mypackageのように自分自身の読み込みも
  • テキスト出力モード拡充のために数式のADT化
    • 数式を分解できるようにする(普通の数式はこれの糖衣構文とする)
    • let-mathに文脈情報を取得できるようにする(出力情報が多くなってしまうかも)

今後の課題はたくさんある

setoazusa「Pandoc使いSATySFiに挑戦する」

動画: https://youtu.be/LzVAH_Bnh6s

スライド: https://slides.com/hiroyuki_onaka/satysficonfi/

Pandocは44種類のデータフォーマットに対応している優れもの

最初はPandocをLuaTeXに変換して使っていたが、フォントやレイアウトの調整が沼だった。

その後CSS組版に切り替えるも脚注の処理がトリッキー

そこでSATySFiに変換する方法に挑戦

環境はUbuntu 20.04 + WSL2

MasWagさんのpandoc-satysfi-templateを使っている→機能が不足していたりバグが残っていたりしているので、それを修正するPRを投げる予定

hikalium「SATySFiのMarkdown機能を使って技術同人誌を書いてる話」

動画: https://youtu.be/6xD3b-J8aPk

スライド: https://docs.google.com/presentation/d/1DHCipNUz_GnyCus-Gylcfx8o05TnWX366WJon1q-Mqc/edit#slide=id.p

OS Girlsという技術同人誌をSATySFiで書いている

SATySFiで書いていると言っても、SATySFiのマークアップ形式で書いているわけではなく、Markdownで書いてSATySFiに食わせている

SATySFiのMarkdownモードでは、行頭コメント内にドキュメント関数に渡す設定を書いておけば、その値が自動でdocument関数に渡される。 その方法を使って色々と本文用の設定をしている。

困ったこと:

  • 画像サイズの指定ができない→画像を紙面幅固定
  • 改ページや番号無し見出しを作れない→h2やh6と言った他のコマンドに割り当てる(悪用)ことで実現

zptmtr「SATySFiの描画結果をテストする」

動画: https://youtu.be/y9oGt0SUjzQ

スライド: https://docs.google.com/presentation/d/e/2PACX-1vTfSutdo8EblmlpfktQzYtrjZnorcaaY9wk7FgUlWDkImLpjzYHIjrmBmRQ2tfdG3xo4HMG-lOI01fy/pub?start=false&loop=false&delayms=3000&slide=id.p

pdf出力結果のテストをしたい!

→snapshotを撮って比較することでテスト

コードに変更が行われたらその結果をsnapchotとして保存し、期待したdiffが得られるかを確認する動作を自動化する

javascript+jestで実装(とりあえず動くものを使いたい)

jest-pdf-snapshotというものを自作して利用(内部でdiff-pdfを使っている)

実装例は satysfi-basesatysfi-class-yabaitechを参照

monaqa「satysfi-easytable: SATySFiで簡易な表組版

動画: https://youtu.be/AdRBls9eXNI

スライド: https://speakerdeck.com/monaqa/satysfi-easytable-satysfi-dejian-yi-nabiao-zu-ban

デフォルトのSATySFiでは表組は複雑なことができるけど書くのが大変

これを解決するために、シンプルな表を簡単に書くことができるeasytableを開発

リポジトリmonaqa/satysfi-easytable

関数の部分適用を使うことで列幅指定も可能に!

罫線や背景色はオプション引数で簡単に指定できる

おわりに

以上でSATySFi Conf 2020の簡単なまとめを終わらせていただきます。

とても有意義で楽しい会となりました。来年も開催したいですね。

超高機能電卓を作る(1日目)

手を動かして一から作ってみないとわからないことが多いので、Rustで電卓を作ることにしました。 目標は実質プログラミング言語になるくらいまで機能を与えることです。

完成するころにはすべてを忘れていそうなので、何か機能を作ったらメモ代わりに記事を書くはずです。

リポジトリpuripuri2100/ralcu

今日作ったもの

lexerとparser、簡単な計算機構

lexer

取得した&strVec<char>に分解し、パターンマッチでトークンに分解していきます。更新可能なusizeを使うことで現在位置を更新していきます。 特に複雑なことをしていないので手書きですぐにできました。

parser

手書きが面倒だったのでLL(1)パーサを自動生成させました。使ったのはllmakerという自作パーサジェネレータです。

二項演算子は優先順位のある左結合なので、左再帰除去をしたりASTの構造を工夫したりして乗り切りました。 優先順位は

  • (*|/)[opsymb]*
  • (+|-)[opsymb]*
  • ^[opsymb]*
  • (=|<|>)[opsymb]*
  • &[opsymb]*
  • |[opsymb]*

としています。

基本的な値はintとfloatとboolのみで、if文が存在しています。

今のところ衝突する構文は存在していないのでparserも簡単にできました。

計算機構

この電卓のASTは再帰型にしたので、再帰関数を使いながら展開していきます。 IntConst, FloatConst, BoolConstならその値をそのまま返し、IfThenElseならそれぞれの中身をさらに展開し1個目の値がBoolConstのtrueなら2個目の値を返し、falseなら3個目の値を返します。BinApplyなら二項演算子用の計算をする関数に渡してその値を返し、Applyなら普通の関数に適用する関数に渡してその値を返します。

二項演算子用の関数では二項演算子用の文字列でパターンマッチをして計算をさせています。例外処理はしておらず、いまのところ失敗する組み合わせではpanic!を使っています。 普通の関数に適用する関数も同様な仕組みです。

この二つは自動生成できるようにしたいですね。

今のところ整数と小数の四則演算、整数同士の比較、整数と小数間の型変換、sinの計算だけはできます。

次にしたいこと

型検査器を実装したいです。あとは例外処理とかprimitiveの自動生成とか。 部分適用できるようにしたいです。

Rust用のLL(1)トークンパーサジェネレータを作った話

概要

構文と抽象構文木へ変換するコードが書かれたファイルを与えることで、Rust用のLL(1)のトークンパーサを自動生成することができるソフトウェアを作りました。 名前はとりあえず"llmaker"にしています。リポジトリpuripuri2100/llmakerです。

構文等を与えるファイルはRustや、RustでのLR(1)パーサジェネレータであるlarlpopと似ている構文なので、それらのシンタックスハイライトが適用できます。

インストールと使い方

まずRustのライブラリ管理ツールであるCargoをインストールしてください。普通にやればRustのコンパイラ等もついてくるはずです。ついてこなかったら頑張って入れてください。

次にllmakerをインストールします。使うだけであれば

cargo install --git https://github.com/puripuri2100/llmaker

でできるはずです。色々と中身を見たいのであれば、リポジトリを手元にcloneし、そこからインストールしても良いでしょう。

git clone https://github.com/puripuri2100/llmaker.git
cd llmaker

make install

起動はllmaker <input file>だけで行えます。構文等が書かれたファイルを食わせるだけです。拡張子は特に決められていません。 出力先のファイル名は自動で生成されますが、調節したい場合はllmaker <input file> -o <output file>で自由に出力先を決められます。

出力されたファイルではparse (tokens: Vec<T>) -> Result<U, ParseError>という関数と、エラー用の列挙型であるParseErrorを提供します。

入力ファイルの書き方。

llmakerでは、Rustの型やコードを解析することはせず、文字列での入力を要求します。気を付けてください。

入力ファイルは大きく分けてヘッダ・設定部分・構文部分の3つに分かれます。

ヘッダでは依存するクレートの読み込みや、必要な関数などを記述することができます。 Rustのコードをそのまま文字列にしたものを並べるだけです。エスケープ文字は\"のみです(\nなどはサポートしていません)。書いた文字列はそのままファイルの先頭部分に出力されます。 例えば、

"use super::lexer;"

"fn hoge () {
  println!(\"hoge\");
}"

のように書けば、出力ファイルには

use super::lexer;

fn hoge () {
  println!("hoge");
}

と出るわけです。

設定部分では、externブロックの中で入力するトークンに関する情報を与えます。

構文は

extern {
  enum "トークンの型" {
    トークン名 => "トークンの具体的な中身",
    トークン名 => "トークンの具体的な中身",
    ...
    トークン名 => "トークンの具体的な中身",
  }
}

となっています。トークン名はアルファベット大文字から始まり、アルファベット・数字・アンダーバーが続く必要があります。

具体的にはこのような感じです。トークンの中身を表す文字列は、パターンマッチで判別できるものにしてください(内部でパターンマッチを使っているので)。ですので、ワイルドカード表記が使えます。

extern {
  enum "(lexer::TokenKind, lexer::Range)" {
    Tok_EOF          => "(lexer::TokenKind::EOF            , _)",
    Tok_VAR          => "(lexer::TokenKind::VAR         (_), _)",
    Tok_CONSTRUCTOR  => "(lexer::TokenKind::CONSTRUCTOR (_), _)",
    Tok_LCURLYBRACES => "(lexer::TokenKind::LCURLYBRACES   , _)",
    Tok_RCURLYBRACES => "(lexer::TokenKind::RCURLYBRACES   , _)",
    Tok_EQ           => "(lexer::TokenKind::EQ             , _)",
    Tok_COMMA        => "(lexer::TokenKind::COMMA          , _)",
    Tok_SEMICOLON    => "(lexer::TokenKind::SEMICOLON      , _)",
    Tok_COLON        => "(lexer::TokenKind::COLON          , _)",
    Tok_ARROW        => "(lexer::TokenKind::ARROW          , _)",
  }
}

構文部分ではBNF表記のようなものを連ねていくだけです。一番トップとなる規則にはpubと付けてください(実際には公開されませんが……)。pubとついている規則が一つもないとエラーが出ます。

構文は

規則名 "出力型" = {
  規則 => {
    "コード"
  },
};

で、具体例は

pub main: "types::Term" = {
  <head: head> <_gr: gr> <setting: setting> <body: body> <_v: Tok_EOF> => {
    "let mut v = head;
    v.reverse();
    (v, setting, body)"
  },
};

head: "types::Head" = {
  <tok: Tok_STR> <tail: head_tail> => {
    "let mut tail_v = tail;
    let (stok, rng) = tok;
    let s = lexer::get_string(stok).unwrap();
    tail_v.push((rng, s));
    tail_v"
  },
  => {"Vec::new()"},
};

のような感じです(main規則がトップの規則なのでpubを付けています。これで、最初にmain規則が呼ばれることになります。)。規則は"<変数名: 規則名・トークン名>"で作られます。規則名は小文字スタートで、トークン名は大文字スタートなのでわかりやすいと思います。規則名やトークン名につけた変数名はコードの中で使えます。 規則を与えないことで<null>()に対応するものが与えられます。例中のhead規則での=> {"Vec::new()"},というやつですね。 <null>規則を含む規則は規則列の先頭に使えないという制約があります。

ヘッダ・設定部分・構文部分は

<ヘッダ>

grammar;

<設定部分>

<構文部分>

という風に並べて書きます。順番が変わると構文解析に失敗しますし、grammar;が抜けても失敗します。気を付けてください。

また、

  • grammar
  • extern
  • enum
  • pub

はそれぞれ予約語です。

実際に使っているファイルを知りたい人はdemoファイルを見てみてください。

使っている技術

iteratorの中でも一つ先読みが可能なPeekableというものを内部で使っています。 それぞれの規則の中の先頭をすべて展開してトークンを取得し、1つ先読みのパターンマッチで振り分けています。

トークンパーサなので、構文規則が適切に組まれているときに、規則をすべて展開すれば必ずトークンに行き当たるという性質を利用しています。

一つの規則につき一つの関数を作り、トークン消費にもそれぞれ関数を割り当てているので、機械的に相互に呼び出すだけで構文解析が完了します。

最後に

構文解析の勉強をしてすぐに作った、人生で初めて作るパーサジェネレータなのでバグや勘違いがあるかもしれません。指摘してくださるとうれしいです。

また、Peekableではなくて位置を数で保持することでk個の先読みを可能にし、トークンが重なった時の衝突を回避できるLL(k)のパーサを作ることができるように改造したいとも思っています。

SATySFiパッケージでユニットテストを行う方法

概要

assert-eqパッケージ・debug-show-valueパッケージ・GitHub Actionsを組み合わせることでSATySFiにもユニットテストを導入することができます。

参考になる記事へのリンク

assert-eqパッケージについては「SATySFiでテストをするためのパッケージ」を、debug-show-valueパッケージについては「SATySFiでlistやoptionなどを文字列化して表示する方法」を、GitHub ActionsでSATySFiを使う方法は「GitHub Actions で SATySFi の文書やパッケージの CI」を、それぞれ参照してください。

testファイルを作る

例えば、以下のようなパッケージでは、

%mypackage.satyh

@require: assert-eq/assert-eq
@require: debug-show-value
@require: list

module: MyPackage :sig

  val my-list-map : ('a -> 'b) -> 'a list -> 'b list
  val +test : [] block-cmd

end = struct

open AssertEq
open DebugShowValue

let my-list-map = List.map

let ctx +test =
  let () =
    assert-eq
      ?:(`check my-list-map`)
      (show-list show-int)
      (my-list-map ((+) 1) [1;2;3])
      ([2;3;4])
  in
  block-nil

end

などのようにしてテスト用コマンドを作り、+test : [] block-cmdとして外部に出せば、簡単にテストが行えます。 内部のみで使う関数も、ユニットテストを行うことができます。

testフォルダ内にtest.satyを作成し、

@require: stdjareport
@import: ../mypackage

document (|
  title = {};
  author = {};
|) '<
  +MyPackage.test;
>

と、中に書き込めば、このファイルをコンパイルするだけでテストに成功しているか失敗しているかがわかるようになります。

CIを回す

satysfi-dockerをGitHub Actionsで使うことで、CIを回すことができます。

リポジトリ内に.github/workflowsというフォルダを作り、その中にci.ymlというファイルを作ります。

その中に

name: CI

on: [push,pull_request] #pushとPRの際にCIが回るようにする


jobs: #job定義
  test: #jobの名前
    runs-on: ubuntu-latest #ubuntuでの実行
    strategy:
      matrix:
        version: [0.0.4] #SATySFiのバージョンは0.0.4でする。0.0.4-dev2020.02.22などのようにすれば開発版も使える。
    container:
      image: amutake/satysfi:${{ matrix.version }}
    steps:
      - uses: actions/checkout@v1
      # opamとsatyrographosを使って依存パッケージをインストールする
      - run: |
          export HOME=/root
          eval $(opam env)
          opam update
          opam install satysfi-assert-eq
          opam install satysfi-debug-show-value
          satyrographos install
          satysfi test.saty
        working-directory: ./test
        # ↑testフォルダでの実行

という風に書き込んでpushすれば、次からpushやPRをするたびにCIが回るようになります。

SATySFiのバージョンを変えたいときや、依存パッケージがさらにある場合には自分なりに改造してみてください。

おわりに

SATySFiにもテストをきちんと行う文化を根付かせたいですね(まずは自分がやらないといけませんが……)

SATySFiでテストをするためのパッケージ

概要

自分が作成したassert-eqというパッケージを使用することで、関数のテストが簡単に行えます。

インストール方法

使用しているプリミティブの都合で、SATySFi v0.0.4以降でないと行けません。

Satyrographosを使う場合

$ opam update
$ opam install satysfi-assert-eq
$ satyrographos install

でインストールができます。

手動で行う場合

まず

$ git clone https://github.com/puripuri2100/SATySFi-assert-eq.git

をすることで手元にリポジトリのコピーを作成します。

次に、LIBROOT/local/packages/以下にassert-eqというフォルダを作ります。

最後に、フォルダ内のassert-eq.satygLIBROOT/local/packages/assert-eq/にコピーします。

これで完了です。

提供する関数

モジュール名はAssertEqです。

  • assert-eq : string?-> ('a -> string) -> 'a -> 'a -> unit
  • \assert-eq : [string?; ('a -> string);'a; 'a] inline-cmd
  • +assert-eq : [string?; ('a -> string);'a; 'a] block-cmd

組版時には\assert-eqコマンドはinline-nilと同じ挙動をし、+assert-eqblock-nilと同じ挙動をします。

assert-eq <function> <left> <right><left><right>が一致すると標準出力でログが、一致しないとエラー報告が出ます。

<function><left><right>での値を文字列に変換するための関数です。

例えば

assert-eq arabic (1+2) 3

ではログが、

assert-eq arabic (1+2) 4

ではエラーが出ます。

ログを見るときにわかりやすいようにラベルを付けることができます。

assert-eq ?:(`label`) arabic (1+2) 3のようにして使えます(\assert-eq+assert-eqでも同様にしてに使えます)。

具体的な使い方

先日記事を書いたdebug-show-valueと組み合わせることで簡単にテストできることを期待しています。

例えば、num-conversionでは

@require: stdjabook
@import: ../num-conversion

@require: assert-eq/assert-eq
@require: debug-show-value/debug-show-value


open NumConversion
open DebugShowValue

in

document (|
  title = {変換テスト};
  author = {\@puripuri2100};
  show-title = false;
  show-toc = false;
|) '<
  +assert-eq(show-opt show-string)(to-zenkaku 1230)(Some(`1230`));
  +assert-eq(show-opt show-int)(from-zenkaku `1230`)(Some(1230));
  +assert-eq(show-opt show-string)(to-zenkaku (-100))(None);
  +assert-eq(show-opt show-int)(from-zenkaku `1230`)(None);


  +assert-eq(show-opt show-string)(to-roman-upper 0)   (None);
  +assert-eq(show-opt show-string)(to-roman-upper 1)   (Some(`I`));
  +assert-eq(show-opt show-string)(to-roman-upper 2)   (Some(`II`));
  +assert-eq(show-opt show-string)(to-roman-upper 3)   (Some(`III`));
  +assert-eq(show-opt show-string)(to-roman-upper 4)   (Some(`IV`));
  +assert-eq(show-opt show-string)(to-roman-upper 5)   (Some(`V`));
  +assert-eq(show-opt show-string)(to-roman-upper 6)   (Some(`VI`));
  +assert-eq(show-opt show-string)(to-roman-upper 7)   (Some(`VII`));
  +assert-eq(show-opt show-string)(to-roman-upper 8)   (Some(`VIII`));
  +assert-eq(show-opt show-string)(to-roman-upper 9)   (Some(`IX`));
  +assert-eq(show-opt show-string)(to-roman-upper 10)  (Some(`X`));
  +assert-eq(show-opt show-string)(to-roman-upper 11)  (Some(`XI`));
  +assert-eq(show-opt show-string)(to-roman-upper 14)  (Some(`XIV`));
  +assert-eq(show-opt show-string)(to-roman-upper 100) (Some(`C`));
  +assert-eq(show-opt show-string)(to-roman-upper 4999)(Some(`MMMMCMXCIX`));
  +assert-eq(show-opt show-string)(to-roman-upper 5000)(None);

  +assert-eq(show-opt show-string)(to-roman-lower 0)   (None);
  +assert-eq(show-opt show-string)(to-roman-lower 1)   (Some(`i`));
  +assert-eq(show-opt show-string)(to-roman-lower 2)   (Some(`ii`));
  +assert-eq(show-opt show-string)(to-roman-lower 3)   (Some(`iii`));
  +assert-eq(show-opt show-string)(to-roman-lower 4)   (Some(`iv`));
  +assert-eq(show-opt show-string)(to-roman-lower 5)   (Some(`v`));
  +assert-eq(show-opt show-string)(to-roman-lower 6)   (Some(`vi`));
  +assert-eq(show-opt show-string)(to-roman-lower 7)   (Some(`vii`));
  +assert-eq(show-opt show-string)(to-roman-lower 8)   (Some(`viii`));
  +assert-eq(show-opt show-string)(to-roman-lower 9)   (Some(`ix`));
  +assert-eq(show-opt show-string)(to-roman-lower 10)  (Some(`x`));
  +assert-eq(show-opt show-string)(to-roman-lower 11)  (Some(`xi`));
  +assert-eq(show-opt show-string)(to-roman-lower 14)  (Some(`xiv`));
  +assert-eq(show-opt show-string)(to-roman-lower 100) (Some(`c`));
  +assert-eq(show-opt show-string)(to-roman-lower 4999)(Some(`mmmmcmxcix`));
  +assert-eq(show-opt show-string)(to-roman-lower 5000)(None);


  +assert-eq(show-opt show-int)(from-roman-upper `I`)        (Some(1));
  +assert-eq(show-opt show-int)(from-roman-upper `II`)       (Some(2));
  +assert-eq(show-opt show-int)(from-roman-upper `III`)      (Some(3));
  +assert-eq(show-opt show-int)(from-roman-upper `IV`)       (Some(4));
  +assert-eq(show-opt show-int)(from-roman-upper `IIII`)     (Some(4));
  +assert-eq(show-opt show-int)(from-roman-upper `V`)        (Some(5));
  +assert-eq(show-opt show-int)(from-roman-upper `VI`)       (Some(6));
  +assert-eq(show-opt show-int)(from-roman-upper `VII`)      (Some(7));
  +assert-eq(show-opt show-int)(from-roman-upper `VIII`)     (Some(8));
  +assert-eq(show-opt show-int)(from-roman-upper `IX`)       (Some(9));
  +assert-eq(show-opt show-int)(from-roman-upper `VIIII`)    (Some(9));
  +assert-eq(show-opt show-int)(from-roman-upper `X`)        (Some(10));
  +assert-eq(show-opt show-int)(from-roman-upper `XI`)       (Some(11));
  +assert-eq(show-opt show-int)(from-roman-upper `XIV`)      (Some(14));
  +assert-eq(show-opt show-int)(from-roman-upper `C`)        (Some(100));
  +assert-eq(show-opt show-int)(from-roman-upper `MMMMCMXCIX`)(Some(4999));

  +assert-eq(show-opt show-int)(from-roman-lower `i`)        (Some(1));
  +assert-eq(show-opt show-int)(from-roman-lower `ii`)       (Some(2));
  +assert-eq(show-opt show-int)(from-roman-lower `iii`)      (Some(3));
  +assert-eq(show-opt show-int)(from-roman-lower `iv`)       (Some(4));
  +assert-eq(show-opt show-int)(from-roman-lower `iiii`)     (Some(4));
  +assert-eq(show-opt show-int)(from-roman-lower `v`)        (Some(5));
  +assert-eq(show-opt show-int)(from-roman-lower `vi`)       (Some(6));
  +assert-eq(show-opt show-int)(from-roman-lower `vii`)      (Some(7));
  +assert-eq(show-opt show-int)(from-roman-lower `viii`)     (Some(8));
  +assert-eq(show-opt show-int)(from-roman-lower `ix`)       (Some(9));
  +assert-eq(show-opt show-int)(from-roman-lower `viiii`)    (Some(9));
  +assert-eq(show-opt show-int)(from-roman-lower `x`)        (Some(10));
  +assert-eq(show-opt show-int)(from-roman-lower `xi`)       (Some(11));
  +assert-eq(show-opt show-int)(from-roman-lower `xiv`)      (Some(14));
  +assert-eq(show-opt show-int)(from-roman-lower `c`)        (Some(100));
  +assert-eq(show-opt show-int)(from-roman-lower `mmmmcmxcix`)(Some(4999));


  +assert-eq(show-opt show-string)(to-kansujix 100)(Some(`一〇〇`));
  +assert-eq(show-opt show-int)(from-kansujix `一〇〇`)(Some(100));
  +assert-eq(show-opt show-string)(to-kansujix (-100))(None);
  +assert-eq(show-opt show-int)(from-kansujix `1〇〇`)(None);
  +assert-eq(show-opt show-int)(from-kansujix `百`)(None);
>

のようにしてテストファイルを書き、コンパイルをしてエラーが出ないかを確かめています。

おわりに

これでようやくテストを回せるようになりました。

使いこなしていきたいものです。