プログラミング言語の構文とセミコロン

文の区切りとしてのセミコロンについて、いくつかのプログラミング言語の事例をみていく記事です

主張や結論があるわけではなく、単に複数のトピックを書いています

C言語のセミコロンと文・宣言

C言語の構文は「文の末尾にセミコロンが必須」とよく説明されます。基本的にそのとおりですが、次の点についても説明がつくように、まずCの構文定義をみていきます

  • int a;f(); のような記述ではセミコロン必須
  • while (1) {} のようにセミコロンがなくてもいいこともある
    • {} で終わる場合はセミコロンがつかない」という誤解
  • struct S {}; のように {} とセミコロンの両方が出てくることもある

C言語の構文は主に 宣言 の3種類に分かれます

(※関数の本体における構文だけ取り上げます。また、ラベルや属性は本題から外れるので無視します)

文の末尾の記号

式についてはいうまでもないので説明は省略します。宣言は後回しにします

文から見ていきます。文の末尾が ;} という観点で、文を3種類に分類します:

  • 明示的にセミコロンで終わる文
  • 複合文
  • 文で終わる文

明示的にセミコロンで終わる文 は文の定義として最後にセミコロンが書かれているものです。例えば式文の定義は expression ; (式の後ろにセミコロン) となっていて、末尾がセミコロンになります。この種の文は、ほかに break, return, do-while などがあります。これらの文は「セミコロン必須」の説明に合致します

次に 複合文 は複数の文 (や宣言) を包んで1つの文とするものです。例えば { f(); g(); } という記述は、関数呼び出しの式文2つから構成される複合文です。複合文の末尾は、見ての通り } で終わります。複合文が存在するため「文の末尾にセミコロンがつかない」状況が発生するわけです

複合文の定義: {} の中に文や宣言を0個以上並べたものという意味

    { statement | declaration...(optional) }

最後に上記以外の文を確認します。これらは文の前方にいろいろと足していく構造とみなせます。例えば while 文の定義は次の通り:

    while ( expression ) statement

この文の末尾はまた別の何らかの文ですが、「文の末尾にある文」をたどっていくと最終的には末尾が文でないものにたどり着き、それはセミコロンで終わる文か、複合文のどちらかです。そのため、文の末尾は必ず ;} で終わることとなります

    // 例: 式文の手前にいろいろくっついているが、
    //     文全体としてはセミコロンで終わる

    if (p) {} else for(;;) while (r) f();
    //                               ^~~~

宣言

(※宣言の構文を深堀りすると本題から逸れるので詳しくは書きません……)

宣言は int a; のように変数を宣言する構文であるだけでなく、typedef int i32;struct S { int x; }; のように型を定義する構文でもあります

宣言の末尾は ; となります。struct S {…}; における {} は前述の複合文とは関係なく、型の記述の一部です。これが「{} とセミコロンの両方が出てくる」ケースに相当します

基本的に、宣言は 型 変数, 変数, …; という構造です。ただし「型」や「変数」の部分は単純な型名や変数名にかぎりません。 型の部分に struct S {…} のような構造体の定義を記述することができます。 さらにその場合、変数の部分は省略可能なので、型; だけで宣言として完結します

    // 型の部分が struct S {…} で、「変数…」の部分が省略された宣言
    struct S { int x; };

このような「型の定義」もあくまで宣言の一種であるため、変数リストの部分を書くこともできます:

    // 「変数…」の部分が obj (変数名) である宣言
    struct S { int x; } obj;

つまり、このように struct S {…} の後ろにまだ変数リストが続けられるので、} で終わりという構文になっていないと考えられます

ちなみに型の定義を宣言の構文の一種にするという設計は、C言語に近い見た目のほかの言語でもあまり踏襲されていなさそうです (後述の Go, Rust など)

参考

JavaScriptの自動セミコロン挿入

JavaScriptはJavaに似た構文を持つ言語です (C言語 → Java → JavaScript みたいな系譜)

JavaScriptには自動セミコロン挿入という機能があります (詳細はリンク先を参照)。 平たくいうと行末にはセミコロンを書かなくていいということです

字句文法 - JavaScript | MDN #自動セミコロン挿入

自動セミコロン挿入に関して、いくつか課題が知られています。1つ目の課題は return の直後に改行を入れると文が区切られるものです。 上記のリンク先にある例を引用します:

return
a + b

// 上記の文は、 ASI によって次のように変換されます

return;
a + b;

上の文は return (a + b); という1つの文のつもりで書かれているかもしれませんが、実際には return;a + b; という2つの文に解釈されます (これは意図しない挙動かもしれない)

あくまで筆者の感覚でいえば、この問題が実際にトラブルになることは少ないと思います。 このケースでは return; 文の後ろにある a + b; の文は絶対に実行されないので、エディタの機能や、リンター (プログラムの誤りを指摘するツール) があれば、警告を受けます。TypeScript を使って型検査をしていれば、関数の返り値の型が void (返り値なし) になるので型エラーになるかもしれません

もう1つの課題は、カッコが前の文にひっつく現象です。次のコードがその例です:

    f()
    (await p).g()

上記のコードは2つの文のつもりかもしれませんが、実際は次の1つの文として解釈されます:

    f()(await p).g();

f() が返す関数に対して (await p) を引数リストとする関数呼び出しの式になってしまっています (途中にあった改行は無視)

この問題も、リンターによって検出可能です (ESLint に function-call-spacing というルールがある)。TypeScript ではたいてい型エラーになるでしょう

