#logo #parser-combinator #lexer #language #parser

parsit

一个非常简单的库,使用 logos 作为词法分析器的递归下降解析器

17次发布

0.2.0 2023年7月22日
0.1.15 2023年7月16日
0.1.14 2023年6月4日
0.1.12 2022年11月13日
0.1.6 2022年9月28日

#109解析工具

30 每月下载量
用于 2 个 crate(通过 forester-rs

自定义许可

85KB
779

Parsit

描述

此库提供一个非常简单且轻量级的解析器(递归下降 ll(1)),用于组合和表达文法。

该库使用 Logos 作为词法分析器和分词器。

前提条件

此库的主要动机是

  • 轻量级:非常小,无需深入研究
  • 透明度:仅有3个结构体和一些方法
  • 速度:良好的速度(感谢 Logos

实现步骤

使用 Logos 创建一组标记

将 logos 添加到依赖项

    logos = "*"
use logos::Logos;

#[derive(Logos, Debug, PartialEq)]
enum Token {
    // Tokens can be literal strings, of any length.
    #[token("fast")]
    Fast,

    #[token(".")]
    Period,

    // Or regular expressions.
    #[regex("[a-zA-Z]+")]
    Text,

    // Logos requires one token variant to handle errors,
    // it can be named anything you wish.
    #[error]
    // We can also use this variant to define whitespace,
    // or any other matches we wish to skip.
    #[regex(r"[ \t\n\f]+", logos::skip)]
    Error,
}

创建一个能够解析给定标记集的解析器

该库提供包含一组标记和辅助方法的 Parsit<'a,T> 实例


struct Parser<'a> {
    inner: Parsit<'a, Token<'a>>,
}

使用 Parsit 实例和辅助方法实现解析函数

辅助工具

  • token!,简化单个标记的比较和匹配
  • 来自 Step 的方法 thenthen_zip
  • 来自 Parsit 的方法 one_or_morezero_or_more

将结果转换为 Result<Structure, ParserError<'a>>

      fn text(&self, pos: usize) -> Result<Vec<Sentence<'a>>, ParseError<'a>> {
    self.inner.zero_or_more(pos, |p| self.sentence(p)).into()
}

完整示例

  use crate::parser::Parsit;
use crate::token;
use crate::step::Step;
use crate::parser::EmptyToken;
use crate::error::ParseError;
use logos::Logos;


#[derive(Logos, Debug, Copy, Clone, PartialEq)]
pub enum Token<'a> {
    #[regex(r"[a-zA-Z-]+")]
    Word(&'a str),

    #[token(",")]
    Comma,
    #[token(".")]
    Dot,

    #[token("!")]
    Bang,
    #[token("?")]
    Question,

    #[regex(r"[ \t\r\n\u000C\f]+", logos::skip)]
    Whitespace,
    #[error]
    Error,
}

#[derive(Debug, Copy, Clone, PartialEq)]
enum Item<'a> {
    Word(&'a str),
    Comma,
}

#[derive(Debug, Clone, PartialEq)]
enum Sentence<'a> {
    Sentence(Vec<Item<'a>>),
    Question(Vec<Item<'a>>),
    Exclamation(Vec<Item<'a>>),
}

struct Parser<'a> {
    inner: Parsit<'a, Token<'a>>,
}

impl<'a> Parser<'a> {
    fn new(text: &'a str) -> Parser<'a> {
        let delegate: Parsit<Token> = Parsit::new(text).unwrap();
        Parser { inner: delegate }
    }

    fn sentence(&self, pos: usize) -> Step<'a, Sentence<'a>> {
        let items = |p| self.inner.one_or_more(p, |p| self.word(p));

        let sentence = |p| items(p)
            .then_zip(|p| token!(self.inner.token(p) => Token::Dot))
            .take_left()
            .map(Sentence::Sentence);

        let exclamation = |p| items(p)
            .then_zip(|p| token!(self.inner.token(p) => Token::Bang))
            .take_left()
            .map(Sentence::Exclamation);
        let question = |p| items(p)
            .then_zip(|p| token!(self.inner.token(p) => Token::Question))
            .take_left()
            .map(Sentence::Question);

        sentence(pos)
            .or_from(pos)
            .or(exclamation)
            .or(question)
            .into()
    }
    fn word(&self, pos: usize) -> Step<'a, Item<'a>> {
        token!(self.inner.token(pos) =>
                     Token::Word(v) => Item::Word(v),
                     Token::Comma => Item::Comma
            )
    }
    fn text(&self, pos: usize) -> Result<Vec<Sentence<'a>>, ParseError<'a>> {
        self.inner.zero_or_more(pos, |p| self.sentence(p)).into()
    }
}


#[test]
fn test() {
    let parser = Parser::new(r#"
            I have a strange addiction,
            It often sets off sparks!
            I really cannot seem to stop,
            Using exclamation marks!
            Anyone heard of the interrobang?
            The poem is for kids.
        "#);

    let result = parser.text(0).unwrap();
    println!("{:?}", result);
}

基本辅助方法

在解析器上

  • token - 提供获取当前标记的方法
  • one_or_more - 提供一个或多个语义
  • zero_or_more - 提供零个或多个语义
  • validate_eof - 确保解析器到达输入的末尾

  • token! - 解析当前标记。通常,它用于以下代码:token!(p.token(pos) => T::Bang => "!")
  • wrap! - 实现类似“左值 右”的简单语法模式,例如:[1,2,3](a,b)
    • 可以处理默认值,如:wrap!(0 => left; value or default; right)
    • 可以处理选项值,如:wrap!(0 => left; value ?; right)
  • seq! - 实现类似“el sep el ...”的简单序列模式,例如:1,2,3
    • 可以在末尾有逗号,表示分隔符可以在序列末尾,如:1,2,3 (,)?

步骤

交替
  • or - 在一个标记的范围内给出一个替代方案
  • or_from - 给出一个回溯选项
组合
  • then - 给出一个省略当前规则的下一个规则的基本组合
  • then_zip - 将当前结果和下一个结果组合成一对
  • then_skip - 解析下一个标记,但丢弃结果,只保留当前标记
  • then_or_none - 将下一个选项与当前选项组合,或者在没有其他选项的情况下返回none
收集
  • take_left - 从一对中删除右值
  • take_right - 从一对中删除左值
  • merge - 将值合并到列表中
  • to_map - 将一对列表转换为映射
转换
  • or_val - 如果值未提供,则用默认值替换值
  • or_none - 如果值未提供,则用none替换值
处理值
  • ok - 将值转换为选项
  • error - 将错误转换为选项
  • map - 转换值
  • combine - 将值与给定步骤中的另一个值组合
  • validate - 验证给定的值,如果验证失败则转换为错误
打印
  • print - 打印步骤
  • print_with - 打印带有指定前缀的步骤
  • print_as - 打印经过值转换的步骤
  • print_with_as - 打印带有值转换和指定前缀的步骤
  • parsit.env - 从源文本中打印位置和env(半径为3个标记)

测试

词法分析器

要测试词法分析器,有来自 crate::parsit::test::lexer_test::* 的服务方法

 use logos::Logos;
use crate::parsit::test::lexer_test::*;

#[derive(Logos, Debug, PartialEq)]
pub enum T<'a> {
    #[regex(r"[a-zA-Z-]+")]
    Word(&'a str),

    #[token(",")]
    Comma,
    #[token(".")]
    Dot,

    #[token("!")]
    Bang,
    #[token("?")]
    Question,

    #[regex(r"[ \t\r\n]+", logos::skip)]
    Whitespace,
    #[error]
    Error,
}

#[test]
fn test() {
    expect::<T>("abc, bcs!", vec![T::Word("abc"), T::Comma, T::Word("bcs"), T::Bang]);
    expect_succeed::<T>("abc, bcs!");
    expect_failed::<T>("abc, bcs >> !");
    expect_failed_with::<T, _>("abc, bcs > !", |e| e.is_bad_token_on(">"));
}

解析器

要测试解析器,有来自 crate::parsit::test::parser_test::* 的服务方法

  • expect : 预期解析给定的值
  • expect_or_env : 预期解析给定的值,否则将打印env(parsit.env
  • expect_pos : 预期解析并获取给定位置的光标
  • expect_pos_or_env : 预期解析并获取给定位置的光标,否则将打印env(parsit.env
  • fail : 应该解析失败
  • fail_on : 在给定位置解析失败
use logos::Logos;
use crate::parsit::test::parser_test::fail;
use crate::parsit::test::parser_test::parsit;
use crate::parsit::token;
use crate::parsit::parser::Parsit;
use crate::parsit::step::Step;

#[derive(Logos, Debug, PartialEq)]
pub enum T<'a> {
    #[regex(r"[a-zA-Z-]+")]
    Word(&'a str),

    #[token(",")]
    Comma,
    #[token(".")]
    Dot,

    #[token("!")]
    Bang,
    #[token("?")]
    Question,

    #[regex(r"[ \t\r\n]+", logos::skip)]
    Whitespace,
    #[error]
    Error,
}

#[test]
fn test_expect() {
    let p = parsit("abc!");
    let bang = |pos: usize| token!(p.token(pos) => T::Bang => "!");
    let word = |pos: usize| token!(p.token(pos) => T::Word(v) => *v);
    let step =
        word(0)
            .then_or_val_zip(bang, "")
            .map(|(a, b)| format!("{}{}", a, b));

    expect(step, "abc!".to_string());
}
#[test]
fn test_expect_or_env() {
    let p = parsit("abc!");
    let bang = |pos: usize| token!(p.token(pos) => T::Bang => "!");
    let word = |pos: usize| token!(p.token(pos) => T::Word(v) => *v);
    let step =
        word(0)
            .then_or_val_zip(bang, "")
            .map(|(a, b)| format!("{}{}", a, b));

    expect_or_env(p,step, "abc!".to_string());
}

#[test]
fn test_pos() {
    let p = parsit("abc!");
    let bang = |pos: usize| token!(p.token(pos) => T::Bang => "!");
    let word = |pos: usize| token!(p.token(pos) => T::Word(v) => v);
    let step = word(0).then_or_val_zip(bang, "");

    expect_pos(step, 2); // the next position to parse
}
#[test]
fn test_pos_or_env() {
    let p = parsit("abc!");
    let bang = |pos: usize| token!(p.token(pos) => T::Bang => "!");
    let word = |pos: usize| token!(p.token(pos) => T::Word(v) => v);
    let step = word(0).then_or_val_zip(bang, "");

    expect_pos_or_env(p,step, 2); // the next position to parse
}

#[test]
fn test_fail() {
    let p = parsit("abc?!");
    let bang = |pos: usize| token!(p.token(pos) => T::Bang => "!");
    let word = |pos: usize| token!(p.token(pos) => T::Word(v) => v);
    let step = word(0).then_zip(bang);

    fail(step);
}

#[test]
fn test_fail_on() {
    let p = parsit("abc?!");
    let bang = |pos: usize| token!(p.token(pos) => T::Bang => "!");
    let word = |pos: usize| token!(p.token(pos) => T::Word(v) => v);
    let step = word(0).then_zip(bang);

    fail_on(step, 1);
}



依赖

~4MB
~72K SLoC