マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

実行中のTypstのバージョンを取得したい話

先週、Typstの新しいバージョンである0.11.0版[2024-03-15]がリリースされた。この版ではintrospection周りの機能に大きな仕様変更が行われている1

このレベルの仕様変更は久しぶり2であるが、ただしChangeLogの情報を見るとわかるように、Typstでは各回の改版において何らかの細かい非互換的変更(breaking change)が行われることが多い。Typstはまだ新しいベータ版のソフトウェアであるため、今のところは「ソフトウェアも仕様の知識も常に最新のものに更新していく」という雰囲気が強く感じられる。しかしTypstの普及がもっと進めば、パッケージ開発者の側で「今動作しているTypstのバージョンを取得してそれによってパッケージの動作を変更したい」という要望も生じてくることだろう。

そういうわけで、本記事では「実行中のTypstのバージョンを取得する方法」について解説する。

前提知識

  • Typstのプログラミングのキホン的な知識。

バージョン判定のフツーの方法

マニフェストで最小要求バージョンを指定する

プログラムコードをパッケージ3として扱う前提で、かつ「指定のバージョンに満たない場合はエラー終了する」という動作で十分である場合は、Typstのパッケージシステムの機能が使える。

パッケージのマニフェストtypst.toml)にはcompilerという項目があり、これでコンパイラ(Typst)の最小要求バージョン」を指定できる。例えば、以下のマニフェストは、当該のパッケージ(mypackage)がTypstの0.11.0版以降を要求することを宣言している。

[package]
name = "mypackage"
version = "1.0.0"
entrypoint = "lib.typ"
compiler = "0.11.0"

従って、mypackageを例えば0.10.0版のTypstで使おうとすると、パッケージ読込の時点でエラーが発生する。

error: package requires typst 0.11.0 or newer (current version is 0.10.0)
  ┌─ \\?\C:\tmp\main.typ:1:8
  │
1 │ #import "@local/mypackage:1.0.0"
  │         ^^^^^^^^^^^^^^^^^^^^^^^^

パッケージを前提とするなら、この方法が簡単であり、かつバージョン指定が“明示的”であるという点でも好ましいだろう。

sys.versionを利用する

パッケージシステムの機能が使える事例に該当しない場合はプログラム中でバージョンを取得するコードを自分で書く必要がある。例えば「バージョンが0.11.0以降か否かによって実行されるコードを変えたい」という場合を考える。つまり、以下のような使い方のできる関数v11-or-laterを実装したい。

if v11-or-later() {
  // 新しいやつ🙂(0.11.0版以降)
} else {
  // 古いやつ🙁(0.11.0版より前)
}

実は、Typstの0.9.0版[2023-10-31]以降にはまさに「実行中のコンパイラのバージョン」を表す定数sys.versionが用意されている。従って、0.9.0版以降を前提にしてよいなら話は簡単になる。sys.versionはversion型の値であり、version型の値は(フツーのsemver的な意味で)大小比較が可能なので、所望のv11-or-laterは以下のように実装できる。

// Typstのバージョンが0.11.0版以降であるか.
let v11-or-later() = {
  sys.version >= version(0, 11, 0)
}

version(0, 11, 0)はversionのコンストラクタ呼出で「引数で指定した整数値をもつversion値」を生成する。

バージョン判定のアレな方法

sys.versionを使った方法は簡単であるが、当然ながら0.9.0版以降であることが前提になる。それより古いTypstではsys.versionが定義されていない(そもそもsysというモジュールが用意されていない)ので、上記のv11-or-laterを実行するとsysを参照しようとした時点でエラーになってしまう。

error: unknown variable: sys
  ┌─ \\?\C:\tmp\main.typ:4:2
  │
4 │   sys.version >= version(0, 11, 0)
  │   ^^^

もちろん、実際にバージョン取得の処理が必要になる頃には0.9.0版は既に“大昔のバージョン”で考慮4する必要がなくなっていそうから、実用上はほぼこれで問題がない可能性が高い。

それでも、ここでは敢えて「0.9.0版より前のバージョンでも安全に(エラーになることなく)実行できるバージョン取得」というアレな機能の実装を試みることにする。

※ただし先述の事情があるので、「0.9.0版より前の個別のバージョンの判別」は不要で「0.9.0版より前のものは単にそうであると判別できること」のみを要件とする。

アレしてみた

……というわけで、作ってみた

let v11-or-later() = {
  if ("\u{2212}" in str(-1)
      or "B" not in str(numbering("\u{3042}A", 2, 1))) {
    // 上の2条件の何れかが成立なら0.9.0版以降なのでsys.versionが使用可能
    sys.version >= version(0, 11, 0)
  } else { // 0.9.0版より前なので偽を返す
    false
  }
}

もちろん上記のコードであればもっと簡単に以下のようにも書ける。

let v11-or-later-x() = {
  (("\u{2212}" in str(-1)
      or "B" not in str(numbering("\u{3042}A", 2, 1)))
      and sys.version >= version(0, 11, 0))
}

それはともかく重要なのは2・3行目に書かれている条件でこれは「コンパイラが0.9.0版以降であるか」を判定している。この2条件の何れかが成立していればほぼ間違いなく0.9.0版以降と判断してよいので、その条件下ではsys.versionを自由に使って「所望のバージョン判定」を実装できる5わけである。

以下では「この2つの条件がどこから出てきたのか」について解説する。基本的には「改版による仕様変更によって動作が変わる点を補足する」という方針に従っている。

