Я занимаюсь изучением синтаксиса языков программирования и пытаюсь отделить фундаментальные закономерности от исторических случайностей. Одна из таких случайностей - почти повсеместное использование терминов выражение (expression) и инструкция (statement) при описании и классификации синтаксиса языков программирования.

Интуитивно кажется, что 2 + 2 и if (x) { foo(); } - это совершенно разные сущности, но при более глубоком анализе оказывается, что подобное разделение искусственное и возникло из-за архитектурных особенностей вычислительных машин почти полвека назад и с тех пор просто «переходит» из языка в язык.

Часть 1. Историческая

Чтобы понять, откуда взялось разделение синтаксиса на выражения (expression) и инструкции (statement), нужно проследить эволюцию синтаксических правил от первых языков высокого уровня.

FORTRAN -> ALGOL 60 -> C: три акта одной пьесы

  • FORTRAN (1957) - разделение «по железу». В языке, созданном для IBM 704, выражения (X + Y*Z) были строго отделены от управляющих инструкций (IF, DO, GOTO). Первые вычисляли значения, вторые управляли потоком исполнения и смешивать их между сбой было нельзя. Эта концепция синтаксиса напрямую отражала архитектуру вычислительных машин того времени: программа понималась как последовательность машинных команд, каждая из которых либо вычисляла операнд, либо изменяла счётчик команд, но не одновременно.

  • ALGOL 60 (1960). В ALGOL 60 ввели составной оператор begin ... end (прообраз блока), но важнее другое: язык разрешил условное выражение. Конструкция if B then E1 else E2 могла появляться в позиции любого выражения, а значит, допускала запись

    x := if a > b then a else b
    

    Это был прямой предшественник тернарного оператора, показавший, что выбор между двумя значениями прекрасно вписывается в роль выражения.

  • C (1972) - фиксация различий Expression vs Statement. Деннис Ритчи (при участии Кена Томпсона, автора языка-предшественника B), проектируя C как «переносимый ассемблер» для PDP-11, закрепил в грамматике чёткое различие между этими понятиями. Такое решение диктовалось стремлением к максимально прямому отображению конструкций языка на машинные инструкции: условный переход jz процессора - это действие, а не способ вычислить значение.

Lisp и параллельная вселенная без statement’ов

Почти синхронно с FORTRAN, в 1958 году, Джон Маккарти создал Lisp, фундаментально основанный на лямбда-исчислении. Здесь нет понятия statement - вся программа состоит из S-выражений: будь то вызов функции, условная конструкция или блок.

В Лиспе конструкция (if (> a b) a b) - это просто выражение, значение которого можно присвоить, передать в функцию или использовать в более сложной композиции. А блок (let ((x 5)) (+ x 1)) возвращает 6, оставаясь обычным выражением.

Это не «расширение» возможностей, а фундаментальный иной принцип: вся программа - это вычисляемые значения, а не последовательность управляющих инструкций. С этой позиции разделение на expression и statement выглядит ненужным.

Часть 2. Современные языки

Интересно посмотреть, насколько глубоко в современных языках укоренилась граница между этими терминами и как она преодолевается.

Классическая модель: C, C++, Java (и Python)

В этих языках if, for, while категорически не могут быть частью выражения. Код

int y = if (x > 0) { 1; } else { 2; }   // ошибка компиляции

недопустим. Разработчики вынуждены либо использовать тернарный оператор:

int y = (x > 0) ? 1 : 2;

либо (в C/C++) прибегать к нестандартным расширениям вроде GCC statement expressions:

int y = ({ if (x > 0) 1; else 2; });    // только с расширениями GCC

В Python ситуация аналогична: if - это инструкция, но начиная с версии 2.5 введено тернарное выражение:

y = 1 if x > 0 else 2

Эта конструкция - точный аналог тернарного оператора ?:, «заплатка» для грамматики, которая не решилась сделать if/else полноценным выражением.

JavaScript: эмуляция выражения через функцию

JavaScript формально сохраняет C-подобное разделение, однако в язык встроены обходные инструменты. Используя IIFE (немедленно вызываемые функциональные выражения), разработчик может «обернуть» statement-логику в контекст, где требуется значение:

let y = (function() {
    if (x > 0) return 1;
    else return 2;
})();

До появления современных JIT-оптимизаций подобный трюк нёс реальные накладные расходы на вызов функции, но он наглядно демонстрирует ручную эмуляцию expression-грамматики поверх statement. Показательно, что раз сообщество идёт на подобные ухищрения - значит, потребность в таком подходе велика.

Ruby: всё есть выражение

В Ruby любая конструкция - выражение, без исключений. Можно написать:

y = if x > 0
  1
else
  2
end

z = case value
    when 1 then "one"
    when 2 then "two"
    else "other"
    end

