2 ポイント 投稿者 GN⁺ 2024-11-03 | 1件のコメント | WhatsAppで共有
  • llamafile 向けの新しい 構文ハイライター を作るために 42 言語を扱ったところ、単純なレキシングでさえ言語ごとの例外や古い文法がびっしり潜んでいることが明らかになった
  • 実装では C++ と GNU gperf を使ってキーワード検索を高速化し、文字列・コメント・キーワード中心の処理は for ループと switch ベースの有限状態機械で解決している
  • C の trigraph、JavaScript の u2028u2029 行終端子、Shell・Perl・Ruby の heredoc、Kotlin・Scala・TypeScript・Swift の文字列補間は レキシングだけで扱うのが難しい事例 として何度も登場する
  • コード行数ベースでは FORTH は 125 行、Ruby は 1042 行まで差が開き、Ruby は << 演算子・heredoc・backquote が衝突するため パースなしのハイライト が特に難しい
  • llamafile の新しいハイライターは Windows 10 上で Meta LLaMA 3.2 3B Instruct により実演され、macOS・Linux・FreeBSD・NetBSD でも動作可能で、構文ハイライトのない ollama と差別化されている

llamafile ハイライターを作ったきっかけ

  • llamafile 向けの新しい 構文ハイライター を作るため、1 か月かけて 42 のプログラミング言語を学習した
  • 対応対象は Ada, Assembly, BASIC, C, C#, C++, COBOL, CSS, D, FORTH, FORTRAN, Go, Haskell, HTML, Java, JavaScript, Julia, JSON, Kotlin, ld, LISP, Lua, m4, Make, Markdown, MATLAB, Pascal, Perl, PHP, Python, R, Ruby, Rust, Scala, Shell, SQL, Swift, Tcl, TeX, TXT, TypeScript, Zig である
  • この一覧は TIOBE Index の大半を含むが、Scratch はテキストではなくブロックを使うため、ハイライト対象から外れている

実装方法: gperf と有限状態機械

  • 基本的な構文ハイライターで最大のボトルネックは、トークンが キーワードかどうかを判定 する際に繰り返し文字列比較が増える点にある
  • C++ と GNU gperf を用いて 完全ハッシュテーブル を生成した
    • 例の gperf 入力では、truefalsenull のような Java 定数をキーワードとして定義している
    • gperf が生成した C ファイルは、衝突のない検索のために単一文字だけを考慮するハッシュ関数を作れる
  • C のハイライトは、約 4,000 個のキーワードを定義しても gperf のおかげで 35 MB/s の速度で処理できる
  • 残りの処理の大半は 有限状態機械 で十分である
    • flexbisonragel なしでも、for ループと switch だけで基本的なハイライターを作れる
    • 文字列、コメント、キーワードに集中すれば、おおむねレキシングのレベルで処理できる
    • C の関数名のような要素までハイライトするには、実際のパースが必要になることもある
  • highlight_ada.cpp が実装例として使われている

llamafile のデモと利用環境

  • 新しいハイライターとチャットボットインターフェースにより llamafile の使い勝手が向上し、ollama にはない 構文ハイライト が差別化要素になっている
  • デモは Windows 10 上で Meta LLaMA 3.2 3B Instruct モデルを使って実行された
  • この llamafile は macOS、Linux、FreeBSD、NetBSD でも実行可能である
  • gemma 27b it のようなオープンウェイトモデルの品質が向上するにつれ、Claude を使う動機は薄れている

C: 単純な言語という印象とは異なるレキシング例外

  • C は単純だという印象に反して、字句要素はかなり独特である
  • trigraph#, [, \\, ^, {, |, }, ~ のような文字を ??=, ??(, ??/, ??), ??', ??<, ??!, ??>, ??- で置き換えられる
    • C23 標準で削除されたが、レガシーソフトウェアのためにコンパイラは今後も対応し続けると見られる
    • 優れた構文ハイライターなら、この文法も処理すべきである
  • C の universal character は、int \\uFEB2 = 1; のように ASCII ソースコード内で Unicode 識別子を表現できる
    • GCC は、標準委員会が許可した特定の Unicode plane でなければエラーを出す
  • C の単一行コメントは、行末バックスラッシュを使うと複数行にまたがることがある
    • Perl、Ruby、Shell のようにソースコード中でバックスラッシュエスケープを許す言語でも、この C 風の動作はサポートしていない
    • Tcl と GNU Make はこの動作をサポートしていると見られる
    • Emacs や Pygments はこれを誤って処理する場合があり、Vim はバックスラッシュ処理を常に正しく行っているようだ
  • C には null preprocessor directive もある
    • 初期の v6 ソースコードの .c ファイルは # だけの行で始まることが多い
    • 今でも有効なコードであり、cc -C -E でコメント保持を要求しても、プリプロセッサに特定のコメントを削除させる用途に使える

