Создание собственного языка программирования с нуля: Часть X — Обработка исключений

Создание собственного языка программирования с нуля: Часть X — Обработка исключений

25 апреля 2023 г.

Добро пожаловать в следующую часть создания собственного языка программирования. В этой части мы продолжим улучшать наш игрушечный язык, реализуя исключения. Вот предыдущие части:

  1. Создание собственного языка программирования с нуля
  2. Построение Ваш собственный язык программирования с нуля: часть II — двухстековый алгоритм Дейкстры
  3. Сборка Ваш собственный язык программирования, часть III: Улучшение лексического анализа с помощью регулярных выражений Lookaheads
  4. Создание собственного языка программирования С нуля, часть 4: реализация функций
  5. Создание собственного языка программирования с нуля : Часть V. Массивы
  6. Создание собственного языка программирования с нуля : Часть VI. Петли
  7. Создание собственного языка программирования с нуля : Часть VII. Классы
  8. Создание собственного языка программирования С нуля: Часть VIII. Вложенные классы
  9. Создание собственного языка программирования с нуля: Часть IX. Гибридное наследование

Полный исходный код доступен на GitHub.

1. Модель исключений

Во-первых, мы определим синтаксические правила того, как мы будем генерировать и обрабатывать исключения, аналогичные синтаксису Ruby:

  1. Чтобы создать исключение, мы будем использовать ключевое слово raise:
raise

  1. Мы должны предоставить сообщение с дополнительной информацией об ошибке:
raise "This is an Exception"

  1. Мы можем указать ошибку как экземпляр класса или любого другого выражения:
class Exception [message]
end

raise new Exception ["This is an Exception message"]

  1. Чтобы предоставить более подробную информацию о возникшем исключении, мы соберем и распечатаем трассировку стека в виде списка инструкций, которые программа выполняла для достижения инструкции, вызывающей исключение:
 1:  do_something []
 2:  
 3:  fun do_something
 4:      new Test :: do_something_else []
 5:  end
 6:
 7:  class Test
 8:
 9:      fun do_something_else
10:          do_even_more []
11:      end
12:
13:      fun do_even_more
14:          raise "A message that describes the error."
15:      end
16:
17:  end

Вывод:

:::предупреждение Сообщение с описанием ошибки.

в Test#do_even_more:14

в Test#do_something_else:10

в do_something:4

на test.toy:1

:::

  1. Для обработки исключения мы будем использовать следующие блоки кода:
begin   
    # Statements raising an Exception
rescue 
    # Handle an Exception
ensure
    # Always executed
end

  1. Чтобы получить доступ к сгенерированному исключению в блоке восстановления, мы можем объявить произвольную переменную после ключевого слова rescue:
begin   
    # Statements raising an Exception
rescue error
    # Access and handle an Exception using `error` variable
    print error
end

2. Лексический анализ

В этом разделе мы рассмотрим лексический анализ как первую стадию процесса компиляции, которая разделяет исходный код на языковые лексемы, такие как ключевое слово, переменная, оператор и т. д.

Чтобы определить лексемы в этой реализации игрушечного языка, я использую выражения регулярных выражений, перечисленные в TokenType перечисление:

package org.example.toylanguage.token;