w = while false
  # тело не выполняется
end  # w равно nil, но сам while - валидное выражение

Даже определение класса или метода возвращает значение последнего выражения. Грамматика Ruby не вводит отдельного нетерминала «statement» - вся программа состоит только из выражений.

Rust: гибрид - ни нашим ни вашим

Rust предлагает своеобразный компромисс: формально в грамматике различие между statement и expression есть (let-декларации, объявления элементов - это не expression), но управляющие конструкции (if, match, loop) целиком отнесены к категории Expression - в отличие от C/C++, где это принципиально невозможно.

При этом в Rust есть немало запутанных правил вокруг конструкций, которые вроде бы expression, но ведут себя странно в зависимости от контекста. Например:

let x = while true { break 5; };  // ОШИБКА! while не умеет так

тогда как очень похожий loop - умеет:

let x = loop { break 5; };  // ОК! x == 5

Различие между loop и while/for в том, что компилятор не может гарантировать, что тело while/for вообще выполнится хоть раз (условие может быть ложным с самого начала), тогда как loop либо бесконечен, либо выходит через break с конкретным значением. Эта асимметрия поведения чисто техническая, но принципиально влияет на синтаксис этих конструкций.

Или вот if со скобками и без них ведёт себя неожиданно:

fn f() -> i32 {
    if x > 0 { 1 } else { 2 } - 1
}

Кажется, что это должно вычислить (if...) - 1. Но нет! Компилятор трактует if {...} else {...} как отдельный statement (потому что он стоит не в «хвостовой» позиции блока), а - 1 - как отдельное выражение - унарный минус. Функция вернёт ошибку типов, потому что последней строкой оказывается -1, а значение if-выражения будет просто отброшено.

Чтобы получить ожидаемое поведение, нужно явно обернуть if в скобки:

let y = (if x > 0 { 1 } else { 2 }) - 1;  // так работает как надо

Но самым странным является поведение точки с запятой, которая может менять тип функции:

fn f() -> i32 {
    5 + 3;   // с точкой с запятой!
    10
}

А если поставить ; после последней строки:

fn f() -> i32 {
    5 + 3;
    10;      // теперь тут ;
}
// ОШИБКА: функция должна вернуть i32, а вернула ()

; (точка с запятой) в Rust - это не просто «конец строки», а оператор, буквально меняющий тип выражения на () (пустой тип). Забытая или лишняя точка с запятой - самая частая причина неочевидных ошибок среди новичков.

Rust действительно продвинулся дальше C в сторону «всё есть expression», но это не единый принцип, а набор точечных, местами противоречащих друг другу правил, каждое из которых решает конкретную инженерную задачу (типовую безопасность, гарантии терминации, разрешение неоднозначности парсинга), а не следствие одной красивой идеи, последовательно применённой везде.

Часть 3. Что на самом деле отличает Statement от Expression?

Если вернуться к изначальному вопросу, так чем же отличается statement от expression?

Отвергнутые критерии

  1. «Statement - выполнение, Expression - вычисление» - оба термина связаны с вычислительными действиями, разница не в природе операции.

  2. «Statement не возвращает значение» - опровергается Ruby, где всё возвращает значение.

  3. «Statement - отдельная категория грамматики» - это просто описание симптома (того, что в конкретном языке зафиксировано на уровне BNF), а не объяснение принципиальных различий между терминами.

Более продуктивным оказывается взгляд на пару statement/expression как на структурную композицию, в которой:

  • Expression - неделимая лексическая единица: литерал, идентификатор, вызов функции, математическая операция.

  • Точка с запятой ; (или её аналоги - перевод строки в Ruby) играет роль оператора-разделителя, который сообщает компилятору: нужно вычислить выражение слева, отбросить его значение и перейти к правой части. Цепочка из нескольких выражений, последовательно соединённых ; и образует statement.

  • Составной блок, ограниченный фигурными скобками {...} - это способ превратить цепочку из нескольких выражений в единое неделимое выражение. Значением блока становится значение последнего выражения в нём.

Таким образом, statement - это не альтернативная категория синтаксиса, а композиция одного или нескольких выражений, соединённых оператором ;, где значение всей цепочки равно значению последнего элемента. Запись:

expression1;
expression2;
...
expressionN

можно прочитать как (expression1 ; expression2 ; ... ; expressionN), и её значением является значение expressionN. А если нужно оформить такую цепочку как единое выражение, то её помещают в фигурные скобки:

{ expression1; expression2; ...; expressionN }

В языках, поддерживающих такую интерпретацию, блоки естественно встают на место выражений в любом контексте - в правой части присваивания, как аргументы функции, как операнды.

А как же оператор запятая в C/C++?

Оператор запятая в C/C++ очень похож на реализацию описанной формальной модели - композицию выражений:

