私たちは毎日プログラムを「書いて」います。if と書き、波括弧で囲み、行末にセミコロンを打つ。
しかし、その「書き方」——構文(シンタックス)——とは正確には何なのでしょうか。なぜ言語ごとに見た目が違うのか。
なぜある言語ではインデントが意味を持ち、別の言語では括弧だらけなのか。このシリーズは、ふだん意識しない「構文」そのものを主役に据え、
そこからパラダイム(プログラミングの様式)がどう生まれ、移り変わってきたのかを辿る旅です。
第1章の目的は、土台となる地図を手に入れることです。ソースコードが実行可能なものへと変わるまでに通過する 三つの層を理解し、「構文」と「意味」がきっぱり別物であることを腹落ちさせます。
ソースコードが通る三つの層
あなたが書いた x = a + b * 2; という一行は、コンパイラ(やインタプリタ)の内部で、
おおむね次の三段階を順番に通過します。これは現代のほぼすべての言語処理系に共通する基本構造です。
graph LR SRC[ソースコード\n文字の並び] LEX[字句解析\nLexical Analysis] PAR[構文解析\nParsing] SEM[意味解析\nSemantic Analysis] OUT[実行可能な形\nへ] SRC --> LEX LEX -->|トークン列| PAR PAR -->|構文木 AST| SEM SEM -->|注釈付き AST| OUT style LEX fill:#8b5cf6,stroke:#6d28d9,color:#fff style PAR fill:#3b82f6,stroke:#2563eb,color:#fff style SEM fill:#14b8a6,stroke:#0d9488,color:#fff
第1層: 字句解析 — 文字をトークンに刻む
最初の仕事は、ベタな文字列をトークン(token)という意味のある最小単位に切り分けることです。 これを字句解析(lexical analysis / tokenizing)と呼び、担当するのが字句解析器(レクサー、スキャナー)です。 日本語の文章を「単語」に分かち書きする作業に似ています。
入力(ただの文字列): x = a + b * 2 ;
字句解析の出力(トークン列):
x → 識別子 (identifier)
= → 演算子 (operator)
a → 識別子 (identifier)
+ → 演算子 (operator)
b → 識別子 (identifier)
* → 演算子 (operator)
2 → リテラル (literal)
; → 区切り (separator) この段階で、空白やコメントといった「人間のための飾り」は捨てられます。ここで登場する基礎用語を整理しておきましょう。 これらは本シリーズで何度も使う語彙です。
| 用語 | 意味 | 例 |
|---|---|---|
| トークン | 意味を持つ最小単位。カテゴリ(種別)を持つ | 識別子・演算子・リテラル など |
| 字句(lexeme) | トークンに対応する実際の文字列 | color という具体的な文字列 |
| キーワード | 言語が特別な意味を割り当てた語 | if while return |
| 予約語 | 識別子に使えない語。キーワードを含む広い概念 | Javaの goto(意味未定義だが予約済み) |
| 識別子 | 変数や関数につける名前 | myVar calcTotal |
| リテラル | コード中に直接書かれた固定値 | 42 "hello" true |
第2層: 構文解析 — トークンを木に組み立てる
トークンの列ができたら、次はそれらが文法的に正しい並びかを検証し、構造を持った木に組み立てます。
これが構文解析(parsing)で、担当するのがパーサです。たとえば a + b * 2 は、
「* は + より強く結びつく」という演算子の優先順位に従って、次のような木になります。
graph TD PLUS["+"] A["a"] MUL["*"] B["b"] TWO["2"] PLUS --> A PLUS --> MUL MUL --> B MUL --> TWO style PLUS fill:#3b82f6,stroke:#2563eb,color:#fff style MUL fill:#8b5cf6,stroke:#6d28d9,color:#fff
構文解析は文法(grammar)という規則の集合に照らして行われます。ほぼすべての言語の構文は 文脈自由文法(CFG: Context-Free Grammar)で記述でき、その記法(BNFなど)と理論は次章のテーマです。 ここで重要なのは、「文法に合っているか」だけを見て、「その処理に意味があるか」はまだ問わない、という点です。
第3層: 意味解析 — 形は正しい、では中身は?
構文木ができても、まだ「正しいプログラム」とは限りません。意味解析(semantic analysis)は、 構文的に正しいコードが意味的にも筋が通っているかを検査します。具体的には次のようなチェックです。
| 検査の種類 | 内容 | 捕まえるエラーの例 |
|---|---|---|
| 型チェック | オペランドの型が整合するか | int に文字列を代入 |
| スコープ解決 | その名前が参照可能か | 宣言されていない変数の使用 |
| 関数シグネチャ検査 | 引数の数・型・戻り値が合うか | 引数の数が足りない呼び出し |
| 制御フロー検査 | break/return が適切な位置にあるか | ループの外での break |
この段階を支える中心的なデータ構造がシンボルテーブルです。変数名・型・スコープ・関数定義を記録し、 「この名前は何者か」を解決します。文脈自由文法だけでは「変数は使う前に宣言せよ」のような 文脈に依存するルールを表現できないため、構文解析とは別の層として意味解析が必要になるのです。
構文と意味は別物である
ここで、このシリーズの根幹となる区別を明確にします。構文(syntax)は「形」、 意味(semantics)は「中身」です。この二つは独立しています。言語学に、この独立性を鮮やかに示した あまりに有名な例文があります。ノーム・チョムスキーが1957年に挙げたものです。
プログラムでも「構文は正しいが意味がおかしい」コードはいくらでも書けます。次の例はすべて パーサは通過する——文法的には完全に正しい——が、意味解析または実行時に問題が露呈します。
int a;
a = "hello"; // 構文OK、しかし型が不整合(意味エラー)
int z = 10 / 0; // 構文OK、しかし実行時にゼロ除算
return 0;
printf("unreachable"); // 構文OK、しかし到達不能なコード そして重要なのは、エラーがどの層で捕まるかが違うということです。
| エラーの種類 | 捕まる層 / タイミング | 例 |
|---|---|---|
| 構文エラー | 字句解析・構文解析(コンパイル時) | セミコロン欠落、括弧の不一致 |
| 静的な意味エラー | 意味解析(コンパイル時) | 型不一致、未宣言変数 |
| 動的な意味エラー | 実行時 | ゼロ除算、配列の範囲外参照 |
CST と AST — 二種類の構文木
「構文木」と一口に言いますが、実は性格の異なる二種類があります。違いを知ると、コンパイラやLinter、 コードフォーマッタがどう動くのかが見えてきます。
| 観点 | 具象構文木 (CST / Parse Tree) | 抽象構文木 (AST) |
|---|---|---|
| 表すもの | パーサが見た通りの忠実な木 | 構造と中身の本質だけ残した木 |
| 細かさ | 高い(全文法規則を反映) | 低い(余計な記号を捨てる) |
| セミコロンや括弧 | ノードとして残る | 基本的に捨てられる |
| サイズ | 大きい | 小さく処理が速い |
| 主な使いどころ | パースの中間生成物 | コンパイラ後段・型検査・変換 |
たとえば return a + 2; をパースすると、CSTは文法の階層をすべて経由した冗長な木になり、
セミコロンも明示的なノードとして含まれます。一方ASTは、本質だけを抜き出したシンプルな木です。
return a + 2; を AST にすると:
ReturnStatement
└─ BinaryOp(+)
├─ Identifier("a")
└─ IntLiteral(2)
セミコロン ";" は構文的なゴミとして捨てられ、
「return文の中身は a+2 という足し算だ」という本質だけが残る 「糖衣構文」という言葉の誕生
最後に、本シリーズ全体を貫くキーワードを紹介します。糖衣構文(syntactic sugar)です。 これは「取り除いても言語の表現能力は変わらないが、人間にとって書きやすく・読みやすくするための構文」を指します。 言語を人間にとって「甘く(sweet)」する、いわば甘味料です。
この言葉を造語したのは、イギリスの計算機科学者ピーター・ランディン(Peter Landin)で、 1964年の論文「The mechanical evaluation of expressions」の中でのことでした。 彼はラムダ計算を使って言語の意味を定義する文脈で、ある書き方を別の本質的な書き方へ「翻訳」できることを示し、 その「見た目だけの甘さ」を糖衣と呼んだのです。
糖衣を「元の素朴な形」へ戻す変換を脱糖(desugaring)と呼びます。たとえば次のように。
// 糖衣構文(アロー関数)
const add = (a, b) => a + b;
// 脱糖後(昔ながらの関数式)
const add = function (a, b) { return a + b; };
現代の言語は糖衣だらけです。リスト内包表記、async/await、分割代入、for-each——
これらは「無くても書けるが、あると人生が楽になる」構文たちです。それらがどう脱糖され、構文がいかに
「書きやすさ・表現力」を作り出すのかは、第5章でたっぷり扱います。まずは「構文=人間のための形であり、
その下には機械のための本質がある」という二層の感覚を持っておいてください。
まとめ — 地図の完成
本章では、構文を語るための土台を整えました。ソースコードは字句解析・構文解析・意味解析の三層を通り、 途中でCSTからASTへと本質が抽出されます。そして構文(形)と意味(中身)は独立しており、 「形は正しいが中身は無意味」なコードが存在します。最後に、人間のための甘味料糖衣構文という概念を手に入れました。
では、構文解析が照らし合わせる「文法」そのものは、どうやって厳密に書き下すのでしょうか。 次章では、ジョン・バッカスとピーター・ナウアが生んだBNFという発明から、文法記述の歴史と理論に分け入ります。
理解度チェック
ソースコードが実行可能な形になるまでに通過する3つの層を、処理される順に並べてください。
矢印ボタンで正しい順序に並べ替えてください