Язык Си в примерах/Простая грамматика

Материал из Викиучебника — открытых книг для открытого мира
Перейти к навигации Перейти к поиску

Язык Си в примерах


  1. Компиляция программ
  2. Простейшая программа «Hello World»
  3. Учимся складывать
  4. Максимум
  5. Таблица умножения
  6. ASCII-коды символов
  7. Верхний регистр
  8. Скобочки
  9. Факториал
  10. Степень числа
  11. Треугольник Паскаля
  12. Корень уравнения
  13. Система счисления
  14. Сортировка
  15. Библиотека complex
  16. Сортировка на основе qsort
  17. RPN-калькулятор
  18. RPN-калькулятор на Bison
  19. Простая грамматика
  20. Задача «Расчёт сопротивления схемы»
  21. Простая реализация конечного автомата
  22. Использование аргументов командной строки
  23. Чтение и печать без использования stdio
  24. Декодирование звукозаписи в формате ADX

Грамматики (подмножества множества слов в некотором алфавите ) можно описывать с помощью правил. Каждое правило состоит из левой и правой части, между которыми стоит стрелка ( -> ).


Данную стрелку можно интерпретировать как «представляет собой» или «состоит из». Например, правило

A -> B C D

можно прочитать как

«Слово типа A представляет собой слияние слова типа B, слова типа C и слова типа D.»

Но в теории формальных грамматик принята другая терминология:

«Из символа A можно вывести последовательность символов B, C и D.»

Расcмотрим три правила, в которых присутствует только один символ S (один нетерминальный символ), и три терминальных (то есть нераскладываемых, таких символов, из которых ничего нельзя вывести кроме их самих) символов '0', '1', '2', '3':

 S -> '0'
 S -> '1' S
 S -> '2' S S
 S -> '3' S S S 

Правило S -> '3' S S S , к примеру, означает, что если a, b и c являются корректными словами (словами, выводимыми из символа S), то и слово 3abc тоже является корректным.

  • Примеры корректных слов: 0, 10, 110, 200, 2100, 2010, 1111111110.
  • Примеры некорректных слов: 01, 00, 300, 3333, 11120.

Приведённая ниже программа на Си определяет корректность введённого слова.

 #include <stdio.h>
 #include <limits.h>
 int Read()
 {
     int head, i;
     scanf("%d", &head);           // Читаем первое число.
     if(head < 0 || head > 3) {    // Возвращаем код ошибки, если оно не из
        return 1;                  //     множества {0, 1, 2, 3} 
     }
     for(i = 0; i < head ; i++) {
        if(Read()) {
            return 1;
        }
     }
     return 0;
 }
 int main()
 {
   if(!Read()) {  
      int n;
      if(scanf("%d", &n) != 0)
         printf("incorect\n");
      else
         printf("correct\n");
   } else {
      printf("incorrect\n");
   }
   return 0;   
 }

Здесь представлен классический рекурсивный способ лексографического разбора.

Программный код можно максимально приблизить к самим правилам:

  ReadS() {
      if( scanf("%d", &n) != 1 ) return 0;
      switch(c) {
        '0': return 1;
        '1': return ReadS();
        '2': return (ReadS() && ReadS() );
        '3': return (ReadS() && ReadS() && ReadS());
         default: return 0;
      }
  }

Задание[править]

Задача 1. Напишите программу, определяющую корректность слова, не используя идеи рекурсии и стека. Для этого обявите переменную n, равную количеству объектов S, которые осталось считать. Изначально n = 1.
Задача 2. Напишите программу, которая определяет, является ли введённое слово выводимым из символа S.
 S ->  A | B
 A -> '(' B* ')'
 B -> '[' A* ']'

Вертикальная черта «|» означает соединительный союз «или».

Здесь используется специальный символ «*». Это указатель количества. Запись B* означает любое количество слов типа B (слов, выводимых из символа B), либо пустое слово. Другими словами, символ «*» означает 0, 1, 2,... раз. Есть также специальные символы + и ?:

  • * -- 0,1, ..., раз.
  • + -- 1,2, ..., раз (то есть, любое число раз, но по крайней мере один раз).
  • ? -- 0 или 1 раз (то есть либо есть, либо нет).

Примеры слов, выводимых из A:

  (), ([]), ([][()()]), ([][][][])  

Примеры слов, выводимых из B:

  [], [()], [([])([])()], [([][][][])]  

Подсказка: определите две функции ReadA и ReadB которые рекурсивно вызывают друг друга.

Код примерно должен быть таким:

 ReadChar(char x) {
    int c;
    if((c=getchar()) != x) {
        ungetc(c);
        return 0;
    } else {
        return 1;
    }
 }
 ReadA() {
     int c;
     if ( !ReadChar( '(' ) ) return 0;  // читаем открывающую скобку
     while(ReadB()) {};                 // читаем B*
     if ( !ReadChar( ')' ) ) return 0;  // читаем закрывающую скобку
     return 1;
 }
 ReadB() {
     ... // по аналогии с ReadA
 }
 ReadS() {
     ReadA || ReadB;
     if(getchar() != EOF) 
         printf ("Incorrect\n");
     else
         printf ("Correct\n");           
 }

Разбор языков (parsing), которые задаются простыми рекурсивными грамматиками, реализуют с помощью рекурсивных функций, которые возвращают 1 (успешно считано) или 0 (не считано).


Задача 4. Прочитайте учебный текст про формальные грамматики. Напишите грамматики следующих языков:
  • язык записи действительных чисел в языке Си.
  • язык записи email адресов.
  • язык записи арифметических выражений над целыми числами со скобками и операциями + * - / .
Задача 5. Просмотрите формальное описание грамматики языка Си. Является ли условие выводимости из символа program необходимым для компиляции программы? Является ли это условие достаточным? Ответ обоснуйте.
Задача 6. Изучите задачу вычисления сопротивления по описанию параллельно-последовательной электрической схемы из сопротивлений — Язык Си в примерах/Задача «Расчёт сопротивления схемы»