惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
L
LINUX DO - 热门话题
H
Hacker News: Front Page
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
Lohrmann on Cybersecurity
Cisco Talos Blog
Cisco Talos Blog
O
OpenAI News
S
Securelist
Security Latest
Security Latest
T
Threat Research - Cisco Blogs
H
Heimdal Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
Microsoft Azure Blog
Microsoft Azure Blog
MyScale Blog
MyScale Blog
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
Latest news
Latest news
N
Netflix TechBlog - Medium
N
News and Events Feed by Topic
D
Docker
D
DataBreaches.Net
A
About on SuperTechFans
T
Tor Project blog
V
V2EX
G
Google Developers Blog
博客园 - Franky
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
I
InfoQ
H
Help Net Security
V2EX - 技术
V2EX - 技术
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Security Affairs
SecWiki News
SecWiki News
The Register - Security
The Register - Security
人人都是产品经理
人人都是产品经理
NISL@THU
NISL@THU
小众软件
小众软件
B
Blog
T
Threatpost
P
Palo Alto Networks Blog
博客园 - 【当耐特】
L
LangChain Blog
AWS News Blog
AWS News Blog
月光博客
月光博客
宝玉的分享
宝玉的分享

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Создание своего языка программирования на Rust #2: Парсер выражений
Дмитрий · 2026-06-07 · via Все публикации подряд на Хабре

Средний

11 мин

3.1K

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

Что будем делать?

Мы напишем парсер для примерно таких выражений: 1+2*(2+8)*2*a+3. Сначало мы добавим парсер для арифметических выражений, а после сразу же займемся сравнениями и логическими операциями. Как писал ранее, мы будем парсить методом рекурсивного спуска, и там у нас будет поддержка приоритета операторов. Сначало */, а потом +-. В результате будет такая цепочка: primary -> unary -> additive -> multiplicative -> comparison -> logical.

Сами обьекты выражений будут типа enum Expr. Будут такие типы:

  1. Expr::Num(f64) - простое число, определяется в primary

  2. Εxpr::Bool(bool) - логическое значение, определяется в primary

  3. Expr::Str(String) - литерал строки, определяется в primary

  4. Expr::Id(String) - идентификатор, определяется в primary

  5. Expr::Unary(UnaryOp, Box<Expr>) - унарнвя операция и новое перечисление - UnaryOp: Neg (-) и Not (!). Определяется в unary.

  6. Expr::Arith(Box<Expr>, ArithOp, Box<Expr>) - арифметические выражения. Тут мы созданим еще одно перечисление - ArithOp: Add, Sub, Mul и Div. Определяется в multiplicative и additive

  7. Expr::Comp(Box<Expr>, CompOp, Box<Expr>) - сравнение. Структура та же, но в качестве оператора новое перечисление - CompOp: Gt (>), Ge (>=), Lt (<), Le (<=), Eq (==), Ne (!=). Определяется в comparison.

  8. Expr::Logic(Box<Expr>, LogicOp, Box<Expr>) - логическое выражение. Тут также новое перечтсление - LogicOp: And (&&) и Or (||). Определяется в logical

В прошлой статье я упомянул, но повторюсь.

primary - определяем, может ли этот токег быть значением я выражении. Это можгут быть: числа, строки, идентификаторы, вызовы функций, скобки (Точнее, их содержимое). Возвращаем Num, Str, Id. Считая скобки, мы можем получить любое выражение.

unary - тут мы берем текущий токен, если это, например, !, то пропускаем его, вызвваем primary, чтобы получить выражение, и возвращаем выражение. Возвращаем Expr::Unary.

multiplicative - тут находим все выражения с умножением и делением и возвращаем выраэение. Возвращаем Expr::Arith

additive - находим выражения сложения и вычитания. Возвращаем Expr::Arith

Начинаем писать!

Создаем новый модуль - parser. Там нам нужны файлы expr.rs, parser.rs и mod.rs. Сначало, конечно же, напишем mod.rs. (Неожиданно, да?)

Покажу сразу итоговый.

parser/mod.rs
pub mod expr;
mod parser;

pub use parser::Parser;

#[derive(Debug, Clone)]
pub struct Info {
    pub line: usize,
    pub offset: usize,
    pub len: usize,
}