コメント文法: Haskell と D

  • C では複数行コメントの中にさらに複数行コメントをネストできない
  • Haskell は {- ... {- ... -} ... -} という形で ネストコメント をサポートする
  • D は C の //, /* ... */ コメントをそのまま採用し、別途 /+ ... +/ という再帰コメント文法を導入している
  • D の字句文法ドキュメントは形式的かつ詳細で、実装に必要な情報を十分に提供している
    • D lexical syntax 文書 では hex 文字列や heredoc 文字列のような細部も扱っている
    • D には \"...\"、backtick 文字列、r\"...\"q\"...\"x\"...\" など多様な文字列形式がある

Tcl と JavaScript: クォートと見えない行終端子

  • Tcl では識別子の中にクォートを含められる
    • puts a\"ba\"b を出力する
    • 変数名にもクォートを入れられるが、参照時には $a\"b ではなく ${a\"b} と書かなければならない
  • JavaScript は正規表現のための組み込み字句文法を持っている
    • /[/]/g のように文字集合の角括弧内にある / はエスケープ不要である
    • 閉じスラッシュだけを単純に走査する方法は、minified コードでは誤ることがある
  • ECMAScript は u2028 LINE SEPARATOR と u2029 PARAGRAPH SEPARATOR を 行終端子 と定義している
    • これらの文字は実質的に \\n のように振る舞う
    • Trojan Source 文字なので、Emacs ではそれぞれ と表示する設定にしている
    • 多くのソフトウェアはこの文字を認識できず、疑問符としてレンダリングすることがある
    • D を除くと、こうした扱いをする他言語は知らないという
  • この特性を利用して、SectorLISP では C と JavaScript の polyglot を作れた
    • lisp.js はブラウザで実行でき、GCC でコンパイルしてローカルでも実行できる
    • llamafile はこのようなコードを正しくハイライトできるが、他のハイライターでは同じ処理を見つけられなかったという

