演算子の優先順位の設計比較

に公開

主要な言語について、演算子の優先順位の設計を比較します。

自作言語の設計等に役立ててください。

算術演算 vs ビット演算

多くの言語では、算術演算がビット演算に優先します。

  1. *, /
  2. +, -
  3. &
  4. |

しかし、GoとSwiftではこれらの階層が混在しています。

  1. *, /, &
  2. +, -, |

半環だと思えば似たようなもの、という発想でしょう。

単項マイナス vs 冪乗

冪乗の演算子 ** をもつ言語では、しばしば単項マイナスとの順序について特別なルールを持ちます。つまり、基数にマイナスがついた場合、それは項全体を反転させると考えます。この場合 -2 ** 2 は 4 ではなく -4 になります。

  • JavaScriptの ** は、基数が前置演算となることを禁止しています。自動的に項全体に適用されることもありません。
  • Perl, PHP, Pythonの ** は、基数についた前置演算を項全体に適用します。
  • Rubyの ** は、基数についた単項マイナスのみを項全体に適用します。その他の演算は基数に適用されます。

XOR vs OR

多くの言語では、ビットごとの排他的論理和 ^ がビットごとの論理和 | に優先します。

  • Perl, Rubyでは ^| は同じ優先順位です。
  • GoとSwiftにおいても、前述の理由により ^| は同じ優先順位です。

等号 vs 大小比較

多くの言語では、大小比較が等号(およびその否定)に優先します。

  • Go, Rust, Swift, Python では、等号と大小比較の優先順位が同じです。

等号の結合性

多くの言語では、等号は左結合です。

  • RustやSwift, PHPでは、等号は非結合的で、同じ優先度で並べるとエラーになります。
  • PerlとPythonは等号・不等号のチェイニングを実装しており、特殊な挙動になります。

等号 vs ビット演算

多くの言語では、等号がビット演算に優先します。

  • Rust, Python, Rubyではビット演算は等号に優先します。
  • Kotlinではビット演算はinfix functionとなるため、やはりビット演算は等号に優先します。
  • GoとSwiftにおいても、前述の理由によりビット演算が等号に優先します。

3項条件演算子の中央式の制限

3項条件演算子の中央式は ?: で囲まれているため、実質的に () で囲まれたのと同じ効果があります。一般に、再帰下降で実装するときは優先度を意図的に選択するため、右辺と同じ優先度という仕様になることが多いと考えられます。いっぽう、パーサージェネレーターの機能で優先度を振りわけると、自然に最低優先度までを許容するようになります。

  • C, Java, C#, Swift, Perl, PHPの ? ... : には優先度制限はありません。
  • JavaScriptとRubyの ? ... : には優先度制限があり、同式以上の優先度をもつものだけを書くことができます。
  • Pythonの if ... else にも優先度制限があり、左辺以上の優先度をもつものだけを書くことができます。

優先度の低い単項演算子と優先度の高い二項演算子の組み合わせ

単項演算子と二項演算子の組み合わせにおいては、優先度が逆転しても曖昧性なくパースできる場合があります。たとえば 1 + yield foo は、 yield が優先度の低い演算子だったとしても曖昧性を生じません。
これも再帰下降で実装する場合は厳格に扱われ、パーサージェネレーターの機能を使っている場合はなるべく緩く扱われる傾向にあります。

JavaScriptのように厳格に規格化されている言語は厳格方式を取り、Rubyのように実装中心で開発されている言語は緩い方式を取る傾向にあるような気がしますが、演算子によってまちまちのため完全な調査はできていません。

資料: 各言語の優先順位表

本稿で調査した言語の優先順位表です。公式ドキュメント、ソースコード、信頼できる解説などを中心に調べていますが、1つの言語あたりの調査にかけている時間はそこまで長くないため、誤りもあるかもしれません。

また、演算子として何を含めるかや何を同一視するか、優先順位の高低をどう考えるかは、複雑なケースでは解釈に揺れがある場合があります。本稿ではおおむね、パーサーの振る舞いに注目して分類しています。また、JavaScriptのようにパーサーと構文検証が分離されている場合は、構文検証も構文の一部としてみなします(仕様として把握できた範囲内)。いっぽう、型検査やランタイムエラーは考慮しません。

表を作るにあたって、あえてyacc上の記載とは異なる表記を採用しているものがあります。これは、実際に検証した結果、yacc上の記載通りに動作していない場合があったからです。間違いを指摘するにあたっては、具体的なコード例を示してもらえると助かります。