第1条件

"\u{2212}" in str(-1)

この式は0.9.0版以降少なくとも現在最新の0.11.0版まではtrue、0.9.0版より前ではfalseになる。ChangeLog0.9.0版の節に以下の項目がある。

The U+2212 MINUS SIGN is now used when displaying a numeric value, in the repr of any numeric value and to replace a normal hyphen in text mode when before a digit. This improves, in particular, how negative integer values are displayed in math mode.

str(-1)等の「負数を文字列に変換した結果」は0.9.0版より前では(他の多くのプログラミング言語と同様に)“-1”(U+002Dの後に“1”)であったが、0.9.0版以降では“−1”(U+2212の後に“1”)となる。恐らく数式で$-1$と書いた結果と合わせるためであろう。このため「str(-1)の結果にU+2212が含まれるか」を調べることで0.9.0版以降か否かが判別できる。

このstrの仕様変更はちょうど0.9.0版で起こっているため、もしこの仕様が今後も維持されるのであればこれだけで目的の「0.9.0版以降か否かの判定」が完遂できるはずである。しかし自分の直感としてはこの仕様が将来変更される可能性6を捨てきれない。そこで“保険”をかけるために入れているのが第2条件である。

第2条件

"B" not in str(numbering("\u{3042}A", 2, 1))

この式は0.11.0版ではtrue(そして将来の版でもほぼ確実にtrue)、0.11.0版より前ではfalseになる。ChangeLog0.11.0版の節に以下の項目がある。

Added support for contemporary Japanese numbering method

0.11.0版ではnumbering関数の書式文字列のカウンタ記号(counting symbol)として“あ”(ひらがなの五十音順)が追加された。つまり0.11.0版以降では以下のようになる。

numbering("あ)", 5) //==>"お)"

※参考記事:

従って、numbering("\u{3042}A", 2, 1)という式の値は以下のようになる(なおU+3042は“あ”である)。

  • 0.11.0版以降では"あA"は2つのカウンタ記号からなる書式と解釈されるので、2に“あ”、1に“A”が適用されて結果は"いA"となる。
  • 0.11.0版より前では“あ”はカウンタ記号ではなく"あA"はカウンタ記号“A”に接頭辞が付いた書式と解釈されるので、2と1の両方に“A”が適用されて結果は"あBあA"となる7

従って「結果に“B”が含まれない」こと8により0.11.0版以降であることを判定している。numbering関数は文書テンプレート作成者が常用する機能であるため、将来に「第2条件の式が再びfalseになる」ような仕様変更が入る可能性は極めて小さいと考えられる。従ってほぼ確実にこの式は「0.11.0版以降であるか否か」の判定に使えることになる。

合わせると

  • 第1条件は0.9.0~0.11.0版でtrueになることが判っている。
  • 第2条件は0.11.0版以降でtrueになることがほぼ確実である。
  • 一方で、0.9.0版より前では第1条件も第2条件もfalseになることが判っている。

以上より、“第1条件 or 第2条件”とすることで「0.9.0版以降か否か」、すなわち「sys.versionを利用できるか否か」を判別できることになる。

バージョン判定のアレアレな方法

同様の手法、すなわち「改版による仕様変更により動作が変わる点を補足する」という方法を活用することで「Typstの(正式リリースの)全てのバージョンを判定する」ようなモジュールを作ってみた。

  • [Typst: To get the version of Typst in use](Gist/zr-tex8r)

このtcversionモジュールは以下の値を提供する。

  • version: 実行中のTypstのバージョンを表す整数の配列9。例えば、0.11.0版であれば(0, 11, 0)となる。

※もちろん0.9.0版以降である場合はsys.versionを見ているので将来のバージョンも正しく判定できる。

モジュールの使用例を示す。

#import "tcversion.typ"
This is Typst version
#tcversion.version.map(str).join(".");.

例えばこの文書を0.6.0版のTypstでコンパイルすると以下の出力が得られる。

出力結果

まとめ

というわけで、皆さんは大昔のTypstのことはサッパリ忘れてフツーに新しいTypstを使っていきましょう!💁


  1. ただし、互換性のために従来の仕様も残している(一部は非推奨の扱い)ので、これ自体は非互換的な変更ではない。
  2. 過去にあった同じレベルの変更というと、例えば0.8.0版[2023-09-13]の「type型の導入」が挙げられる。
  3. 公式レポジトリに登録するパッケージとローカルにインストールするパッケージの両方を含む。
  4. もし考慮するにしても「そんな古いバージョンではエラー終了するのが妥当で、問題は単にエラーメッセージが的確でないくらいである」となる可能性が高いだろう。
  5. Typstは“動的な言語”なので、たとえ非存在のsys.versionを参照するコードがあったとしても、それが実際に実行されない限りエラーにはならない。
  6. 少し仕様が変わっても対応できる可能性を増やすため==での完全一致判定でなくinでの部分一致判定を使っている。
  7. numberingの書式文字列の仕様はかなりヤヤコシイがこの場合は接頭辞も反復される。
  8. 第1条件のときと同様に完全一致でなく部分一致で判定している。2に“A”が適用されて“B”が発生するか否かは「“あ”がカウンタ記号か否か」によって完全に決まると考えられるからである。
  9. 「version型」は0.9.0版で導入されたものでそれより前には存在しないので代わりに配列(array)を使っている。