Add comparison operators.

This commit is contained in:
Jeremy Wall 2018-03-24 08:58:16 -05:00
parent f132449379
commit 37bb75b891
6 changed files with 617 additions and 18 deletions

View File

@ -2,9 +2,7 @@
## Boolean operations and type ## Boolean operations and type
* equality (for everything)
* contains (for lists or strings) * contains (for lists or strings)
* less than or greater than (for numeric types)
## Query Language (Experimental) ## Query Language (Experimental)
@ -26,6 +24,7 @@ Some options here could be:
# Minor Fixes and Polish # Minor Fixes and Polish
* Casting between types?
* Better error messages. * Better error messages.
* Allow trailing commas. * Allow trailing commas.
* Flags should allow different seperators for prefixed flags. * Flags should allow different seperators for prefixed flags.

View File

@ -74,8 +74,6 @@ pub enum TokenType {
PUNCT, PUNCT,
} }
// FIXME(jwall): We should probably implement copy for this.
/// Defines a Token representing a building block of UCG syntax. /// Defines a Token representing a building block of UCG syntax.
/// ///
/// Token's are passed to the parser stage to be parsed into an AST. /// Token's are passed to the parser stage to be parsed into an AST.
@ -604,6 +602,12 @@ pub enum BinaryExprType {
Sub, Sub,
Mul, Mul,
Div, Div,
Equal,
GT,
LT,
NotEqual,
GTEqual,
LTEqual,
} }
/// Represents an expression with a left and a right side. /// Represents an expression with a left and a right side.

View File

