**Pratt パーサーを直感的に把握する**

2026/03/30 21:31

**Pratt パーサーを直感的に把握する**

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

「元の要約は正確で明確ですので、そのまま繰り返すことができます。」

Text to translate

(The original summary is accurate and clear, so it can be repeated as‑is.)

本文

2026‑03‑26

既にご存知のように、

a + b * c + d

a + (b * c) + d
と計算されます。 しかし、その規則を機械が正確に理解できるように、どのように表現すればよいのでしょうか?

コンパイラで最も一般的に使われている解決策は 抽象構文木(AST) です。
AST では演算子がそのオペランドより上に位置し、評価は下から上へと行われます:子ノードを先に計算し、その後で演算子を適用します。

     +
    / \
   +   d
  / \
 a   *
    / \
   b   c

この木構造は

(a + (b * c)) + d
の所望の評価順序を、プログラムで扱いやすい形式にエンコードしています。


パース(字句解析)

パースは平坦なテキストからこの構造を導き出します。
計算機科学の研究対象として数十年もの間注目されており、多くの場合過剰に複雑化されています。

1. 混在する優先順位

パースの難点は「混在する優先順位」― 優先順位が方向を変えるケース ― にあります。
利用者が書くプログラムが 増加(左から右へ優先度が上がる)か 減少(左から右へ優先度が下がる)のいずれかであると仮定した場合、木構造はどうなるでしょう?

減少優先順位: もっとも左側の演算子を最初に評価します。
つまり掛け算より足し算が高く、足し算より比較が高い等です。
最初の演算子は木の一番深い位置に配置され、最後の演算子は浅い位置になります。
結果として得られる木は 左寄り(left‑leaning)です。

       <
      / \
     +   d
    / \
   *   c
  / \
 a   b

増加優先順位: 今回は左側の演算子が浅く、右側の演算子が深くなります。
各演算子は右側の結果に依存するためです。
木は 右寄り(right‑leaning)になります。

       >
      / \
     a   +
        / \
       b   *
          / \
         c   d

2. 等しい優先順位

等しい優先順位をどうエンコードするのが妥当でしょうか?
算術では左から右への評価(左結合)が慣例です。一方、C の代入演算子は右結合です。

すべての演算子が左結合であると仮定すると、左寄りの木で表現します。
演算子列に対し、(x_i) を i 番目 の演算子の優先順位とすると:

減少 : 弱い減少 – x_i ≥ x_{i+1}  (等しい場合も含む)
増加   : 強い増加   – x_i < x_{i+1}

したがって、同じ優先順位の演算子二つは「減少」ケースとまったく同じようにエンコードされます。

3. 方向転換が一度だけある場合

増加 → 減少 の方向転換(逆も同様)を持つ式を考えます。
例として

I = (a > b + c * d)
を取り上げ、以下の木で表します。

       >
      / \
     a   +
        / \
       b   *
          / \
         c   d

この木は右寄りです。
今度、

*
と同じかそれ以下の優先順位を持つ新しい演算子で
I
を拡張するとします。
増加性が失われるので、単に右寄りの木を続けても正しくエンコードできません。

そこで必要なのは 左寄り のサブツリーです。どこに配置すべきか?
新演算子の各優先順位レベルを可視化するとパターンが見えてきます:

[I] * e:
       >
      / \
     a   +
        / \
       b   *
          / \
         *   e
        / \
       c   d

[I] + e:
       >
      / \
     a   +
        / \
       +   e
      / \
     b   *
        / \
       c   d

[I] == e:
       ==
      /  \
     >    e
    / \
   a   +
      / \
     b   *
        / \
       c   d

左寄りの木は 等しいかそれ以下の優先順位を持つ最初の演算子 で始まります。
その低い優先順位の演算子は、少なくとも現在の演算子より後に評価される必要がありますが、さらに前にある複数の演算子も同様です。
それらは右寄りの木の「背骨(spine)」上にあります。

結論: 方向転換演算子に出会ったときには、背骨を逆さまに歩いて「最初に評価されるべきすべての演算子」を集めます。
この集まり―右寄りの部分木―が新しい演算子の左子ノードとなります。そしてその左側で独自の左寄りサブツリーが始まります。

これが「Pratt パース」の歩き戻し(walk‑back)手順です。
すべての式はこうした転換の列に過ぎないので、これだけで十分です。


パース – 擬似コード

1. 右寄りケース

右寄り木は再帰を使い、下から上へ構築できます:

def parse():
    left = leaf()

    if peek() is not None:
        op = advance()
        right = parse()
        return Node(op, left, right)

    return left

parse()
はまず
leaf()
を呼び出してリテラル(例:
a
)を処理し、トークンストリームから消費します。その後
peek()
で次のトークン(演算子)があるか確認。
有効なプログラムなら演算子が来るので
advance()
でそれを取り込み、右辺の再帰呼び出しへ進みます。

