- •Лабораторные работы
- •1. Порядок выполнения работы.
- •2. Содержание отчета.
- •Лабораторная работа № 1.1. Создание каркаса транслятора.
- •Лабораторная работа № 1.2. Разбор исходного файла.
- •1. Порядок выполнения работы.
- •2. Содержание отчета.
- •Лабораторная работа №3.1. Синтаксический анализатор.
- •1. Порядок выполнения работы.
- •2. Содержание отчета.
- •Лабораторная работа №5. Разработка генератора кода
- •1. Порядок выполнения работы.
- •2. Теоретическая часть
- •3. Содержание отчета
- •Лабораторным работам
- •Вариант 1
- •Вариант 2
- •Вариант 3
- •Вариант 4
- •Вариант 5
- •Вариант 6
- •Вариант 7
- •Вариант 8
- •Вариант 9
- •Вариант 10
- •Вариант 11
- •Вариант 12
- •Вариант 13
- •Вариант 14
- •Вариант 15
- •Вариант 16
- •Курсовой проект
- •Оформление
- •Пустые строки
- •Пробелы в строке
- •Локальные переменные
- •Комментарии
- •Инструкции (statements)
- •Оформление if, if-else, if-else if-else
- •Оформление for, foreach
- •Оформление while, do-while
- •Оформление switch
- •Оформление try-catch
- •Указания по оформлению псевдокода
Лабораторная работа № 1.2. Разбор исходного файла.
Методы статического класса System.IO.File для работы с файлами. Чтение, запись текстового файла. Класс System.IO.StreamReader для реализации потокового чтения из файла.
Классы C#, обеспечивающие работу с файловой системой, расположены в пространстве имен System.IO . Поэтому для доступа к этим классам в коде без указания пространства имен необходимо использовать директиву подключения namespace:
using System.IO;
Статический класс File предоставляет основные функции для работы с файлами, в том числе нужные нам чтение файла в строку и запись строки в файл.
Чтение файла в строку. Для выбора исходного файла в структуре файловой системы используется элемент управления OpenFileDialog. Метод ShowDialog() отобразит диалог выбора файла на экран, а о том, какую кнопку в диалоге нажал пользователь можно узнать, сравнив значение, возвращенное методом с константами статического класса DialogResult. При значении, равном DialogResult.OK путь к выбранному файлу в файловой системе можно получить через свойство FileName экземпляра OpenFileDialog. Это значение нужно передать в метод ReadAllText класса File. Метод вернет нам содержимое исходного файла.
Запись строки в файл. Необходимо использовать метод WriteAllText класса File. В качестве параметров ему передаются имя файла и исходная строка. Широко распространенной практикой является проверка на существование файла перед записью с помощью метода Exists класса File.
Посимвольное чтение из файла. Класс StreamReader предоставляет методы чтения следующего символа, строки или блока в файле. Однако на каждом шаге необходимо сохранять данные о текущей строке, текущем символе и позиции символа в строке. Довольно логичным будет реализовать статический класс-обертку над StreamReader, который обеспечил бы нам не только чтение из файла,но и хранение этих данных. Реализуем этот функционал в классе Reader, созданном в ходе первой лабораторной работы (файл Reader.cs) Ниже приведен псевдокод данного класса.
Reader
{
номерСтроки;
позицияСимволаВСтроке;
текущийСимвол;
StreamReader streamReader;
читатьСледующийСимвол()
{
текущийСимвол = streamReader.Read();
если( текущийСимвол == -1 )
текущийСимвол = конецФайла;
иначе если( текущийСимвол == переводСтроки )
{
номерСтроки++;
позицияСимволаВСтроке = 0;
}
иначе если( текущийСимвол == возвратКаретки или текущийСивол == табуляция )
читатьследующийСивол();
иначе
позицияСимволаВСтроке++;
}
инициализировать( путьКФайлу )
{
если файлСуществует( путьКФайлу )
{
streamReader = новый StreamReader( путьКФайлу );
номерСтроки = 1;
позицияСимволаВСтроке = 0;
читатьСледующийСимвол();
}
}
закрыть()
{
streamReader.close();
}
}
Протестировать работу класса Reader в коде формы можно, например, так:
Reader. инициализировать( путьКФайлу );
Пока ( Reader.текущийСимвол != конецФайла )
{
вывести Reader.номерСтроки;
вывести Reader. позицияСимволаВСтроке;
вывести Reader. текущийСимвол;
Reader. читатьСледующийСимвол();
}
Замечания по коду:
Все поля и методы должны быть объявлены как static;
В соответствии с принципами ООП все поля должны быть закрытыми (это замечание касается всех лабораторных работ). Для чтения полей необходимо реализовать методы-свойства. Ни одно из полей НЕ должно записываться вне класса.
Необходимо предусмотреть проблемные ситуации: указан некорректный путь к файлу, многократная инициализация класса (в этом случае необходимо закрывать streamReader перед его пересозданием).
Примеры кода:
Чтение файла в строку.
if(openFileDialog1.ShowDialog() == DialogResult.OK)
{
codeFile = openFileDialog1.FileName.ToString();
string code = System.IO.File.ReadAllText(codeFile);
}
Запись строки в файл.
if(File.Exists(codeFile))
{
File.WriteAllText(codeFile, code);
}
Посимвольное чтение из файла.
Файл Reader.cs в проекте.
Лабораторная работа №1.3. Лексический анализатор.
Общие принципы лексического разбора, разбор ключевых слов, идентификаторов и числовых констант.
Для задания типа лексемы обычно используется перечисление, включающее в себя все возможные в исходном языке типы: ключевые слова, операторы, идентификаторы и числа.
public enum Lexems
{
None, Name, Number, Begin, End, If, Then, Multiplication, Division, Plus,
Equal, Less, LessOrEqual, Semi, Assign,LeftBracket,EOF, …
};
В рамках класса LexicalAnalyzer будем реализовывать методы лексического анализа: хранение ключевых слов языка и их распознавание, разбор текущей лексемы в зависимости от её типа.
Работа с ключевыми словами языка. Для ключевого слова языка объявим структуру Keyword, сопоставляющую ключевое слово с лексемой определенного типа.
private struct Keyword
{
public string word;
public Lexems lex;
}
Хранить ключевые слова будем в массиве keywords, для определения текущей позиции в массиве объявим поле keywordsPointer (в C# невозможно динамически изменить длину массива, поэтому нам нужно хранить текущую позицию,чтобы не обращаться к пустым элементам и не превысить заданную статически длину массива). Ниже представлен псевдокод методов добавления ключевого слова в массив и проверки, является ли идентфикатор ключевым словом.
добавитьКлючевоеСлово( ключевоеСлово, лексема )
{
Keyword kw = новый Keyword();
kw.word = ключевоеСлово;
kw.lex = лексема;
keywords[keywordsPointer++] = kw;
}
лексема получитьКлючевоеСлово( ключевоеСлово )
{
от keywordsPointer – 1 до 0
если( keywords[keywordsPointer].word == ключевоеСлово )
вернуть keywords[keywordsPointer].lex;
вернуть Lexems.Name;
}
Разбор лексем. Для хранения текущей лексемы и текущего идентификатора объявим соответствующие поля currentLexem и currentName. Основным методом анализатора будет public метод для разбора следующей лексемы. Для считывания следующего символа будем использовать метод читатьСледующийСимвол() класса Reader, реализованного на прошлой лабораторной работе.
разобратьСледующуюЛексему()
{
пока( Reader.текущийСимвол == пробел )
Reader. читатьСледующийСимвол();
если( Reader.текущийСимвол является буквой )
разобратьИдентификатор();
иначе если( Reader.текущийСимвол является цифрой )
разобратьЧисло();
иначе если( Reader.текущийСимвол == переводСтроки )
{
Reader.читатьСледующийСимвол();
currentLexem = Lexems.Разделитель;
}
иначе если( Reader.текущийСимвол == ‘<’ )
{
Reader.читатьСледующийСимвол();
если( Reader.текущийСимвол == ‘=’ )
{
Reader. читатьСледующийСимвол();
currentLexem = Lexems.МеньшеИлиРавно;
}
иначе
currentLexem = Lexems.Меньше;
}
иначе если( Reader.текущийСимвол == ‘+’ )
{
Reader. читатьСледующийСимвол();
currentLexem = Lexems.Плюс;
}
………………………………………………………………
Иначе
Ошибка.НедопустимыйСимвол
}
Выше приведен неполный псевдокод метода – в нем реализованы не все случаи ветвления в зависимости от текущего символа. Ниже приведен псевдокод метода разбора идентификатора.
разобратьИдентификатор()
{
идентификатор = пустаяСтрока;
выполнять
{
идентификатор += Reader.текущийСимвол;
Reader. читатьСледующийСимвол();
}
пока( Reader.текущийСимвол является буквой );
currentName = идентификатор;
currentLexem = получитьКлючевоеСлово( идентификатор );
}
Для тестирования функционала класса LexicalAnalyzer можно использовать следующий код:
LexicalAnalyzer.инициализировать();
Пока(LexicalAnalyzer.текущаяЛексема != Lexems.конецФайла)
{
вывести LexicalAnalyzer.текущаяЛексема;
LexicalAnalyzer.разобратьСледующуюЛексему();
}
Замечания по коду:
По соглашению для разделения инструкций во входном языке используется перевод строки, в идентификаторах используются только буквы, а числа могут быть только целыми.
Метод разобратьЧисло() реализуется по аналогии с методом разобратьИдентификатор().
Проверка,является ли символ буквой или цифрой, осуществляется с помощью статических методов класса сhar: сhar.isLetter( символ ) и сhar.isDigit( символ ).
При разборе идентификаторов необходимо учитывать максимальную длину имени, а при разборе чисел - переполнение типа int.
Необходимо также реализовать метод инициализации, в который будут вынесены очистка используемых полей, а также вызов метода добавитьКлючевоеСлово() для всех ключевых слов входного языка.
Примеры кода:
Файл LexicalAanlyzer.cs в проекте.
ЛАБОРАТОРНАЯ РАБОТА №2. Таблица имен.
Таблица имен. Реализация элемента таблицы. Общие принципы хранения идентификаторов внутри таблицы, класс System.Collections.Generic.LinkedList<T> . Реализация методов регистрации идентификатора, получения идентификатора из таблицы по имени.
Для задания типа и категории идентификатора логично определить перечисления:
public enum tCat
{
Const, Var, Type
};
public enum tType
{
None, Int, Bool
};
Перечисление tType хранит возможные типы данных исходного языка: void, int, bool; tCat - категории идентификаторов: константа, переменная, тип данных.
Следующим шагом будет объявление структуры Идентификатор. Полями структуры будут имя идентификатора, его тип и категория.
Хранить идентификаторы внутри таблицы имен будем в связанном списке. Реализация класса ТаблицаИмен (NameTable.cs - в проекте) на псевдокоде приведена ниже.
Класс NameTable
{
СвязанныйСписок<Идентификатор> идентификаторы;
Идентификатор добавитьИдентификатор( имя, категория )
{
Идентификатор идентификатор = новый Идентификатор();
идентификатор.имя = имя;
идентификатор.категория = категория;
идентификаторы.добавитьВКонец( идентификатор );
вернуть идентификатор;
}
Идентификатор найтиПоИмени( имя )
{
УзелСвязанногоСписка<Идентификатор> узел = идентификаторы.Первый;
пока( узел != НУЛЛ и узел.Значение.имя != имя )
узел = узел.Следующий;
вернуть узел == НУЛЛ ? новый Идентификатор() : узел.Значение;
}
}
Тестирование работы этого класса можно провести следующим образом: если в процессе лексического анализа кода текущая лексема является идентификатором и её еще нет в таблице идентификаторов, добавляем её в таблицу. Далее получаем список идентификаторов и последовательно выводим их. Псевдокод приведен ниже.
LexicalAnalyzer.инициализировать();
Пока( LexicalAnalyzer.текущаяЛексема != Lexems.конецФайла )
{
если( LexicalAnalyzer.текущаяЛексема == Lexems.Имя
И NameTable.найтиПоИмени( LexicalAnalyzer.текущееИмя ).эквивалентно( новый Идентификатор() ) )
NameTable.добавитьИдентификатор( LexicalAnalyzer.текущееИмя, tCat.Var );
LexicalAnalyzer.разобратьСледующуюЛексему();
}
УзелСвязанногоСписка<Идентификатор> узел = NameTable.получитьИдентификаторы().Первый;
пока( узел != НУЛЛ )
{
Вывести узел.Значение.Имя;
узел = узел.Следующий;
}
Замечания по коду:
В класс ТаблицаИмен необходимо добавить методы для инициализации класса и получения списка всех идентификаторов.
В методы класса ТаблицаИмен необходимо добавить контроль за возникновением исключений: в том числе, предусмотреть ситуации добавления идентификатора с неуникальным именем и попытку найти несуществующий в таблице идентификатор.
Примеры кода:
Файл NameTable.cs в проекте.
ЛАБОРАТОРНАЯ РАБОТА №3.
РАЗРАБОТКА СИНТАКСИЧЕСКОГО АНАЛИЗАТОРА