Синтаксис (програмування)

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку
Підсвічування синтаксису та відступи часто використовуються для допомоги програмістам у розпізнаванні елементів вихідного коду. Цей код Python використовує кольорове виділення.

Синтаксис комп'ютерної мови — це сукупність правил, що визначають комбінації символів, які вважаються правильно структурованим документом або фрагментом цієї мови[1]. Це стосується як мов програмування, результатом якого є початковий код, так і мов розмітки, результатом якого є дані.

Синтаксис мови визначає її форму[2]. Текстові комп'ютерні мови базуються на послідовностях символів, а візуальні мови програмування — на просторовому макеті та зв'язках між символами (які можуть бути текстовими чи графічними). Про документи, які синтаксично не правильні, кажуть, що вони мають синтаксичну помилку[en]. Під час розробки синтаксису мови, розробник починає писати приклади як правильних, так і неправильних рядків, перш ніж спробувати з'ясувати загальні правила з цих прикладів[3].

Отже, синтаксис відноситься до форми, і він протиставляється семантиці — значенням. При використанні комп'ютерних мов семантична обробка зазвичай відбувається після синтаксичної; однак, в деяких випадках для повного синтаксичного аналізу необхідна семантична обробка, і вони виконуються або разом або одночасно. У компіляторі синтаксичний аналіз включає передній план, тоді як семантичний аналіз включає вихідний (і проміжну фазу, якщо цю фазу розрізняють).

Рівні синтаксису[ред. | ред. код]

Синтаксис комп'ютерної мови, як правило, розрізняють на три рівні:

  • Слова — лексичний рівень, що визначає, як символи утворюють лексеми;
  • Фрази — рівень граматики, який визначає, як лексеми утворюють фрази;
  • Контекст — визначення, на що посилаються імена об'єктів або змінних, якщо типи є придатними, тощо.

Такий підхід забезпечує модульність, яка дозволяє описувати та обробляти кожен рівень окремо і часто незалежно. По-перше, лексер перетворює лінійну послідовність символів у лінійну послідовність лексем (також відомо як «лексичний аналіз»). По-друге, аналізатор перетворює лінійну послідовність лексем у ієрархічне дерево синтаксису (відомо як «синтаксичний аналіз»). По-третє, контекстуальний аналіз визначає імена та перевіряє типи. Ця модульність іноді можлива, але у більшості мов світу попередній крок залежить від пізнішого кроку — наприклад, хакерство лексерів[en] у C відбувається тому, що токенізація залежить від контексту. Навіть у цих випадках синтаксичний аналіз часто сприймається як наближення до ідеальної моделі.

Стадії синтаксичного розбору можна розділити на дві частини: дерево розбору, або «дерево конкретного синтаксису», яке визначається граматикою, але, як правило, занадто детальне для практичного використання, і абстрактне синтаксичне дерево (АСД), що спрощує задачу до зручної форми. Етапи АСД та контекстуального аналізу можна вважати формою семантичного аналізу, оскільки вони додають значення та інтерпретацію синтаксису, або ручну реалізацію синтаксичних правил, яку було б важко або незручно описати чи реалізувати формально.

Рівні зазвичай відповідають рівням в ієрархії Чомських. Слова належать регулярній мові, породжуваній безконтекстною мовою[en] (КВМ), як правило, детермінованою безконтекстною мовою[en] (ДКВМ), зазначеною в граматиці структури[en] фрази, яка є граматикою типу 2, поданою як породжувальні правила[en] у формі Бекуса-Наура (ФБН). Граматики фраз часто задаються в набагато більш обмежених граматиках, ніж повні контекстно-вільні граматики, щоб зробити їх більш легкими для синтаксичного аналізу; в той час як аналізатор LR[en] може аналізувати будь-яку ДКВМ в лінійному часі, простий синтаксичний аналізатор LALR[en] і ще простіший синтаксичний аналізатор LL ефективніші, але можуть аналізувати тільки граматики, породжувальні правила яких є обмеженими.