...
public enum TokenType {
    Comment("#.*"),
    LineBreak("[nr]"),
    Whitespace("[st]"),
    Keyword("(if|elif|else|end|print|input|class|fun|return|loop|in|by|break|next|assert)(?=s|$)(?!_)"),
    GroupDivider("([|]|,|{|}|.{2}|(:(?!:)))"),
    Logical("(true|false)(?=s|$)(?!_)"),
    Numeric("([-]?(?=[.]?[0-9])[0-9]*(?![.]{2})[.]?[0-9]*)"),
    Null("(null)(?=,|s|$)(?!_)"),
    This("(this)(?=,|s|$)(?!_)"),
    Text(""([^"]*)""),
    Operator("(+|-|*{1,2}|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}s+new|:{2}|(|)|(new|and|or|as|is)(?=s|$)(?!_))"),
    Variable("[a-zA-Z_]+[a-zA-Z0-9_]*");

    ...
}

Каждая строка кода на игрушечном языке обрабатывается с помощью этих регулярных выражений и с помощью LexicalParser, мы преобразуем исходный код в Token лексемы. Чтобы разобрать новые слова, объявленные в правилах исключений (raise, begin, rescue, ensure), нам нужно добавьте их в регулярное выражение лексемы Keyword:

...
public enum TokenType {
    ...
    Keyword("(if|elif|else|end|print|input|class|fun|return|loop|in|by|break|next|assert|raise|begin|rescue|ensure)(?=s|$)(?!_)"),
    ...
}

3. Синтаксический анализ

В этом разделе мы преобразуем лексемы, полученные в результате лексического анализа, в окончательные утверждения.

3.1 Преобразование лексем в операторы

Чтобы преобразовать объявленные лексемы Keyword в операторы, нам нужно определить операторы. Для реализации оператора мы используем оператор. Реализованный RaiseExceptionStatement должен содержать поднятое выражение:

package org.example.toylanguage.statement;

@RequiredArgsConstructor
@Getter
public class RaiseExceptionStatement implements Statement {
    private final Expression expression;

    @Override
    public void execute() {
        // TODO raise an Exception
    }
}

Оператор для обработки Exception также будет реализовывать интерфейс Statement, но в отличие от RaiseExceptionStatement, он должен включать вложенные операторы для каждого из этих трех блоков:

begin   
    # Begin block
rescue error 
    # Rescue block
ensure
    # Ensure block
end

Каждое из этих утверждений является реализацией CompositeStatement, будет содержать вложенные операторы внутри себя, что позволяет выполнять несколько операторов. Чтобы записать исключение в переменную ошибки для блока rescue, мы определим errorVariable как строковое поле:

package org.example.toylanguage.statement;

@RequiredArgsConstructor
@Getter
public class HandleExceptionStatement implements Statement {
    private final CompositeStatement bodyStatement;
    private final CompositeStatement rescueStatement;
    private final CompositeStatement ensureStatement;
    private final String errorVariable;

    @Override
    public void execute() {
        // TODO handle an Exception
    }
}

Следующим шагом является преобразование маркеров исключения в реализации RaiseExceptionStatement и HandleExceptionStatement. Чтобы преобразовать токены в операторы, мы используем StatementParser< /а>. В этом конкретном случае, чтобы преобразовать токен ключевого слова, нам нужно изменить StatementParser#parseKeywordStatement(Token token), который анализирует оператор в зависимости от первого слова, которое начинается с оператора .

Давайте добавим новые первые слова в блок переключателей для создания и обработки исключения: raise для RaiseExceptionStatement и begin для HandleExceptionStatement:

package org.example.toylanguage;

public StatementParser {
    ...
    private void parseKeywordStatement(Token token) {
        switch (token.getValue()) {
            ...
            case "raise":
                parseRaiseExceptionStatement();
                break;
            case "begin":
                parseHandleExceptionStatement();
                break;
            default:
                throw new SyntaxException(String.format("Failed to parse a keyword: %s", token.getValue()));
        }
    }
    ...
}

Чтобы создать RaiseExceptionStatement, нам нужно только прочитать вызываемое выражение. Для чтения выражений мы используем ExpressionReader. , который анализирует полное выражение, пока не достигнет начала следующего оператора:

private void parseRaiseExceptionStatement() {
    Expression expression = ExpressionReader.readExpression(tokens);
    ...
}

После создания RaiseExceptionStatement нам нужно добавить его в StatementParser#compositeStatement как вложенный оператор во внешний оператор:

private void parseRaiseExceptionStatement() {
    Expression expression = ExpressionReader.readExpression(tokens);
    RaiseExceptionStatement statement = new RaiseExceptionStatement(expression);
    compositeStatement.addStatement(statement);
}

Чтобы создать HandleExceptionOperator, нам нужно прочитать три блока: begin (body), rescue и ensure, а не забыв прочитать слово end в конце, которое обозначает конец оператора обработки исключений:

private void parseHandleExceptionStatement() {
    // read begin block
    CompositeStatement beginStatement = ...;

    // read rescue block
    CompositeStatement rescueStatement = ...;
    String errorVariable = ...;

    // read ensure block
    CompositeStatement ensureStatement = ..;

    // skip the end keyword
    tokens.next(TokenType.Keyword, "end");

    // construct a statement
    HandleExceptionStatement statement = new HandleExceptionStatement(beginStatement, rescueStatement, ensureStatement, errorVariable);
    compositeStatement.addStatement(statement);
}

Начнем с блока begin. Чтобы проанализировать вложенные операторы внутри него, нам нужно будет использовать метод StatementParser#parse(StatementParser, CompositeStatement, DefinitionScope). В качестве первого аргумента он принимает экземпляр StatementParser внешнего блока кода. Второй аргумент — это CompositeStatement, в котором будут собраны все вложенные операторы в анализируемом блоке. Третий аргумент — DefinitionScope< /a>, который используется для записи всех структур (классов и функций), объявленных внутри анализируемого блока. Если мы хотим ограничить доступ к структурам, объявленным внутри начального блока, из внешнего блока, мы должны открыть новый DefinitionScope:

Метод StatementParser#parse(StatementParser, CompositeStatement, DefinitionScope) будет считывать все вложенные операторы, пока мы не достигнем завершающего слова, обозначающего конец этого блока. В настоящее время, чтобы проверить, встретили ли мы завершающее слово, мы используем StatementParser#hasNextStatement(). Давайте добавим новые слова rescue и ensure, чтобы гарантировать остановку синтаксического анализа операторов при встрече с этими блоками:

Далее давайте прочитаем второй блок rescue. Он может отсутствовать, если пользователь не хочет перехватывать и обрабатывать исключение:

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

Теперь давайте прочитаем вложенные операторы, как мы ранее читали операторы begin:

И, наконец, давайте закончим третий блок ensure. Это может быть необязательным блоком rescue:

3.2 Выполнение операторов исключений

3.2.1 RaiseExceptionStatement

Когда мы выполняем RaiseExceptionStatement, каждый из последующих операторов должен быть уведомлен о сбое программы, и выполнение должно быть остановлено. Чтобы разделить это событие между другими операторами, мы введем класс ExceptionContext, который будет содержать сведения об исключении:

Класс Exception предоставит подробную информацию о возникшем исключении, включая записи о перемещении приложения внутри него для печати трассировки стека.

Далее мы добавим несколько методов для создания и обработки исключения:

Затем давайте завершим RaiseExceptionStatement#execute() и уведомим другие операторы с помощью ExeceptionContext:

Если пользователь не указал выражение ошибки, мы можем вывести текстовое выражение по умолчанию:

Зная, что ExceptionContext будет уведомлен о поднятом Exception, мы должны проверить, что никакие последующие операторы не будут выполняться, если Exception возникнет. В настоящее время все операторы в любом блоке кода выполняются реализациями CompositeStatement. Для каждой реализации CompositeStatement, в которой мы повторяем вложенные операторы с помощью CompositeStatement#statements2Execute, нам необходимо установить проверку после каждого выполняемого оператора в случае возникновения исключения, а в положительном случае остановить исполнение:

При установке этих изменений операторы прекратят выполнение после поднятого оператора Exception. В конце выполнения программы мы должны, если возникло исключение, и напечатать сообщение об исключении:

Чтобы распечатать Exception, мы будем использовать метод ExceptionContext#printStackTrace(), который в дальнейшем также будет отображать записи о перемещении приложения:

3.2.2. HandleExceptionStatement

Для обработки исключения завершим реализацию HandleExceptionStatement#execute(). Он будет состоять из трех частей для каждого из определенных блоков:

Каждый из блоков должен выполняться в новом MemoryScope, ограничивающий доступ к переменным, объявленным во вложенном блоке, из внешнего блока:

//begin block
MemoryContext.pushScope(MemoryContext.newScope());
try {
    bodyStatement.execute();
} finally {
    MemoryContext.endScope();
}

Блок rescue является необязательным и должен выполняться только в том случае, если мы перехватили исключение в ExceptionContext:

// rescue block
if (rescueStatement != null && ExceptionContext.isRaised()) {

    MemoryContext.pushScope(MemoryContext.newScope());

   try {
        rescueStatement.execute();
    } finally {
        MemoryContext.endScope();
    }
}

Если этот блок спасает исключение, мы должны сообщить ExceptionContext, что ошибка обнаружена:

// rescue block
if (rescueStatement != null && ExceptionContext.isRaised()) {

    MemoryContext.pushScope(MemoryContext.newScope());
    ExceptionContext.rescueException();

    try {
        rescueStatement.execute();
    } finally {
        MemoryContext.endScope();
    }
}

Наконец, для этого блока мы должны инициализировать переменную error, предоставленную пользователем, со значением Exception, полученным из ExceptionContext:

// rescue block
if (rescueStatement != null && ExceptionContext.isRaised()) {

    MemoryContext.pushScope(MemoryContext.newScope());
    if (errorVariable != null) {
        MemoryContext.getScope().setLocal(errorVariable, ExceptionContext.getException().getValue());
    }

    ExceptionContext.rescueException();

    try {
        rescueStatement.execute();
    } finally {
        MemoryContext.endScope();
    }
}

Третий блок ensure также может быть необязательным, как и блок rescue:

// ensure block
if (ensureStatement != null) {
    MemoryContext.pushScope(MemoryContext.newScope());
    try {
        ensureStatement.execute();
    } finally {
        MemoryContext.endScope();
    }
}

3.3* Добавление трассировки стека

В этом подразделе мы соберем записи о перемещении приложения во время его выполнения и отобразим полную трассировку стека для возникших исключений:

:::предупреждение Сообщение с описанием ошибки.

в Test#do_even_more:14

в Test#do_something_else:10

в do_something:4

на test.toy:1

:::

3.3.1 Определение оператора трассировки

Чтобы собрать трассировку стека, каждая из наших реализаций Statement должна содержать информацию об имени блока и номере строки. Мы можем преобразовать интерфейс Statement в абстрактный класс, определяющий эти два поля: blockName и rowNumber:

package org.example.toylanguage.statement;

@RequiredArgsConstructor
@Getter
public abstract class Statement {
    private final Integer rowNumber;
    private final String blockName;

    public abstract void execute();
}

Доступ к rowNumber можно получить из Токен, содержащий слово, обозначающее начало оператора:

public class StatementParser {
    ...

    private void parseKeywordStatement(Token rowToken) {
        switch (rowToken.getValue()) {
            case "print":
                parsePrintStatement(rowToken);
                break;
            case "input":
                parseInputStatement(rowToken);
                break;
            case "if":
                parseConditionStatement(rowToken);
                break;
            case "class":
                parseClassDefinition(rowToken);
                break;
            case "fun":
                parseFunctionDefinition(rowToken);
                break;
            case "return":
                parseReturnStatement(rowToken);
                break;
            case "loop":
                parseLoopStatement(rowToken);
                break;
            case "break":
                parseBreakStatement(rowToken);
                break;
            case "next":
                parseNextStatement(rowToken);
                break;
            case "assert":
                parseAssertStatement(rowToken);
                break;
            case "raise":
                parseRaiseExceptionStatement(rowToken);
                break;
            case "begin":
                parseHandleExceptionStatement(rowToken);
                break;
            default:
                throw new SyntaxException(String.format("Failed to parse a keyword: %s", rowToken.getValue()));
        }
    }

    ...
}

Структуры языка игрушек, которые у нас есть в настоящее время, — это классы и функции.

Чтобы установить ClassStatement#blockName, мы можем использовать имя класса, полученное из ClassDetails#getName():

public class StatementParser {
    ...

    private void parseClassDefinition(Token rowToken) {
        // read class details
        ClassDetails classDetails = readClassDetails();
        ...

        // add class definition
        ...
        ClassStatement classStatement = new ClassStatement(rowToken.getRow(), classDetails.getName());
        ...

        //parse class's statements
        ...
    }

    ...
}

Чтобы установить FunctionStatement#blockName, мы можем использовать имя функции. В дополнение к имени мы можем указать имя класса, если функция объявлена ​​внутри класса:

public class StatementParser {
    ...

    private void parseFunctionDefinition(Token rowToken) {
        Token type = tokens.next(TokenType.Variable);
        ...

        //add function definition
        String blockName = type.getValue();
        if (compositeStatement instanceof ClassStatement) {
            blockName = compositeStatement.getBlockName() + "#" + blockName;
        }
        FunctionStatement functionStatement = new FunctionStatement(rowToken.getRow(), blockName);
        ...
    }

    ...
}

Другие операторы не определяют структуры и могут повторно использовать имена блоков классов и функций, ссылаясь на внешний блок кода с помощью StatementParser#compositeStatement#getBlockName(), например:

public class StatementParser {
     ...

     private void parsePrintStatement(Token rowToken) {
        ...
        PrintStatement statement = new PrintStatement(rowToken.getRow(), compositeStatement.getBlockName(), expression);
        ...
    }

    ...

    private void parseInputStatement(Token rowToken) {
        ...
        InputStatement statement = new InputStatement(rowToken.getRow(), compositeStatement.getBlockName(), variable.getValue(), scanner::nextLine);
        ...
    }

    ...
}

При текущем способе создания корневого CompositeStatement мы должны указать корневое имя в ToyLanguage, который может быть определен как имя файла:

public class ToyLanguage {

    @SneakyThrows
    public void execute(Path path) {
        String sourceCode = Files.readString(path);
        List<Token> tokens = LexicalParser.parse(sourceCode);

        DefinitionContext.pushScope(DefinitionContext.newScope());
        MemoryContext.pushScope(MemoryContext.newScope());
        try {
            CompositeStatement statement = new CompositeStatement(null, path.getFileName().toString());
            StatementParser.parse(tokens, statement);
            statement.execute();
        } finally {
            DefinitionContext.endScope();
            MemoryContext.endScope();

            if (ExceptionContext.isRaised()) {
                ExceptionContext.printStackTrace();
            }
        }
    }
}

3.3.2 Сбор трассировки стека

Теперь каждый оператор содержит имя блока и номер строки. Давайте добавим набор операторов в ExceptionContext#Exception:

public class ExceptionContext {
    @Getter
    private static Exception exception;
    private static boolean raised;

    public static boolean raiseException(Value<?> value) {
        exception = new Exception(value, new Stack<>());
        raised = true;
    }

    public static void rescueException() {
        exception = null;
        raised = false;
    }

    public static boolean isRaised() {
        return raised;
    }

    public static void addTracedStatement(Statement statement) {
        if (isRaised()) {
            exception.stackTrace.add(statement);
        }
    }

    public static void printStackTrace() {
        System.err.println(exception);
        rescueException();
    }

    @RequiredArgsConstructor
    @Getter
    public static class Exception {
        private final Value<?> value;
        private final List<Statement> stackTrace;

        @Override
        public String toString() {
            return String.format("%s%n%s",
                    value,
                    stackTrace
                            .stream()
                            .map(st -> String.format("%4sat %s:%d", "", st.getBlockName(), st.getRowNumber()))
                            .collect(Collectors.joining("n"))
            );
        }
    }
}

ExceptionContext#addTracedStatement(Statement) должен вызываться каждым выражением, содержащим выражение, после вызова Expression#evaluate():

package org.example.toylanguage.statement;

public class ExpressionStatement extends Statement {
    ...

    @Override
    public void execute() {
        expression.evaluate();
        ExceptionContext.addTracedStatement(this);
    }
}

package org.example.toylanguage.statement;

public class PrintStatement extends Statement {
    ...

    @Override
    public void execute() {
        Value<?> value = expression.evaluate();
        System.out.println(value);
        ExceptionContext.addTracedStatement(this);
    }
}

package org.example.toylanguage.statement;

public class RaiseExceptionStatement extends Statement {
    ...

    @Override
    public void execute() {
        Value<?> value = expression.evaluate();
        if (value == NullValue.NULL_INSTANCE) {
            value = new TextValue("Empty exception");
        }
        ExceptionContext.raiseException(value);
        ExceptionContext.addTracedStatement(this);
    }
}

package org.example.toylanguage.statement;

public class ReturnStatement extends Statement {
    ...

    @Override
    public void execute() {
        Value<?> result = expression.evaluate();
        ReturnContext.getScope().invoke(result);
        ExceptionContext.addTracedStatement(this);
    }
}

4 Подведение итогов

В этой части мы реализовали простую модель для создания и обработки исключений. Еще один шаг к созданию полноценного языка программирования. Вот несколько примеров, которые вы можете запустить: raise_exception.toy и handle_exception.toy.

Фото Тони Пепе на Unsplash


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE