Привет всем!
Добро пожаловать в третью часть серии уроков по новому 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,
}
Здесь нет ничего особенного.
В качестве следующего шага мы немного изменим правило производства 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,
))
};
}
Мы можем использовать его для выполнения, например, операции умножения:
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 откликов!
Оставайтесь на связи!