Shell と heredoc の境界事例

  • Shell の heredoc は cat <<EOF ... EOF の形で複数行文字列を書く
  • quoted heredoc 文法 cat <<'END' は変数置換を無効にする
  • heredoc マーカーを空文字列にすると、次の空行で heredoc が終わる
    • 例のプログラムは helloworld を 2 行で出力する
  • Shell、Ruby、Perl のように heredoc をサポートする言語では、同じ行に複数の heredoc を置ける
  • Shell は Tcl と同様、# が常にコメント開始とは限らない
    • ${x#hi-} では # は接頭辞の削除に使われ、例では there を出力する

文字列補間: 状態スタックが必要な言語たち

  • Kotlin の文字列は \" で始まっていても、文字列補間のために { で別の状態に入ることがある
  • TypeScript、Swift、Kotlin、Scala は、文字列の中に実際のコードを入れる方向で文字列補間を強くサポートしている
  • Kotlin、Scala、TypeScript をハイライトするには、中括弧を数えて パーサー状態スタック を維持する必要がある
    • TypeScript は比較的単純で、有限状態機械にいくつか状態を追加するだけで済む
    • Kotlin と Scala は double quote と triple quote の両方で補間をサポートするため、文字列レキシングだけで約 13 個の独立状態が必要だった
    • Swift は \"\\(var)\" 補間構文と triple quote をサポートするが、実装には 10 個の状態が必要だった

Swift, C#, FORTH: 文字列を囲む方法の違い

  • Swift は \"...\"\"\"\"...\"\"\"/regex/ 文字列を任意個の # で囲む文法をサポートしている
    • 両側の # の数は一致していなければならない
    • この方法は文字列内にクォートや正規表現の区切りを入れる問題を解決する
  • C# は Python の triple quote 複数行文字列に似ているが、開始と終了により多くのクォート数を対応させて使える
    • 左側に置いたクォート数が右側の終了条件になる
    • 任意個のクォートを許すことで、古典的な Python triple quote より妥当性ルールが減り、有限状態機械でより単純にデコードできるとみなしている
  • FORTH は空白境界ですべてをトークン化する
    • 文字列開始構文 c\" も 1 つのトークンである
    • c\" hello world\" は他言語の \"hello world\" と同じ意味である

FORTRAN と COBOL: 固定カラム規則

  • llamafile は、FORTRAN と COBOL のプログラマが引退した後でも銀行システム保守に役立つユースケースとして提示されている
  • エアギャップ環境で制御可能な AI として Gemma 27b に COBOL や FORTRAN のコードを依頼できるという
  • FORTRAN の 固定カラム規則 は次のとおり
    • 1 列目に *cC を置くとその行はコメントになる
    • 6 列目に空白以外の文字を置くと、80 文字を超える行を継続できる
    • 1〜5 列目に数字を置くとラベルになる
  • COBOL の規則は次のとおり
    • 7 列目に * を置くとコメントになる
    • 7 列目に - を置くと、80 文字を超える行を継続できる
    • 1〜6 列目には行番号が入る

Zig と Lua: 複数行文字列への別解

  • Zig は 2 本のバックスラッシュで始まる 複数行文字列 文法を持つ
    • この文法により、Python の triple quote 文字列で textwrap.dedent() を呼ぶ必要がなくなる
    • 欠点としてセミコロンが見栄えよくないとしている
    • Go、Scala、Python のようにセミコロン不要の言語が検討する価値のある文字列文法として提案している
  • Lua の複数行文字列は [[...]] をベースにし、角括弧の間に任意個の = を入れられる
    • [==[ ... ]==] のように開始と終了の = の数を一致させる
    • 同じ方式をコメントにも使える
    • --[[ ... ]]--[==[ ... ]==] のどちらも可能である

Assembly: 方言とプリプロセッサの組み合わせ

  • Assembly は方言が多岐に分かれており、ハイライトが難しい言語の 1 つである
  • llamafile は AT&T、nasm など複数の構文を妥当に扱うことを目指している
  • キーワードは、行の最初の識別子であり、かつコロンの後ろにない項目として扱えば、大半の assembly コードをそれらしく見せられる
  • コメント文法も単純ではない
    • 元の UNIX コメントは単一の / だけでよかった
    • GNU as は今でも行頭位置でのみこのコメントをサポートしている
    • Clang は fixed comment をサポートしないため、オープンソースコードでは実用的に使いにくいという
  • 元の UNIX assembler は文字リテラルに閉じクォートを使わなかった
    • 'x は文字 x の値 0x78 を表す
    • GNU as はこれを今もサポートするが、LLVM はサポートしない
    • 既存コードにこの文法があるので、良いハイライターなら対応すべきである
  • GNU assembler は引用付き識別子を許すため、シンボルにほぼあらゆる文字を入れられる
  • Assembly は C プリプロセッサや m4 と一緒に使われることが多い
    • dnlm4_dnlC で始まる行もコメントとして処理する必要がある

Ada と BASIC: 小さな文法がレキシングを揺さぶる例

  • Ada はレキシング自体はかなり単純だが、シングルクォートの使い方が独特である
    • C のように 'x' という文字リテラルを持てる
    • Foo'Size のように属性参照でもシングルクォートを使う
    • Character'(')')'Image は文字を宣言し、Image 関数で文字列表現へ変換する
  • Commodore BASIC の例は、構文ハイライトのさまざまな前提を崩す
    • 文字列は行末で閉じクォートを省略できる
    • 変数名には $ のような sigil が付く
    • goto のようなキーワードは識別子の中でも積極的にレキシングされる
  • Visual BASIC には #1/1/2024# のような 日付リテラル 文法がある
  • Visual BASIC は #If DEBUG Then#Else#End If のようなプリプロセッサディレクティブも持ち、レキシングが厄介である

Perl: Shell とプログラミング言語の間の複雑さ

  • Perl は Shell とプログラミング言語の中間にあり、両方の複雑さを受け継いでいる
  • 正規表現を言語の第一級要素にし、その影響は Python など他言語にも及んだ
  • Perl の置換構文は s/hello/Perl/i のように sed に似ている
    • / の代わりに任意の句読点文字を区切りに使える
    • s!hello!Perl!i のようにスラッシュが正規表現内に入るとき便利である
    • s{hello}{Perl}i のように対応する文字を使う場合は追加文字が必要になる
  • Perl には、文字列のようにハイライトすべき魔法の接頭辞が多い
    • msyqrqwqqqx などがさまざまな区切りとともに使われる
  • y/x/y/ を除算と誤判定しないには文脈を見る必要がある
    • Perl の変数はスカラー $、配列 @、ハッシュ % のような sigil を持つため、文法全体をパースしなくても区別の助けになる
  • Perl では、ソースコード内に man page 用の POD 文書を入れる慣習がある
    • 行頭の =word が POD 文書の開始となり、=cut で終わる