Загалом, контекстна структура може бути описана контекстно-залежною граматикою та автоматично проаналізована такими засобами, як граматика атрибутів[en], хоча, як правило, цей крок виконується вручну, за допомогою правил розв'язання імен[en] та перевірки типу, і реалізується через таблицю символів, який зберігає назви та типи для кожної області застосування.

Були написані інструменти, які автоматично генерують лексер з лексичної специфікації, написаної в регулярних виразах, і парсер з граматики фраз, написаної в ФБН: це дозволяє використовувати декларативне програмування, а не процедурне чи функціональне. Помітним прикладом є пара lex[en]-Yacc. Вони автоматично створюють конкретне синтаксичне дерево. Після цього автор-аналізатор повинен вручну написати код, який описує, як це перетворюється на абстрактне синтаксичне дерево. Контекстний аналіз також зазвичай виконується вручну. Незважаючи на існування цих автоматичних інструментів, синтаксичний аналіз часто реалізовується вручну з різних причин — можливо, структура фрази не є контекстною, або альтернативна реалізація покращує ефективність чи повідомлення про помилки, або дозволяє граматику змінювати легше. Парсери часто пишуться на функціональних мовах, таких як Haskell, або на мовах скриптів, таких як Python або Perl, або на C або C++.

Приклади помилок[ред. | ред. код]

Наприклад, (add 1 1) — є синтаксично правильною програмою Lisp (за умови, що функція «add» існує, інакше дозвіл імен не виконується), яка додає 1 і 1. Однак наступні дії є неприпустимими:

