Pull to refresh

Основы конструирования компиляторов. Лексический анализ на C#

Reading time 4 min
Views 32K
Задачей лексического анализа является разбить входную последовательность (в моем случае код на языке «Паскаль») на слова и лексемы.

Для начала я создал 5 типизированных листов для хранения данных, а именно: идентификаторов, констант, ключевых слов, разделителей и свертки. Также необходим массив разделителей
static char[] limiters = {',', '.', '(', ')', '[', ']', ':', ';', '+', '-', '*', '/', '<', '>', '@'};

и массив ключевых слов. Я ограничился одиннадцатью ключевыми словами, так как статья написана как начальный пример реализации лексического анализа языка «Паскаль» на языке C#.
Итак, массив ключевых слов:
static string[] reservedWords = { "program", "var", "real", "integer", "begin", "for", "downto", "do", "begin", "end", "writeln" };

На входе мы имеем путь к .txt-файлу с программой в переменной path. Считываем весь в файл в AllTextProgram и начинаем.
StreamReader sr = new StreamReader(path, Encoding.Default);
AllTextProgram = sr.ReadToEnd();
sr.Close();

В цикле for берем последовательно каждый символ из AllTextProgram и отправляем его в метод Analysis. Отдельно я рассматриваю лишь два исключения. Первое – это апострофы, которые в моем случае встречаются при использовании ключевого слова «writeln». Если встречаем апостроф, то отходим от правил и идем по переменной AllTextPorgram, пока ни найдем второй апостроф. Второе – это комментарии в программе. Для последних я просто считаю число открывающихся и закрывающихся фигурных скобок и ожидаю пока разница ни будет равна нулю.
Казалось бы программа должна зациклиться, если последующий апостроф не найден или кол-во фигурных скобок не равно 0. Да, это верно, в этих циклах необходимо добавить дополнительные условия выхода из проверки. Эти условия для систематизации я решил сделать на этапе синтаксического анализа.
for (i = 0; i < AllTextProgram.Length; i++)
      {
        char c = AllTextProgram[i];
        if (AllTextProgram[i] == '\'')
        {
          temp += '\'';
          i++;
          while (AllTextProgram[i] != '\'')
          {
            temp += AllTextProgram[i];
            i++;
          }
          temp += '\'';
          type = 2;
          Result(temp);
          temp = null;
        }
        if (AllTextProgram[i] == '{')
        {
          int chet = 1;
          while (chet != 0)
          {
            i++;
            if (AllTextProgram[i] == '{')
              chet++;
            if (AllTextProgram[i] == '}')
              chet--;
          }
        }
        Analysis(AllTextProgram[i]);
      }

Переменная type отвечает за номер таблицы, к которому относится лексема.
Идентификаторы – таблица 1
Константы – таблица 2
Ключевые слова – таблица 3
Разделители – таблица 4