凡例

precは小さいほど結合度が高いものとする。

両側を識別可能な終端記号で囲まれている構成をprimaryとして扱い、primaryは原則として優先度0とする。primaryに優先度の高い後置演算子を含めることも多いが、本稿ではこれはprimaryに含めない。primary(優先度0)の構文はテーブルに明記しない。

演算子が複数ある場合は以下の説明をつける:

  • (infix): 中置演算子
  • (pre): 前置演算子
  • (post): 後置演算子
  • (circumfix): 括弧系。基本的にprimary expressionになるので省略する。

fixityは以下の通り。

  • L: 左結合。
    • 前置演算子の場合、ネストできない。
    • 後置演算子の場合、ネストできる。
  • R: 右結合。
    • 前置演算子の場合、ネストできる。
    • 後置演算子の場合、ネストできない。
  • N: 非結合。
    • 前置演算子の場合、ネストできない。
    • 後置演算子の場合、ネストできない。
  • C: チェイン形式。右側に結合させた場合とも左側に結合させた場合とも異なる動作をする。

三項条件演算子 ? ... : は以下の2つの役割を分けて考える。

  • 演算子の両側の式に対して、 ? ... : を1つの中置演算子と見なしたもの。 ? ... :(infix) と表記。
  • 演算子の内側の式に対して、 ?: を括弧対と見なしたもの。 ? ... :(circumfix) と表記。ただし実際の表では省略する。

+= など複合代入演算子は優先度が同じ場合 op= と総称する。

C および C++

prec fixity op
1 L ::
2 L ()(post), [](post), ++(post), --(post), ., ->
3 R ++(pre), --(pre), +(pre), -(pre), !, ~, ()(pre), *(pre), &(pre), sizeof, co_await, new, new[], delete, delete[]
4 L .*, ->*
5 L *(infix), /, %
6 L +(infix), -(infix)
7 L <<, >>
8 L <=>
9 L <, <=, >, >=
10 L ==, !=
11 L &(infix)
12 L ^
13 L |
14 L &&
15 L ||
16 R ? ... :(infix), throw, co_yield, op=
17 L ,
  • ? ... : の内部の式の優先度制限はない

Java

prec fixity op
1 L [](post), .<sup>※1</sup> <sup>※2</sup>
2 L ++(post), --(post)
3 R ++(pre), --(pre), +(pre), -(pre), !, ~, ()(pre)<sup>※3</sup>, switch ... }(circumfix)<sup>※4</sup>
4 L *, /, %
5 L +(infix), -(infix)
6 L <<, >>, >>>
7 L <, <=, >, >=, instanceof
8 L ==, !=
9 L &
10 L ^
11 L |
12 L &&
13 L ||
14 R ? ... :(infix)<sup>※5</sup>
15 R =, op=, ->
  • ※1 メソッド呼び出し構文の場合、括弧も含む
  • ※2 newはcircumfixとみなせるためここには記載していない
  • ※3 cast operatorの右辺に単項のプラス・マイナスは出現できない。これは二項演算子との曖昧性のため。
  • ※4 switch expressionはprimary expressionに近いが、postfix operatorを付与できないためここに記載している。
  • ※5 ? ... : の右辺にlambdaが出現可能。これはlambdaが完全なinfix operatorではなく、左辺に特別な形の構文しか許さないため。
  • ※6 ? ... : の内部の式の優先度制限はない

Kotlin

prec fixity op
1 L [](post), ()(post), <>(post), ++(post), --(post), ., ?., ::
2 R ++(pre), --(pre), +(pre), -(pre), !, @(infix), @(pre)<sup>※2</sup>
3 L :, as, as?
4 L *(infix), /, %
5 L +(infix), -(infix)
6 L .., ..<
7 L (any ident)
8 L ?:
9 L in, !in, is, !is
10 L <, <=, >, >= <sup>※3</sup>
11 L ==, !=, ===, !==
12 L &&
13 L ||
14 N =, op=
  • ※1 公式ドキュメントの文法定義を参考にしたが、パーサーの振る舞いとの対応が不明瞭だったため不正確な可能性がある。
  • ※2 アノテーション。 @ 単体ではなく、続く情報を含めて全体で接頭辞として振る舞う。
  • ※3 genericCallLikeComparison 非終端記号にも用途不明のcall suffixがあるが、明らかに曖昧なのにここに定義がある理由はよくわからなかった。経験者の意見求む。

C#

