Как стать автором
Обновить
1465.98
OTUS
Цифровые навыки от ведущих экспертов

Генерация AST на Rust

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.6K

Привет, Хабр!

Сегодня мы рассмотрим одну из тем систем компиляции — генерацию абстрактного синтаксического дерева или просто AST на Rust. Создадим свое собственное AST, разберем, как структурировать синтаксическое дерево, и рассмотрим, как использовать возможности Rust для создания парсеров и обработки узлов дерева.

Процесс генерации AST

Итак, абстрактное синтаксическое дерево — это структура данных, которая описывает синтаксическую структуру исходного кода в виде дерева. Узлы дерева представляют синтаксические конструкции: операторы, переменные, выражения и т.д.

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

Определение структуры AST в Rust

Для начала нужно создать структуру данных, которая будет представлять узлы дерева. В Rust удобнее всего использовать enum для представления разных типов узлов дерева:

#[derive(Debug, Clone)]
enum Expr {
    Number(i32),
    Variable(String),
    Add(Box<Expr>, Box<Expr>),
    Subtract(Box<Expr>, Box<Expr>),
    Multiply(Box<Expr>, Box<Expr>),
    Divide(Box<Expr>, Box<Expr>),
    FunctionCall(String, Vec<Expr>),
}

Здесь Expr — это перечисление, которое описывает узлы дерева для выражений. Используем Box<Expr> для рекурсивных структур, таких как арифметические операции, чтобы избежать проблем с определением размера структур на этапе компиляции. Почему используется Box? В Rust для рекурсивных структур размер типа должен быть известен на этапе компиляции. Box указывает на данные в куче и имеет фиксированный размер.

Создание простого парсера

Теперь займемся созданием простого парсера, который будет обрабатывать строки с арифметическими выражениями, переменными и вызовами функций и преобразовывать их в наше дерево. Для этого воспользуемся библиотекой nom. Это мощная библиотека парсер-комбинаторов, которая позволяет строить парсеры на основе небольших компонентов. Парсер-комбинаторы — это функции, которые принимают строку на вход и возвращают результат парсинга или ошибку. Их можно комбинировать для построения сложных парсеров.

Добавим в проект зависимость:

[dependencies]
nom = "7.0"

Теперь начнем писать парсер:

use nom::{
    IResult,
    character::complete::{alpha1, digit1, char},
    branch::alt,
    combinator::map_res,
    sequence::tuple,
    multi::fold_many0,
    multi::separated_list0,
    bytes::complete::tag,
};

fn parse_number(input: &str) -> IResult<&str, Expr> {
    let (input, num) = map_res(digit1, |s: &str| s.parse::<i32>())(input)?;
    Ok((input, Expr::Number(num)))
}

fn parse_variable(input: &str) -> IResult<&str, Expr> {
    let (input, var) = alpha1(input)?; // Переменные начинаются с букв
    Ok((input, Expr::Variable(var.to_string())))
}

fn parse_function_call(input: &str) -> IResult<&str, Expr> {
    let (input, (func_name, _, args, _)) = tuple((
        alpha1,          // имя функции
        char('('),       // открывающая скобка
        separated_list0(char(','), parse_expr), // аргументы функции
        char(')')        // закрывающая скобка
    ))(input)?;
    Ok((input, Expr::FunctionCall(func_name.to_string(), args)))
}

fn parse_factor(input: &str) -> IResult<&str, Expr> {
    alt((
        parse_number,
        parse_variable,
        parse_function_call,
    ))(input)
}

fn parse_term(input: &str) -> IResult<&str, Expr> {
    let (input, initial) = parse_factor(input)?;

    fold_many0(
        tuple((alt((tag("*"), tag("/"))), parse_factor)),
        initial,
        |acc, (op, val)| match op {
            "*" => Expr::Multiply(Box::new(acc), Box::new(val)),
            "/" => Expr::Divide(Box::new(acc), Box::new(val)),
            _ => unreachable!(),
        },
    )(input)
}

fn parse_expr(input: &str) -> IResult<&str, Expr> {
    let (input, initial) = parse_term(input)?;

    fold_many0(
        tuple((alt((tag("+"), tag("-"))), parse_term)),
        initial,
        |acc, (op, val)| match op {
            "+" => Expr::Add(Box::new(acc), Box::new(val)),
            "-" => Expr::Subtract(Box::new(acc), Box::new(val)),
            _ => unreachable!(),
        },
    )(input)
}