以下は先ほどの

[I]
を処理する過程です:

-- 下に向かう(advance)

(1) parse(0)
    a > b + c * d

(2) parse('>')   →  a > b [+] c * d

(3) parse('+')   →  a > b + c [*] d

(4) parse('*')   →  a > b + c * d [None]

-- 上に向かう(構築)

(4)  d
(3)  [*]
     / \
    c   d
(2)  [+]
     / \
    b   *
       / \
      c   d
(1)  [>]
     / \
    a   +
       / \
      b   *
         / \
        c   d

減少優先順位を扱うには、再帰呼び出しに現在の優先順位を渡します:

def parse(prev_prec=0):
    left = leaf()

    if peek() is not None and prec(peek()) > prev_prec:
        op = advance()
        right = parse(prec(op))
        return Node(op, left, right)

    return left

エンドオブファイルを最低優先順位のトークンとみなせば、

peek()
が条件を自然に失敗させて
None
チェックを省略できます:

def parse(prev_prec=0):
    left = leaf()

    if prec(peek()) > prev_prec:
        op = advance()
        right = parse(prec(op))
        return Node(op, left, right)

    return left

2. 左寄りケース

parse()
が再帰するとき、スタックフレームに左子と最小優先順位をプッシュします。
この呼び出しスタックは常に 増加する 優先順位で構成されます。

そのため、戻る際には 減少順 で各レベルを訪れます。

peek()
がバインドできる最初のレベルが正しいレベルです:それより上はすべて低い優先順位(
parse()
がまだ返ってこない)で、深く進むことはありません。

したがって

if
while
に置き換えます:

def parse(prev_prec=0):
    left = leaf()

    while prec(peek()) > prev_prec:
        op = advance()
        right = parse(prec(op))
        left = Node(op, left, right)

    return left

これが Pratt パーサ の完成形です。

while
ループは先ほど説明した歩き戻し手順に相当します:転換演算子が現れると、呼び出しスタックを上へ戻り適切なレベルを見つけてからループで消費し、左寄りサブツリーを構築します。

例:

[I] * e
I = a > b + c * d
)のトレース

-- 下に向かう
(1) a [>] b  +  c  *  d  *  e
(2) a  >  b [+] c  *  d  *  e
(3) a  >  b  +  c [*] d  *  e
(4) a  >  b  +  c  *  d [*] e  FAIL

-- 上に向かう(構築)

(4)  d
(3) iteration 1:
     [*]
     / \
    c   d
iteration 2:
     [*]
     / \
    *   e
   / \
  c   d
(2) [+]
     / \
    b   *
       / \
      *   e
     / \
    c   d
(1) [>]
     / \
    a   +
       / \
      b   *
         / \
        *   e
       / \
      c   d

左寄りサブツリー

(c * d) * e
は完全にフレーム 3 の
while
内で構築されます。


右結合

実際にはすべての演算子は 左バインディングパワー(LBP)右バインディングパワー(RBP) を持ちます。
これまで扱ってきた演算子は LBP と RBP が同じでした。

LBP は「左側の式をどれだけ強く引き付けるか」を示し、

peek()
while
条件で判定します。
RBP は「右側の式をどれだけ強く引き付けるか」を示し、再帰呼び出しに渡す
prev_prec
に相当します。

左結合演算子では LBP と RBP が等しいため、2 つ目の

*
parse()
の再帰呼び出しを起こさず、同じレベルでループが続きます。
右結合演算子(例:代入)では逆にしたいので、RBP を LBP より低く設定します。これにより連続する
=
が再帰呼び出しに渡され、
a = (b = c)
のように構築されます。

def parse(prev_prec=0):
    left = leaf()

    while lbp(peek()) > prev_prec:
        op = advance()
        right = parse(rbp(op))
        left = Node(op, left, right)

    return left

左結合なら

rbp = lbp
、右結合なら
rbp = lbp - 1
と設定します。


まとめ

Pratt パースは巧妙に見えますが、本質は「木構造は優先順位によって左寄りか右寄りになる」という単純な幾何学的直感に基づいています。
優先順位が下がるときは背骨を逆さまに歩き、正しい位置で新しい演算子を配置します。

この視点からアルゴリズムは直感的になり、実装もコンパクトになります。

同じ日のほかのニュース

一覧に戻る →

2026/04/02 8:35

新しいC++バックエンド for `ocamlc`

