Speed up EDN parser (fixes #445) (#581) r=nalexander

Fixes from @kevinmehall.

* Prefer character sets over backtracking in the EDN parser.
* Avoid duplicate effort when parsing floats in the EDN parser.
* Clean up duplicate position tracking code.

This turns out to have little performance impact, but makes the grammar
much cleaner.

* Fix EDN work to pass tests with correct numeric precedence.
This commit is contained in:
Richard Newman 2018-03-05 20:33:51 -08:00 committed by GitHub
parent 30bf827d16
commit 9b23cf3945
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 182 deletions

View file

@ -34,105 +34,40 @@ use types::{SpannedValue, Span, ValueAndSpan};
// TODO: Support tagged elements // TODO: Support tagged elements
// TODO: Support discard // TODO: Support discard
pub nil -> ValueAndSpan = pub nil -> SpannedValue = "nil" { SpannedValue::Nil }
start:#position "nil" end:#position { pub nan -> SpannedValue = "#f" whitespace+ "NaN" { SpannedValue::Float(OrderedFloat(NAN)) }
ValueAndSpan {
inner: SpannedValue::Nil,
span: Span::new(start, end)
}
}
pub nan -> ValueAndSpan = pub infinity -> SpannedValue = "#f" whitespace+ s:$(sign) "Infinity"
start:#position "#f" whitespace+ "NaN" end:#position { { SpannedValue::Float(OrderedFloat(if s == "+" { INFINITY } else { NEG_INFINITY })) }
ValueAndSpan {
inner: SpannedValue::Float(OrderedFloat(NAN)),
span: Span::new(start, end)
}
}
pub infinity -> ValueAndSpan = pub boolean -> SpannedValue
start:#position "#f" whitespace+ s:$(sign) "Infinity" end:#position { = "true" { SpannedValue::Boolean(true) }
ValueAndSpan { / "false" { SpannedValue::Boolean(false) }
inner: SpannedValue::Float(OrderedFloat(if s == "+" { INFINITY } else { NEG_INFINITY })),
span: Span::new(start, end)
}
}
pub boolean -> ValueAndSpan =
start:#position "true" end:#position {
ValueAndSpan {
inner: SpannedValue::Boolean(true),
span: Span::new(start, end)
}
} /
start:#position "false" end:#position {
ValueAndSpan {
inner: SpannedValue::Boolean(false),
span: Span::new(start, end)
}
}
digit = [0-9] digit = [0-9]
alphanumeric = [0-9a-zA-Z] alphanumeric = [0-9a-zA-Z]
octaldigit = [0-7] octaldigit = [0-7]
validbase = [3][0-6] / [12][0-9] / [2-9] validbase = [3][0-6] / [12][0-9] / [2-9]
hex = [0-9a-fA-F] hex = [0-9a-fA-F]
sign = "-" / "+" sign = [+-]
pub bigint -> ValueAndSpan = pub bigint -> SpannedValue = b:$( sign? digit+ ) "N"
start:#position b:$( sign? digit+ ) "N" end:#position { { SpannedValue::BigInteger(b.parse::<BigInt>().unwrap()) }
ValueAndSpan { pub octalinteger -> SpannedValue = "0" i:$( octaldigit+ )
inner: SpannedValue::BigInteger(b.parse::<BigInt>().unwrap()), { SpannedValue::Integer(i64::from_str_radix(i, 8).unwrap()) }
span: Span::new(start, end) pub hexinteger -> SpannedValue = "0x" i:$( hex+ )
} { SpannedValue::Integer(i64::from_str_radix(i, 16).unwrap()) }
}
pub octalinteger -> ValueAndSpan = pub basedinteger -> SpannedValue = b:$( validbase ) "r" i:$( alphanumeric+ )
start:#position "0" i:$( octaldigit+ ) end:#position { { SpannedValue::Integer(i64::from_str_radix(i, b.parse::<u32>().unwrap()).unwrap()) }
ValueAndSpan {
inner: SpannedValue::Integer(i64::from_str_radix(i, 8).unwrap()),
span: Span::new(start, end)
}
}
pub hexinteger -> ValueAndSpan = pub integer -> SpannedValue = i:$( sign? digit+ ) !("." / ([eE]))
start:#position "0x" i:$( hex+ ) end:#position { { SpannedValue::Integer(i.parse::<i64>().unwrap()) }
ValueAndSpan {
inner: SpannedValue::Integer(i64::from_str_radix(i, 16).unwrap()),
span: Span::new(start, end)
}
}
pub basedinteger -> ValueAndSpan = pub float -> SpannedValue = f:$(sign? digit+ ("." digit+)? ([eE] sign? digit+)?)
// Only allow values 2-36 { SpannedValue::Float(OrderedFloat(f.parse::<f64>().unwrap())) }
start:#position b:$( validbase ) "r" i:$( alphanumeric+ ) end:#position {
ValueAndSpan {
inner: SpannedValue::Integer(i64::from_str_radix(i, b.parse::<u32>().unwrap()).unwrap()),
span: Span::new(start, end)
}
}
pub integer -> ValueAndSpan = number -> SpannedValue = ( bigint / basedinteger / hexinteger / octalinteger / integer / float )
start:#position i:$( sign? digit+ ) end:#position {
ValueAndSpan {
inner: SpannedValue::Integer(i.parse::<i64>().unwrap()),
span: Span::new(start, end)
}
}
frac = sign? digit+ "." digit+
exp = sign? digit+ ("e" / "E") sign? digit+
frac_exp = sign? digit+ "." digit+ ("e" / "E") sign? digit+
// The order here is important - frac_exp must come before (exp / frac) or the
// parser assumes exp or frac when the float is really a frac_exp and fails
pub float -> ValueAndSpan =
start:#position f:$( frac_exp / exp / frac ) end:#position {
ValueAndSpan {
inner: SpannedValue::Float(OrderedFloat(f.parse::<f64>().unwrap())),
span: Span::new(start, end)
}
}
// TODO: \newline, \return, \space and \tab // TODO: \newline, \return, \space and \tab
special_char = quote / tab special_char = quote / tab
@ -140,19 +75,13 @@ quote = "\\\""
tab = "\\tab" tab = "\\tab"
char = [^"] / special_char char = [^"] / special_char
pub text -> ValueAndSpan = pub text -> SpannedValue = "\"" t:$( char* ) "\""
start:#position "\"" t:$( char* ) "\"" end:#position { { SpannedValue::Text(t.to_string()) }
ValueAndSpan {
inner: SpannedValue::Text(t.to_string()),
span: Span::new(start, end)
}
}
// RFC 3339 timestamps. #inst "1985-04-12T23:20:50.52Z" // RFC 3339 timestamps. #inst "1985-04-12T23:20:50.52Z"
// We accept an arbitrary depth of decimals. // We accept an arbitrary depth of decimals.
// Note that we discard the timezone information -- all times are translated to UTC. // Note that we discard the timezone information -- all times are translated to UTC.
pub inst_string -> DateTime<Utc> = inst_string -> DateTime<Utc> =
"#inst" whitespace+ "\"" d:$( [0-9]*<4> "-" [0-2][0-9] "-" [0-3][0-9] "#inst" whitespace+ "\"" d:$( [0-9]*<4> "-" [0-2][0-9] "-" [0-3][0-9]
"T" "T"
[0-2][0-9] ":" [0-5][0-9] ":" [0-6][0-9] [0-2][0-9] ":" [0-5][0-9] ":" [0-6][0-9]
@ -165,7 +94,7 @@ pub inst_string -> DateTime<Utc> =
.map_err(|_| "invalid datetime") // Oh, rustpeg. .map_err(|_| "invalid datetime") // Oh, rustpeg.
} }
pub inst_micros -> DateTime<Utc> = inst_micros -> DateTime<Utc> =
"#instmicros" whitespace+ d:$( digit+ ) { "#instmicros" whitespace+ d:$( digit+ ) {
let micros = d.parse::<i64>().unwrap(); let micros = d.parse::<i64>().unwrap();
let seconds: i64 = micros / 1000000; let seconds: i64 = micros / 1000000;
@ -173,7 +102,7 @@ pub inst_micros -> DateTime<Utc> =
Utc.timestamp(seconds, nanos) Utc.timestamp(seconds, nanos)
} }
pub inst_millis -> DateTime<Utc> = inst_millis -> DateTime<Utc> =
"#instmillis" whitespace+ d:$( digit+ ) { "#instmillis" whitespace+ d:$( digit+ ) {
let millis = d.parse::<i64>().unwrap(); let millis = d.parse::<i64>().unwrap();
let seconds: i64 = millis / 1000; let seconds: i64 = millis / 1000;
@ -181,26 +110,16 @@ pub inst_millis -> DateTime<Utc> =
Utc.timestamp(seconds, nanos) Utc.timestamp(seconds, nanos)
} }
pub inst -> ValueAndSpan = inst -> SpannedValue = t:(inst_millis / inst_micros / inst_string)
start:#position t:(inst_millis / inst_micros / inst_string) end:#position { { SpannedValue::Instant(t) }
ValueAndSpan {
inner: SpannedValue::Instant(t),
span: Span::new(start, end)
}
}
pub uuid_string -> Uuid = uuid_string -> Uuid =
"\"" u:$( [a-f0-9]*<8> "-" [a-f0-9]*<4> "-" [a-f0-9]*<4> "-" [a-f0-9]*<4> "-" [a-f0-9]*<12> ) "\"" { "\"" u:$( [a-f0-9]*<8> "-" [a-f0-9]*<4> "-" [a-f0-9]*<4> "-" [a-f0-9]*<4> "-" [a-f0-9]*<12> ) "\"" {
Uuid::parse_str(u).expect("this is a valid UUID string") Uuid::parse_str(u).expect("this is a valid UUID string")
} }
pub uuid -> ValueAndSpan = pub uuid -> SpannedValue = "#uuid" whitespace+ u:uuid_string
start:#position "#uuid" whitespace+ u:(uuid_string) end:#position { { SpannedValue::Uuid(u) }
ValueAndSpan {
inner: SpannedValue::Uuid(u),
span: Span::new(start, end)
}
}
namespace_divider = "." namespace_divider = "."
namespace_separator = "/" namespace_separator = "/"
@ -208,8 +127,8 @@ namespace_separator = "/"
// TODO: Be more picky here // TODO: Be more picky here
// Keywords follow the rules of symbols, except they can (and must) begin with : // Keywords follow the rules of symbols, except they can (and must) begin with :
// e.g. :fred or :my/fred. See https://github.com/edn-format/edn#keywords // e.g. :fred or :my/fred. See https://github.com/edn-format/edn#keywords
symbol_char_initial = [a-z] / [A-Z] / [0-9] / [*!_?$%&=<>] symbol_char_initial = [a-zA-Z0-9*!_?$%&=<>]
symbol_char_subsequent = [a-z] / [A-Z] / [0-9] / [-*!_?$%&=<>] symbol_char_subsequent = [a-zA-Z0-9*!_?$%&=<>-]
symbol_namespace = symbol_char_initial symbol_char_subsequent* (namespace_divider symbol_char_subsequent+)* symbol_namespace = symbol_char_initial symbol_char_subsequent* (namespace_divider symbol_char_subsequent+)*
symbol_name = ( symbol_char_initial+ symbol_char_subsequent* ) symbol_name = ( symbol_char_initial+ symbol_char_subsequent* )
@ -217,76 +136,47 @@ plain_symbol_name = symbol_name / "..." / "."
keyword_prefix = ":" keyword_prefix = ":"
pub symbol -> ValueAndSpan = pub symbol -> SpannedValue =
start:#position
ns:( sns:$(symbol_namespace) namespace_separator { sns })? ns:( sns:$(symbol_namespace) namespace_separator { sns })?
n:$(plain_symbol_name) n:$(plain_symbol_name)
end:#position { { SpannedValue::from_symbol(ns, n) }
ValueAndSpan {
inner: SpannedValue::from_symbol(ns, n),
span: Span::new(start, end)
}
}
pub keyword -> ValueAndSpan = pub keyword -> SpannedValue =
start:#position
keyword_prefix keyword_prefix
ns:( sns:$(symbol_namespace) namespace_separator { sns })? ns:( sns:$(symbol_namespace) namespace_separator { sns })?
n:$(symbol_name) n:$(symbol_name)
end:#position { { SpannedValue::from_keyword(ns, n) }
ValueAndSpan {
inner: SpannedValue::from_keyword(ns, n),
span: Span::new(start, end)
}
}
pub list -> ValueAndSpan = pub list -> SpannedValue = "(" __ v:(value)* __ ")"
start:#position "(" __ v:(value)* __ ")" end:#position { { SpannedValue::List(LinkedList::from_iter(v)) }
ValueAndSpan {
inner: SpannedValue::List(LinkedList::from_iter(v)),
span: Span::new(start, end)
}
}
pub vector -> ValueAndSpan = pub vector -> SpannedValue = "[" __ v:(value)* __ "]"
start:#position "[" __ v:(value)* __ "]" end:#position { { SpannedValue::Vector(v) }
ValueAndSpan {
inner: SpannedValue::Vector(v),
span: Span::new(start, end)
}
}
pub set -> ValueAndSpan = pub set -> SpannedValue = "#{" __ v:(value)* __ "}"
start:#position "#{" __ v:(value)* __ "}" end:#position { { SpannedValue::Set(BTreeSet::from_iter(v)) }
ValueAndSpan {
inner: SpannedValue::Set(BTreeSet::from_iter(v)),
span: Span::new(start, end)
}
}
pair -> (ValueAndSpan, ValueAndSpan) = pair -> (ValueAndSpan, ValueAndSpan) =
k:(value) v:(value) { k:(value) v:(value) {
(k, v) (k, v)
} }
pub map -> ValueAndSpan = pub map -> SpannedValue = "{" __ v:(pair)* __ "}"
start:#position "{" __ v:(pair)* __ "}" end:#position { { SpannedValue::Map(BTreeMap::from_iter(v)) }
ValueAndSpan {
inner: SpannedValue::Map(BTreeMap::from_iter(v)),
span: Span::new(start, end)
}
}
// It's important that float comes before integer or the parser assumes that // It's important that float comes before integer or the parser assumes that
// floats are integers and fails to parse // floats are integers and fails to parse
pub value -> ValueAndSpan = pub value -> ValueAndSpan =
__ v:(nil / nan / infinity / boolean / float / octalinteger / hexinteger / basedinteger / inst / uuid / bigint / integer / text / keyword / symbol / list / vector / map / set) __ { __ start:#position v:(nil / nan / infinity / boolean / number / inst / uuid / text / keyword / symbol / list / vector / map / set) end:#position __ {
v ValueAndSpan {
inner: v,
span: Span::new(start, end)
}
} }
// Clojure (and thus EDN) regards commas as whitespace, and thus the two-element vectors [1 2] and // Clojure (and thus EDN) regards commas as whitespace, and thus the two-element vectors [1 2] and
// [1,,,,2] are equivalent, as are the maps {:a 1, :b 2} and {:a 1 :b 2}. // [1,,,,2] are equivalent, as are the maps {:a 1, :b 2} and {:a 1 :b 2}.
whitespace = (" " / "\r" / "\n" / "\t" / ",") whitespace = [ \r\n\t,]
comment = ";" [^\r\n]* ("\r" / "\n")? comment = ";" [^\r\n]* [\r\n]?
__ = (whitespace / comment)* __ = (whitespace / comment)*

