Привет читающим!

Эта статья будет значительно короче двух предыдущих, и будущие статьи будут такие же короткие, потому что так легче писать и искать ошибки. Также, думаю, читать так будет удобнее.

Как будем писать?

Я много как пытался писать это раньше, и нашел наиболее удобный для себя способ.

У Stmt будет публичный статический метод define(&Parser). Он будет смотреть токены и пытаться понял, какой стейтмент находится на текущей позиции. Для этого мы сделаем методы парсера peek и error публичными, чтобы мы могли использовать извне. Этот метод будет возвращать один из вариантов нового перечисления - StmtKind. В парсере будет метод stmt, он будет проверять, что вернул define и вызывать соответствующий метод для парсинга этого стейтмента.

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

Сразу сделаем peek и error публичными:

pub fn error(&self, msg: &str, token: Token) -> String { ... }

pub fn peek(&self, offset: i8) -> Token { ... }

В пакете parser создаем новый файл - ast.rs.

parser/ast.rs
use super::Parser;
use super::expr::Expr;
use crate::lexer::token::TKind;

#[derive(Debug)]
pub enum Stmt {
    Assign(String, AssignOp, Expr),
    Print(Vec<Expr>),
}

#[derive(Debug)]
pub enum StmtKind {
    Assign,
    Print,
}

#[derive(Debug, Default)]
pub enum AssignOp {
    #[default]
    Assign,
}

Возможно, вы подумаете зачем я создал перечисление AssignOp? Все просто. На данный момент у нас нет токенов для других вариантов присвоения типа +=, только =, и пока что тут будет 1 стандартный вариант, другие, возможно, добавим позже.

Поясню за параметры вариантов Stmt:

Assign(String, AssignOp, Expr) - String тут выступает в качестве идентификатора переменной. AssignOp соответственно тип присвоения. Expr значение, которое мы будем присваивать.

Print(Vec<Expr>) - вектор выражений тут собственно просто все то, что мы будем выводить.

Метод define

impl Stmt {
    pub fn define(pr: &Parser) -> StmtKind {
        match (pr.peek(0).kind, pr.peek(1).kind) {
            (TKind::Id(_), TKind::Assign) => StmtKind::Assign,
            (TKind::Print, _) => StmtKind::Print,
            _ => panic!("{}", pr.error("Unknown statement", pr.peek(0))),
        }
    }
}

Тут тоже все довольно просто. Проверяем первые 2 типа токена с текущей позиции, и смотрим, что это за токены. Если это идентификатор и знак равно (Мы в будущем, как я писал ранее, добавим другие варианты), то это присвоение, если это кейворд print, то это соответственно Print. В случае, если мы не нашли подходящий паттерн, то паникуем с сообщением о неизвестном стейтменте.

Print стейтмент

Он посложнее, чем Assign, так что начнем с него. Сразу напишем удобный метод для парсинга передаваемых аргументов

parser/parser.rs: Parser
fn parse_args(&mut self) -> Vec<Expr> {
    let mut args = Vec::new();
    while self.peek(0).kind != TKind::Eof {
        let value = self.expr();
        args.push(value);
        let current = self.peek(0);
        if current.kind == TKind::RParen {
            break;
        }
        if !self.check(TKind::Comma) {
            panic!(
                "{}",
                self.error(
                    &format!("Expected ')' or ',', found {:?}", current),
                    current
                )
            );
        }
    }
    args
}

В бесконечном цикле, пока текущий токен не равен EOF: парсим выражение и сразу добавляем в список аргументов. Проверяем следующий токен, если это запятая, то просто продолжаем цикл, в случае если это закрывающая скобка, то завершаем цикл. Если это не запятая и не закрывающая скобка, то выдаем ошибку о неожиданном токене.

fn parse_print(&mut self) -> Stmt {
    self.advance(1);
    if !self.check(TKind::LParen) {
        let current = self.peek(0);
        panic!(
            "{}",
            self.error(&format!("Expected '(', found {:?}", current), current)
        );
    }
    let args = self.parse_args();
    if !self.check(TKind::RParen) {
        let current = self.peek(0);
        panic!(
            "{}",
            self.error(&format!("Expected ')', found {:?}", current), current)
        );
    }
    Stmt::Print(args)
}

Assign стейтмент

fn parse_assign(&mut self) -> Stmt {
    let id = match self.peek(0).kind {
        TKind::Id(id) => {
            self.advance(1);
            id
        }
        _ => unreachable!("{:?}", self.peek(0).kind),
    };
    let assign = AssignOp::default();
    self.advance(1);
    let value = self.expr();
    Stmt::Assign(id, assign, value)
}

Сначала получаем идентификатор. Он не может быть не идентификатором, так как define это уже проверил, так что ставим unreachable для других вариантов.

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

Парсинг

fn stmt(&mut self) -> Stmt {
    match Stmt::define(self) {
        StmtKind::Assign => self.parse_assign(),
        StmtKind::Print => self.parse_print(),
    }
}

Этот метод мы будем вызывать для парсинга стейтментов, он определяет тип и вызывает соответствующие методы.

Создаем метод для парсинга списка стейтменов:

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

Пока текущий токен не EOF парсим стейтменты и добавляем в список.

Добавим новый тест в tests/parsert.rs

fn statement() {
    let source = "
a = 4
print(\"a is \", a)
"
    .trim();
    let tokens = Lexer::new(source).tokenize();
    println!("Source: {}", source);
    let stmts = Parser::new(tokens, source).parse();
    println!("Statements:");
    for stmt in stmts {
        println!("{:?}", stmt);
    }
}
Source: a = 4
print("a is ", a)
Statements:
Assign("a", Assign, Num(4.0, Info { line: 0, offset: 4, len: 1 }))
Print([Str("a is ", Info { line: 1, offset: 6, len: 7 }), Id("a", Info { line: 1, offset: 15, len: 3 })])

Итог

Теперь парсер уже может парсить первые стейтменты! В следующей статье мы напишем парсинг для if-elif-else.

Комментарии (2)


  1. fedyapetukhov09
    08.06.2026 08:44

    Идея норм, но пока всё довольно хрупкоdefine через peek быстро упрётся в ограничения, а panic в парсере потом будет мешать нормальной обработке ошибок. Это скорее заготовка, чем стабильная архитектура.


    1. kiquarsl Автор
      08.06.2026 08:44

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

      Это статьи в вобщем про то, как написать простенький прототип языка, а не продакшн-продукт.