## 日本語訳: 新しいC++バックエンドが `ocamlc` に追加され、インクリメントされていない C ランタイムと外部関数インタフェースを置き換えました。著者は、ユーザーが指定した上限まで素数を生成するプログラムでその使用例を示しています。このプログラムは OCaml の List モジュールの一部を純粋に関数型スタイルで再実装しています。プログラムは `primes.cpp` という慣用的な C++ コードへ翻訳され、`Cons`、`I`、`ifthenelse` などのテンプレートメタプログラミング構造を含みます。`g++ -Dlimit=100 primes.cpp` でコンパイルすると、`print` が型ではないためにコンパイラー風エラーが発生し、出力形式は古い C プリプロセッサのエラー(OCaml の `::` の代わりにネストされた `Cons<hd, tl>`)を模倣します。生成されたコードはデフォルトテンプレート深度を増やす (`-ftemplate-depth=999999`) ことでのみ大きな上限を扱うことができます。著者のマシンでは、`limit = 10000` を実行すると約 30 秒で 10000 以下のすべての素数が出力され、約 11 GiB のメモリを使用します。clang++ は遅く、セグフォールトする可能性があります。アルゴリズム自体は非効率的です——コンテナライブラリからの優先度付きキュー/レフトヒープに基づく改良された純粋関数型素数生成器は、同じ上限で実行時間を約 8 秒、メモリ使用量を約 3.1 GiB に削減します。今後の作業では、このアプローチを他言語へ拡張することが目標です;Rust は部分的な実装特殊化がサポートされれば OCaml プログラムを実行できるようになります。 この改訂された要約は、すべての主要ポイントを反映し、不当な推測を避け、明確な主旨を提示し、あいまいまたは混乱を招く表現を排除しています。

2026/04/02 2:11

NASAの「アーテミス II」クルーが月へ発進します

## Japanese Translation: > **Artemis II 発射成功:** オリオンのソーラーアレイ翼が午後6時59分に完全展開し、各翼は約15,000セルを含み、およそ63フィート(19メートル)にわたります。 > > **主要推進マイルストーン:** 固体ロケットブースターが午後6時37分に分離;SLSコアステージの主エンジンカットオフは午後6時43分に発生;その後、コアステージ分離が午後6時59分に行われ、最初の推進フェーズが終了しました。 > > **打ち上げタイミング:** 打ち上げウィンドウは午後6時24分(EDT)で開き、ロケットコンプレックス-39Bからの離陸は午後6時35分に実施されました。 > > **事前準備:** 最終天気ブリーフィング(約80%が許可)、クルー服チェック、ハッチ閉鎖、打ち上げ中止システム検証をカウントダウン前に完了しました。コアステージとICPSのタンク作業はLH₂/LOX のスロー・フィル→ファスト・フィル→リペンリッシュ段階で行われました。 > > **離陸後の計画マヌーバー:** オリオンは低軌道上昇マヌーバー(PRM)を実施し、その後遠地点上昇バーナー(ARB)で深宇宙軌道を形成します。 > > **クルーと運用:** 乗員の4名は司令官レイド・ウィズマン、パイロットビクター・グローバー、クリスティーナ・コッホ、およびCSA宇宙飛行士ジェレミー・ハンセンです。NASAはケネディ宇宙センターで午後9時に打ち上げ後の記者会見を開催し、その後クルーは中間液体推進ステージを使用した近接操作デモンストレーションの準備に取り組みます。

2026/04/02 6:36

DRAM の価格がホビイスト向けのSBC市場を潰しつつあります。

## Japanese Translation: ### 改訂要約 ホビイスト向けのシングルボードコンピュータ(SBC)市場は、DRAM価格が急騰したため圧迫を受けています。Raspberry Pi は LPDDR4 RAM を搭載したすべての Pi モデルの価格を引き上げ、新しい 3 GB‑RAM の Pi 4 を $83.75 に設定し、16 GB の Pi 5 を $299.99 にしました。これらの値上げは主に LPDDR チップのコスト増によるもので、現在ボードコストの大部分を占めています。その結果、4 GB 以上の RAM を搭載したボードは多くのホビイストにとって手が届かないものとなっています。以前はお得だったミニ PC は 8 GB バリアントで $250 を超え、同様に使用済み PC も 4 GB 超で $250 を上回る価格になっています。Radxa は昨年新しいボードをリリースし続けましたが、ほかのベンダーは発売を減速または停止しています。Raspberry Pi の創設者 Eben Upton は「メモリ価格は現在の非常に高い水準で永続するわけではない」と述べており、その期間は不確実です。著者自身のプロジェクトは学習コストを下げ、破損リスクを減らすために $100 未満の部品を対象としています。DRAM 価格が高止まりする場合、ホビイストは古い SBC やマイクロコントローラへ戻る可能性が高く、手頃な選択肢が狭まり、小規模ベンダーは事業停止のリスクに直面します。Raspberry Pi の強力なマイクロコントローラエコシステムと産業基盤はある程度のレジリエンスを提供しますが、ホビイストセグメント全体での多様性は減少する可能性があります。