(_ 1 1) lexical error: '_' is not valid 
(add 1 1 parsing error: missing closing ')'

Зверніть увагу на те, що лексер не може ідентифікувати першу помилку — все, що він знає, це те, що після створення токена LEFT_PAREN, після '(' подальша частина програми непридатна, оскільки, згідно правил, жодне слова не починається з '_'. Друга помилка виявляється на етапі синтаксичного аналізу: синтаксичний аналізатор визначив породжувальне правило «list» через токен '(' (як єдиний збіг), і тому може видати повідомлення про помилку; у загальному випадку вона може бути неоднозначною[en].

Помилки типу і неоголошені помилки змінних іноді вважаються синтаксичними помилками, коли вони виявляються під час компіляції (що зазвичай буває при компіляції строго типізованих мов), хоча зазвичай такі помилки класифікуються як семантичні[4][5][6].

Як приклад — код Python

 'a' + 1 

містить помилку типу, так як вона додає рядковий літерал до цілочисельного літералу. Типи помилок такого роду можуть бути виявлені під час компіляції: вони можуть бути знайденими під час аналізу (аналізу фрази), якщо компілятор використовує окремі правила, які дозволяють «integerLiteral + integerLiteral», але не «stringLiteral + integerLiteral», хоча більш імовірно, що компілятор буде використовувати правило парсингу, яке дозволяє всі вирази виду «LiteralOrIdentifier + LiteralOrIdentifier», і тоді помилки будуть виявлені в ході контекстуального аналізу (при перевірці типу). У деяких випадках ця перевірка не виконується компілятором, і ці помилки виявляються лише під час виконання.

У мовах з динамічною типізацією багато помилок типу можна виявити лише під час виконання. Наприклад, код Python

 a + b 

синтаксично коректний на рівні фраз, але коректність типів a і b може бути визначена тільки під час виконання, оскільки змінні не мають типів у Python, а лише значення. Якщо існують розбіжності щодо того, чи повинна помилка типу, виявлена компілятором, називатися синтаксичною помилкою (а не статичною семантичною помилкою), помилки типу, які можна виявити лише під час виконання програми, завжди розглядаються як семантичні, а не синтаксичні помилки.

Визначення синтаксису[ред. | ред. код]

Файл:Python add5 parse.svg
Розбір дерева коду Python з токенізацією вставки

Синтаксис текстових мов програмування зазвичай визначається з допомогою комбінації регулярних виразів (для лексичної структури) та нотації Бекуса-Наура (для граматичної структури) для індуктивного визначення синтаксичних категорій та символів терміналу. Синтаксичні категорії визначаються правилами, вони називаються породжувальними[en] та визначають до якої синтаксичної категорії належать значення[2]. Термінальні символи — це конкретні символи або рядки символів (наприклад, ключові слова, такі як define, if, let або void), з яких будуються синтаксично коректні програми.

Мова може мати різні еквівалентні граматики, такі як еквівалентні регулярні вирази (на лексичному рівні) або різні правила фраз, які генерують одну й ту саму мову. Використання більш широкої категорії граматик, такої як граматика LR, може дозволити більш короткі або більш прості граматики у порівнянні з обмеженими категоріями, такими як граматика LL, яка може зажадати більш довгих граматик з великою кількістю правил. Різні, але еквівалентні граматики фраз дають різні дерева синтаксичного аналізу, хоча базова мова (набір допустимих документів) одна і та сама.

Приклад: Lisp S-вирази[ред. | ред. код]

Нижче наведена проста граматика, визначена з використанням нотації регулярних виразів і розширеної нотації Бекуса-Наура. Приклад описує синтаксис S-виразів та даних для мови програмування Lisp, який визначає породжувальне правило[en] для синтаксичних категорій expression, atom, number, symbol та list:

expression = atom  | list
atom    = number | symbol  
number   = [+-]?['0'-'9']+
symbol   = ['A'-'Z']['A'-'Z''0'-'9'].*
list    = '(', expression*, ')'

Ця граматика визначає наступне:

  • вираз — це атом або список;
  • атом — це число або символ;
  • число — це неперервна послідовність однієї або декількох десяткових цифр, яким необов'язково передує знак плюс або мінус;
  • символ — це літера за якою йдуть нуль або більше будь-яких символів (за винятком пробілів);
  • список — це узгоджена пара дужок всередині якої нуль або більше виразів;

Тут десяткові цифри, символи верхнього і нижнього регістру, а також круглі дужки є термінальними символами.

Нижче наведені приклади правильно побудованих послідовностей лексем в цій граматиці: '12345', '()', '(A B C232 (1))'

Складні граматики[ред. | ред. код]

Граматику, необхідну для визначення мови програмування, можна класифікувати за її положенням в ієрархії Чомскі. Граматика фраз більшості мов програмування може бути визначена за допомогою граматики типу 2, тобто вони є контекстно-вільними граматиками[7], хоча загальний синтаксис є контекстно-залежним (через оголошення змінних і вкладені області), отже, типом 1. Однак є винятки, і для деяких мов граматика фраз має тип 0 (Turing-complete).

В деяких мовах, таких як Perl і Lisp, специфікація (чи реалізація) мови дозволяє створювати конструкції, які виконуються на етапі синтаксичного аналізу. Крім того, ці мови мають конструкції, які дозволяють програмісту змінювати поведінку аналізатора. Ця комбінація ефективно розмиває відмінність між синтаксичним розбором та виконанням, а також робить аналіз синтаксису нерозв'язною проблемою в цих мовах, що означає, що фаза синтаксичного аналізу може не завершитися. Наприклад, в Perl можна виконати код під час синтаксичного аналізу з використанням оператора BEGIN, а прототипи функцій Perl можуть змінити синтаксичну інтерпретацію і, можливо, навіть синтаксичну валідність коду[8]. У розмовній мові це називається «тільки Perl може аналізувати Perl» (тому що код повинен виконуватися під час синтаксичного аналізу і може змінювати граматику), або більш сильно «навіть Perl не може аналізувати Perl» (бо це нерозв'язне). Аналогічно, макроси Lisp, введені синтаксисом defmacro, також виконуються під час синтаксичного аналізу, а це означає, що компілятор Lisp повинен мати всю систему часу виконання Lisp. На відміну від цього, макроси C є просто заміною рядків і не вимагають виконання коду[9][10].