prec fixity op
1 L ()(post), [](post), ?[], ++(post), --(post), !(post), ., ?., -> <sup>※</sup>
2 R ++(pre), --(pre), +(pre), -(pre), !(pre), ~, ^, ()(pre), await, *(pre), &(pre)
3 N ..(infix), ..(pre), ..(post)
4 L *(infix), /, %
5 L +(infix), -(infix)
6 L <<, >>
7 L <, <=, >, >=, is, as
8 L ==, !=
9 L &
10 L ^
11 L |
12 L &&
13 L ||
14 R ??, throw
15 R ? ... :, =>, from ... in
  • ※ newやtypeof演算子等はcircumfixとみなせるためここには記載していない
  • ? ... : の内部の式の優先度制限はない

Go

prec fixity op
1 R +(pre), -(pre), !, ^(pre), *(pre), &(pre), <-
2 L *(infix), /, %, <<, >>, &(infix), &^
3 L +(infix), -(infix), |, ^(infix)
4 L ==, !=, <, <=, >, >=
5 L &&
6 L ||

Rust

prec fixity op
1 L ::(infix), ::(pre)
2 L ()(post)<sup>※1</sup>, [](post), .<sup>※2</sup>, ?
3 R -(pre), !(pre), *(pre), &(pre)
4 L as
5 L *(infix), /, %
6 L +, -(infix)
7 L <<, >>
8 L &(infix)
9 L ^
10 L |(infix)
11 N ==, !=, <, <=, >, >=
12 L &&
13 L ||(infix)
14 N .., ..=
15 R =, op=
16 R return, break, |(pre)<sup>※3</sup>, ||(pre)<sup>※3</sup>
  • ※1 フィールドアクセス構文と併用するとメソッド呼び出しになる
  • ※2 awaitも含む
  • ※3 クロージャ

Swift

Swiftはユーザー定義の中置演算子の優先度をカスタマイズできる。

prec fixity op
1 L ()(post), [](post), .(post)<sup>※</sup>, !, ?, user-defined postfix
2 R &(pre)<sup>※</sup>, user-defined prefix
3 L/R ? ... :<sup>※</sup>, =<sup>※</sup>, op=<sup>※</sup>, is, as, as?, as!, user-defined infix
4 N await
5 N try, try?, try!
  • ※ タプルインデックス、メソッド呼び出し、 .self, .init() を含む
  • ※ 内部にprimary expressionのみ許可
  • ※ 三項条件演算子、代入演算子、複合代入演算子の右辺には await, try, try?, try! が出現可能

標準定義の中置演算子の優先度を展開すると以下の通り。

prec fixity op
1 L ()(post), [](post), .(post)<sup>※</sup>, !, ?, user-defined postfix
2 R &(pre)<sup>※</sup>, user-defined prefix
3 N <<, &<<, >>, &>>
4 L *, &*, /, %, &
5 L +, &+, -, &-, |, ^
6 N ..., ..<
7 N is, as, as?, as!
8 R ??
9 N ==, !=, ===, !==, <, <=, >, >=, ~=
10 L &&
11 R ||
12 R ? ... :
13 R =, op=
14 N await
15 N try, try?, try!

JavaScript