JavaScript のコミュニティでは、セミコロンが省略可能なケースでも書く派と、書かない派が実際両方いるようです

Goのセミコロン省略

Go言語はC言語風の見た目の構文を持つ言語です。 JavaScriptとは別の方法で、セミコロンを書かなくていいようになっています。 仕様書によると字句解析の段階でセミコロンを挿入するようです

  • 行末に特定の種類のトークンがあったら、その後ろにセミコロンを挿入する
  • カッコを閉じる直前のセミコロンがなくてもよいことにする

(※字句解析はコンパイラの初期の処理工程。ソースコードをトークンという基本単位の列に分割すること)

セミコロン挿入の対象となるトークンの種類は平たくいえば「文の末尾になりうるトークン」のようです。 例えば次のコードでは 2 の直後にセミコロンが入ります

    a = 1 +   // + は文の末尾にならないから挿入されない
        2

この方法は興味深い特性があります。 上記の例のように、二項演算式の演算子は行末におくことになります (行頭に置いたら分割されてしまう)。 また、末尾カンマが実質的に必須になります

    a := []int{
        1,
        2, // このカンマは必須
    }

JavaScript でみた「カッコが前の行とくっつく」現象は発生しません

スタイルに若干の制約がつくものの、シンプルなルールでセミコロン省略を実現できて、意図しない挙動にもなりづらい、という印象です

参考: #Semicolons The Go Programming Language Specification - The Go Programming Language

Rustのセミコロン

Rustは比較的新しいシステムプログラミング言語です。これもC言語のような見た目の構文です。前述の JavaScript や Go と違って、セミコロン省略の仕組みを持っていません。

なぜその設計にしたかの公式的なドキュメントは見つかりませんでした。 ここでは経緯ではなく想像の話として、セミコロン省略の仕組みがあった場合に発生する問題について書きます

Rustのブロック式は、「文の並び + 最後の式(省略可)」という構造で、最後の式がそのブロックの値になるという式です。 コードの見た目として、途中の式文と最後の式の違いは「式の後ろにセミコロンがついているか」です

    let a = {
        f();
        g()
    //     ^~~~~ セミコロンがない
    };

仮に Goと同様のセミコロン自動挿入があったとします。 この例では g() の直後にセミコロンが挿入されて式文になってしまいます。 改行をおかず同一行に } を置く (g() } とする) ことで対処可能です

あるいは、ブロック式の構文から「最後の式」をなくし、最後の 式文 の値を使う仕様も考えられます。 その場合は最後の式文が意図せず値になってしまうケースがありそうです。 最後の式文の値が使われてほしくないケースは明示的に空文を置くことで対処可能です

参考:

Haskellのレイアウト依存構文

Haskellは関数型プログラミング言語の一種です。 いままで挙げた言語とは異なり、C言語の構文にまったく似てません。 C言語の視点でいえば、「文」に相当する構文要素はなく、「式」が実質的に文の役割をしています。 そして、式の並びを区切るためのセミコロンがあります

Haskellはレイアウトにもとづく一定の規則でセミコロンや波カッコを挿入する仕組みがあります

筆者はHaskellには詳しくないですが、以下の(やや古い)ページによると、次の規則でトークンが補われるそうです:

  • 一部のキーワードの直後に { がなかったら挿入する
  • 同じ深さに字下げされている行が続いたらセミコロンを挿入する
  • 字下げが浅い行が出てきたら } を挿入して閉じる
    • また、} によって構文エラーを回復できるときも挿入する

参考:

実際に } によって構文エラーが回復される例をリンク先から引用します:

    let x = e; y = x in e'

レイアウトだけみるとbの右辺が x in e' になりますが、それだと構文エラーになってしまうため、} が挿入されて、

    let { x = e; y = x } in e'

となります

レイアウトという仕組みを使う点で、セミコロンだけでなくブロックの構造を決めるための波カッコも書かなくてよくなっていること、構文エラーを判断基準にしていることが興味深いところです

F# のリスト

F# は .NET の関数型言語です (C# の仲間)。 構文は OCaml という言語をベースとしていて、それに前述の Haskell のようなレイアウト依存のルールを加えたものになっています

F# ではリストの区切りにセミコロンを使うのも特徴的です。このセミコロンも改行によって省略可能です

    //  xs = [ 0; 1; 2 ] と同じ
    let xs =
        [ 0
          1
          2 ]

セミコロン省略の仕組みをリストの区切りにも流用できているのが興味深いところです

内包表記と式文

※追記(2025-02):このセクションは余分だと思いますが、「文面の調整」作業を行う前の初稿に書かれているので、内容を維持するために残してあります

F# にはリスト内包表記の構文があります

    //  xs = [ 0; 1; 4; 9; 16 ]
    let xs = [ for n in 0..4 -> n * n ]

内包表記の中の構文は柔軟で、if式やループも使えます

    //  a = [ "fizz"; "1"; "2"; "fizz"; "4" ]
    let a =
        [ for n in 0..4 do
            if n % 3 = 0 then
                "fizz"
            else
                string n ]

この [] の中をみると、式文を通るたびにリストに値が追加されていくというふうにみなせます。 そう考えると、普通の式の並びであるリストも式文の並びであるといえるので、同一の仕組みによって省略可能なのは自然だったかもしれません

締め

C言語の構文から始めて省略する言語 (JS, Go) と省略しない言語 (Rust)、レイアウト依存の言語 (Haskell, F#) などについて書きました。 ほかにも無数の言語があります。 多様なアプローチがあっておもしろいですね


追記(2025-02): 文章を全体的に調整しました

関連記事