#[macro_export]
macro_rules! info {
    ($line:expr, $offset:expr, $len:expr) => {
        $crate::parser::Info {
            line: $line,
            offset: $offset,
            len: $len,
        }
    };
    ($tkn:expr) => {
        info!($tkn.line, $tkn.offset, $tkn.len)
    };
}

Для Expr, нам тоже понадобится отладочная информация, и для этого у нас есть структура Info. С помощью ее, мы будет делать отладку в интерпритаторе, а так как и стейтменты будут структурами, я решил поместить его в mod.rs. Также тут у нас будет макрос, он просто создает обьект Info. А длинный, где мы в ручную передаем параметры, мы оставим для стейтментов, чтобы и их отлаживать.

parser/expr.rs
use super::Info;

pub type BExpr = Box<Expr>;

#[derive(Debug, Clone)]
pub enum Expr {
    Num(f64, Info),
    Str(String, Info),
    Id(String, Info),
    Bool(bool, Info),
    Unary(UnaryOp, BExpr, Info),
    Arith(BExpr, ArithOp, BExpr, Info),
    Comp(BExpr, CompOp, BExpr, Info),
    Logic(BExpr, LogicOp, BExpr, Info),
}

impl Expr {
    pub fn info(&self) -> Info {
        match self {
            Expr::Str(_, info) => info.clone(),
            Expr::Num(_, info) => info.clone(),
            Expr::Id(_, info) => info.clone(),
            Expr::Bool(_, info) => info.clone(),
            Expr::Arith(_, _, _, info) => info.clone(),
            Expr::Comp(_, _, _, info) => info.clone(),
            Expr::Logic(_, _, _, info) => info.clone(),
            Expr::Unary(_, _, info) => info.clone(),
        }
    }
}

#[derive(Debug, Clone)]
pub enum ArithOp {
    Add, // +
    Sub, // -
    Mul, // *
    Div, // /
}

#[derive(Debug, Clone)]
pub enum CompOp {
    Gt, // >
    Ge, // >=
    Lt, // <
    Le, // <=
    Eq, // ==
    Ne, // !=
}

#[derive(Debug, Clone)]
pub enum LogicOp {
    And, // &&
    Or,  // ||
}

#[derive(Debug, Clone)]
pub enum UnaryOp {
    Not, // !
    Neg, // -
}

Я создал тип BExpr, чтобы каждый раз не писать Box<Expr>. Как видите тут есть перечисления операторов: ArithOp, CompOp, LogicOp. Также самое главное печисление - Expr. Я ранее уже рассказывал для чего это, так что повторяться не буду. Метод info тут просто копирует инфо текущего ввражения, чтобы потом можно быстро строить их удобнее. (Увидити вообщем)

Теперь создаем стуктуру Parser.

parser/parser.rs
use super::expr::{ArithOp, BExpr, CompOp, Expr, LogicOp};
use crate::lexer::token::{TKind, Token};

pub struct Parser<'a> {
    pos: usize,
    tokens: Vec<Token>,
    lines: Vec<&'a str>,
}

impl<'a> Parser<'a> {
    pub fn new(tokens: Vec<Token>, source: &'a str) -> Self {
        let lines: Vec<&str> = source.lines().collect();
        Self {
            tokens,
            pos: 0,
            lines,
        }
    }
}

Заранее импортируем Expr и его операторы, а также Token и TKind (всевдоним TokenKind). В метод new мы передаем токены, из которых будем строить AST (Abstract Syntax Tree) и исходник, из которого мы получаем все строки. lines нам нужен также для ошибок, как и в лексере.

Теперь напишем вспомогательные методы. Их будет меньше и буду проще (на данном этапе).

parser/parser.rs: Parser
use std::mem::discriminant;

...

fn check(&mut self, kind: TKind) -> bool {
    if discriminant(&self.peek(0).kind) == discriminant(&kind) {
        self.advance(1);
        true
    } else {
        false
    }
}


