From 1adf3cc39a6c49eeaa5ebdda702587ed3afd8e76 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 16 Jun 2020 22:14:46 +0800 Subject: [PATCH] Support for trailing commas. --- README.md | 11 +- RELEASES.md | 1 + src/fn_native.rs | 8 +- src/parser.rs | 249 +++++++++++++++++++++++-------------------- src/scope.rs | 2 +- src/token.rs | 10 +- tests/arrays.rs | 1 + tests/call_fn.rs | 4 +- tests/functions.rs | 5 + tests/internal_fn.rs | 6 ++ tests/maps.rs | 4 + 11 files changed, 173 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 401b49fc..2ee5e6b3 100644 --- a/README.md +++ b/README.md @@ -1607,6 +1607,8 @@ The following methods (mostly defined in the [`BasicArrayPackage`](#packages) bu ```rust let y = [2, 3]; // array literal with 2 elements +let y = [2, 3,]; // trailing comma is OK + y.insert(0, 1); // insert element at the beginning y.insert(999, 4); // insert element at the end @@ -1751,6 +1753,8 @@ ts.obj = y; // object maps can be assigned completely (by value copy let foo = ts.list.a; foo == 42; +let foo = #{ a:1,}; // trailing comma is OK + let foo = #{ a:1, b:2, c:3 }["a"]; foo == 1; @@ -2115,7 +2119,12 @@ fn add(x, y) { return x + y; } -print(add(2, 3)); +fn sub(x, y,) { // trailing comma in parameters list is OK + return x - y; +} + +print(add(2, 3)); // prints 5 +print(sub(2, 3,)); // prints -1 - trailing comma in arguments list is OK ``` ### Implicit return diff --git a/RELEASES.md b/RELEASES.md index b19caf44..6fec7a48 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -22,6 +22,7 @@ New features * Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. * `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters. * Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`. +* Supports trailing commas on array literals, object map literals, function definitions and function calls. Version 0.15.0 diff --git a/src/fn_native.rs b/src/fn_native.rs index b7805ff5..6c9eba10 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -126,7 +126,7 @@ impl CallableFunction { pub fn get_native_fn(&self) -> &FnAny { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), - Self::Iterator(_) | Self::Script(_) => panic!(), + Self::Iterator(_) | Self::Script(_) => unreachable!(), } } /// Get a shared reference to a script-defined function definition. @@ -136,7 +136,7 @@ impl CallableFunction { /// Panics if the `CallableFunction` is not `Script`. pub fn get_shared_fn_def(&self) -> Shared { match self { - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), Self::Script(f) => f.clone(), } } @@ -147,7 +147,7 @@ impl CallableFunction { /// Panics if the `CallableFunction` is not `Script`. pub fn get_fn_def(&self) -> &ScriptFnDef { match self { - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), Self::Script(f) => f, } } @@ -159,7 +159,7 @@ impl CallableFunction { pub fn get_iter_fn(&self) -> IteratorFn { match self { Self::Iterator(f) => *f, - Self::Pure(_) | Self::Method(_) | Self::Script(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Script(_) => unreachable!(), } } /// Create a new `CallableFunction::Pure`. diff --git a/src/parser.rs b/src/parser.rs index acbb59e5..5d0bd902 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -518,7 +518,7 @@ impl Expr { ))) } - _ => panic!("cannot get value of non-constant expression"), + _ => unreachable!("cannot get value of non-constant expression"), } } @@ -543,7 +543,7 @@ impl Expr { Self::Array(x) if x.0.iter().all(Self::is_constant) => "array".to_string(), - _ => panic!("cannot get value of non-constant expression"), + _ => unreachable!("cannot get value of non-constant expression"), } } @@ -728,7 +728,7 @@ fn eat_token(input: &mut TokenStream, token: Token) -> Position { let (t, pos) = input.next().unwrap(); if t != token { - panic!( + unreachable!( "expecting {} (found {}) at {}", token.syntax(), t.syntax(), @@ -836,7 +836,11 @@ fn parse_call_expr( let settings = settings.level_up(); loop { - args.push(parse_expr(input, state, settings)?); + match input.peek().unwrap() { + // id(...args, ) - handle trailing comma + (Token::RightParen, _) => (), + _ => args.push(parse_expr(input, state, settings)?), + } match input.peek().unwrap() { // id(...args) @@ -1082,42 +1086,47 @@ fn parse_array_literal( let mut arr = StaticVec::new(); - if !match_token(input, Token::RightBracket)? { - while !input.peek().unwrap().0.is_eof() { - if state.max_array_size > 0 && arr.len() >= state.max_array_size { - return Err(PERR::LiteralTooLarge( - "Size of array literal".to_string(), - state.max_array_size, - ) - .into_err(input.peek().unwrap().1)); - } - - let expr = parse_expr(input, state, settings.level_up())?; - arr.push(expr); - - match input.peek().unwrap() { - (Token::Comma, _) => eat_token(input, Token::Comma), - (Token::RightBracket, _) => { - eat_token(input, Token::RightBracket); - break; - } - (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightBracket.into(), - "to end this array literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(*pos)), - (_, pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this array literal".into(), - ) - .into_err(*pos)) - } - }; + while !input.peek().unwrap().0.is_eof() { + if state.max_array_size > 0 && arr.len() >= state.max_array_size { + return Err(PERR::LiteralTooLarge( + "Size of array literal".to_string(), + state.max_array_size, + ) + .into_err(input.peek().unwrap().1)); } + + match input.peek().unwrap() { + (Token::RightBracket, _) => { + eat_token(input, Token::RightBracket); + break; + } + _ => { + let expr = parse_expr(input, state, settings.level_up())?; + arr.push(expr); + } + } + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBracket, _) => (), + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBracket.into(), + "to end this array literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this array literal".into(), + ) + .into_err(*pos)) + } + }; } Ok(Expr::Array(Box::new((arr, settings.pos)))) @@ -1133,77 +1142,82 @@ fn parse_map_literal( let mut map = StaticVec::new(); - if !match_token(input, Token::RightBrace)? { - while !input.peek().unwrap().0.is_eof() { - const MISSING_RBRACE: &str = "to end this object map literal"; + while !input.peek().unwrap().0.is_eof() { + const MISSING_RBRACE: &str = "to end this object map literal"; - let (name, pos) = match input.next().unwrap() { - (Token::Identifier(s), pos) => (s, pos), - (Token::StringConst(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) if map.is_empty() => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(pos), - ) - } - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(pos), - ) - } - (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), - }; - - match input.next().unwrap() { - (Token::Colon, _) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) => { - return Err(PERR::MissingToken( - Token::Colon.into(), - format!( - "to follow the property '{}' in this object map literal", - name - ), - ) - .into_err(pos)) - } - }; - - if state.max_map_size > 0 && map.len() >= state.max_map_size { - return Err(PERR::LiteralTooLarge( - "Number of properties in object map literal".to_string(), - state.max_map_size, - ) - .into_err(input.peek().unwrap().1)); + match input.peek().unwrap() { + (Token::RightBrace, _) => { + eat_token(input, Token::RightBrace); + break; } + _ => { + let (name, pos) = match input.next().unwrap() { + (Token::Identifier(s), pos) => (s, pos), + (Token::StringConst(s), pos) => (s, pos), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) if map.is_empty() => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + MISSING_RBRACE.into(), + ) + .into_err(pos)) + } + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + MISSING_RBRACE.into(), + ) + .into_err(pos)) + } + (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), + }; - let expr = parse_expr(input, state, settings.level_up())?; - map.push(((name, pos), expr)); + match input.next().unwrap() { + (Token::Colon, _) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Colon.into(), + format!( + "to follow the property '{}' in this object map literal", + name + ), + ) + .into_err(pos)) + } + }; - match input.peek().unwrap() { - (Token::Comma, _) => { - eat_token(input, Token::Comma); - } - (Token::RightBrace, _) => { - eat_token(input, Token::RightBrace); - break; - } - (Token::Identifier(_), pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this object map literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(*pos)), - (_, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(*pos), + if state.max_map_size > 0 && map.len() >= state.max_map_size { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + state.max_map_size, ) + .into_err(input.peek().unwrap().1)); } + + let expr = parse_expr(input, state, settings.level_up())?; + map.push(((name, pos), expr)); + } + } + + match input.peek().unwrap() { + (Token::Comma, _) => { + eat_token(input, Token::Comma); + } + (Token::RightBrace, _) => (), + (Token::Identifier(_), pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this object map literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), + (_, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) } } } @@ -1309,7 +1323,7 @@ fn parse_primary( parse_index_chain(input, state, expr, settings.level_up())? } // Unknown postfix operator - (expr, token) => panic!( + (expr, token) => unreachable!( "unknown postfix operator '{}' for {:?}", token.syntax(), expr @@ -2290,7 +2304,7 @@ fn parse_stmt( let return_type = match input.next().unwrap() { (Token::Return, _) => ReturnType::Return, (Token::Throw, _) => ReturnType::Exception, - _ => panic!("token should be return or throw"), + _ => unreachable!(), }; match input.peek().unwrap() { @@ -2356,15 +2370,20 @@ fn parse_fn( let sep_err = format!("to separate the parameters of function '{}'", name); loop { - match input.next().unwrap() { - (Token::Identifier(s), pos) => { - state.push((s.clone(), ScopeEntryType::Normal)); - params.push((s, pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) => { - return Err(PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos)) - } + match input.peek().unwrap() { + (Token::RightParen, _) => (), + _ => match input.next().unwrap() { + (Token::Identifier(s), pos) => { + state.push((s.clone(), ScopeEntryType::Normal)); + params.push((s, pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err( + PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos) + ) + } + }, } match input.next().unwrap() { diff --git a/src/scope.rs b/src/scope.rs index bb15b777..97d3770e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -417,7 +417,7 @@ impl<'a> Scope<'a> { pub fn set_value(&mut self, name: &'a str, value: T) { match self.get_index(name) { None => self.push(name, value), - Some((_, EntryType::Constant)) => panic!("variable {} is constant", name), + Some((_, EntryType::Constant)) => unreachable!("variable {} is constant", name), Some((index, EntryType::Normal)) => { self.0.get_mut(index).unwrap().value = Dynamic::from(value) } diff --git a/src/token.rs b/src/token.rs index 0bb0cbe3..d5f84902 100644 --- a/src/token.rs +++ b/src/token.rs @@ -294,7 +294,7 @@ impl Token { Export => "export", As => "as", EOF => "{EOF}", - _ => panic!("operator should be match in outer scope"), + _ => unreachable!("operator should be match in outer scope"), }) .into(), } @@ -548,7 +548,7 @@ impl<'a> TokenIterator<'a> { 'x' => 2, 'u' => 4, 'U' => 8, - _ => panic!("should be 'x', 'u' or 'U'"), + _ => unreachable!(), }; for _ in 0..len { @@ -667,14 +667,14 @@ impl<'a> TokenIterator<'a> { '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', ], - _ => panic!("unexpected character {}", ch), + _ => unreachable!(), }; radix_base = Some(match ch { 'x' | 'X' => 16, 'o' | 'O' => 8, 'b' | 'B' => 2, - _ => panic!("unexpected character {}", ch), + _ => unreachable!(), }); while let Some(next_char_in_hex) = self.peek_next() { @@ -1077,7 +1077,7 @@ impl<'a> TokenIterator<'a> { } ('~', _) => return Some((Token::PowerOf, pos)), - ('\0', _) => panic!("should not be EOF"), + ('\0', _) => unreachable!(), (ch, _) if ch.is_whitespace() => (), (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedChar(ch))), pos)), diff --git a/tests/arrays.rs b/tests/arrays.rs index 741156e9..8ea61cdf 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -6,6 +6,7 @@ fn test_arrays() -> Result<(), Box> { let engine = Engine::new(); assert_eq!(engine.eval::("let x = [1, 2, 3]; x[1]")?, 2); + assert_eq!(engine.eval::("let x = [1, 2, 3,]; x[1]")?, 2); assert_eq!(engine.eval::("let y = [1, 2, 3]; y[1] = 5; y[1]")?, 5); assert_eq!( engine.eval::(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?, diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 29f7abca..dd46450d 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -68,7 +68,7 @@ fn test_call_fn_private() -> Result<(), Box> { let r: INT = engine.call_fn(&mut scope, &ast, "add", (40 as INT, 2 as INT))?; assert_eq!(r, 42); - let ast = engine.compile("private fn add(x, n) { x + n }")?; + let ast = engine.compile("private fn add(x, n, ) { x + n }")?; assert!(matches!( *engine.call_fn::<_, INT>(&mut scope, &ast, "add", (40 as INT, 2 as INT)) @@ -83,7 +83,7 @@ fn test_call_fn_private() -> Result<(), Box> { fn test_anonymous_fn() -> Result<(), Box> { let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), - "fn calc(x, y, z) { (x + y) * z }", + "fn calc(x, y, z,) { (x + y) * z }", "calc", )?; diff --git a/tests/functions.rs b/tests/functions.rs index ae12e052..6a94256a 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -7,6 +7,11 @@ fn test_functions() -> Result<(), Box> { assert_eq!(engine.eval::("fn add(x, n) { x + n } add(40, 2)")?, 42); + assert_eq!( + engine.eval::("fn add(x, n,) { x + n } add(40, 2,)")?, + 42 + ); + assert_eq!( engine.eval::("fn add(x, n) { x + n } let a = 40; add(a, 2); a")?, 40 diff --git a/tests/internal_fn.rs b/tests/internal_fn.rs index 764cf601..67417344 100644 --- a/tests/internal_fn.rs +++ b/tests/internal_fn.rs @@ -10,6 +10,12 @@ fn test_internal_fn() -> Result<(), Box> { engine.eval::("fn add_me(a, b) { a+b } add_me(3, 4)")?, 7 ); + + assert_eq!( + engine.eval::("fn add_me(a, b,) { a+b } add_me(3, 4,)")?, + 7 + ); + assert_eq!(engine.eval::("fn bob() { return 4; 5 } bob()")?, 4); Ok(()) diff --git a/tests/maps.rs b/tests/maps.rs index a7aef8dd..8061f1ed 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -12,6 +12,10 @@ fn test_map_indexing() -> Result<(), Box> { engine.eval::(r#"let x = #{a: 1, b: 2, c: 3}; x["b"]"#)?, 2 ); + assert_eq!( + engine.eval::(r#"let x = #{a: 1, b: 2, c: 3,}; x["b"]"#)?, + 2 + ); assert_eq!( engine.eval::( r#"