Что мы сделали:

  • Добавили parse_variable для парсинга переменных, состоящих из букв.

  • Добавили parse_function_call, чтобы парсить вызовы функций с аргументами.

  • Функция parse_factor теперь может парсить числа, переменные и вызовы функций.

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

fn parse_expr_with_errors(input: &str) -> Result<Expr, String> {
    match parse_expr(input) {
        Ok((_, expr)) => Ok(expr),
        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(format!("Parsing error: {:?}", e)),
        Err(nom::Err::Incomplete(_)) => Err("Input is incomplete".to_string()),
    }
}

Теперь парсер обрабатывает ошибки и выводит их пользователю.

Визуализация AST

Теперь визуализируем AST, чтобы наглядно показать структуру дерева. Уже реализовали функцию pretty_print, которая выводит дерево на экран:

impl Expr {
    fn pretty_print(&self, indent: usize) {
        let padding = " ".repeat(indent);
        match self {
            Expr::Number(n) => println!("{}Number({})", padding, n),
            Expr::Variable(v) => println!("{}Variable({})", padding, v),
            Expr::FunctionCall(name, args) => {
                println!("{}FunctionCall: {}", padding, name);
                for arg in args {
                    arg.pretty_print(indent + 2);
                }
            }
            Expr::Add(lhs, rhs) => {
                println!("{}Add:", padding);
                lhs.pretty_print(indent + 2);
                rhs.pretty_print(indent + 2);
            }
            Expr::Subtract(lhs, rhs) => {
                println!("{}Subtract:", padding);
                lhs.pretty_print(indent + 2);
                rhs.pretty_print(indent + 2);
            }
            Expr::Multiply(lhs, rhs) => {
                println!("{}Multiply:", padding);
                lhs.pretty_print(indent + 2);
                rhs.pretty_print(indent + 2);
            }
            Expr::Divide(lhs, rhs) => {
                println!("{}Divide:", padding);
                lhs.pretty_print(indent + 2);
                rhs.pretty_print(indent + 2);
            }
        }
    }
}

Вывод:

Add:
  Number(5)
  Multiply:
    Variable(x)
    Number(3)

Этот вывод демонстрирует следующее:

  • Корневой узел — это операция сложения Add.

  • Левый операнд — это число 5 Number(5), оно отображается с отступом в 2 пробела.

  • Правый операнд — это операция умножения Multiply, которая в свою очередь состоит из переменной x и числа 3.

    • Переменная x выводится как Variable(x) с отступом в 4 пробела.

    • Число 3 выводится как Number(3) с тем же отступом в 4 пробела, так как это правая часть операции умножения.

Этот вывод показывает структуру AST, соответствующую выражению 5 + (x * 3).

Оптимизация: свертка констант и устранение мёртвого кода

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

impl Expr {
    fn optimize(&self) -> Expr {
        match self {
            Expr::Add(lhs, rhs) => {
                let left = lhs.optimize();
                let right = rhs.optimize();
                if let Expr::Number(l) = left {
                    if let Expr::Number(r) = right {
                        return Expr::Number(l + r); // Оптимизация констант
                    }
                }
                if let Expr::Number(0) = right {
                    return left; // x + 0 => x
                }
                if let Expr::Number(0) = left {
                    return right; // 0 + x => x
                }
                Expr::Add(Box::new(left

), Box::new(right))
            }
            Expr::Multiply(lhs, rhs) => {
                let left = lhs.optimize();
                let right = rhs.optimize();
                if let Expr::Number(l) = left {
                    if let Expr::Number(r) = right {
                        return Expr::Number(l * r); // Оптимизация констант
                    }
                }
                Expr::Multiply(Box::new(left), Box::new(right))
            }
            Expr::FunctionCall(_, _) => self.clone(), // В будущем можно добавить интерпретацию вызовов функций
            _ => self.clone(),
        }
    }
}

Функция optimize автоматически упрощает выражения и вычисляет константы на этапе компиляции.

Заключение

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


Archi против всех: как бесплатный архитектурный инструмент покоряет сердца миллионов? Обсудим на открытом уроке 23 сентября. На уроке мы:

  • Рассмотрим критерии для выбора архитектурных инструментов в компании

  • Сделаем обзор ведущих мировых и российских инструментов

  • Затронем основные фичи бесплатного инструмента Archi

  • Смоделируем простой кейс

Записаться на урок можно настранице курса «Archimate. Basic».

Теги:
Хабы:
Всего голосов 13: ↑11 и ↓2+15
Комментарии2

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS