326 lines
8.9 KiB
Rust
326 lines
8.9 KiB
Rust
// `impl Trait` is not required for this parser but we use to to show that it can be used to
|
|
// significantly simplify things
|
|
|
|
#![cfg(feature = "std")]
|
|
|
|
#[macro_use]
|
|
extern crate criterion;
|
|
|
|
#[macro_use]
|
|
extern crate combine;
|
|
|
|
use std::{collections::HashMap, fs::File, io::Read, path::Path};
|
|
|
|
use {
|
|
combine::{
|
|
error::{Commit, ParseError},
|
|
parser::{
|
|
char::{char, digit, spaces, string},
|
|
choice::{choice, optional},
|
|
function::parser,
|
|
repeat::{many, many1, sep_by},
|
|
sequence::between,
|
|
token::{any, satisfy, satisfy_map},
|
|
},
|
|
stream::{
|
|
buffered,
|
|
position::{self, SourcePosition},
|
|
IteratorStream,
|
|
},
|
|
EasyParser, Parser, Stream, StreamOnce,
|
|
},
|
|
criterion::{black_box, Bencher, Criterion},
|
|
};
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
enum Value {
|
|
Number(f64),
|
|
String(String),
|
|
Bool(bool),
|
|
Null,
|
|
Object(HashMap<String, Value>),
|
|
Array(Vec<Value>),
|
|
}
|
|
|
|
fn lex<Input, P>(p: P) -> impl Parser<Input, Output = P::Output>
|
|
where
|
|
P: Parser<Input>,
|
|
Input: Stream<Token = char>,
|
|
<Input as StreamOnce>::Error: ParseError<
|
|
<Input as StreamOnce>::Token,
|
|
<Input as StreamOnce>::Range,
|
|
<Input as StreamOnce>::Position,
|
|
>,
|
|
{
|
|
p.skip(spaces())
|
|
}
|
|
|
|
fn integer<Input>() -> impl Parser<Input, Output = i64>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
lex(many1(digit()))
|
|
.map(|s: String| {
|
|
let mut n = 0;
|
|
for c in s.chars() {
|
|
n = n * 10 + (c as i64 - '0' as i64);
|
|
}
|
|
n
|
|
})
|
|
.expected("integer")
|
|
}
|
|
|
|
fn number<Input>() -> impl Parser<Input, Output = f64>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
let i = char('0').map(|_| 0.0).or(integer().map(|x| x as f64));
|
|
let fractional = many(digit()).map(|digits: String| {
|
|
let mut magnitude = 1.0;
|
|
digits.chars().fold(0.0, |acc, d| {
|
|
magnitude /= 10.0;
|
|
match d.to_digit(10) {
|
|
Some(d) => acc + (d as f64) * magnitude,
|
|
None => panic!("Not a digit"),
|
|
}
|
|
})
|
|
});
|
|
|
|
let exp = satisfy(|c| c == 'e' || c == 'E').with(optional(char('-')).and(integer()));
|
|
lex(optional(char('-'))
|
|
.and(i)
|
|
.map(|(sign, n)| if sign.is_some() { -n } else { n })
|
|
.and(optional(char('.')).with(fractional))
|
|
.map(|(x, y)| if x >= 0.0 { x + y } else { x - y })
|
|
.and(optional(exp))
|
|
.map(|(n, exp_option)| match exp_option {
|
|
Some((sign, e)) => {
|
|
let e = if sign.is_some() { -e } else { e };
|
|
n * 10.0f64.powi(e as i32)
|
|
}
|
|
None => n,
|
|
}))
|
|
.expected("number")
|
|
}
|
|
|
|
fn json_char<Input>() -> impl Parser<Input, Output = char>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
parser(|input: &mut Input| {
|
|
let (c, committed) = any().parse_lazy(input).into_result()?;
|
|
let mut back_slash_char = satisfy_map(|c| {
|
|
Some(match c {
|
|
'"' => '"',
|
|
'\\' => '\\',
|
|
'/' => '/',
|
|
'b' => '\u{0008}',
|
|
'f' => '\u{000c}',
|
|
'n' => '\n',
|
|
'r' => '\r',
|
|
't' => '\t',
|
|
_ => return None,
|
|
})
|
|
});
|
|
match c {
|
|
'\\' => committed.combine(|_| back_slash_char.parse_stream(input).into_result()),
|
|
'"' => Err(Commit::Peek(Input::Error::empty(input.position()).into())),
|
|
_ => Ok((c, committed)),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn json_string<Input>() -> impl Parser<Input, Output = String>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
between(char('"'), lex(char('"')), many(json_char())).expected("string")
|
|
}
|
|
|
|
fn object<Input>() -> impl Parser<Input, Output = Value>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
let field = (json_string(), lex(char(':')), json_value()).map(|t| (t.0, t.2));
|
|
let fields = sep_by(field, lex(char(',')));
|
|
between(lex(char('{')), lex(char('}')), fields)
|
|
.map(Value::Object)
|
|
.expected("object")
|
|
}
|
|
|
|
#[inline]
|
|
fn json_value<Input>() -> impl Parser<Input, Output = Value>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
json_value_()
|
|
}
|
|
|
|
// We need to use `parser!` to break the recursive use of `value` to prevent the returned parser
|
|
// from containing itself
|
|
parser! {
|
|
#[inline]
|
|
fn json_value_[Input]()(Input) -> Value
|
|
where [ Input: Stream<Token = char> ]
|
|
{
|
|
let array = between(
|
|
lex(char('[')),
|
|
lex(char(']')),
|
|
sep_by(json_value(), lex(char(','))),
|
|
).map(Value::Array);
|
|
|
|
choice((
|
|
json_string().map(Value::String),
|
|
object(),
|
|
array,
|
|
number().map(Value::Number),
|
|
lex(string("false").map(|_| Value::Bool(false))),
|
|
lex(string("true").map(|_| Value::Bool(true))),
|
|
lex(string("null").map(|_| Value::Null)),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn json_test() {
|
|
use self::Value::*;
|
|
|
|
let input = r#"{
|
|
"array": [1, ""],
|
|
"object": {},
|
|
"number": 3.14,
|
|
"small_number": 0.59,
|
|
"int": -100,
|
|
"exp": -1e2,
|
|
"exp_neg": 23e-2,
|
|
"true": true,
|
|
"false" : false,
|
|
"null" : null
|
|
}"#;
|
|
let result = json_value().easy_parse(input);
|
|
let expected = Object(
|
|
vec![
|
|
("array", Array(vec![Number(1.0), String("".to_string())])),
|
|
("object", Object(HashMap::new())),
|
|
("number", Number(3.14)),
|
|
("small_number", Number(0.59)),
|
|
("int", Number(-100.)),
|
|
("exp", Number(-1e2)),
|
|
("exp_neg", Number(23E-2)),
|
|
("true", Bool(true)),
|
|
("false", Bool(false)),
|
|
("null", Null),
|
|
]
|
|
.into_iter()
|
|
.map(|(k, v)| (k.to_string(), v))
|
|
.collect(),
|
|
);
|
|
match result {
|
|
Ok(result) => assert_eq!(result, (expected, "")),
|
|
Err(e) => {
|
|
println!("{}", e);
|
|
panic!();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn test_data() -> String {
|
|
let mut data = String::new();
|
|
File::open(&Path::new(&"benches/data.json"))
|
|
.and_then(|mut file| file.read_to_string(&mut data))
|
|
.unwrap();
|
|
data
|
|
}
|
|
|
|
fn bench_json(bencher: &mut Bencher<'_>) {
|
|
let data = test_data();
|
|
let mut parser = json_value();
|
|
match parser.easy_parse(position::Stream::new(&data[..])) {
|
|
Ok((Value::Array(_), _)) => (),
|
|
Ok(_) => panic!(),
|
|
Err(err) => {
|
|
println!("{}", err);
|
|
panic!();
|
|
}
|
|
}
|
|
bencher.iter(|| {
|
|
let result = parser.easy_parse(position::Stream::new(&data[..]));
|
|
black_box(result)
|
|
});
|
|
}
|
|
|
|
fn bench_json_core_error(bencher: &mut Bencher<'_>) {
|
|
let data = test_data();
|
|
let mut parser = json_value();
|
|
match parser.parse(position::Stream::new(&data[..])) {
|
|
Ok((Value::Array(_), _)) => (),
|
|
Ok(_) => panic!(),
|
|
Err(err) => {
|
|
println!("{}", err);
|
|
panic!();
|
|
}
|
|
}
|
|
bencher.iter(|| {
|
|
let result = parser.parse(position::Stream::new(&data[..]));
|
|
black_box(result)
|
|
});
|
|
}
|
|
|
|
fn bench_json_core_error_no_position(bencher: &mut Bencher<'_>) {
|
|
let data = test_data();
|
|
let mut parser = json_value();
|
|
match parser.parse(&data[..]) {
|
|
Ok((Value::Array(_), _)) => (),
|
|
Ok(_) => panic!(),
|
|
Err(err) => {
|
|
println!("{}", err);
|
|
panic!();
|
|
}
|
|
}
|
|
bencher.iter(|| {
|
|
let result = parser.parse(&data[..]);
|
|
black_box(result)
|
|
});
|
|
}
|
|
|
|
fn bench_buffered_json(bencher: &mut Bencher<'_>) {
|
|
let data = test_data();
|
|
bencher.iter(|| {
|
|
let buffer =
|
|
buffered::Stream::new(position::Stream::new(IteratorStream::new(data.chars())), 1);
|
|
let mut parser = json_value();
|
|
match parser.easy_parse(position::Stream::with_positioner(
|
|
buffer,
|
|
SourcePosition::default(),
|
|
)) {
|
|
Ok((Value::Array(v), _)) => {
|
|
black_box(v);
|
|
}
|
|
Ok(_) => panic!(),
|
|
Err(err) => {
|
|
println!("{}", err);
|
|
panic!();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn bench(c: &mut Criterion) {
|
|
c.bench_function("json", bench_json);
|
|
c.bench_function("json_core_error", bench_json_core_error);
|
|
c.bench_function(
|
|
"json_core_error_no_position",
|
|
bench_json_core_error_no_position,
|
|
);
|
|
c.bench_function("buffered_json", bench_buffered_json);
|
|
}
|
|
|
|
criterion_group!(json, bench);
|
|
criterion_main!(json);
|