Ruby: 最も難しかったレキシング対象

  • Ruby は前述の言語群の和集合のように見え、文法が形式的に十分文書化されていないと評価されている
  • Ruby マニュアルの syntax 文書 は細部の説明が軽めだと見ている
  • Ruby は backquote 文法をサポートする一方で、backquote をメソッド名のようにも使えるため、ハイライターがそれを文字列と見るべきかどうか判定しにくい
  • Ruby には << 演算子と heredoc の両方がある
    • 実際のコードには options[:includes] <<arg; true のような形が存在する
    • これは heredoc のように見えることがあり、Emacs も誤って処理する
  • puts \"This is #{<<HERE.strip} evil\" のように、文字列補間の中に heredoc が入るコードも有効である
  • 42 言語の中で Ruby が最も衝撃的だったとされ、Ruby はパースなしにはレキシングできないかもしれず、パースしても解釈可能かどうか確信しにくいと見ている

実装コード行数で見た複雑さ

  • 各言語ハイライター実装のコード行数で見ると、FORTH が最も単純で Ruby が最も複雑である
  • 最も短い実装は highlight_forth.cpp の 125 行である
  • 比較的短い実装には m4 132 行、Ada 149 行、LISP 160 行、MATLAB 166 行、COBOL 186 行、BASIC 199 行、FORTRAN 200 行などがある
  • 中規模の実装には JavaScript 337 行、TypeScript 371 行、Kotlin 387 行、Scala 387 行、Assembly 447 行、C 449 行、Swift 455 行、D 521 行が含まれる
  • 最も長い実装は Shell 570 行、Perl 583 行、highlight_ruby.cpp 1042 行である

