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

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks
Martin Fowler
Martin Fowler
宝玉的分享
宝玉的分享
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
M
MIT News - Artificial intelligence
U
Unit 42
博客园 - 三生石上(FineUI控件)
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
I
InfoQ
WordPress大学
WordPress大学
H
Help Net Security
D
Docker
B
Blog
腾讯CDC
A
About on SuperTechFans
Recent Announcements
Recent Announcements
雷峰网
雷峰网
有赞技术团队
有赞技术团队
C
Check Point Blog
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания
Создание своего языка программирования на 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

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

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