fn error(&self, msg: &str, token: Token) -> String {
    let line = self.lines[token.line];
    let header = format!("Error in {}:{} - {msg}", token.line, token.offset);
    let err_line = format!("{} | {line}", token.line);
    let point = format!(
        "{} | {}{}",
        " ".repeat(token.line.to_string().len()),
        " ".repeat(token.offset),
        "^".repeat(token.len),
    );
    format!("{header}\n{err_line}\n{point}\n")
}

fn peek(&self, offset: i8) -> Token {
    let idx = self.pos + offset as usize;
    self.tokens.get(idx).unwrap().clone()
}

fn advance(&mut self, offset: u8) {
    self.pos += offset as usize;
}

В advance мы обновляем только self.pos.

В peek я решил убрать проверку и просто сделал unwrap. Снова предупрежу: используйте его на свой страх и риск, используйте только для прототипов. Но чтобы не писать много кода, мы оставим его.

В error мы будем передовать токен, и будем брать информацию из него.

check проверяет, соответствует ли тип текущего токена переданому, в случае успеха пропускаем его и возвращаем true, а в ином случае просто возвращаем false. discriminant нам нужен, чтобы сранивать только тип токена, а не его содержимое.

Начинаем писать парсинг!

Начнем с primary.

parser/parser.rs: Parser.primary
fn primary(&mut self) -> Expr {
    let current = self.peek(0);
    match current.kind {
        TKind::NumLit(n) => {
            self.advance(1);
            Expr::Num(n, info!(current))
        }
        TKind::StrLit(s) => {
            self.advance(1);
            Expr::Str(s, info!(current))
        }
        TKind::Id(id) => {
            self.advance(1);
            Expr::Id(id, info!(current))
        }
        TKind::Bool(truth) => {
            self.advance(1);
            Expr::Bool(truth, info!(current))
        }
        _ => panic!("{}", self.error("Unexpected token in primary", current)),
    }
}

Мы берем текущий токен и проверяем его. Если он может быть значением в выражении, то пропускаем его и создаем соответствующий обьект Expr. В случае не валидного токена паникуем на неожиданный токен. Скобки также будут обрабатываться здесь, но добавим их позже.

parser/parser.rs: Parser.unary
fn unary(&mut self) -> Expr {
    let current = self.peek(0);
    match current.kind {
        TKind::Minus => {
            self.advance(1);
            let primary = self.primary();
            let info = primary.info();
            let info = info!(info.line, info.offset - 1, info.len + 1);
            Expr::Unary(UnaryOp::Neg, Box::new(primary), info)
        }
        TKind::Bang => {
            self.advance(1);
            let primary = self.primary();
            let info = primary.info();
            let info = info!(info.line, info.offset - 1, info.len + 1);
            Expr::Unary(UnaryOp::Not, Box::new(primary), info)
        }
        _ => self.primary(),
    }
}

Тут смотрим на текущий токен. Если это - или !, то соотаетственно обрабатываем и возвращаем Expr::Unary, в ином случае возвращаем обычный primary. Для примерв возмем вариант с TKind::Minus.

TKind::Minus => {
    self.advance(1);
    let primary = self.primary();
    let info = primary.info();
    let info = info!(info.line, info.offset - 1, info.len + 1);
    Expr::Unary(UnaryOp::Neg, Box::new(primary), info)
}

Мы пропускаем текущий токен (минус) и берем primary. Далее нам нужно создать Info. Просто взять info!(primary) не получится, так как минус тоже должен выходить в Info. Так что берем Info из primary, в макрос передаем номер строки, отступ от начала строки - 1. Отнимаем 1, так как primary начинается сразу после унарного оператора, и там мы захватим и сам оператор. В длинну передаем длинну primary + 1, чтобы выровнять. В конце возвращаем негативный primary и Info, который мы создали. С TKind логика схожа, просто используем UnaryOp::Not.

Теперь и multiplicative.

fn multiplicative(&mut self) -> Expr {
    let mut left = self.unary();
    loop {
        let op_token = self.peek(0);
        let op = match op_token.kind {
            TKind::Star => ArithOp::Mul,
            TKind::Slash => ArithOp::Div,
            _ => break,
        };
        self.advance(1);
        let right = self.unary();
        left = Expr::Arith(Box::new(left), op, Box::new(right), info!(op_token))
    }
    left
}