int y = (foo(), bar(), 42);   // foo() и bar() вычислены ради побочного эффекта, y == 42

Но есть принципиальное отличие: оператор запятая - это всегда Expression и не может выйти за пределы одного expression-контекста.

int y = foo(), bar();   // Это НЕ comma-expression! Это два отдельных объявления переменных!
int z = (foo(), bar());  // а вот это - настоящий comma operator, нужны скобки

Без явных скобок оператор , (запятая) в контексте объявления переменных или списка аргументов функции интерпретируется грамматикой иначе - как разделитель в списке (declarator-list, argument-list). И эта неоднозначность грамматики C/C++ разрешается только контекстом.

Кроме того, оператор ,(запятая) ограничен уровнем expression и не может содержать statement-конструкции:

int y = (if (x > 0) 1; else 2, 42);   // ОШИБКА - if не Expression, нельзя вставить внутрь ","

И напоследок, comma operator имеет самый низкий приоритет среди операторов C, и область его применения жёстко ограничена контекстами, где парсер может быть точно уверен, что это не разделитель списка:

f(a, b, c);   // это вызов с тремя аргументами, а не comma-expression!
f((a, b), c); // а вот это уже - comma-expression как первый аргумент

Часть 4. Формальная грамматика универсального синтаксиса

Если попробовать описать expression и statement в общем виде, получится красивая взаимная рекурсия между ними:

Program     := Statement

Expression  := atomic-expr
             | Expression binary-op Expression
             | unary-op Expression
             | Expression '(' arg-list ')'          // вызов функции
             | '{' Statement '}'                     // сгруппированный Statement - тоже Expression!

Statement   := Expression
             | Expression ';' Statement              // рекурсивная композиция через ;

arg-list    := Expression (',' Expression)*

Где:

  • Statement - это либо одно выражение, либо выражение, за которым следует ; и новый statement (рекурсивно).

  • Expression - это атомарное выражение (идентификатор, литерал, операция, вызов) либо { statement }, то есть блок, внутри которого находится statement, но который сам рассматривается как единое выражение.

Именно взаимная рекурсия, при которой блоки могут содержать statement, а statement строится из выражений, придаёт такой модели логичность и стройность, тогда как разные языки её по-разному ограничивают:

  • C-подобные языки разрешают переход { statement } -> expression только для простых выражений, но запрещают для управляющих конструкций. Поэтому if (x) { ... } нельзя использовать как выражение, хотя блок в фигурных скобках сам по себе мог бы быть выражением, если бы грамматика это допускала. Более того, GCC-расширение ({ ... }) буквально реализует такое правило: { statement } интерпретируется как expression, тогда как стандартный C так не умеет.

  • Lisp-подобные языки и Ruby вообще не нуждаются в отдельном нетерминале statement - в этих языках всё является выражением.

  • Rust оказался где-то посередине. В нём есть категория объявлений (let, fn, mod), которые являются инструкциями и не могут быть частью выражения. Тем не менее все управляющие конструкции (if, match, loop и т. д.) являются выражениями.

Заключение

Разделение на expression и statement, привычное для многих поколений разработчиков C-подобных языков - не логическая необходимость, а историческая случайность, закреплённая архитектурой машин фон Неймана, которую перенесли из FORTRAN в ALGOL, а затем в C. Оно зафиксировалось в грамматиках как «удобное» решение на заре компиляторостроения, но не имеет фундаментальных причин на уровне синтаксиса.

Анализ развития синтаксиса разных языков программирования показывает, что statement можно рассматривать как структурную композицию выражений, а блоки в скобках - как инструмент обратной «упаковки» такой последовательности в единое целое. Понимание истинной природы statement избавляет от ненужных условностей и даёт единый взгляд на синтаксис, в котором точка с запятой - всего лишь оператор, а фигурные скобки - не отличительный признак statement, а мост между двумя мирами, которых на самом деле никогда не было.

Комментарии (6)


  1. mbait
    02.08.2026 09:35

    Вместо тысячи слов хотелось бы увидеть пример дизайна языка, где нет разделения на statements и expressions, но и нет уродливого побочного синтаксиса. Скажем, решаем мы, что последнее выражение это значение блока while, но тут мы пишем последней строкой break. И приходится либо задавать break возвращаемый тип, либо... у нас снова разделение.


    1. rsashka Автор
      02.08.2026 09:35

      Ruby ?


      1. mbait
        02.08.2026 09:35

        То есть, вы просто посмотрели синтаксис языка и решили, что в компиляторе нет разделения на expr/stmt?


        1. rsashka Автор
          02.08.2026 09:35

          Что значит “посмотрел синтаксис”? Это фундаментальная особенность языка.


    1. ImagineTables
      02.08.2026 09:35

      Nemerle


      1. rsashka Автор
        02.08.2026 09:35

        Точно!