Синтаксис проти семантики[ред. | ред. код]

Синтаксис мови описує форму допустимої програми, але не надає ніякої інформації про значення програми або результати виконання цієї програми. Значення, що надається комбінації символів, обробляється семантикою. Не всі синтаксично правильні програми є семантично правильними. Існує багато синтаксично правильних програм, проте, вони утворені не правильно відповідно до правил мови; і можуть (в залежності від специфікації мови і обґрунтованості реалізації[en]) привести до помилки при перекладі або виконанні. У деяких випадках такі програми можуть проявляти невизначену поведінку. Навіть коли програма чітко визначена в мові, вона все одно може мати значення, яке не передбачувалося автором.

Під час використання природної мови як прикладу, можемо зробити висновок, що неможливо призначати значення граматично правильному реченню, або речення може бути помилковим:

  • «Безбарвні зелені ідеї люто сплять[en]»: граматично правильно утворене речення, але не має прийнятного значення.
  • «Джон — одружений холостяк»: граматично правильно утворене речення, але виражає значення, яке не може бути правдою.

Наступний фрагмент мови C синтаксично коректний, але виконує операцію, яка не визначена семантично (оскільки p — це нульовий вказівник, операції p-> real і p-> im не мають сенсу):

 complex *p = NULL;
 complex abs_p = sqrt (p->real * p->real + p->im * p->im);

Або простіший приклад:

 int x;
 printf("%d", x);

є синтаксично допустимим, але не є семантично визначеним, оскільки використовує неініціалізовану змінну[en]. Незважаючи на те, що компілятори для деяких мов програмування (наприклад, Java і C#) будуть виявляти помилки неініціалізованих змінних такого роду, вони повинні розглядатися як семантичні помилки, а не помилки синтаксису[6][11].

Див. також[ред. | ред. код]

Щоб швидко порівняти синтаксис різних мов програмування, перегляньте список з прикладами програми «Hello world!» на різних мовах:

Примітки[ред. | ред. код]

  1. What is Syntax?. www.computerhope.com (en). Процитовано 2019-08-05. 
  2. а б Friedman, Daniel P.; Mitchell Wand; Christopher T. Haynes (1992). Essentials of Programming Languages (вид. 1st). The MIT Press. ISBN 0-262-06145-7. 
  3. Smith, Dennis (1999). Designing Maintainable Software. Springer Science & Business Media. 
  4. Aho, Alfred V.; Monica S. Lam; Ravi Sethi; Jeffrey D. Ullman (2007). Compilers: Principles, Techniques, and Tools (вид. 2nd). Addison Wesley. ISBN 0-321-48681-1. Section 4.1.3: Syntax Error Handling, pp.194–195.
  5. Louden, Kenneth C. (1997). Compiler Construction: Principles and Practice. Brooks/Cole. ISBN 981-243-694-4.  Exercise 1.3, pp.27–28.
  6. а б Semantic Errors in Java. 
  7. Michael Sipser (1997). Introduction to the Theory of Computation. PWS Publishing. ISBN 0-534-94728-X.  Section 2.2: Pushdown Automata, pp.101–114.
  8. В наступних обговореннях можна знайти приклади:
  9. An Introduction to Common Lisp Macros. Apl.jhu.edu. 1996-02-08. Архів оригіналу за 2013-08-06. Процитовано 2013-08-17. 
  10. The Common Lisp Cookbook - Macros and Backquote. Cl-cookbook.sourceforge.net. 2007-01-16. Процитовано 2013-08-17. 
  11. Issue of syntax or semantics?

Посилання[ред. | ред. код]