1件のコメント

 
GN⁺ 2024-11-03
Hacker Newsの意見
  • Cのトライグラフの中でいちばん気に入っているのは、do_action() ??!??! handle_error() みたいな形
    特殊なエラー処理構文のように見えるが、実は ??!??!|| の論理 OR に変換され、短絡評価の規則によって do_action() が 0 以外の値を返したときに handle_error() を実行する構造なので満足感がある

  • 面白く読んだが、むしろ Lisp 側の観点により共感するようになった
    私の理解では、構文は言語の中でそれほど重要な部分ではなく、助けになるより障害になりやすいので、他のことに集中できるようできるだけ単純で均一であるべきだという立場だ
    ただ、Lisp で構造的編集を学ぶことは、今のところ助けより障害に近く、いつかは報われるのだろうと思う

    • 単純な構文はコンピュータにはよいかもしれないが、構文は主に人間が読み書きするために設計されている
      Lisp のように単純化すると、構文についての議論が意味論の問題へ押しやられるだけで、階層が変わるにすぎない
      複雑な意味論を持つ単純な構文より、複雑な構文のほうが読み書きしやすいと考える。構文エラーにはすぐフィードバックがあるが、意味エラーは実行時まで隠れていることもある
    • Lisp には、レクサーを再プログラムできるリーダーマクロがあり、見えている構造を構文木に変える過程もマクロで扱える
      たとえば https://pyret.org/ のようなものもある。実際、Lisp が単純だったり、必ずしも均一だったりするとまでは言いにくい
    • 構造的編集が障害だったというのは少し意外で、ひとつ助言すると parinfer を使い、slurpbarfraise の3つのコマンドのキーバインドだけ覚えればよい
      この4つだけでもpareditの利点を複雑さなしに 95% ほど享受でき、残りのコツは慣れてから学べばよい
  • 「誰が C を毎秒 35MB でシンタックスハイライトしたがるのかは分からないが、今はそれが可能だ」というくだりがあったが、速いとはいえ tcc はかなり古いコンピュータでも C を毎秒 29MB コンパイルしてバイナリコードにする: https://bellard.org/tcc/#speed
    もっと速くすることもできるだろうが、おそらく必要ではない

  • 筆者が見落としている点がいくつかある
    TypeScript、Swift、Kotlin、Scala だけが文字列補間に実際のコードを入れる極端なところまで行ったわけではなく、C#、Python、JavaScript、Ruby、Shell、Make も同様のことをサポートしている
    Tcl はコードとデータの区別が難しいため、{ } は事実上特殊な文字列区切りであり、xyzzy {#hello world} における #hello world がコメントなのか文字列なのかは、評価時点になるまで分からない
    PostgreSQL には便利なドル引用文字列があり、https://www.postgresql.org/docs/current/sql-syntax-lexical.h... のように 'Dianne''s horse'$$Dianne's horse$$$SomeTag$Dianne's horse$SomeTag$ は同じ意味になる

    • Perl でも可能
      \"I have $foo $bar's: @{[$bar x $foo]}\" のような文字列は I have 5 x's: xxxxx を出力できる
      @{[...]} 構文は、スカラーだけでなく配列補間も可能な Perl の特性を利用したもので、内側の [...] は配列参照を作り、外側の @{...} がそれを逆参照する。Perl インタプリタはその内側の式に任意のコードを許可する
    • 「文字列の中にコードが入る」というより、特殊な文字列連結構文と見なすほうが正しいと思う
      そうすれば \"foo { toUpper(\"bar { x + y } bar\") } foo\" のようにネストでき、+ が文字列連結なら \"foo \" + toUpper(\"bar \" + (x + y) + \" bar\") + \" foo\" と実質的に同じになる
      実際にそう動く言語があるのかは分からない
    • Ruby はこれを極限まで押し進めている
      Ruby は好きだが、文字列補間と heredoc を混ぜた puts \"This is #{< のような有効な Ruby を擁護するのは難しい
      また Ruby では空白も引用文字になりうる。左辺オペランドのない文脈では % が引用文字列の開始となり、次の文字が引用の種類を示すため、% hello hello を含む文字列になる。%(this is a string)%{this is a string} はよいが、空白を使うものは実際に見たこともなく、irb でも正しく扱えないので、削除されてもよいと思う
    • Scala の文字列補間はパターンマッチの対象としても使える
      val s"${a} + ${b}" = "1 + 2" を使うと、a1b2 になる
    • C# の補間文字列の優れた点は、遅延評価されること
      以前は、ホットループ内の log.trace($\"Entering iteration {i} for customer {c.ID} [{c.ShortName}]\"); のようなコードが、ロガーが早期リターンする前であっても毎回 string.Concat を呼んでいたため、多くのロガーが独自の補間を実装していた
      C# では DefaultInterpolatedStringHandler やカスタムハンドラーパターンを受け取るオーバーロードを宣言でき、このオーバーロードが優先されることで、実際にログが必要かを確認してから文字列生成を遅らせられる
  • ここで触れられていない、ほとんどのシンタックスハイライタを壊す別の文法上の特異点がある
    Java では Unicode エスケープ が文字列の中だけでなくどこにでも現れうる
    たとえば class Foo\u007b} は有効なクラスで、// コメント内の \u000A が実際の改行として扱われうるため、assert の例も予想と違う動作になる

    • これを正しくシンタックスハイライトできないのは セキュリティ問題 だとも言える
      Unicode エスケープでブロックコメントを終了できるので、Java ソースファイル内のコメントに悪意あるコードを隠すには、コメント内に Unicode エスケープのブロックがありそうな言い訳を作ればよい
      この特性を知らない開発者は、コメントアウトされているものとして見過ごしてしまう可能性が高い
    • Java でこういうものは一度も見たことがないが、実際に役立つ用途はあるのだろうか?
  • 「Ruby は文法を理解しようとするあらゆる試みをすり抜ける言語」という一節があるが、任意に再プログラム可能なレキサ を持つ TeX から見ればかわいいものだ

    • Lisp のリーダーマクロでもレキサをプログラムできる
  • 「すべての C プログラマは複数行コメントの中に複数行コメントを入れられないことを知っている」とあるが、Standard ML プログラマには意外な制約かもしれない
    (* (* Nested (**) *) comment *) のような ネストしたコメント は有効で、その後に続く val _ = print "hello, world\n" も正常に実行される
    C が登場した当時は表現力のある言語だと見なされていたことを考えると、ネストコメントが言語に入らなかったのは興味深い

    • その発言には面白い点が 3 つある
      ML には 1 行コメントがなかったので、それも同じくらい意外な制約だし、C を「表現力がある」と呼ぶのは初めて聞いたが、1972 年にアセンブリと比べるならそうだったのかもしれない
      そしてコメント文法が言語の表現力とどう関係するのかは分からず、定義上まったく関係ないと思う
    • ネストコメントをレキシングするには スタック、あるいは少なくともネスト深度カウンタが必要になる
      伝統的に字句解析は正規表現のような有限状態オートマトンだけを使う領域と考えられていたので、そうした機能はレキシングの範囲外に見えていた
    • C でも #if 0 を使えばコメントをネストする方法が 1 つある
      #if 0 ... #if 0 ... #endif ... #endif のように囲める
    • Pascal も例にあるような ネストコメント構文 を以前から常にサポートしていた
    • これは Standard ML だけでなく一般の ML にも当てはまる
  • joe のシンタックスハイライト実装を再利用可能な形で見てみたい
    https://joe-editor.sf.net/ の形式は、Python f-string を正しくハイライトできるほど強力だ
    関連する文書と例は https://github.com/cmur2/joe-syntax/blob/joe-4.4/misc/HowItW...https://gist.github.com/irdc/6188f11b1e699d615ce2520f03f1d0d... にある

    • joe の DFA 方式パース をもとにレキサとパーサをいくつも作ってみた
      状態と遷移の文法は標準ツールよりずっと理解しやすかった
      欠点は規則集合が冗長になり、理想的な構造化がやや難しい点だが、コードからすべての生成規則を推論しやすくなるという意味ではむしろ利点だと思う
    • 興味深いことに、Python f-string は 3.12 で文法が変わった ため、ハイライトもバージョンによって変える必要がある
  • 筆者は TeX のシンタックスハイライトを試したことがないようだ
    精神衛生のためにはよいことかもしれないが、TeX はたいてい 解釈なしでは完全なシンタックスハイライトが不可能
    パースだけでも不十分で、各文字の役割を再定義できるため、「これから K は { で、C は } だ」のようなことまでできる
    実際、arXiv の論文の中にもこの呪われた機能を使っているものがある

    • https://github.com/Mozilla-Ocho/llamafile/blob/main/llamafil... を作ったが、ハードドライブで見つけた .tex ファイルについては壊れず、かなりうまくハイライトできている
      目標は現実世界の利用の 99.9% をカバーすることだ。そうすれば LLM が出力しそうなものも大半はカバーできる可能性が高い
      難解な文法でも、文字列やコメントがファイル末尾まで続いて残りのソースコードを覆ってしまいさえしなければ、たいてい大きな問題にはならない
    • \makeatletter が「何かを at 文字にする」という意味ではなく、パース時に @ 文字を文字として扱う という意味だと知っても、なかなか信じがたかった
    • Common Lisp も同様に リードテーブル を再定義できる。ただし arXiv ではそこまで乱用されていない可能性が高い
  • Vimのような優れた構文ハイライトエンジンを書くのは簡単ではないと思う
    構文ハイライトには、特定の形でネストされた内容にそれぞれ異なる規則を適用する文脈処理が必要になる
    Vimの構文ハイライタでは matchregion の2種類の項目を宣言でき、match は単純な字句規則で、region は開始・終了・中間をそれぞれマッチする式を持つ
    項目は特定の領域内にあるときだけ有効になるよう宣言でき、包含関係も相互に宣言できる
    この基本的な意味構造の上に、さまざまな特殊ケース向けの機能が追加されている。Justineでも面接の場でその場ですぐ作るのは難しく、一晩課題くらいにはなると思う

    • 扱いが難しい例として、TXR言語に組み込まれたTXR Lispがある
      manpageのHTML変換結果を受け取り、TXRマニュアルのHTMLに整える genman スクリプトがここにある: https://www.kylheku.com/cgit/txr/tree/genman.txr
      白く見える部分はリテラルテンプレートで、Lispコードは @(do ...) のようなディレクティブの中に入る。TXRキーワードは紫、TXR Lispキーワードは緑で表示され、同じ単語でも文脈によって異なる
      準文字列にもネストした文法を含めることができ、その中にさらに埋め込みコードや準文字列を入れられる。txr.vimtl.vim の構文定義ファイルはどちらも https://www.kylheku.com/cgit/txr/tree/genvim.txr から生成される
    • 素朴には、構文ハイライタをきちんと作るにはASTにパースしたあと、トークンを走査しながらノード型に応じて色を更新すればよいと思っていた
      そうしないのは効率の問題なのだろう。たとえば、画面に見えている部分だけをハイライトする代わりに、ファイル全体をパースする必要があり、コストが大きくなるかもしれない
    • Justineが面接でそれを作れないほうに賭けるつもりはない ;)