Написание нового языка программирования. Часть III: Система типов

Привет всем!

Добро пожаловать в третью часть серии уроков по новому LR-языку. Вот ссылка на вторую часть. В прошлый раз мы успешно реализовали первую версию интерпретатора языка LR. Мы смогли писать программы с переменными целочисленного типа и использовать их в арифметических выражениях, присваивая результаты другим переменным.

Состояние кода к концу этого урока можно было найти в ветке part_3 репозитория GitHub. Изменения, сделанные сегодня, можно увидеть в коммите e5e04bc

Цель на сегодня

Цель на сегодня — расширить язык для поддержки типов String и float! Мы также немного улучшим отчет об ошибках во время выполнения. Мы хотели бы улучшить LR-язык, чтобы он мог выполнять следующую программу:

{
    let hello: String = "Hello";
    let world: String = "World";
    let hello_word: String = hello + " " + world;

    let pi: Float = 3.14;
    let r: Int = 5;
    let square: Float = 2 * pi * r * r;
    let value: String = "The square of the circle with the r = " + r + " is " + square;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Представление значений и типов

Значения

В качестве напоминания, вот наше текущее представление выражения в AST:

pub enum Expr {
    Number(i32),
    Identifier(String),
    Op(Box<Expr>, Opcode, Box<Expr>),
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Number(i32) является темой сегодняшнего обсуждения. Он возникает каждый раз, когда мы видим числовой литерал в коде LR-программы:

Num: i32 = {
    r"-?[0-9]+" => i32::from_str(<>).unwrap()
};
Term: Box<Expr> = {
    Num => Box::new(Expr::Number(<>)),
    Identifier => Box::new(Expr::Identifier(<>)),
    "(" <Expr> ")"
};
Вход в полноэкранный режим Выход из полноэкранного режима

Однако сейчас мы хотели бы поддерживать больше типов, поэтому имеет смысл переименовать Number в Constant. Тип i32 в качестве значения больше не будет работать, поэтому мы заменим его следующим перечислением:

pub enum Value {
    Int(i32),
    Float(f32),
    String(String),
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Обновленный раздел грамматики выглядит следующим образом:

IntNum: i32 = {
    r"-?[0-9]+" => i32::from_str(<>).unwrap()
};

FloatNum: f32 = {
    r"-?[0-9]+.[0-9]+" => f32::from_str(<>).unwrap()
};

StringLiteral: String = {
    r#""[^"]*""# => <>[1..<>.len() - 1].to_owned()
};

Term: Box<Expr> = {
    IntNum => Box::new(Expr::Constant(Value::Int(<>))),
    FloatNum => Box::new(Expr::Constant(Value::Float(<>))),
    StringLiteral => Box::new(Expr::Constant(Value::String(<>))),
    Identifier => Box::new(Expr::Identifier(<>)),
    "(" <Expr> ")"
};
Ввести полноэкранный режим Выйти из полноэкранного режима

Мы вводим 2 новых типа литералов: число, содержащее точку (FloatNum) и любые символы в двойных кавычках (StringLiteral). Странное выражение <>[1..<>.len() - 1] просто удаляет первый и последний символ литерала, так как мы хотели бы получить строковый литерал внутри двойных кавычек, но без них.

Типы

Существует два способа работы с типами переменных. Первый вариант — вывести их на основе присваиваемого значения. Второй — явно указывать тип при объявлении переменной. Первый вариант часто является вариацией второго, при этом указатель типа является необязательным, поэтому для упрощения мы пока реализуем вариант 2.

Во-первых, нам нужно определить перечисление Type:

pub enum Type {
    Int,
    Float,
    String,
}
Вход в полноэкранный режим Выход из полноэкранного режима

и грамматику для него:

Type: Type = {
    "Int" => Type::Int,
    "String" => Type::String,
    "Float" => Type::Float,
}
Enter fullscreen mode Выход из полноэкранного режима

Здесь нет ничего особенного.

В качестве следующего шага мы немного изменим правило производства Statement.

Statement: Statement = {
    "let" <Identifier> ":" <Type> "=" <Expr> ";" => Statement::new_definition(<>),
    <Identifier> "=" <Expr> ";" => Statement::new_assignment(<>)
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Хотя мы оставляем правило присваивания прежним, мы делаем обязательным указание типа при объявлении переменной. Оператор <> теперь будет передавать 3 аргумента методу new_definition, поэтому мы должны изменить и его:

pub enum Statement {
    Assignment {
        identifier: String,
        expression: Box<Expr>,
    },
    Definition {
        identifier: String,
        expression: Box<Expr>,
        value_type: Type,
    },
}

impl Statement {
    pub fn new_assignment(identifier: String, expression: Box<Expr>) -> Self {
        Self::Assignment {
            identifier,
            expression,
        }
    }

    pub fn new_definition(identifier: String, value_type: Type, expression: Box<Expr>) -> Self {
        Self::Definition {
            identifier,
            value_type,
            expression,
        }
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Структуру StatementBody больше нельзя использовать повторно, поэтому мы просто избавимся от нее.

Черты дисплея

Это единственные изменения, которые нам пришлось сделать, чтобы добавить поддержку системы типов в нашу грамматику и дерево синтаксиса. Прежде чем мы начнем вносить изменения в часть интерпретатора, давайте реализуем отображение для наших AST-структур. Это поможет выводить их с помощью {} в удобочитаемой форме и позволит нам создавать красивые сообщения об ошибках во время выполнения. Вот пример реализации признака Display для перечисления Expr:

impl Display for Expr {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Expr::Constant(v) => write!(f, "{}", v),
            Expr::Identifier(id) => write!(f, "{}", id),
            Expr::Op(e1, op, e2) => write!(f, "({} {} {})", e1, op, e2),
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Обновление интерпретатора

Грамматика и AST готовы, однако наш код еще не компилируется, потому что нам нужно внести изменения в структуру Frame для хранения типа значения, а также реализовать арифметические выражения для нового перечисления.

Хранение переменных

Начнем с тривиального изменения — заменим тип поля local_variables HashMap<String, i32> на local_variables: HashMap<String, Value>. Все методы структуры Frame теперь должны принимать и возвращать Value вместо i32 соответственно.

Мы также должны выполнить дополнительную проверку в методах assign_value и define_variable: мы не хотим позволить присвоить значение, скажем, типа String переменной, объявленной как Integer:

pub fn assign_value(&mut self, variable_name: &str, value: Value) -> Result<(), VariableError> {
    if let Some(variable) = self.local_variables.get_mut(variable_name) {
        if Type::from(variable.deref()) == Type::from(&value) {
            *variable = value;
            Ok(())
        } else {
            Err(VariableError::TypeMismatch(
                Type::from(&value),
                variable_name.to_owned(),
                Type::from(variable.deref()),
            ))
        }
    } else if let Some(parent) = self.parent.as_mut() {
        parent.assign_value(variable_name, value)
    } else {
        Err(VariableError::UndefinedVariable(variable_name.to_owned()))
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Чтобы представить этот тип ошибки, мы вводим новое перечисление ошибок VariableError, а также переносим в него тип ошибки UndefinedVariable:

#[derive(Error, Debug)]
pub enum VariableError {
    #[error("Variable {0} is not defined")]
    UndefinedVariable(String),
    #[error("Unable to assign the value of type {0} to variable '{1}' of type {2}")]
    TypeMismatch(Type, String, Type),
}
Вход в полноэкранный режим Выход из полноэкранного режима

Пожалуйста, обратитесь к frame.rs, чтобы увидеть полный исходный код обновленной структуры Frame.

Арифметические операции

Если мы попытаемся скомпилировать код сейчас, компилятор rust выдаст нам ошибку, объясняя, что он не знает, как применять +, -, * и / между экземплярами перечисления Value. К счастью, все, что нам нужно сделать, это реализовать для него трейты Add, Sub, Mul и Div!

Основная проблема, которую нам предстоит решить, — это выяснить тип результата операции. Очевидно, что 5 + 5 — это 10, но что должно произойти, когда вы напишете 5 + "hello"? Будет ли это ошибка или вернется строка, содержащая 5hello?

Установим следующие правила:

  • Результатом деления Int является Int, операции с остатком % пока не поддерживаются.
  • Для любой операции, включающей float и int, тип результата должен быть float, и мы должны приводить int к float перед операцией.
  • При добавлении чего-либо к строке получается новая строка. Никакие другие операции, кроме +, не поддерживаются для строки.

Учитывая это, мы можем реализовать признак Add для Value:

impl Add for Value {
    type Output = Result<Value, OperationError>;

    fn add(self, other: Self) -> Self::Output {
        let value = match self {
            Value::Int(v1) => match other {
                Value::Int(v2) => Value::Int(v1 + v2),
                Value::Float(v2) => Value::Float(v1 as f32 + v2),
                Value::String(v2) => Value::String(v1.to_string() + v2.as_str()),
            },
            Value::Float(v1) => match other {
                Value::Int(v2) => Value::Float(v1 + v2 as f32),
                Value::Float(v2) => Value::Float(v1 + v2),
                Value::String(v2) => Value::String(v1.to_string() + v2.as_str()),
            },
            Value::String(v1) => match other {
                Value::Int(v2) => Value::String(v1 + v2.to_string().as_str()),
                Value::Float(v2) => Value::String(v1 + v2.to_string().as_str()),
                Value::String(v2) => Value::String(v1 + v2.as_str()),
            },
        };
        Ok(value)
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что несмотря на то, что add никогда не возвращает ошибку, тип возврата для него все еще Result<Value, OperationError>. Это сделано для того, чтобы сохранить согласованность с другими реализациями трейтов и избежать изменений в сигнатуре метода, когда мы будем добавлять новые типы в будущем.

Пока что перечисление ошибок имеет только один вариант:

#[derive(Error, Debug)]
pub enum OperationError {
    #[error("Operation {0} {1} {2} is not defined")]
    IncompatibleTypes(Type, Opcode, Type),
}
Войти в полноэкранный режим Выход из полноэкранного режима

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

macro_rules! error {
    ($type_1:ident, $op:ident, $type_2:ident) => {
        Err(OperationError::IncompatibleTypes(
            Type::$type_1,
            Opcode::$op,
            Type::$type_2,
        ))
    };
}
Enter fullscreen mode Выйти из полноэкранного режима

Мы можем использовать его для выполнения, например, операции умножения:

impl Mul for Value {
    type Output = Result<Value, OperationError>;

    fn mul(self, other: Self) -> Self::Output {
        match self {
            Value::Int(v1) => match other {
                Value::Int(v2) => Ok(Value::Int(v1 * v2)),
                Value::Float(v2) => Ok(Value::Float(v1 as f32 * v2)),
                Value::String(_) => error!(Int, Mul, String),
            },
            Value::Float(v1) => match other {
                Value::Int(v2) => Ok(Value::Float(v1 * v2 as f32)),
                Value::Float(v2) => Ok(Value::Float(v1 * v2)),
                Value::String(_) => error!(Float, Mul, String),
            },
            Value::String(_) => match other {
                Value::Int(_) => error!(String, Mul, Int),
                Value::Float(_) => error!(String, Mul, Float),
                Value::String(_) => error!(String, Mul, String),
            },
        }
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Я опускаю реализацию признаков Sub и Div, так как они идентичны приведенным выше. Вы можете обратиться к файлу operations.rs, чтобы взглянуть на них.

Собираем все вместе

Осталось только внести ряд «косметических» изменений в метод evalutate_expression. Вот его обновленный код:

#[derive(Error, Debug)]
pub enum ExpressionError {
    #[error("Unable to evalutate expression {0}: {1}")]
    VariableError(String, VariableError),
    #[error("Unable to evalutate expression {0}: {1}")]
    OperationError(String, OperationError),
}

pub fn evalutate_expression(frame: &mut Frame, expr: &Expr) -> Result<Value, ExpressionError> {
    match expr {
        Expr::Constant(n) => Ok(n.clone()),
        Expr::Op(exp1, opcode, exp2) => {
            let result = match opcode {
                Opcode::Mul => {
                    evalutate_expression(frame, exp1)? * evalutate_expression(frame, exp2)?
                }
                Opcode::Div => {
                    evalutate_expression(frame, exp1)? / evalutate_expression(frame, exp2)?
                }
                Opcode::Add => {
                    evalutate_expression(frame, exp1)? + evalutate_expression(frame, exp2)?
                }
                Opcode::Sub => {
                    evalutate_expression(frame, exp1)? - evalutate_expression(frame, exp2)?
                }
            };
            result.map_err(|e| ExpressionError::OperationError(expr.to_string(), e))
        }
        Expr::Identifier(variable) => frame
            .variable_value(variable)
            .map_err(|e| ExpressionError::VariableError(expr.to_string(), e)),
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Во-первых, мы вводим перечисление ExpressionError для обертывания новых типов ошибок, определенных выше. Обратите внимание, что для формирования сообщения об ошибке мы используем признаки Display, реализованные для AST-структур. Это позволяет нам, например, возвращать пользователю красивые сообщения об ошибках во время выполнения:

Unable to evaluate expression ("hello" / 5): Operation String / Int is not defined"
Вход в полноэкранный режим Выход из полноэкранного режима

Во-вторых, метод variable_value и арифметическая операция теперь могут вернуть ошибку, поэтому нам нужно обработать ее и привести к ExpressionError.

Резюме

Вот и все! Теперь мы можем запустить вышеупомянутую программу. Вот как выглядит кадр стека после выполнения:

Frame {
    parent: None,
    local_variables: {
        "r": Int(
            5,
        ),
        "hello_word": String(
            "Hello World",
        ),
        "square": Float(
            157.0,
        ),
        "world": String(
            "World",
        ),
        "pi": Float(
            3.14,
        ),
        "value": String(
            "The square of the circle with the r = 5 is 157",
        ),
        "hello": String(
            "Hello",
        ),
    },
}
Вход в полноэкранный режим Выход из полноэкранного режима

Удивительно, не правда ли?

План на IV часть — добавить поддержку типа boolean и реализовать операторы if!

Я опубликую ее, если этот пост наберет 15 откликов!

Оставайтесь на связи!

Оцените статью
devanswers.ru
Добавить комментарий