View file

@ -163,6 +163,12 @@ impl From<SpannedValue> for Value {
} }
} }
impl From<ValueAndSpan> for Value {
fn from(src: ValueAndSpan) -> Value {
src.inner.into()
}
}
/// Creates `from_$TYPE` helper functions for Value and SpannedValue, /// Creates `from_$TYPE` helper functions for Value and SpannedValue,
/// like `from_float()` or `from_ordered_float()`. /// like `from_float()` or `from_ordered_float()`.
macro_rules! def_from { macro_rules! def_from {
@ -617,7 +623,10 @@ mod test {
#[test] #[test]
fn test_print_edn() { fn test_print_edn() {
assert_eq!("1234N", Value::from_bigint("1234").unwrap().to_string());
let string = "[ 1 2 ( 3.14 ) #{ 4N } { foo/bar 42 :baz/boz 43 } [ ] :five :six/seven eight nine/ten true false nil #f NaN #f -Infinity #f +Infinity ]"; let string = "[ 1 2 ( 3.14 ) #{ 4N } { foo/bar 42 :baz/boz 43 } [ ] :five :six/seven eight nine/ten true false nil #f NaN #f -Infinity #f +Infinity ]";
let data = Value::Vector(vec![ let data = Value::Vector(vec![
Value::Integer(1), Value::Integer(1),
Value::Integer(2), Value::Integer(2),

View file

@ -60,7 +60,7 @@ fn s_plain(name: &str) -> Value {
macro_rules! fn_parse_into_value { macro_rules! fn_parse_into_value {
($name: ident) => { ($name: ident) => {
fn $name<'a, T>(src: T) -> Result<Value, ParseError> where T: Into<&'a str> { fn $name<'a, T>(src: T) -> Result<Value, ParseError> where T: Into<&'a str> {
parse::$name(src.into()).map(|x| x.inner.into()) parse::$name(src.into()).map(|x| x.into())
} }
} }
} }
@ -98,7 +98,7 @@ fn test_nil() {
#[test] #[test]
fn test_span_nil() { fn test_span_nil() {
assert_eq!(parse::nil("nil").unwrap(), ValueAndSpan { assert_eq!(parse::value("nil").unwrap(), ValueAndSpan {
inner: SpannedValue::Nil, inner: SpannedValue::Nil,
span: Span(0, 3) span: Span(0, 3)
}); });
@ -120,7 +120,7 @@ fn test_nan() {
#[test] #[test]
fn test_span_nan() { fn test_span_nan() {
assert_eq!(parse::nan("#f NaN").unwrap(), ValueAndSpan { assert_eq!(parse::value("#f NaN").unwrap(), ValueAndSpan {
inner: SpannedValue::Float(OrderedFloat(f64::NAN)), inner: SpannedValue::Float(OrderedFloat(f64::NAN)),
span: Span(0, 6) span: Span(0, 6)
}); });
@ -150,11 +150,11 @@ fn test_infinity() {
#[test] #[test]
fn test_span_infinity() { fn test_span_infinity() {
assert_eq!(parse::infinity("#f -Infinity").unwrap(), ValueAndSpan { assert_eq!(parse::value("#f -Infinity").unwrap(), ValueAndSpan {
inner: SpannedValue::Float(OrderedFloat(f64::NEG_INFINITY)), inner: SpannedValue::Float(OrderedFloat(f64::NEG_INFINITY)),
span: Span(0, 12) span: Span(0, 12)
}); });
assert_eq!(parse::infinity("#f +Infinity").unwrap(), ValueAndSpan { assert_eq!(parse::value("#f +Infinity").unwrap(), ValueAndSpan {
inner: SpannedValue::Float(OrderedFloat(f64::INFINITY)), inner: SpannedValue::Float(OrderedFloat(f64::INFINITY)),
span: Span(0, 12) span: Span(0, 12)
}); });
@ -172,12 +172,12 @@ fn test_boolean() {
#[test] #[test]
fn test_span_boolean() { fn test_span_boolean() {
assert_eq!(parse::boolean("true").unwrap(), ValueAndSpan { assert_eq!(parse::value("true").unwrap(), ValueAndSpan {
inner: SpannedValue::Boolean(true), inner: SpannedValue::Boolean(true),
span: Span(0, 4) span: Span(0, 4)
}); });
assert_eq!(parse::boolean("false").unwrap(), ValueAndSpan { assert_eq!(parse::value("false").unwrap(), ValueAndSpan {
inner: SpannedValue::Boolean(false), inner: SpannedValue::Boolean(false),
span: Span(0, 5) span: Span(0, 5)
}); });
@ -235,19 +235,19 @@ fn test_octalinteger() {
#[test] #[test]
fn test_span_integer() { fn test_span_integer() {
assert_eq!(parse::integer("42").unwrap(), ValueAndSpan { assert_eq!(parse::value("42").unwrap(), ValueAndSpan {
inner: SpannedValue::Integer(42), inner: SpannedValue::Integer(42),
span: Span(0, 2) span: Span(0, 2)
}); });
assert_eq!(parse::hexinteger("0xabc111").unwrap(), ValueAndSpan { assert_eq!(parse::value("0xabc111").unwrap(), ValueAndSpan {
inner: SpannedValue::Integer(11256081), inner: SpannedValue::Integer(11256081),
span: Span(0, 8) span: Span(0, 8)
}); });
assert_eq!(parse::basedinteger("2r111").unwrap(), ValueAndSpan { assert_eq!(parse::value("2r111").unwrap(), ValueAndSpan {
inner: SpannedValue::Integer(7), inner: SpannedValue::Integer(7),
span: Span(0, 5) span: Span(0, 5)
}); });
assert_eq!(parse::octalinteger("011").unwrap(), ValueAndSpan { assert_eq!(parse::value("011").unwrap(), ValueAndSpan {
inner: SpannedValue::Integer(9), inner: SpannedValue::Integer(9),
span: Span(0, 3) span: Span(0, 3)
}); });
@ -266,7 +266,6 @@ fn test_uuid() {
.expect("valid UUID"); .expect("valid UUID");
let actual = parse::uuid("#uuid \"550e8400-e29b-41d4-a716-446655440000\"") let actual = parse::uuid("#uuid \"550e8400-e29b-41d4-a716-446655440000\"")
.expect("parse success") .expect("parse success")
.inner
.into(); .into();
assert_eq!(self::Value::Uuid(expected), actual); assert_eq!(self::Value::Uuid(expected), actual);
} }
@ -291,7 +290,7 @@ fn test_span_bigint() {
let max_i64 = i64::max_value().to_bigint().unwrap(); let max_i64 = i64::max_value().to_bigint().unwrap();
let bigger = &max_i64 * &max_i64; let bigger = &max_i64 * &max_i64;
assert_eq!(parse::bigint("85070591730234615847396907784232501249N").unwrap(), ValueAndSpan { assert_eq!(parse::value("85070591730234615847396907784232501249N").unwrap(), ValueAndSpan {
inner: SpannedValue::BigInteger(bigger), inner: SpannedValue::BigInteger(bigger),
span: Span(0, 39) span: Span(0, 39)
}); });
@ -307,13 +306,13 @@ fn test_float() {
assert_eq!(float("77.88e99").unwrap(), Float(OrderedFloat(77.88e99f64))); assert_eq!(float("77.88e99").unwrap(), Float(OrderedFloat(77.88e99f64)));
assert_eq!(float("-9.9E-9").unwrap(), Float(OrderedFloat(-9.9E-9f64))); assert_eq!(float("-9.9E-9").unwrap(), Float(OrderedFloat(-9.9E-9f64)));
assert!(float("42").is_err()); assert_eq!(float("42").unwrap(), Float(OrderedFloat(42f64)));
assert!(float("nil").is_err()); assert!(float("nil").is_err());
} }
#[test] #[test]
fn test_span_float() { fn test_span_float() {
assert_eq!(parse::float("42.0").unwrap(), ValueAndSpan { assert_eq!(parse::value("42.0").unwrap(), ValueAndSpan {
inner: SpannedValue::Float(OrderedFloat(42f64)), inner: SpannedValue::Float(OrderedFloat(42f64)),
span: Span(0, 4) span: Span(0, 4)
}); });
@ -332,7 +331,7 @@ fn test_text() {
#[test] #[test]
fn test_span_text() { fn test_span_text() {
assert_eq!(parse::text("\"hello world\"").unwrap(), ValueAndSpan { assert_eq!(parse::value("\"hello world\"").unwrap(), ValueAndSpan {
inner: SpannedValue::Text("hello world".to_string()), inner: SpannedValue::Text("hello world".to_string()),
span: Span(0, 13) span: Span(0, 13)
}); });
@ -359,11 +358,11 @@ fn test_symbol() {
#[test] #[test]
fn test_span_symbol() { fn test_span_symbol() {
assert_eq!(parse::symbol("hello").unwrap(), ValueAndSpan { assert_eq!(parse::value("hello").unwrap(), ValueAndSpan {
inner: SpannedValue::from_symbol(None, "hello"), inner: SpannedValue::from_symbol(None, "hello"),
span: Span(0, 5) span: Span(0, 5)
}); });
assert_eq!(parse::symbol("hello/world").unwrap(), ValueAndSpan { assert_eq!(parse::value("hello/world").unwrap(), ValueAndSpan {
inner: SpannedValue::from_symbol("hello", "world"), inner: SpannedValue::from_symbol("hello", "world"),
span: Span(0, 11) span: Span(0, 11)
}); });
@ -389,11 +388,11 @@ fn test_keyword() {
#[test] #[test]
fn test_span_keyword() { fn test_span_keyword() {
assert_eq!(parse::keyword(":hello").unwrap(), ValueAndSpan { assert_eq!(parse::value(":hello").unwrap(), ValueAndSpan {
inner: SpannedValue::from_keyword(None, "hello"), inner: SpannedValue::from_keyword(None, "hello"),
span: Span(0, 6) span: Span(0, 6)
}); });
assert_eq!(parse::keyword(":hello/world").unwrap(), ValueAndSpan { assert_eq!(parse::value(":hello/world").unwrap(), ValueAndSpan {
inner: SpannedValue::from_keyword("hello", "world"), inner: SpannedValue::from_keyword("hello", "world"),
span: Span(0, 12) span: Span(0, 12)
}); });