私たちは毎日プログラムを「書いて」います。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
a + b * 2 の構文木。* が + より下(=先に計算される)に位置することで優先順位が表現される

構文解析は文法(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という発明から、文法記述の歴史と理論に分け入ります。

理解度チェック

問題 0 / 50%
Q1

ソースコードが実行可能な形になるまでに通過する3つの層を、処理される順に並べてください。

矢印ボタンで正しい順序に並べ替えてください

1意味解析(型やスコープを検査する)
2構文解析(トークンを構文木に組み立てる)
3字句解析(文字をトークンに刻む)