Создаем left, куда помещаем self.unary, это будет левым операндом. Далее в бесконечном цикле: проверяем текущий токен, если это знак умножения или деления, то возвощаем соответствующий ArithOp, в случае, если это любой другой токен, просто выходим из цикла и возвращаем left.

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

У additive логика абсолютно та же, просто заменяем операторы на соответствующие.

fn additive(&mut self) -> Expr {
    let mut left = self.multiplicative();
    loop {
        let op_token = self.peek(0);
        let op = match op_token.kind {
            TKind::Plus => ArithOp::Add,
            TKind::Minus => ArithOp::Sub,
            _ => break,
        };
        self.advance(1);
        let right = self.multiplicative();
        left = Expr::Arith(Box::new(left), op, Box::new(right), info!(op_token))
    }
    left
}

Для обобщения создаем метод expr, туда мы, возможно, добавим тернарный оператор, но пока без него:

fn expr(&mut self) -> Expr {
    self.additive()
}

И еще один новый метод - parse_exprs. Он будет просто парсить выражения по порядку, используем его для тестов и отладки.

pub fn parse_exprs(&mut self) -> Vec<Expr> {
    let mut exprs = vec![];
    while self.peek(0).kind != TKind::Eof {
        let expr = self.expr();
        exprs.push(expr);
    }
    exprs
}

Ну и собственно создаем новый тест в папке tests - parsert.rs

use guidzy::lexer::Lexer;
use guidzy::parser::Parser;

#[test]
fn main() {
    let source = "
2 + 2 * 2
"
    .trim();
    let tokens = Lexer::new(source).tokenize();
    println!("Source: {}\nTokens:", source);
    for tkn in &tokens {
        println!("{:?}", tkn);
    }
    let exprs = Parser::new(tokens, source).parse_exprs();
    println!("Expressions:");
    for ex in exprs {
        println!("{:?}", ex);
    }
}

Для запуска теста с выводом используем cargo test -- --nocapture. Вывод будет таковым:

Source: 2 + 2 * 2
Tokens:
Token { kind: NumLit(2.0), len: 1, pos: 0, line: 0, offset: 0 }
Token { kind: Plus, len: 1, pos: 2, line: 0, offset: 2 }
Token { kind: NumLit(2.0), len: 1, pos: 4, line: 0, offset: 4 }
Token { kind: Star, len: 1, pos: 6, line: 0, offset: 6 }
Token { kind: NumLit(2.0), len: 1, pos: 8, line: 0, offset: 8 }
Token { kind: Eof, len: 1, pos: 9, line: 0, offset: 9 }
Expressions:
Arith(Num(2.0, Info { line: 0, offset: 0, len: 1 }), Add, Arith(Num(2.0, Info { line: 0, offset: 4, len: 1 }), Mul, Num(2.0, Info { line: 0, offset: 8, len: 1 }), Info { line: 0, offset: 6, len: 1 }), Info { line: 0, offset: 2, len: 1 })

Согласитесь, довольно нечитаемый вывод? Чтобы сделать его читаемым, реализуем трейт fmt::Display для Expr и всех операторов:

parser/expr.rs
use std::fmt;

...

impl fmt::Display for ArithOp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ArithOp::Add => write!(f, "+"),
            ArithOp::Sub => write!(f, "-"),
            ArithOp::Mul => write!(f, "*"),
            ArithOp::Div => write!(f, "/"),
        }
    }
}

impl fmt::Display for CompOp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CompOp::Gt => write!(f, ">"),
            CompOp::Ge => write!(f, ">="),
            CompOp::Lt => write!(f, "<"),
            CompOp::Le => write!(f, "<="),
            CompOp::Eq => write!(f, "=="),
            CompOp::Ne => write!(f, "!="),
        }
    }
}

impl fmt::Display for LogicOp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LogicOp::And => write!(f, "&&"),
            LogicOp::Or => write!(f, "||"),
        }
    }
}

impl fmt::Display for UnaryOp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            UnaryOp::Neg => write!(f, "-"),
            UnaryOp::Not => write!(f, "!"),
        }
    }
}