prec fixity op
1 L ()(post)<sup>※1</sup>, [](post), ., `
2 R new<sup>※2</sup>
3 N ++(post), --(post), ++(pre), --(pre)
4 R +(pre), -(pre), !, ~, await, typeof, delete, void
5 R **<sup>※3</sup>
6 L *, %, /
7 L +(infix), -(infix)
8 L <<, >>, >>>
9 L <, <=, >, >=, instanceof, in
10 L ==, !=, ===, !==
11 L &
12 L ^
13 L |
14 L &&
15 L ||
16 L ??<sup>※4</sup>
17 R ? ... :
18 R =, op=, =>, yield
19 L ,
  • ※1 newがついている場合 (e.g. new Event()) を含む。この場合、newは対応可能な呼び出しのうち最も内側のものに優先してマッチする。
  • ※2 newが関数呼び出しの括弧と対応せずに登場した場合 (new Object) に限る。
  • ※3 ** の左辺に、すぐ上位の優先度を持つ +, - などが直接登場することはできない。右辺には登場できる。 -2 ** 2 は違反だが 2 ** -2 は通る。
  • ※4 ?? の左辺や右辺に &&|| が直接登場することはできない。したがって正確には、 &&/||?? は互いに比較不可能な優先度を持っていると考えるのが正しい。
  • ※5 ? ... : の内部の式には優先度制限があり、 ,= などを直接置くことができない。

Perl

prec fixity op
1 L ->
2 N ++(post), --(post), ++(pre), --(pre)
3 R **<sup>※1</sup>
4 R +(pre), -(pre), !, ~, ~., \\
5 L =~, !~
6 L *, /, %, x
7 L +(infix), -(infix), .
8 L <<, >>
9 N named unary operators<sup>※2</sup>
10 N isa
11 C <, <=, >, >=, lt, le, gt, ge
12 C ==, !=, <=>, ~~, eq, ne, cmp
13 L &, &.
14 L |, |., ^, ^.
15 L &&
16 L ||, ^^, //
17 N .., ...
18 R ? ... :(infix)
19 R =, op=, goto, last, next, redo, dump
20 L ,, =>
21 N list operators<sup>※3</sup>
22 R not
23 L and
24 L or, xor
  • ※1 冪乗演算子の右辺には、優先度が1つ低い前置演算子がそのまま登場できると考えられる (yaccのprecを使った実装のため)
  • ※2 named unary operators ... 1引数関数で、オプション形式であるか、または呼び出しが括弧をともなわないもの
  • ※3 list operators ... ここでは、リスト引数を取り、呼び出しが括弧をともなわないもの。また、この優先度になるのはトークンの右側との関わりにおいてで、左側に対しては高い優先度として振る舞う (= ここの前後を強制的に右結合にする)
  • ※4 ? ... : の内部の式の優先度制限はない

PHP

prec fixity op
1 R clone, new
2 R **
3 L ++(post), --(post), ++(pre), --(pre), +(pre), -(pre), ~, ()(pre), @
4 L instanceof
5 R !
6 L *, /, %
7 L +, -, .<sup>※1</sup>
8 L <<, >>
9 L .<sup>※1</sup>
10 L |>
11 N <, <=, >, >=
12 N ==, !=, <>, ===, !==, <=>
13 L &(infix), &(pre)
14 L ^
15 L |
16 L &&
17 L `||``
18 L ??
19 L/N ? ... :(infix)<sup>※2</sup>
20 R =, op=
21 N yield from
22 N =>
23 N yield
24 N print
25 L and
26 L xor
27 L or
  • ※1 バージョンにより優先度が異なる
  • ※2 バージョンにより結合性が異なる
  • ※3 ? ... : の内部の式の優先度制限はないと考えられる

Python

prec fixity op
1 L ()(post), [](post), .`
2 N await
3 R **<sup>※1</sup>
4 L +(pre), -(pre), ~
5 L *, @, /, //, %
6 L +(infix), -(infix)
7 L <<, >>
8 L &
9 L ^
10 L |
11 C ==, !=, <, <=, >, >=, is, is not, in, not in
12 R not
13 L and
14 L or
15 R if ... else<sup>※2 ※3 ※4</sup>
16 R lambda
17 N :=
  • ※1 ** の右辺には、優先度の1つ低い前置演算子を直接含むことができる。
  • ※2 if ... else は、左辺と中央が逆転していることを除くと、他の言語の ? ... : と同様。
  • ※3 Pythonの if ... else は中央の優先度に制限があり、 or より低い優先度の演算子をそのまま含めることはできない。
  • ※4 右辺には、1つ優先度の低い lambda を直接含むことができる。

Ruby

prec fixity op
1 R +(pre), !, ~
2 R **<sup>※</sup>
3 R -(pre)
4 L *, /, %
5 L +, -
6 L <<, >>
7 L &
8 L |, ^
9 L <, <=, >, >=
10 N ==, !=, =~, !~, ===, <=>
11 L &&
12 L ||
13 N ..(infix), ...(infix), ..(post)<sup>※</sup>, ...(post)<sup>※</sup>, ..(pre)<sup>※</sup>, ...(pre)<sup>※</sup>
14 R ? ... :
15 L rescue
16 R =, op=
17 R defined?, not<sup>※</sup>
18 L and, or
19 L if(infix), unless(infix), while(infix), until(infix), in
  • ** の右辺に前置演算子の - は出現可能。
  • ※ 後置の範囲演算子は、より優先度の高い演算子の左辺に出現可能 (1 .. == 2)。同様に、前置の範囲演算子は、より優先度の高い演算子の右辺に出現可能 (1 == .. 2)。
  • ? ... : の中央には優先度制限があり、 ? ... : より優先度の低い演算子は直接出現できない。 ? ... : 自身は出現できる。
  • defined?, not は、より優先度の高い演算子の右辺に出現可能。

Discussion