Теперь настало время поговорить что же происходит в методе Analysis. Это лучше сделать в виде комментариев к коду. Сразу отмечу, что в переменной temp будем хранить промежуточный результат работы, пока ни встретим разделитель. Встретив разделитель, нам необходимо отправить переменную temp в метод Result, чтобы определить к идентификатору, константе или ключевому слову относится нынешняя лексема.
static void Analysis(char nextChar)
  {
   int acsiiCode = (int)nextChar; // Получаем код символа
   // Проверяем относится ли код символа к буквам английского алфавита и знаку нижнего подчеркивания
   if (((acsiiCode >= 65) && (acsiiCode <= 90)) || ((acsiiCode >= 97) && (acsiiCode <= 122)) || (acsiiCode == 95)
   {
    if (temp == null)
     type = 1;
    temp += nextChar;
    return;
   }
   // Проверяем относится ли код символа к цифрам или к точке
   if (((acsiiCode >= 48) && (acsiiCode <= 57)) || (acsiiCode == 46))
   {
   // Отдельная проверка, если код символа относиться к точке проверяем, чтобы в переменной temp     // было число. Если в temp число, значит теперь
   // мы считаем его дробным, в противном случае это что-то другое
    if (acsiiCode == 46)
    {
     int out_r;
     if (!int.TryParse(temp, out out_r))
      goto not_the_number;
    }
    if (temp == null)
     type = 2;
    temp += nextChar;
    return;
   }
   not_the_number:
   if ((nextChar == ' ' || nextChar == '\n') && temp != null)
   {
    Result(temp);
    temp = null;
    return;
   }
   // И последняя проверка на принадлежность к массиву разделителей. Не забываем о двойном разделителе «:=» и двойных условных разделителях.
   foreach (char c in limiters)
   {
    if (nextChar == c)
    {
      if (temp != null)
      Result(temp);
      type = 3;
      if (nextChar == ':' && AllTextProgram[i+1] == '=')
      {
       temp = nextChar.ToString() + AllTextProgram[i + 1];
       Result(temp);
       temp = null;
       return;
      }
      if (nextChar == '<' && (AllTextProgram[i + 1] == '>' || AllTextProgram[i + 1] == '='))
      {
       temp = nextChar.ToString() + AllTextProgram[i + 1];
       Result(temp);
       temp = null;
       return;
      }
      if (nextChar == '>' && AllTextProgram[i + 1] == '=')
      {
       temp = nextChar.ToString() + AllTextProgram[i + 1];
       Result(temp);
       temp = null;
       return;
      }
      temp = nextChar.ToString();
      Result(temp);
      temp = null;
      return;
    }
   }

Заключительным для рассматрения является методе Result, где мы определяем, что же мы нашли. В самом начале проверяем переменную temp на принадлежность к ключевым словам. Проверяем в начале, чтобы не перепутать найденную лексему с идентификатором. Если найденное не является ключевым словом, то через switch проверяем тип таблицы, определенный заранее в методе Analysis. Не зыбываем проверить, чтобы найденный идентификатор/константа/разделитель уже не значился в таблице.
static void Result(string temp)
    {
      for (int j = 0; j < reservedWords.Length; j++)
      {
        if (temp == reservedWords[j])
        {
          for (int i = 0; i < tableR.Count; i++)
          {
            if (temp == tableR[i])
            {
              LConv.Add("3" + i);
              return;
            }
          }
          tableR.Add(temp);
          LConv.Add("3" + (tableR.Count - 1));
          return;
        }
      }
      switch (type)
      {
        case 1:
          for (int j = 0; j < tableI.Count; j++)
          {
            if (temp == tableI[j])
            {
              LConv.Add("1" + j);
              return;
            }
          }
          tableI.Add(temp);
          LConv.Add("1" + (tableI.Count - 1));
          break;
        case 2:
          for (int j = 0; j < tableC.Count; j++)
          {
            if (temp == tableC[j])
            {
              LConv.Add("2" + j);
              return;
            }
          }
          tableC.Add(temp);
          LConv.Add("2" + (tableC.Count - 1));
          break;
        case 3:
          for (int j = 0; j < tableL.Count; j++)
          {
            if (temp == tableL[j])
            {
              LConv.Add("4" + j);
              return;
            }
          }
          tableL.Add(temp);
          LConv.Add("4" + (tableL.Count - 1));
          break;
      }
    }

И легкий пример, чтобы показать как это все работает.
На входе программа:

program main; { pro{g}am }
var   sum: real;
     f, per_1, x_1,i:integer;
begin
  {sum:=(-x_1+2.5)*4 - (x_1-6)*((((x_1+2))));}
  x_1:=18; 
  f:= 456;
  for i:=10 downto per_1-f+i*(x_1+1) do
  begin
    per_1 := per_1 + x_1*sum*f;
     x_1:=-x_1-1;
     f:=(f+1)*(x_1-24700);
     sum:=(x_1+2.5)*4 - (x_1-6)*(x_1+2);
  end;
   writeln('summa = ', sum);
end.


И результат работы:

Tags:
Hubs:
+1
Comments 7
Comments Comments 7

Articles