impl fmt::Display for Expr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Expr::Num(n, _) => write!(f, "{n}"),
            Expr::Str(s, _) => write!(f, "\"{s}\""),
            Expr::Id(id, _) => write!(f, "{id}"),
            Expr::Bool(b, _) => write!(f, "{b}"),
            Expr::Arith(l, op, r, _) => write!(f, "({l} {op} {r})"),
            Expr::Comp(l, op, r, _) => write!(f, "({l} {op} {r})"),
            Expr::Logic(l, op, r, _) => write!(f, "({l} {op} {r})"),
            Expr::Unary(op, expr, _) => write!(f, "{op}{expr}"),
        }
    }
}

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

Source: 2 + 2 * 2
Tokens:
Token { kind: NumLit(2.0), len: 1, pos: 0, line: 0, offset: 0 }
Token { kind: Plus, len: 1, pos: 2, line: 0, offset: 2 }
Token { kind: NumLit(2.0), len: 1, pos: 4, line: 0, offset: 4 }
Token { kind: Star, len: 1, pos: 6, line: 0, offset: 6 }
Token { kind: NumLit(2.0), len: 1, pos: 8, line: 0, offset: 8 }
Token { kind: Eof, len: 1, pos: 9, line: 0, offset: 9 }
Expressions:
(2 + (2 * 2))

Добавляем операторы сравнения

Тут все просто, логика такая же как и в additive и multiplicative, просто проверяем другие операторы и возвращаем другой тип выражения.

parser/parser.rs: Parser
fn logical(&mut self) -> Expr {
    let mut left = self.comparison();
    loop {
        let op_token = self.peek(0);
        let op = match op_token.kind {
            TKind::And => LogicOp::And,
            TKind::Or => LogicOp::Or,
            _ => break,
        };
        self.advance(1);
        let right = self.comparison();
        left = Expr::Logic(Box::new(left), op, Box::new(right), info!(op_token))
    }
    left
}
fn comparison(&mut self) -> Expr {
    let mut left = self.additive();
    loop {
        let op_token = self.peek(0);
        let op = match op_token.kind {
            TKind::Gt => CompOp::Gt,
            TKind::Ge => CompOp::Ge,
            TKind::Lt => CompOp::Lt,
            TKind::Le => CompOp::Le,
            TKind::Eq => CompOp::Eq,
            TKind::Ne => CompOp::Ne,
            _ => break,
        };
        self.advance(1);
        let right = self.additive();
        left = Expr::Comp(Box::new(left), op, Box::new(right), info!(op_token))
    }
    left
}
Обновим тест tests/parsert.rs
use guidzy::lexer::Lexer;
use guidzy::parser::Parser;

#[test]
fn main() {
    let source = "
2 + 2 * 2 > 4 / 2 + 2 && true != false
"
    .trim();
    let tokens = Lexer::new(source).tokenize();
    println!("Source: {}\nTokens:", source);
    for tkn in &tokens {
        println!("{:?}", tkn);
    }
    let exprs = Parser::new(tokens, source).parse_exprs();
    println!("Expressions:");
    for ex in exprs {
        println!("{}", ex);
    }
}

Вывод будет таким:

Source: 2 + 2 * 2 > 4 / 2 + 2 && true != false
Tokens:
Token { kind: NumLit(2.0), len: 1, pos: 0, line: 0, offset: 0 }
Token { kind: Plus, len: 1, pos: 2, line: 0, offset: 2 }
Token { kind: NumLit(2.0), len: 1, pos: 4, line: 0, offset: 4 }
Token { kind: Star, len: 1, pos: 6, line: 0, offset: 6 }
Token { kind: NumLit(2.0), len: 1, pos: 8, line: 0, offset: 8 }
Token { kind: Gt, len: 1, pos: 10, line: 0, offset: 10 }
Token { kind: NumLit(4.0), len: 1, pos: 12, line: 0, offset: 12 }
Token { kind: Slash, len: 1, pos: 14, line: 0, offset: 14 }
Token { kind: NumLit(2.0), len: 1, pos: 16, line: 0, offset: 16 }
Token { kind: Plus, len: 1, pos: 18, line: 0, offset: 18 }
Token { kind: NumLit(2.0), len: 1, pos: 20, line: 0, offset: 20 }
Token { kind: And, len: 2, pos: 22, line: 0, offset: 22 }
Token { kind: Bool(true), len: 6, pos: 25, line: 0, offset: 25 }
Token { kind: Ne, len: 2, pos: 30, line: 0, offset: 30 }
Token { kind: Bool(false), len: 7, pos: 33, line: 0, offset: 33 }
Token { kind: Eof, len: 1, pos: 38, line: 0, offset: 38 }
Expressions:
(((2 + (2 * 2)) > ((4 / 2) + 2)) && (true != false))

