Интерпретатор |
|
Главная страница | Просмотр банковских выписок | Текстовый редактор | Интерпретатор | Игра "Осиновый лес" | CMS | Дневник |
|
Java, Swing (Single document interface), в качестве шаблона приложения (WeekendTextEditor) Наращиваю сложность. Вообще-то, программисту со сложностью надо бороться (Стив Макконел. Совершенный код. Глава 34. Основы мастерства), но это справедливо для текстов программ. А я наращиваю сложность задачи. Создание своего языка программирования — полезное дело. Во-первых, это интересно. Во-вторых, его можно использовать, например, для приёмочного тестирования, то есть для написания приёмочных тестов (Роберт Мартин. Быстрая разработка программ. Глава 4 Тестирование. Приемочные тесты. "Обычно тесты разрабатываются с помощью специального языка подготовки сценариев, разрабатываемого для заказчиков приложения."). Или, например… Вы пытались написать программу расчёта заработной платы? Какие-то начисления, удержания, причём все они зависят от огромного количества условий. Можно, конечно, всё это учесть, запрограммировать. А можно дать бухгалтеру простой язык для описания правил расчётов. Главное — не говорить бухгалтеру слов "язык программирования", а называть это "настройка" начислений и удержаний. :) Итак, я беру свой текстовый редактор и делю его центральную часть на две: в верхней части оставляю редактор, а в нижней размещаю панель, в которой будет отображаться всё, что будет выводить мой интерпретатор. Затем добавляю меню "Выполнить" с пунктами "Выполнить" и "Остановить". Реализую интерпретатор придуманного языка прораммирования Weekend Game Language (расширение файлов по умолчанию - WGL), выполняющий программу, открытую/написанную в редакторе. Для разработки использован Eclipse. Проект расположен здесь: https://github.com/weekend-game/weekendinterpreter/ (EN) и здесь: https://gitflic.ru/project/weekend-game/weekendinterpreter/ (RU). |
Как запустить программу |
Скачайте репозиторий на свой компьютер. Всё необходимое для работы программы расположено в папке app. Зайдите в папку app и запустите программу двойным кликом по WeekendInterpreter.jar или, если он не запускается, двойным кликом по WeekendInterpreter.bat. Если и последнее не запускает приложение, то скачайте и установите Java 11 или новее и снова попробуйте способы, описанные выше. |
Как открыть проект в Eclipse |
В Eclipse, в меню выберите File – Import... В появившемся окне выберите Existing Projects into Workspace. Укажите папку скаченного вами репозитория и нажмите кнопку Finish. Проект откроется в Eclipse. В Package Explorer (в левой части экрана) дважды кликните на файле WeekendInterpreter.java. Файл откроется для редактирования (в центральной части экрана). Запустите программу на выполнение, нажав Ctrl+F11 или так, как Вам удобно запускать программы в Eclipse. |
Как работать с программой |
В верхней части окна приложения пишем программу на языке, который я назвал Weekend Game Language (расширение файлов по умолчанию - WGL). Запускаем на выполнение клавишей F5 или кнопкой с зелёным квадратом на инструментальной линейке, или выбрав из меню "Выполнить" пункт "Выполнить". Если программа зациклилась, то, чтобы остановить её, нажимаем клавишу Escape или кнопку с красным квадратом на инструментальной линейке, или выбираем из меню "Выполнить" пункт "Остановить". |
Описание языка программирования Weekend Game Language |
Для описания синтаксиса языка удобно использовать Форму Бэкуса-Наура (BNF). Однако наиболее популярные учебники по различным языкам программирования её не используют. Что ж, пожалуй, и я без неё обойдусь. Комментарий REM Пример программы Переменная Переменная в языке обозначается буквой латинского алфавита. Таким образом, можно использовать 26 переменных. Переменные имеют целый тип. Не очень впечатляет? Но надо понимать, что это только проба разработки интерпретатора, знание того, как это вообще делается, заготовка для будущей адаптации под конкретные нужды, Hello world в написании интерпретаторов. Строка это любая последовательность символов, заключённая в двойные кавычки. Присваивание, выражение a = 7 b = 8 * (a + 5) Поддерживаются операции: унарный минус, +, -, *, /, % (остаток по модулю), ^ (возведение в степень). Приоритет операций традиционный, но можно изменять круглыми скобками. Вывод (в область вывода) PRINT "a = ", a Вывод с переводом строки PRINTLN PRINTLN "b = ", b PRINTLN "b - 37 = ", b – 37 Т.е. после PRINT или PRINTLN перечисляются через запятую строки, переменные или выражения.
Ввод INPUT "Укажите значение X: ", x На экране появится окно ввода значения, и надо будет ввести значение. Можно не вводить, но это будет интерпретироваться как ввод 0. Конечно хорошо бы вводить значение непосредственно в области вывода интерпретатора. Но это несколько усложняет код, а в данном случае важно, чтобы реализация была максимально простой и легко читалась. Безусловный переход (да простит меня Эдсгер Вибе Дейкстра) GOTO 10 Число 10 — это метка строки программы. Чтобы поставить в строке метку и таким образом указать, куда следует переходить, напишите это число в самом начале строки. Например: 10 PRINTLN "Метка 10" Кстати, в операторе GOTO в качестве метки не обязательно использовать константу. Это может быть переменная или произвольное выражение. Условный переход или выполнение одной команды по условию IF a > 5 THEN GOTO 20 Не обязательно писать GOTO, можно написать любой оператор, но только один. Поддерживаются операции сравнения: <, >, =, #. Цикл FOR i = 3 TO 7 операторы NEXT Подпрограмма 1000 PRINTLN "Это подпрограмма 1000" RETURN У подпрограмм нет имён. Они идентифицируются метками. Располагать их следует после основной программы. Заканчиваеся подпрограмма командой RETURN Вызов подпрограммы GOSUB 1000 Завершение программы END В конце программы можно вообще ничего не писать, но после текста основной программы могут следовать подпрограммы, и тогда надо использовать END, чтобы их разделить. Пример программы Применение всех вышеописанных конструкций можно посмотреть, запустив программу CommandsDemo.wgl (включена в репозиторий). |
Как программа написана |
Интерпретатор сделан на основе ранее созданного текстового редактора, как расширение текстового редактора возможностью запустить на выполнение редактируемый файл, если, конечно, файл содержит программу. Интерпретатор является довольно самостоятельным (пакет game.weekend.interpreter). Редактор предоставляет ему только имя текущего файла и панель для отображения сообщений и текста, выводимого командой языка PRINT. Запуск интерпретатора осуществляется методом Runner.run(). Этот метод открывает текущий файл редактора и в отдельном потоке создаёт объект класса Interpreter, запуская его работу вызовом метода execute(). В конструкторе Interpreter создаются необходимые для работы объекты, существующие в течение всего времени работы интерпретатора. Объект класса Text - это оболочка для интерпретируемого файла. Он предоставляет методы для удобного чтения текста программы, но это слишком низкий уровень для интерпретации. Объект класса Text используется читателем лексем TokenReader. TokenReader, используя Text, читает программу и возвращает очередную лексему. Лексема - это объект класса Token, который может быть (поле type): разделителем (DELIMITER), строкой (STRING), числом (NUMBER), переменной (VARIABLE), командой (COMMAND). Поскольку никаких изменений этот объект не производит со своими данными, я решил обойтись и без методов get. Переменные класса объявлены финальными, и изменить их нельзя. В методе execute() происходит последовательное чтение лексем. Если лексема - команда, то вызывается метод, отвечающий за реализацию команды. Все они находятся в классе Command. Если лексема - переменная, то считаю, что это оператор присваивания. Переменная Как говорилось в описании языка, он поддерживает использование только 26 переменных, и переменная - это буква латинского алфавита. За работу с переменными отвечает класс Variables. Класс содержит массив из 26 элементов, где хранит значения переменных, и два метода для получения значения и присвоения значения указанной переменной. Вот такой простой класс. Присваивание Итак, если лексема - переменная, то читаем следующую лексему и ожидаем, что это разделитель (DELIMITER), а именно символ =. Если это не так, то ошибка. Если так, то читаем следующее за ним выражение методом Expressions.getExp(). Вообще, всякий раз, когда мы ожидаем, что далее идёт выражение, мы будем вызывать метод Expressions.getExp(). Этот метод немного сложен для понимания, если вы не знакомы с термином «метод рекурсивного спуска». Но, говоря по-простому, мы читаем очередную лексему, запоминаем её в переменной класса token, читаем число методом level2(), возвращаем лексему в читатель лексем для последующего чтения и возвращаем прочитанное число в вызывающую программу. Ничего не понятно? Полный бред? Ну, я предупреждал. level2() отвечает за операции + и -. Это самые низкоприоритетные операции. В этом методе читаем число посредством level7(). Вообще-то, читаем посредством level3(), но пропустим уровни 3, 4, 5 и 6 для облегчения понимания. Level7() вернет значение из лексемы, сохраненной в переменной класса token. Это обязательно должно быть число или переменная. И тут же заменит содержимое token на новую лексему. Далее смотрим в переменной класса token: это не + ли или - ли? Если так, то читаем новую лексему и запоминаем её на месте ранее прочитанной. Читаем второе число методом level7() и выполняем + или - с полученными числами и возвращаем результат. Если это не был + или -, то просто вернем значение, которое было получено на более низком уровне. level3(), 4, 5 и 6 - это методы, аналогичные level2(), но предназначенные для операций *, /, %, ^, унарных + и -, скобок. То есть это реализация приоритета вычисления операций. Высший уровень - это число или переменная, далее круглые скобки, унарные + или -, ^, *, /, % и, наконец, + и -. Теперь опишу реализацию команд. Команда REM Даю команду TokenReader-у перевести строку в читаемом файле вызовом TokenReader.nextLine(). Другими словами, пропускаю всё, что написано до конца строки. Команда PRINT В цикле читаю лексемы. Если это перевод строки или конец файла, то цикл заканчивается. Если это строка, то вывожу эту строку, иначе пытаюсь прочитать и вычислить выражение. Результат вывожу. Если следующая лексема - это ';' или ',', то продолжаю цикл, а иначе заканчиваю работу. Команда PRINTLN Это та же самая команда PRINT, но в конце вывожу перевод строки. Команда INPUT Читаю следующую лексему. Если это строка, то запоминаю её и читаю следующую лексему. Тут я ожидаю, что прочитана переменная, куда будет помещен результат ввода пользователя. Собственно, для ввода я использую JOptionPane.showInputDialog(). Понимаю, что это не очень хорошо, и было бы лучше сделать ввод в панели вывода, но этот интерпретатор - только упражнение в написании интерпретаторов, и я решил не усложнять программу. Команда GOTO Читаю выражение. Подразумеваю, что за GOTO следует метка строки (число), куда следует передать управление. Методом Labels.goToLabel(метка) передаю управление на указанную метку. Тут следует отвлечься на реализацию меток. Метки За работу с метками отвечает объект класса Labels. При создании объекта Labels он сканирует текст программы и читает число в начале каждой строки. Если такое обнаруживается, то в ArrayList помещается номер метки и номер строки, где она встретилась. Понятно, что имея такой список, легко по номеру метки получить номер строки в тексте программы, ей соответствующей. И затем, используя TokenReader.setLine(номер_строки), установить текущую позицию для дальнейшего чтения лексем. Команда IF Читаю выражение - левое выражение. Затем читаю лексему и надеюсь получить "<", ">" , "=" или "#". Затем читаю правое выражение. Делаю соответствующее сравнение левого и правого выражения, и если результат ИСТИНА, то читаю следующую лексему. Она должна быть THEN. Это должно быть THEN. Если результат ЛОЖЬ, я перехожу на следующую строку для считывания лексем, то есть продолжаю интерпретацию со строки, следующей за IF. Таким образом, команда, следующая за THEN, не будет выполнена. Команда FOR Читаю лексему и проверяю, переменная ли это. Это будет переменная цикла. Затем должна следовать лексема '=' . Затем выражение - это начальное значение переменной цикла. Затем обязательна лексема 'TO' . Затем опять читаю выражение - это финальное значение переменной цикла. Если что-то из этого пошло не так, то это считается ошибкой. Создаю объект ForItem, который состоит из трех полей: имя переменной цикла, её конечное значение, номер строки начала тела цикла (номер строки, следующей сразу за командой FOR), и помещаю его в стек. На этом всё. Стек тут нужен для обработки вложенных циклов. Я ожидаю, что после команды FOR последуют какие-то команды - тело цикла, которые и начнут интерпретироваться сейчас, и затем обязательно последует команда NEXT - завершение цикла. Команда NEXT Извлекаю из стека объект ForItem. Увеличиваю переменную цикла на единицу (да, в данной реализации переменная цикла всегда только увеличивается и всегда только на единицу). Если её значение всё ещё меньше или равно финальному значению, то опять помещаю в стек объект ForItem и ставлю текущей строкой строку начала тела цикла. Если нет, то ничего не делаю. И таким образом, далее будут обрабатываться лексемы, следующие за NEXT. Иначе говоря, я завершаю повторение тела цикла. Команда GOSUB После лексемы GOSUB читаю выражение. Да, в данной реализации подпрограммы идентифицируются не именами, а номерами. Затем запоминаю номер строки, следующей командой GOSUB, и помещаю её в стеке подпрограмм (самый обычный стек). Перехожу на строку, отмеченную указанной меткой (номером подпрограммы). Я ожидаю, что подпрограмма будет закончена командой RETURN. Команда RETURN Читаю из стека ранее помещенный туда номер строки, следующей за командой GOSUB, и ставлю её текущей для дальнейшего чтения TokenReader. Иначе говоря, я завершаю выполнение подпрограммы и передаю управление для дальнейшего выполнения программы. Команда END Метода, соответствующего этой лексеме, в классе Commands нет. Если встречается эта лексема, то интерпретатор просто завершает работу. На этом всё. Ну а более подробно о работе программы можно узнать, если скачать проект, открыть его в Eclipse, почитать тексты классов, что-нибудь менять, запускать программу и смотреть, что из этого получается. |
Итоги |
Сделана реализация вот такого языка, похожего то ли на Fortran, то ли на доисторический Basic, на которых писали наши папы, бабушки и некоторые из нас. Как проба написания интерпретатора - это интересно. Может пригодиться как заготовка для адаптации под конкретные нужды. |
Хорошо бы… |
Сделать нумерацию строк в редакторе и выделение цветом ключевых слов. |
|
Главная страница | Просмотр банковских выписок | Текстовый редактор | Интерпретатор | Игра "Осиновый лес" | CMS | Дневник |
Смотрите мои проекты на https://github.com/weekend-game (EN) или https://gitflic.ru/user/weekend-game (RU). Пишите мне по адресу weekend_game@mail.ru |