@ -113,6 +113,65 @@ impl Val {
) )
} }
// TODO(jwall): Unit Tests for this.
pub fn equal(&self, target: &Self, pos: Position) -> Result<bool, error::Error> {
// first we do a type equality comparison
match (self, target) {
// Empty values are always equal.
(&Val::Empty, &Val::Empty) => Ok(true),
(&Val::Int(ref i), &Val::Int(ref ii)) => Ok(i == ii),
(&Val::Float(ref f), &Val::Float(ref ff)) => Ok(f == ff),
(&Val::Boolean(ref b), &Val::Boolean(ref bb)) => Ok(b == bb),
(&Val::String(ref s), &Val::String(ref ss)) => Ok(s == ss),
(&Val::List(ref ldef), &Val::List(ref lldef)) => {
if ldef.len() != lldef.len() {
Ok(false)
} else {
for (i, v) in ldef.iter().enumerate() {
// TODO(jwall): We should probably do a slightly better error message here.
try!(v.equal(lldef[i].as_ref(), pos.clone()));
}
Ok(true)
}
}
(&Val::Tuple(ref ldef), &Val::Tuple(ref lldef)) => {
if ldef.len() != lldef.len() {
Ok(false)
} else {
for (i, v) in ldef.iter().enumerate() {
let field_target = &lldef[i];
eprintln!(
"left field: '{}', right field: '{}'",
v.0.val, field_target.0.val
);
if v.0.val != field_target.0.val {
// field name equality
eprintln!("Field Not equal!!!");
return Ok(false);
} else {
eprintln!("Field Equal!!!");
// field value equality.
if !try!(v.1.equal(field_target.1.as_ref(), v.0.pos.clone())) {
return Ok(false);
}
}
}
Ok(true)
}
}
(&Val::Macro(_), &Val::Macro(_)) => Err(error::Error::new(
"Macros are not comparable",
error::ErrorType::TypeFail,
pos,
)),
(me, tgt) => Err(error::Error::new(
format!("Types differ for {}, {}", me, tgt),
error::ErrorType::TypeFail,
pos,
)),
}
}
/// Returns the fields if this Val is a tuple. None otherwise. /// Returns the fields if this Val is a tuple. None otherwise.
pub fn get_fields(&self) -> Option<&Vec<(Positioned<String>, Rc<Val>)>> { pub fn get_fields(&self) -> Option<&Vec<(Positioned<String>, Rc<Val>)>> {
if let &Val::Tuple(ref fs) = self { if let &Val::Tuple(ref fs) = self {
@ -676,6 +735,128 @@ impl Builder {
} }
} }
fn do_deep_equal(
&self,
pos: &Position,
left: Rc<Val>,
right: Rc<Val>,
) -> Result<Rc<Val>, Box<Error>> {
Ok(Rc::new(Val::Boolean(try!(
left.equal(right.as_ref(), pos.clone())
))))
}
fn do_not_deep_equal(
&self,
pos: &Position,
left: Rc<Val>,
right: Rc<Val>,
) -> Result<Rc<Val>, Box<Error>> {
Ok(Rc::new(Val::Boolean(!try!(
left.equal(right.as_ref(), pos.clone())
))))
}
fn do_gt(&self, pos: &Position, left: Rc<Val>, right: Rc<Val>) -> Result<Rc<Val>, Box<Error>> {
// first ensure that left and right are numeric vals of the same type.
if let &Val::Int(ref l) = left.as_ref() {
if let &Val::Int(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l > r)));
}
}
if let &Val::Float(ref l) = left.as_ref() {
if let &Val::Float(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l > r)));
}
}
Err(Box::new(error::Error::new(
format!(
"Incompatible types for numeric comparison {} with {}",
left.type_name(),
right.type_name()
),
error::ErrorType::TypeFail,
pos.clone(),
)))
}
fn do_lt(&self, pos: &Position, left: Rc<Val>, right: Rc<Val>) -> Result<Rc<Val>, Box<Error>> {
// first ensure that left and right are numeric vals of the same type.
if let &Val::Int(ref l) = left.as_ref() {
if let &Val::Int(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l < r)));
}
}
if let &Val::Float(ref l) = left.as_ref() {
if let &Val::Float(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l < r)));
}
}
Err(Box::new(error::Error::new(
format!(
"Incompatible types for numeric comparison {} with {}",
left.type_name(),
right.type_name()
),
error::ErrorType::TypeFail,
pos.clone(),
)))
}
fn do_ltequal(
&self,
pos: &Position,
left: Rc<Val>,
right: Rc<Val>,
) -> Result<Rc<Val>, Box<Error>> {
if let &Val::Int(ref l) = left.as_ref() {
if let &Val::Int(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l <= r)));
}
}
if let &Val::Float(ref l) = left.as_ref() {
if let &Val::Float(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l <= r)));
}
}
Err(Box::new(error::Error::new(
format!(
"Incompatible types for numeric comparison {} with {}",
left.type_name(),
right.type_name()
),
error::ErrorType::TypeFail,
pos.clone(),
)))
}
fn do_gtequal(
&self,
pos: &Position,
left: Rc<Val>,
right: Rc<Val>,
) -> Result<Rc<Val>, Box<Error>> {
if let &Val::Int(ref l) = left.as_ref() {
if let &Val::Int(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l >= r)));
}
}
if let &Val::Float(ref l) = left.as_ref() {
if let &Val::Float(ref r) = right.as_ref() {
return Ok(Rc::new(Val::Boolean(l >= r)));
}
}
Err(Box::new(error::Error::new(
format!(
"Incompatible types for numeric comparison {} with {}",
left.type_name(),
right.type_name()
),
error::ErrorType::TypeFail,
pos.clone(),
)))
}
fn eval_binary(&self, def: &BinaryOpDef) -> Result<Rc<Val>, Box<Error>> { fn eval_binary(&self, def: &BinaryOpDef) -> Result<Rc<Val>, Box<Error>> {
let kind = &def.kind; let kind = &def.kind;
let v = &def.left; let v = &def.left;
@ -687,6 +868,12 @@ impl Builder {
&BinaryExprType::Sub => self.subtract_vals(&def.pos, left, right), &BinaryExprType::Sub => self.subtract_vals(&def.pos, left, right),
&BinaryExprType::Mul => self.multiply_vals(&def.pos, left, right), &BinaryExprType::Mul => self.multiply_vals(&def.pos, left, right),
&BinaryExprType::Div => self.divide_vals(&def.pos, left, right), &BinaryExprType::Div => self.divide_vals(&def.pos, left, right),
&BinaryExprType::Equal => self.do_deep_equal(&def.pos, left, right),
&BinaryExprType::GT => self.do_gt(&def.pos, left, right),
&BinaryExprType::LT => self.do_lt(&def.pos, left, right),
&BinaryExprType::GTEqual => self.do_gtequal(&def.pos, left, right),
&BinaryExprType::LTEqual => self.do_ltequal(&def.pos, left, right),
&BinaryExprType::NotEqual => self.do_not_deep_equal(&def.pos, left, right),
} }
} }
@ -1147,6 +1334,223 @@ mod test {
); );
} }
#[test]
fn test_eval_equal_exprs() {
let b = Builder::new();
test_expr_to_val(
vec![
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::Int(value_node!(2, 1, 1)),
right: Box::new(Expression::Simple(Value::Int(value_node!(2, 1, 1)))),
pos: Position::new(1, 0),
}),
Val::Boolean(true),
),
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::Float(value_node!(2.0, 1, 1)),
right: Box::new(Expression::Simple(Value::Float(value_node!(2.0, 1, 1)))),
pos: Position::new(1, 0),
}),
Val::Boolean(true),
),
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::String(value_node!("foo".to_string(), 1, 1)),
right: Box::new(Expression::Simple(Value::String(value_node!(
"foo".to_string(),
1,
1
)))),
pos: Position::new(1, 0),
}),
Val::Boolean(true),
),
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::Tuple(value_node!(
vec![
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)),
right: Box::new(Expression::Simple(Value::Tuple(value_node!(
vec![
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)))),
pos: Position::new(1, 0),
}),
Val::Boolean(true),
),
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::Tuple(value_node!(
vec![
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)),
right: Box::new(Expression::Simple(Value::Tuple(value_node!(
vec![
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blush".to_string(),
1,
1
))),
),
],
1,
1
)))),
pos: Position::new(1, 0),
}),
Val::Boolean(false),
),
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::Tuple(value_node!(
vec![
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)),
right: Box::new(Expression::Simple(Value::Tuple(value_node!(
vec![
(
make_tok!("bosh", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)))),
pos: Position::new(1, 0),
}),
Val::Boolean(false),
),
(
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Equal,
left: Value::Tuple(value_node!(
vec![
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)),
right: Box::new(Expression::Simple(Value::Tuple(value_node!(
vec![
(
make_tok!("bash", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("bar", 1, 1),
Expression::Simple(Value::Int(value_node!(1, 1, 1))),
),
(
make_tok!("foo", 1, 1),
Expression::Simple(Value::String(value_node!(
"blah".to_string(),
1,
1
))),
),
],
1,
1
)))),
pos: Position::new(1, 0),
}),
Val::Boolean(false),
),
],
b,
);
}
#[test] #[test]
fn test_eval_simple_expr() { fn test_eval_simple_expr() {
test_expr_to_val( test_expr_to_val(

View File

@ -97,7 +97,7 @@
//! character. //! character.
//! //!
//! ``` ucg //! ``` ucg
//! "foo"; // a smiple string //! "foo"; // a simple string
//! "I'm a \"fine\" looking string"; // escaped quotes in a string. //! "I'm a \"fine\" looking string"; // escaped quotes in a string.
//! ``` //! ```
//! //!
@ -216,16 +216,75 @@
//! //!
//! #### Binary operators //! #### Binary operators
//! //!
//! ucg supports the following operators, +, -, *, /; Each one is type safe and infers the types from the values they operate on. //! ##### Numeric operators
//! The operators expect both the left and right operands to be of the same type. All of the operators are valid on integers and floats. //!
//! The + operator can additionally concatenate strings or arrays. //! ucg supports the following numeric operators, `+`, `-`, `*`, `/` Each one is type safe and infers the types
//! from the values they operate on. The operators expect both the left and right operands to be of the same
//! type.
//!
//! ```ucg
//! 1 + 1; // result is 2
//! ```
//!
//! ##### Concatenation
//!
//! ucg supports concatenation using the `+` operator. It is typesafe expecting both sides to be of the same type.
//! You can concatenate strings or lists but not tuples.
//!
//! ```ucg
//! "foo " + "bar" // result is "foo bar"
//! [1,2] + [3,4]; // result is [1,2,3,4]
//! ```
//!
//! ##### Comparison
//!
//! ucg supports comparison using the `==`, `!=`, `>`, `<`, `>=`, `<=` operators. They are type safe and expect both
//! sides to be of the same type.
//!
//! The `>`, `<`, `>=`, and `>=` operators are only supported on numeric types.
//!
//! ``ucg
//! 1 > 2; // result is false
//! 2 < 3; // result is true
//! 10 > "9"; // This is a compile error.
//! ```
//!
//! The equality operators `==` and `!=` are supported for all types and will perform deep equal comparisons on complex
//! types.
//!
//! ```ucg
//! let tpl1 = {
//! foo = "bar",
//! one = 1
//! };
//! let tpl2 = tpl1{}; // copy the tpl1 tuple
//! tpl1 == tpl2; // returns true
//! let tpl3 = tpl1{duck="quack"};
//! tpl1 == tpl3; // returns false
//! ```
//!
//! Note that tuple fields are ordered so a tuple will only be equal if the fields are both in the same order and
//! have the same values in them.
//!
//! ##### Boolean
//!
//! ucg supports the following operators, `+`, `-`, `*`, `/`, `==`, `!=`, `>`, `<`, `>=`, `<=`, `&&`, `||` Each one is type safe and
//! infers the types from the values they operate on. The operators expect both the left and right operands to be of the same
//! type. All of the operators except `&&` and `||` are valid on integers and floats. The + operator can additionally concatenate
//! strings or arrays. The `&&` and `||` operators are only valid on booleans.
//! //!
//! ```ucg //! ```ucg
//! 1 + 1; // result is 2 //! 1 + 1; // result is 2
//! "foo " + "bar" // result is "foo bar" //! "foo " + "bar" // result is "foo bar"
//! [1,2] + [3,4]; // result is [1,2,3,4] //! [1,2] + [3,4]; // result is [1,2,3,4]
//! 1 == 1; // result is true.
//! 1 > 1; // result is false
//! 1 >= 1; // result is true;
//! ``` //! ```
//! //!
//! The Equality comparison can be done on any type and will perform a deep equal comparison.
//!
//!
//! #### Copy expressions //! #### Copy expressions
//! //!
//! ucg Tuples support a form of reuse with copy on write semantics. You can copy a tuple and selectively overwrite fields or add new //! ucg Tuples support a form of reuse with copy on write semantics. You can copy a tuple and selectively overwrite fields or add new

View File

@ -286,6 +286,30 @@ named!(div_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!("/"), BinaryExprType::Div) do_binary_expr!(punct!("/"), BinaryExprType::Div)
); );
named!(eqeq_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!("=="), BinaryExprType::Equal)
);
named!(not_eqeq_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!("!="), BinaryExprType::NotEqual)
);
named!(lt_eqeq_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!("<="), BinaryExprType::LTEqual)
);
named!(gt_eqeq_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!(">="), BinaryExprType::GTEqual)
);
named!(gt_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!(">"), BinaryExprType::GT)
);
named!(lt_expression<TokenIter, Expression, error::Error>,
do_binary_expr!(punct!("<"), BinaryExprType::LT)
);
fn expression_to_grouped_expression(e: Expression) -> ParseResult<Expression> { fn expression_to_grouped_expression(e: Expression) -> ParseResult<Expression> {
Ok(Expression::Grouped(Box::new(e))) Ok(Expression::Grouped(Box::new(e)))
} }
@ -652,6 +676,12 @@ named!(expression<TokenIter, Expression, error::Error>,
complete!(sub_expression) | complete!(sub_expression) |
complete!(mul_expression) | complete!(mul_expression) |
complete!(div_expression) | complete!(div_expression) |
complete!(eqeq_expression) |
complete!(not_eqeq_expression) |
complete!(lt_eqeq_expression) |
complete!(gt_eqeq_expression) |
complete!(gt_expression) |
complete!(lt_expression) |
complete!(grouped_expression) | complete!(grouped_expression) |
complete!(macro_expression) | complete!(macro_expression) |
complete!(format_expression) | complete!(format_expression) |
@ -1135,7 +1165,42 @@ mod test {
pos: Position::new(1, 1), pos: Position::new(1, 1),
}) })
); );
assert_parse!(
expression("1 > 1"),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::GT,
left: Value::Int(value_node!(1, 1, 1)),
right: Box::new(Expression::Simple(Value::Int(value_node!(1, 1, 5)))),
pos: Position::new(1, 1),
})
);
assert_parse!(
expression("1 < 1"),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::LT,
left: Value::Int(value_node!(1, 1, 1)),
right: Box::new(Expression::Simple(Value::Int(value_node!(1, 1, 5)))),
pos: Position::new(1, 1),
})
);
assert_parse!(
expression("1 <= 1"),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::LTEqual,
left: Value::Int(value_node!(1, 1, 1)),
right: Box::new(Expression::Simple(Value::Int(value_node!(1, 1, 5)))),
pos: Position::new(1, 1),
})
);
assert_parse!(
expression("1 >= 1"),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::GTEqual,
left: Value::Int(value_node!(1, 1, 1)),
right: Box::new(Expression::Simple(Value::Int(value_node!(1, 1, 5)))),
pos: Position::new(1, 1),
})
);
assert_parse!( assert_parse!(
expression("1+1"), expression("1+1"),
Expression::Binary(BinaryOpDef { Expression::Binary(BinaryOpDef {

View File

@ -181,6 +181,32 @@ named!(pcttok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, "%") do_tag_tok!(TokenType::PUNCT, "%")
); );
// TODO(jwall): Comparison operators.
named!(eqeqtok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, "==")
);
named!(notequaltok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, "!=")
);
named!(gttok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, ">")
);
named!(gtequaltok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, ">=")
);
named!(ltequaltok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, "<=")
);
named!(lttok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, "<")
);
named!(equaltok( Span ) -> Token, named!(equaltok( Span ) -> Token,
do_tag_tok!(TokenType::PUNCT, "=") do_tag_tok!(TokenType::PUNCT, "=")
); );
@ -310,6 +336,12 @@ named!(token( Span ) -> Token,
comment | // Note comment must come before slashtok comment | // Note comment must come before slashtok
slashtok | slashtok |
pcttok | pcttok |
eqeqtok |
notequaltok |
complete!(gtequaltok) |
complete!(ltequaltok) |
gttok |
lttok |
fatcommatok | // Note fatcommatok must come before equaltok fatcommatok | // Note fatcommatok must come before equaltok
equaltok | equaltok |
semicolontok | semicolontok |
@ -670,32 +702,68 @@ mod tokenizer_test {
} }
} }
#[test] macro_rules! assert_token {
fn test_boolean() { ($input:expr, $typ:expr, $msg:expr) => {
let result = token(LocatedSpan::new("true")); let result = token(LocatedSpan::new($input));
assert!( assert!(
result.is_done(), result.is_done(),
format!("result {:?} is not a boolean", result) format!("result {:?} is not a {}", result, $msg)
); );
if let nom::IResult::Done(_, tok) = result { if let nom::IResult::Done(_, tok) = result {
assert_eq!(tok.fragment, "true"); assert_eq!(tok.fragment, $input);
assert_eq!(tok.typ, TokenType::BOOLEAN); assert_eq!(tok.typ, $typ);
} }
}
}
#[test]
fn test_boolean() {
assert_token!("true", TokenType::BOOLEAN, "boolean");
}
#[test]
fn test_eqeqtok() {
assert_token!("==", TokenType::PUNCT, "==");
}
#[test]
fn test_notequaltok() {
assert_token!("!=", TokenType::PUNCT, "!=");
}
#[test]
fn test_gttok() {
assert_token!(">", TokenType::PUNCT, ">");
}
#[test]
fn test_lttok() {
assert_token!("<", TokenType::PUNCT, "<");
}
#[test]
fn test_gteqtok() {
assert_token!(">=", TokenType::PUNCT, ">=");
}
#[test]
fn test_lteqtok() {
assert_token!("<=", TokenType::PUNCT, "<=");
} }
#[test] #[test]
fn test_tokenize_one_of_each() { fn test_tokenize_one_of_each() {
let result = tokenize(LocatedSpan::new( let result = tokenize(LocatedSpan::new(
"let import macro select as => [ ] { } ; = % / * \ "let import macro select as => [ ] { } ; = % / * \
+ - . ( ) , 1 . foo \"bar\" // comment\n ; true false", + - . ( ) , 1 . foo \"bar\" // comment\n ; true false == < > <= >= !=",
)); ));
assert!(result.is_ok(), format!("result {:?} is not ok", result)); assert!(result.is_ok(), format!("result {:?} is not ok", result));
let v = result.unwrap(); let v = result.unwrap();
for (i, t) in v.iter().enumerate() { for (i, t) in v.iter().enumerate() {
println!("{}: {:?}", i, t); println!("{}: {:?}", i, t);
} }
assert_eq!(v.len(), 29); assert_eq!(v.len(), 35);
assert_eq!(v[28].typ, TokenType::END); assert_eq!(v[34].typ, TokenType::END);
} }
#[test] #[test]