На последок добавим скобки. В primary добавляем новую проверку:

TKind::LParen => {
    self.advance(1);
    let expr = self.expr();
    if !self.check(TKind::RParen) {
        panic!(
            "{}",
            self.error(&format!("Expected ')', found {:?}", current), current)
        );
    }
    expr
}

Тут пропускаем левую скобку, после скобки парсим полноценное выражение и проверяем токен на закрывающую скобку. Если это не закрывающая скобка, то вызываем ошибку, в ином случае возвращаем выражение.

Обновим тест tests/parsert.rs
use guidzy::lexer::Lexer;
use guidzy::parser::Parser;

#[test]
fn main() {
    let source = "
2 + 2 * 2 > 4 / 2 + 2 && true != false
(2 + 2) * 2
"
    .trim();
    let tokens = Lexer::new(source).tokenize();
    println!("Source: {}\nTokens:", source);
    for tkn in &tokens {
        println!("{:?}", tkn);
    }
    let exprs = Parser::new(tokens, source).parse_exprs();
    println!("Expressions:");
    for ex in exprs {
        println!("{}", ex);
    }
}

И теперь вывод такой:

Source: 2 + 2 * 2 > 4 / 2 + 2 && true != false
(2 + 2) * 2
Tokens:
Token { kind: NumLit(2.0), len: 1, pos: 0, line: 0, offset: 0 }
Token { kind: Plus, len: 1, pos: 2, line: 0, offset: 2 }
Token { kind: NumLit(2.0), len: 1, pos: 4, line: 0, offset: 4 }
Token { kind: Star, len: 1, pos: 6, line: 0, offset: 6 }
Token { kind: NumLit(2.0), len: 1, pos: 8, line: 0, offset: 8 }
Token { kind: Gt, len: 1, pos: 10, line: 0, offset: 10 }
Token { kind: NumLit(4.0), len: 1, pos: 12, line: 0, offset: 12 }
Token { kind: Slash, len: 1, pos: 14, line: 0, offset: 14 }
Token { kind: NumLit(2.0), len: 1, pos: 16, line: 0, offset: 16 }
Token { kind: Plus, len: 1, pos: 18, line: 0, offset: 18 }
Token { kind: NumLit(2.0), len: 1, pos: 20, line: 0, offset: 20 }
Token { kind: And, len: 2, pos: 22, line: 0, offset: 22 }
Token { kind: Bool(true), len: 6, pos: 25, line: 0, offset: 25 }
Token { kind: Ne, len: 2, pos: 30, line: 0, offset: 30 }
Token { kind: Bool(false), len: 7, pos: 33, line: 0, offset: 33 }
Token { kind: LParen, len: 1, pos: 39, line: 1, offset: 0 }
Token { kind: NumLit(2.0), len: 1, pos: 40, line: 1, offset: 1 }
Token { kind: Plus, len: 1, pos: 42, line: 1, offset: 3 }
Token { kind: NumLit(2.0), len: 1, pos: 44, line: 1, offset: 5 }
Token { kind: RParen, len: 1, pos: 45, line: 1, offset: 6 }
Token { kind: Star, len: 1, pos: 47, line: 1, offset: 8 }
Token { kind: NumLit(2.0), len: 1, pos: 49, line: 1, offset: 10 }
Token { kind: Eof, len: 1, pos: 50, line: 1, offset: 11 }
Expressions:
(((2 + (2 * 2)) > ((4 / 2) + 2)) && (true != false))
((2 + 2) * 2)

Итог

Теперь у нас есть парсер, который парсит выражения со скобками и приоритетом операторов :D

В следующей статье уже будем добавлять парсер для стейтментов.

Результаты текущей статьи можно посмотреть в этом репозитории.