Nom

Parsing data in Rust

Дисклеймер

  • Призвано быть частью прелюдии перед афтепати
  • Написано с расчетом что поймут даже дети
  • Цель доклада рассказать о очевидном
  • Не рекомендуется беременным женщинам

Вначале было слово...

...и слово было RegExp

Разбор и Проверка Email по RFC 5322

Существующие проблемы при использовании RegExp

Дикий невнятный синтаксис

  • Для небольших простых выражений все "легко"
  • Для сложных вариативных выражений - избыточен и не читаем
  • Составление комплексных выражений превращается в ад
  • Неоднородность - выяснить какой вариант правильный, и действительно покрывает задачу почти нереально.

Обработка ошибок...

отсутствует

  • Нет внятной обработки ошибок - выражение или работает, или не работает вовсе.
  • Невозможно отладить выполнение, и отследить в каком именно месте произошла ошибка.
  • Нет гибкого способа отработать часть выражения: ни игнорировать, ни обработать неверную часть.

RegExp это дорогое удовольствие

  • Производительность сильно падает по мере усложнения выражения, и увеличения входных данных.
  • Из-за низкой эффективности для сложных комбинаций порождает множество костылей.
  • Костыли приводят к конструкциям при которых один и тот же кусок данных прогоняют по очереди множеством regexp-s 
  • Потребляет много памяти.

Комбинаторные Парсеры

  • Можно описывать извлечение рекурсивных структур данных.
  • Разбиение выражений, на простые и реюзабельные.
  • Простое извлечение данных и приведение типов.
  • Удобно тестировать и расширять.
  • Удобная обработка ошибок.

В дело вступает Nom

  • Практичный и понятный декларативный стиль.
  • Стиль обеспечивается за счет использования макросов.
  • Легкая интеграция с тестированием каждой комбинации.
  • Парсинг как текстовых так и бинарных данных.
  • Высокая скорость разбора данных.
  • Безопасность и экономность.

Что уже написано на Nom?

  • Парсеры языков: Rust, GLSL, Lua, SQL
  • Парсеры протоколов: HTTP, TLS, IRC, IMAP
  • А также: TAR, CSV, TOML

Напишем свой первый простой парсер

Задача

Написать простой парсер абстрактной css функции вида:

func(10px,10deg)
  • Извлечь имя функции
  • Обработать список внутри (разделенный запятой)
  • Аргументов может быть неограниченное количество
  • Аргументы могут иметь типы:
    Угол: deg или rad
    Длина: px или %

Первая комбинация: Углы

use nom::{digit, IResult, Err, Needed};
std::str;

#[derive(Debug, Clone, PartialEq)]
pub struct AngleRepr<'a, 'b> {
  pub value: &'a str,
  pub angle: &'b str,
}

named!(angle_type<&[u8], &str>, alt!(
  tag!("rad") => { |_| "radians" } |
  tag!("deg") => { |_| "degrees" }
));

named!(pub angle(&[u8]) -> UnitRepr, do_parse!(
  value: digit      >>
  angle: angle_type >>
  (UnitRepr::Angle(AngleRepr {
    value: str::from_utf8(value).unwrap(),
    angle
  }))
));

Также по аналогии...

#[derive(Debug, Clone, PartialEq)]
pub struct LengthRepr<'a, 'b> {
  pub value: &'a str,
  pub unit: &'b str,
}

named!(length_type<&[u8], &str>, alt!(
  tag!("%")   => { |_| "percent" } |
  tag!("px")  => { |_| "point" }
));

named!(pub length(&[u8]) -> UnitRepr, do_parse!(
  value: digit      >>
  unit: length_type >>
  (UnitRepr::Length(LengthRepr {
    value: str::from_utf8(value).unwrap(),
    unit
  }))
));

Унифицируем Unit как Enum

#[derive(Debug, Clone, PartialEq)]
pub enum UnitRepr<'a, 'b> {
  Length(LengthRepr<'a, 'b>),
  Angle(AngleRepr<'a, 'b>),
}

named!(pub unit(&[u8]) -> UnitRepr, alt!(length | angle));

Объявляем основной комбинатор

use nom::alpha;
use std::str;

#[derive(Debug, Clone, PartialEq)]
pub struct TransformFunction<'a, 'b, 'c> {
  pub args: Vec<UnitRepr<'a, 'b>>,
  pub name: &'c str,
}

named!(fn_name(&[u8]) -> &[u8], ws!(alpha));

named!(args(&[u8]) -> Vec<UnitRepr>,
  delimited!(
    char!('('),
      separated_list!(char!(','), unit),
    char!(')')
  )
);

named!(pub transform_parse(&[u8]) -> TransformFunction, do_parse!(
  name: fn_name >>
  args: args    >>
  (TransformFunction {
    name: str::from_utf8(name).unwrap(),
    args,
  })
));

Тестируем

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn transform_function_parse() {
    use properties::parse::{LengthRepr, AngleRepr};

    let my_str = "func(10px,10deg)";
    let parsed = transform_parse(my_str.as_bytes())
      .expect("Can't parse transform").1;

    let expected = TransformFunction {
      args: vec![
        UnitRepr::Length(LengthRepr {
          value: "10",
          unit: "point",
        }),
        UnitRepr::Angle(AngleRepr {
          value: "10",
          angle: "degrees",
        }),
      ],
      name: "func",
    };
    assert_eq!(parsed, expected);
  }
}

И Запускаем!

Оно работает!

если нет то тоже сойдет

The End

Вопросы?

Nom: parsing data in Rust

By Anton Shramko

Nom: parsing data in Rust

  • 1,738