aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZyX <kp-pav@yandex.ru>2017-09-03 21:58:16 +0300
committerZyX <kp-pav@yandex.ru>2017-10-08 22:25:03 +0300
commit430e516d3ac1235c1ee3009a8a36089bf278440e (patch)
tree8c5adecf383ae70cd61044d98873f71d135050b3
parent919223c23ae3c8c904f35e7d605b1cf14d44a5f0 (diff)
downloadrneovim-430e516d3ac1235c1ee3009a8a36089bf278440e.tar.gz
rneovim-430e516d3ac1235c1ee3009a8a36089bf278440e.tar.bz2
rneovim-430e516d3ac1235c1ee3009a8a36089bf278440e.zip
viml/parser/expressions: Start creating expressions parser
Currently supported nodes: - Register as it is one of the simplest value nodes (even numbers are not that simple with that dot handling). - Plus, both unary and binary. - Parenthesis, both nesting and calling. Note regarding unit tests: it stores data for AST in highlighting in strings in place of tables because luassert fails to do a good job at representing big tables. Squashing a bunch of data into a single string simply yields more readable result.
-rw-r--r--src/nvim/viml/parser/expressions.c625
-rw-r--r--src/nvim/viml/parser/expressions.h74
-rw-r--r--test/unit/eval/helpers.lua5
-rw-r--r--test/unit/helpers.lua28
-rw-r--r--test/unit/viml/expressions/parser_spec.lua887
5 files changed, 1615 insertions, 4 deletions
diff --git a/src/nvim/viml/parser/expressions.c b/src/nvim/viml/parser/expressions.c
index c29fac9cb4..b54f2eb237 100644
--- a/src/nvim/viml/parser/expressions.c
+++ b/src/nvim/viml/parser/expressions.c
@@ -13,10 +13,18 @@
#include "nvim/types.h"
#include "nvim/charset.h"
#include "nvim/ascii.h"
+#include "nvim/lib/kvec.h"
#include "nvim/viml/parser/expressions.h"
#include "nvim/viml/parser/parser.h"
+typedef kvec_withinit_t(ExprASTNode **, 16) ExprASTStack;
+
+typedef enum {
+ kELvlOperator, ///< Operators: function call, subscripts, binary operators, …
+ kELvlValue, ///< Actual value: literals, variables, nested expressions.
+} ExprASTLevel;
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "viml/parser/expressions.c.generated.h"
#endif
@@ -144,6 +152,7 @@ LexExprToken viml_pexpr_next_token(ParserState *const pstate, const bool peek)
// Environment variable.
case '$': {
+ // FIXME: Parser function can’t be thread-safe with vim_isIDc.
CHARREG(kExprLexEnv, vim_isIDc);
break;
}
@@ -183,6 +192,7 @@ LexExprToken viml_pexpr_next_token(ParserState *const pstate, const bool peek)
ret.data.var.autoload = (
memchr(pline.data + 2, AUTOLOAD_CHAR, ret.len - 2)
!= NULL);
+ // FIXME: Resolve ambiguity with an argument to the lexer function.
// Previous CHARREG stopped at autoload character in order to make it
// possible to detect `is#`. Continue now with autoload characters
// included.
@@ -372,3 +382,618 @@ viml_pexpr_next_token_adv_return:
}
return ret;
}
+
+// start = s ternary_expr s EOC
+// ternary_expr = binop_expr
+// ( s Question s ternary_expr s Colon s ternary_expr s )?
+// binop_expr = unaryop_expr ( binop unaryop_expr )?
+// unaryop_expr = ( unaryop )? subscript_expr
+// subscript_expr = subscript_expr subscript
+// | value_expr
+// subscript = Bracket('[') s ternary_expr s Bracket(']')
+// | s Parenthesis('(') call_args Parenthesis(')')
+// | Dot ( PlainIdentifier | Number )+
+// # Note: `s` before Parenthesis('(') is only valid if preceding subscript_expr
+// # is PlainIdentifier
+// value_expr = ( float | Number
+// | DoubleQuotedString | SingleQuotedString
+// | paren_expr
+// | list_literal
+// | lambda_literal
+// | dict_literal
+// | Environment
+// | Option
+// | Register
+// | var )
+// float = Number Dot Number ( PlainIdentifier('e') ( Plus | Minus )? Number )?
+// # Note: `1.2.3` is concat and not float. `"abc".2.3` is also concat without
+// # floats.
+// paren_expr = Parenthesis('(') s ternary_expr s Parenthesis(')')
+// list_literal = Bracket('[') s
+// ( ternary_expr s Comma s )*
+// ternary_expr? s
+// Bracket(']')
+// dict_literal = FigureBrace('{') s
+// ( ternary_expr s Colon s ternary_expr s Comma s )*
+// ( ternary_expr s Colon s ternary_expr s )?
+// FigureBrace('}')
+// lambda_literal = FigureBrace('{') s
+// ( PlainIdentifier s Comma s )*
+// PlainIdentifier s
+// Arrow s
+// ternary_expr s
+// FigureBrace('}')
+// var = varchunk+
+// varchunk = PlainIdentifier
+// | Comparison("is" | "is#" | "isnot" | "isnot#")
+// | FigureBrace('{') s ternary_expr s FigureBrace('}')
+// call_args = ( s ternary_expr s Comma s )* s ternary_expr? s
+// binop = s ( Plus | Minus | Dot
+// | Comparison
+// | Multiplication
+// | Or
+// | And ) s
+// unaryop = s ( Not | Plus | Minus ) s
+// s = Spacing?
+//
+// Binary operator precedence and associativity:
+//
+// Operator | Precedence | Associativity
+// ---------+------------+-----------------
+// || | 2 | left
+// && | 3 | left
+// cmp* | 4 | not associative
+// + - . | 5 | left
+// * / % | 6 | left
+//
+// * comparison operators:
+//
+// == ==# ==? != !=# !=?
+// =~ =~# =~? !~ !~# !~?
+// > ># >? <= <=# <=?
+// < <# <? >= >=# >=?
+// is is# is? isnot isnot# isnot?
+//
+// Used highlighting groups and assumed linkage:
+//
+// NVimInvalid -> Error
+// NVimInvalidValue -> NVimInvalid
+// NVimInvalidOperator -> NVimInvalid
+// NVimInvalidDelimiter -> NVimInvalid
+//
+// NVimOperator -> Operator
+// NVimUnaryOperator -> NVimOperator
+// NVimBinaryOperator -> NVimOperator
+// NVimComparisonOperator -> NVimOperator
+// NVimTernaryOperator -> NVimOperator
+//
+// NVimParenthesis -> Delimiter
+//
+// NVimInvalidSpacing -> NVimInvalid
+// NVimInvalidTernaryOperator -> NVimInvalidOperator
+// NVimInvalidRegister -> NVimInvalidValue
+// NVimInvalidClosingBracket -> NVimInvalidDelimiter
+// NVimInvalidSpacing -> NVimInvalid
+//
+// NVimUnaryPlus -> NVimUnaryOperator
+// NVimBinaryPlus -> NVimBinaryOperator
+// NVimRegister -> SpecialChar
+// NVimNestingParenthesis -> NVimParenthesis
+// NVimCallingParenthesis -> NVimParenthesis
+
+/// Allocate a new node and set some of the values
+///
+/// @param[in] type Node type to allocate.
+/// @param[in] level Node level to allocate
+static inline ExprASTNode *viml_pexpr_new_node(const ExprASTNodeType type)
+ FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC
+{
+ ExprASTNode *ret = xmalloc(sizeof(*ret));
+ ret->type = type;
+ ret->children = NULL;
+ ret->next = NULL;
+ return ret;
+}
+
+typedef enum {
+ kEOpLvlInvalid = 0,
+ kEOpLvlParens,
+ kEOpLvlTernary,
+ kEOpLvlOr,
+ kEOpLvlAnd,
+ kEOpLvlComparison,
+ kEOpLvlAddition, ///< Addition, subtraction and concatenation.
+ kEOpLvlMultiplication, ///< Multiplication, division and modulo.
+ kEOpLvlUnary, ///< Unary operations: not, minus, plus.
+ kEOpLvlSubscript, ///< Subscripts.
+ kEOpLvlValue, ///< Values: literals, variables, nested expressions, …
+} ExprOpLvl;
+
+typedef enum {
+ kEOpAssNo= 'n', ///< Not associative / not applicable.
+ kEOpAssLeft = 'l', ///< Left associativity.
+ kEOpAssRight = 'r', ///< Right associativity.
+} ExprOpAssociativity;
+
+static const ExprOpLvl node_type_to_op_lvl[] = {
+ [kExprNodeMissing] = kEOpLvlInvalid,
+ [kExprNodeOpMissing] = kEOpLvlMultiplication,
+
+ [kExprNodeNested] = kEOpLvlParens,
+ [kExprNodeComplexIdentifier] = kEOpLvlParens,
+
+ [kExprNodeTernary] = kEOpLvlTernary,
+
+ [kExprNodeBinaryPlus] = kEOpLvlAddition,
+
+ [kExprNodeUnaryPlus] = kEOpLvlUnary,
+
+ [kExprNodeSubscript] = kEOpLvlSubscript,
+ [kExprNodeCall] = kEOpLvlSubscript,
+
+ [kExprNodeRegister] = kEOpLvlValue,
+ [kExprNodeListLiteral] = kEOpLvlValue,
+ [kExprNodePlainIdentifier] = kEOpLvlValue,
+};
+
+static const ExprOpAssociativity node_type_to_op_ass[] = {
+ [kExprNodeMissing] = kEOpAssNo,
+ [kExprNodeOpMissing] = kEOpAssNo,
+
+ [kExprNodeNested] = kEOpAssNo,
+ [kExprNodeComplexIdentifier] = kEOpAssLeft,
+
+ [kExprNodeTernary] = kEOpAssNo,
+
+ [kExprNodeBinaryPlus] = kEOpAssLeft,
+
+ [kExprNodeUnaryPlus] = kEOpAssNo,
+
+ [kExprNodeSubscript] = kEOpAssLeft,
+ [kExprNodeCall] = kEOpAssLeft,
+
+ [kExprNodeRegister] = kEOpAssNo,
+ [kExprNodeListLiteral] = kEOpAssNo,
+ [kExprNodePlainIdentifier] = kEOpAssNo,
+};
+
+#ifdef UNIT_TESTING
+#include <stdio.h>
+REAL_FATTR_UNUSED
+static inline void viml_pexpr_debug_print_ast_stack(
+ const ExprASTStack *const ast_stack,
+ const char *const msg)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_ALWAYS_INLINE
+{
+ fprintf(stderr, "\n%sstack: %zu:\n", msg, kv_size(*ast_stack));
+ for (size_t i = 0; i < kv_size(*ast_stack); i++) {
+ const ExprASTNode *const *const eastnode_p = (
+ (const ExprASTNode *const *)kv_A(*ast_stack, i));
+ if (*eastnode_p == NULL) {
+ fprintf(stderr, "- %p : NULL\n", (void *)eastnode_p);
+ } else {
+ fprintf(stderr, "- %p : %p : %c : %zu:%zu:%zu\n",
+ (void *)eastnode_p, (void *)(*eastnode_p), (*eastnode_p)->type,
+ (*eastnode_p)->start.line, (*eastnode_p)->start.col,
+ (*eastnode_p)->len);
+ }
+ }
+}
+#define PSTACK(msg) \
+ viml_pexpr_debug_print_ast_stack(&ast_stack, #msg)
+#define PSTACK_P(msg) \
+ viml_pexpr_debug_print_ast_stack(ast_stack, #msg)
+#endif
+
+/// Handle binary operator
+///
+/// This function is responsible for handling priority levels as well.
+static void viml_pexpr_handle_bop(ExprASTStack *const ast_stack,
+ ExprASTNode *const bop_node,
+ ExprASTLevel *const want_level_p)
+ FUNC_ATTR_NONNULL_ALL
+{
+ ExprASTNode **top_node_p = NULL;
+ ExprASTNode *top_node;
+ ExprOpLvl top_node_lvl;
+ ExprOpAssociativity top_node_ass;
+ assert(kv_size(*ast_stack));
+ const ExprOpLvl bop_node_lvl = node_type_to_op_lvl[bop_node->type];
+ do {
+ ExprASTNode **new_top_node_p = kv_last(*ast_stack);
+ ExprASTNode *new_top_node = *new_top_node_p;
+ assert(new_top_node != NULL);
+ const ExprOpLvl new_top_node_lvl = node_type_to_op_lvl[new_top_node->type];
+ const ExprOpAssociativity new_top_node_ass = (
+ node_type_to_op_ass[new_top_node->type]);
+ if (top_node_p != NULL
+ && ((bop_node_lvl > new_top_node_lvl
+ || (bop_node_lvl == new_top_node_lvl
+ && new_top_node_ass == kEOpAssNo)))) {
+ break;
+ }
+ kv_drop(*ast_stack, 1);
+ top_node_p = new_top_node_p;
+ top_node = new_top_node;
+ top_node_lvl = new_top_node_lvl;
+ top_node_ass = new_top_node_ass;
+ } while (kv_size(*ast_stack));
+ // FIXME Handle right and no associativity correctly
+ *top_node_p = bop_node;
+ bop_node->children = top_node;
+ assert(bop_node->children->next == NULL);
+ kvi_push(*ast_stack, top_node_p);
+ kvi_push(*ast_stack, &bop_node->children->next);
+ *want_level_p = kELvlValue;
+}
+
+/// Get highlight group name
+#define HL(g) (is_invalid ? "NVimInvalid" #g : "NVim" #g)
+
+/// Highlight current token with the given group
+#define HL_CUR_TOKEN(g) \
+ viml_parser_highlight(pstate, cur_token.start, cur_token.len, \
+ HL(g))
+
+/// Allocate new node, saving some values
+#define NEW_NODE(type) \
+ viml_pexpr_new_node(type)
+
+/// Set position of the given node to position from the given token
+///
+/// @param cur_node Node to modify.
+/// @param cur_token Token to set position from.
+#define POS_FROM_TOKEN(cur_node, cur_token) \
+ do { \
+ cur_node->start = cur_token.start; \
+ cur_node->len = cur_token.len; \
+ } while (0)
+
+/// Allocate new node and set its position from the current token
+///
+/// If previous token happened to contain spacing then it will be included.
+///
+/// @param cur_node Variable to save allocated node to.
+/// @param typ Node type.
+#define NEW_NODE_WITH_CUR_POS(cur_node, typ) \
+ do { \
+ cur_node = NEW_NODE(typ); \
+ POS_FROM_TOKEN(cur_node, cur_token); \
+ if (prev_token.type == kExprLexSpacing) { \
+ cur_node->start = prev_token.start; \
+ cur_node->len += prev_token.len; \
+ } \
+ } while (0)
+
+// TODO(ZyX-I): actual condition
+/// Check whether it is possible to have next expression after current
+///
+/// For :echo: `:echo @a @a` is a valid expression. `:echo (@a @a)` is not.
+#define MAY_HAVE_NEXT_EXPR \
+ (kv_size(ast_stack) == 1)
+
+/// Record missing operator: for things like
+///
+/// :echo @a @a
+///
+/// (allowed) or
+///
+/// :echo (@a @a)
+///
+/// (parsed as OpMissing(@a, @a)).
+#define OP_MISSING \
+ do { \
+ if (flags & kExprFlagsMulti && MAY_HAVE_NEXT_EXPR) { \
+ /* Multiple expressions allowed, return without calling */ \
+ /* viml_parser_advance(). */ \
+ goto viml_pexpr_parse_end; \
+ } else { \
+ assert(*top_node_p != NULL); \
+ ERROR_FROM_TOKEN_AND_MSG(cur_token, _("E15: Missing operator: %.*s")); \
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeOpMissing); \
+ cur_node->len = 0; \
+ viml_pexpr_handle_bop(&ast_stack, cur_node, &want_level); \
+ is_invalid = true; \
+ goto viml_pexpr_parse_process_token; \
+ } \
+ } while (0)
+
+/// Set AST error, unless AST already is not correct
+///
+/// @param[out] ret_ast AST to set error in.
+/// @param[in] pstate Parser state, used to get error message argument.
+/// @param[in] msg Error message, assumed to be already translated and
+/// containing a single %token "%.*s".
+/// @param[in] start Position at which error occurred.
+static inline void east_set_error(ExprAST *const ret_ast,
+ const ParserState *const pstate,
+ const char *const msg,
+ const ParserPosition start)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_ALWAYS_INLINE
+{
+ if (!ret_ast->correct) {
+ return;
+ }
+ const ParserLine pline = pstate->reader.lines.items[start.line];
+ ret_ast->correct = false;
+ ret_ast->err.msg = msg;
+ ret_ast->err.arg_len = (int)(pline.size - start.col);
+ ret_ast->err.arg = pline.data + start.col;
+}
+
+/// Set error from the given kExprLexInvalid token and given message
+#define ERROR_FROM_TOKEN_AND_MSG(cur_token, msg) \
+ east_set_error(&ast, pstate, msg, cur_token.start)
+
+/// Set error from the given kExprLexInvalid token
+#define ERROR_FROM_TOKEN(cur_token) \
+ ERROR_FROM_TOKEN_AND_MSG(cur_token, cur_token.data.err.msg)
+
+/// Parse one VimL expression
+///
+/// @param pstate Parser state.
+/// @param[in] flags Additional flags, see ExprParserFlags
+///
+/// @return Parsed AST.
+ExprAST viml_pexpr_parse(ParserState *const pstate, const int flags)
+ FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
+{
+ ExprAST ast = {
+ .correct = true,
+ .err = {
+ .msg = NULL,
+ .arg_len = 0,
+ .arg = NULL,
+ },
+ .root = NULL,
+ };
+ ExprASTStack ast_stack;
+ kvi_init(ast_stack);
+ kvi_push(ast_stack, &ast.root);
+ // Expressions stack:
+ // 1. *last is NULL if want_level is kExprLexValue. Indicates where expression
+ // is to be put.
+ // 2. *last is not NULL otherwise, indicates current expression to be used as
+ // an operator argument.
+ ExprASTLevel want_level = kELvlValue;
+ LexExprToken prev_token = { .type = kExprLexMissing };
+ bool highlighted_prev_spacing = false;
+ do {
+ LexExprToken cur_token = viml_pexpr_next_token(pstate, true);
+ if (cur_token.type == kExprLexEOC) {
+ if (flags & kExprFlagsDisallowEOC) {
+ if (cur_token.len == 0) {
+ // It is end of string, break.
+ break;
+ } else {
+ // It is NL, NUL or bar.
+ //
+ // Note: `<C-r>=1 | 2<CR>` actually yields 1 in Vim without any
+ // errors. This will be changed here.
+ cur_token.type = kExprLexInvalid;
+ cur_token.data.err.msg = _("E15: Unexpected EOC character: %.*s");
+ const ParserLine pline = (
+ pstate->reader.lines.items[cur_token.start.line]);
+ const char eoc_char = pline.data[cur_token.start.col];
+ cur_token.data.err.type = ((eoc_char == NUL || eoc_char == NL)
+ ? kExprLexSpacing
+ : kExprLexOr);
+ }
+ } else {
+ break;
+ }
+ }
+ LexExprTokenType tok_type = cur_token.type;
+ const bool token_invalid = (tok_type == kExprLexInvalid);
+ bool is_invalid = token_invalid;
+viml_pexpr_parse_process_token:
+ if (tok_type == kExprLexSpacing) {
+ if (is_invalid) {
+ viml_parser_highlight(pstate, cur_token.start, cur_token.len,
+ HL(Spacing));
+ } else {
+ // Do not do anything: let regular spacing be highlighted as normal.
+ // This also allows later to highlight spacing as invalid.
+ }
+ goto viml_pexpr_parse_cycle_end;
+ } else if (is_invalid && prev_token.type == kExprLexSpacing
+ && !highlighted_prev_spacing) {
+ viml_parser_highlight(pstate, prev_token.start, prev_token.len,
+ HL(Spacing));
+ is_invalid = false;
+ highlighted_prev_spacing = true;
+ }
+ ExprASTNode **const top_node_p = kv_last(ast_stack);
+ ExprASTNode *cur_node = NULL;
+ // Keep these two asserts separate for debugging purposes.
+ assert(want_level == kELvlValue || *top_node_p != NULL);
+ assert(want_level != kELvlValue || *top_node_p == NULL);
+ switch (tok_type) {
+ case kExprLexEOC: {
+ assert(false);
+ }
+ case kExprLexInvalid: {
+ ERROR_FROM_TOKEN(cur_token);
+ tok_type = cur_token.data.err.type;
+ goto viml_pexpr_parse_process_token;
+ }
+ case kExprLexRegister: {
+ if (want_level == kELvlValue) {
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeRegister);
+ cur_node->data.reg.name = cur_token.data.reg.name;
+ *top_node_p = cur_node;
+ want_level = kELvlOperator;
+ viml_parser_highlight(pstate, cur_token.start, cur_token.len,
+ HL(Register));
+ } else {
+ // Register in operator position: e.g. @a @a
+ OP_MISSING;
+ }
+ break;
+ }
+ case kExprLexPlus: {
+ if (want_level == kELvlValue) {
+ // Value level: assume unary plus
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeUnaryPlus);
+ *top_node_p = cur_node;
+ kvi_push(ast_stack, &cur_node->children);
+ HL_CUR_TOKEN(UnaryPlus);
+ } else if (want_level < kELvlValue) {
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeBinaryPlus);
+ viml_pexpr_handle_bop(&ast_stack, cur_node, &want_level);
+ HL_CUR_TOKEN(BinaryPlus);
+ }
+ want_level = kELvlValue;
+ break;
+ }
+ case kExprLexParenthesis: {
+ if (cur_token.data.brc.closing) {
+ if (want_level == kELvlValue) {
+ if (kv_size(ast_stack) > 1) {
+ const ExprASTNode *const prev_top_node = *kv_Z(ast_stack, 1);
+ if (prev_top_node->type == kExprNodeCall) {
+ // Function call without arguments, this is not an error.
+ // But further code does not expect NULL nodes.
+ kv_drop(ast_stack, 1);
+ goto viml_pexpr_parse_no_paren_closing_error;
+ }
+ }
+ is_invalid = true;
+ ERROR_FROM_TOKEN_AND_MSG(cur_token, _("E15: Expected value: %.*s"));
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeMissing);
+ cur_node->len = 0;
+ *top_node_p = cur_node;
+ } else {
+ // Always drop the topmost value: when want_level != kELvlValue
+ // topmost item on stack is a *finished* left operand, which may as
+ // well be "(@a)" which needs not be finished.
+ kv_drop(ast_stack, 1);
+ }
+viml_pexpr_parse_no_paren_closing_error: {}
+ ExprASTNode **new_top_node_p = NULL;
+ while (kv_size(ast_stack)
+ && (new_top_node_p == NULL
+ || ((*new_top_node_p)->type != kExprNodeNested
+ && (*new_top_node_p)->type != kExprNodeCall))) {
+ new_top_node_p = kv_pop(ast_stack);
+ }
+ if (new_top_node_p != NULL
+ && ((*new_top_node_p)->type == kExprNodeNested
+ || (*new_top_node_p)->type == kExprNodeCall)) {
+ if ((*new_top_node_p)->type == kExprNodeNested) {
+ HL_CUR_TOKEN(NestingParenthesis);
+ } else {
+ HL_CUR_TOKEN(CallingParenthesis);
+ }
+ } else {
+ // “Always drop the topmost value” branch has got rid of the single
+ // value stack had, so there is nothing known to enclose. Correct
+ // this.
+ if (new_top_node_p == NULL) {
+ new_top_node_p = top_node_p;
+ }
+ is_invalid = true;
+ HL_CUR_TOKEN(NestingParenthesis);
+ ERROR_FROM_TOKEN_AND_MSG(
+ cur_token, _("E15: Unexpected closing parenthesis: %.*s"));
+ cur_node = NEW_NODE(kExprNodeNested);
+ cur_node->start = cur_token.start;
+ cur_node->len = 0;
+ // Unexpected closing parenthesis, assume that it was wanted to
+ // enclose everything in ().
+ cur_node->children = *new_top_node_p;
+ *new_top_node_p = cur_node;
+ assert(cur_node->next == NULL);
+ }
+ kvi_push(ast_stack, new_top_node_p);
+ want_level = kELvlOperator;
+ } else {
+ if (want_level == kELvlValue) {
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeNested);
+ *top_node_p = cur_node;
+ kvi_push(ast_stack, &cur_node->children);
+ HL_CUR_TOKEN(NestingParenthesis);
+ } else if (want_level == kELvlOperator) {
+ if (prev_token.type == kExprLexSpacing) {
+ // For some reason "function (args)" is a function call, but
+ // "(funcref) (args)" is not. AFAIR this somehow involves
+ // compatibility and Bram was commenting that this is
+ // intentionally inconsistent and he is not very happy with the
+ // situation himself.
+ if ((*top_node_p)->type != kExprNodePlainIdentifier
+ && (*top_node_p)->type != kExprNodeComplexIdentifier) {
+ OP_MISSING;
+ }
+ }
+ NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeCall);
+ viml_pexpr_handle_bop(&ast_stack, cur_node, &want_level);
+ HL_CUR_TOKEN(CallingParenthesis);
+ } else {
+ // Currently it is impossible to reach this.
+ assert(false);
+ }
+ want_level = kELvlValue;
+ }
+ break;
+ }
+ }
+viml_pexpr_parse_cycle_end:
+ prev_token = cur_token;
+ highlighted_prev_spacing = false;
+ viml_parser_advance(pstate, cur_token.len);
+ } while (true);
+viml_pexpr_parse_end:
+ if (want_level == kELvlValue) {
+ east_set_error(&ast, pstate, _("E15: Expected value: %.*s"), pstate->pos);
+ } else if (kv_size(ast_stack) != 1) {
+ // Something may be wrong, check whether it really is.
+
+ // Pointer to ast.root must never be dropped, so “!= 1” is expected to be
+ // the same as “> 1”.
+ assert(kv_size(ast_stack));
+ // Topmost stack item must be a *finished* value, so it must not be
+ // analyzed. E.g. it may contain an already finished nested expression.
+ kv_drop(ast_stack, 1);
+ while (ast.correct && kv_size(ast_stack)) {
+ const ExprASTNode *const cur_node = (*kv_pop(ast_stack));
+ // This should only happen when want_level == kELvlValue.
+ assert(cur_node != NULL);
+ switch (cur_node->type) {
+ case kExprNodeOpMissing:
+ case kExprNodeMissing: {
+ // Error should’ve been already reported.
+ break;
+ }
+ case kExprNodeCall: {
+ // TODO(ZyX-I): Rehighlight as invalid?
+ east_set_error(
+ &ast, pstate,
+ _("E116: Missing closing parenthesis for function call: %.*s"),
+ cur_node->start);
+ break;
+ }
+ case kExprNodeNested: {
+ // TODO(ZyX-I): Rehighlight as invalid?
+ east_set_error(
+ &ast, pstate,
+ _("E110: Missing closing parenthesis for nested expression"
+ ": %.*s"),
+ cur_node->start);
+ break;
+ }
+ case kExprNodeBinaryPlus:
+ case kExprNodeUnaryPlus:
+ case kExprNodeRegister: {
+ // It is OK to see these in the stack.
+ break;
+ }
+ // TODO(ZyX-I): handle other values
+ }
+ }
+ }
+ kvi_destroy(ast_stack);
+ return ast;
+}
+
+#undef NEW_NODE
+#undef HL
diff --git a/src/nvim/viml/parser/expressions.h b/src/nvim/viml/parser/expressions.h
index 52354760a5..13888562df 100644
--- a/src/nvim/viml/parser/expressions.h
+++ b/src/nvim/viml/parser/expressions.h
@@ -111,6 +111,80 @@ typedef struct {
} data; ///< Additional data, if needed.
} LexExprToken;
+/// Expression AST node type
+typedef enum {
+ kExprNodeMissing = 'X',
+ kExprNodeOpMissing = '_',
+ kExprNodeTernary = '?', ///< Ternary operator, valid one has three children.
+ kExprNodeRegister = '@', ///< Register, no children.
+ kExprNodeSubscript = 's', ///< Subscript, should have two or three children.
+ kExprNodeListLiteral = 'l', ///< List literal, any number of children.
+ kExprNodeUnaryPlus = 'p',
+ kExprNodeBinaryPlus = '+',
+ kExprNodeNested = 'e', ///< Nested parenthesised expression.
+ kExprNodeCall = 'c', ///< Function call.
+ /// Plain identifier: simple variable/function name
+ ///
+ /// Looks like "string", "g:Foo", etc: consists from a single
+ /// kExprLexPlainIdentifier token.
+ kExprNodePlainIdentifier = 'i',
+ /// Complex identifier: variable/function name with curly braces
+ kExprNodeComplexIdentifier = 'I',
+} ExprASTNodeType;
+
+typedef struct expr_ast_node ExprASTNode;
+
+/// Structure representing one AST node
+struct expr_ast_node {
+ ExprASTNodeType type; ///< Node type.
+ /// Node children: e.g. for 1 + 2 nodes 1 and 2 will be children of +.
+ ExprASTNode *children;
+ /// Next node: e.g. for 1 + 2 child nodes 1 and 2 are put into a single-linked
+ /// list: `(+)->children` references only node 1, node 2 is in
+ /// `(+)->children->next`.
+ ExprASTNode *next;
+ ParserPosition start;
+ size_t len;
+ union {
+ struct {
+ int name; ///< Register name, may be -1 if name not present.
+ } reg; ///< For kExprNodeRegister.
+ } data;
+};
+
+enum {
+ /// Allow multiple expressions in a row: e.g. for :echo
+ ///
+ /// Parser will still parse only one of them though.
+ kExprFlagsMulti = (1 << 0),
+ /// Allow NL, NUL and bar to be EOC
+ ///
+ /// When parsing expressions input by user bar is assumed to be a binary
+ /// operator and other two are spacings.
+ kExprFlagsDisallowEOC = (1 << 1),
+ /// Print errors when encountered
+ ///
+ /// Without the flag they are only taken into account when parsing.
+ kExprFlagsPrintError = (1 << 2),
+} ExprParserFlags;
+
+/// Structure representing complety AST for one expression
+typedef struct {
+ /// True if represented AST is correct and can be executed. Incorrect ones may
+ /// still be used for completion, or in linters.
+ bool correct;
+ /// When AST is not correct this message will be printed.
+ ///
+ /// Uses `emsgf(msg, arg_len, arg);`, `msg` is assumed to contain only `%.*s`.
+ struct {
+ const char *msg;
+ int arg_len;
+ const char *arg;
+ } err;
+ /// Root node of the AST.
+ ExprASTNode *root;
+} ExprAST;
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "viml/parser/expressions.h.generated.h"
#endif
diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua
index 5bc482216e..d7399182f7 100644
--- a/test/unit/eval/helpers.lua
+++ b/test/unit/eval/helpers.lua
@@ -1,5 +1,6 @@
local helpers = require('test.unit.helpers')(nil)
+local ptr2key = helpers.ptr2key
local cimport = helpers.cimport
local to_cstr = helpers.to_cstr
local ffi = helpers.ffi
@@ -91,10 +92,6 @@ local function populate_partial(pt, lua_pt, processed)
return pt
end
-local ptr2key = function(ptr)
- return tostring(ptr)
-end
-
local lst2tbl
local dct2tbl
diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua
index a5ca7b069b..d3d14a5ca2 100644
--- a/test/unit/helpers.lua
+++ b/test/unit/helpers.lua
@@ -783,6 +783,31 @@ local function kvi_new(ct)
return kvi_init(ffi.new(ct))
end
+local function make_enum_conv_tab(lib, values, skip_pref, set_cb)
+ child_call_once(function()
+ local ret = {}
+ for _, v in ipairs(values) do
+ local str_v = v
+ if v:sub(1, #skip_pref) == skip_pref then
+ str_v = v:sub(#skip_pref + 1)
+ end
+ ret[tonumber(lib[v])] = str_v
+ end
+ set_cb(ret)
+ end)
+end
+
+local function ptr2addr(ptr)
+ return tonumber(ffi.cast('intptr_t', ffi.cast('void *', ptr)))
+end
+
+local s = ffi.new('char[64]', {0})
+
+local function ptr2key(ptr)
+ ffi.C.snprintf(s, ffi.sizeof(s), '%p', ffi.cast('void *', ptr))
+ return ffi.string(s)
+end
+
local module = {
cimport = cimport,
cppimport = cppimport,
@@ -808,6 +833,9 @@ local module = {
kvi_size = kvi_size,
kvi_init = kvi_init,
kvi_new = kvi_new,
+ make_enum_conv_tab = make_enum_conv_tab,
+ ptr2addr = ptr2addr,
+ ptr2key = ptr2key,
}
return function()
return module
diff --git a/test/unit/viml/expressions/parser_spec.lua b/test/unit/viml/expressions/parser_spec.lua
new file mode 100644
index 0000000000..c4bb067391
--- /dev/null
+++ b/test/unit/viml/expressions/parser_spec.lua
@@ -0,0 +1,887 @@
+local helpers = require('test.unit.helpers')(after_each)
+local viml_helpers = require('test.unit.viml.helpers')
+local itp = helpers.gen_itp(it)
+
+local make_enum_conv_tab = helpers.make_enum_conv_tab
+local child_call_once = helpers.child_call_once
+local conv_enum = helpers.conv_enum
+local ptr2key = helpers.ptr2key
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local eq = helpers.eq
+
+local pline2lua = viml_helpers.pline2lua
+local new_pstate = viml_helpers.new_pstate
+local intchar2lua = viml_helpers.intchar2lua
+local pstate_set_str = viml_helpers.pstate_set_str
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h')
+
+local east_node_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprNodeMissing',
+ 'kExprNodeOpMissing',
+ 'kExprNodeTernary',
+ 'kExprNodeRegister',
+ 'kExprNodeSubscript',
+ 'kExprNodeListLiteral',
+ 'kExprNodeUnaryPlus',
+ 'kExprNodeBinaryPlus',
+ 'kExprNodeNested',
+ 'kExprNodeCall',
+ 'kExprNodePlainIdentifier',
+ 'kExprNodeComplexIdentifier',
+}, 'kExprNode', function(ret) east_node_type_tab = ret end)
+
+local function conv_east_node_type(typ)
+ return conv_enum(east_node_type_tab, typ)
+end
+
+local eastnodelist2lua
+
+local function eastnode2lua(pstate, eastnode, checked_nodes)
+ local key = ptr2key(eastnode)
+ if checked_nodes[key] then
+ checked_nodes[key].duplicate_key = key
+ return { duplicate = key }
+ end
+ local typ = conv_east_node_type(eastnode.type)
+ local ret = {}
+ checked_nodes[key] = ret
+ ret.children = eastnodelist2lua(pstate, eastnode.children, checked_nodes)
+ local str = pstate_set_str(pstate, eastnode.start, eastnode.len)
+ local ret_str
+ if str.error then
+ ret_str = 'error:' .. str.error
+ else
+ ret_str = ('%u:%u:%s'):format(str.start.line, str.start.col, str.str)
+ end
+ if typ == 'Register' then
+ typ = typ .. ('(name=%s)'):format(
+ tostring(intchar2lua(eastnode.data.reg.name)))
+ end
+ ret_str = typ .. ':' .. ret_str
+ local can_simplify = true
+ for k, v in pairs(ret) do
+ can_simplify = false
+ end
+ if can_simplify then
+ ret = ret_str
+ else
+ ret[1] = ret_str
+ end
+ return ret
+end
+
+eastnodelist2lua = function(pstate, eastnode, checked_nodes)
+ local ret = {}
+ while eastnode ~= nil do
+ ret[#ret + 1] = eastnode2lua(pstate, eastnode, checked_nodes)
+ eastnode = eastnode.next
+ end
+ if #ret == 0 then
+ ret = nil
+ end
+ return ret
+end
+
+local function east2lua(pstate, east)
+ local checked_nodes = {}
+ return {
+ err = (not east.correct) and {
+ msg = ffi.string(east.err.msg),
+ arg = ('%u:%s'):format(
+ tonumber(east.err.arg_len),
+ ffi.string(east.err.arg, east.err.arg_len)),
+ } or nil,
+ ast = eastnodelist2lua(pstate, east.root, checked_nodes),
+ }
+end
+
+local function phl2lua(pstate)
+ local ret = {}
+ for i = 0, (tonumber(pstate.colors.size) - 1) do
+ local chunk = pstate.colors.items[i]
+ local chunk_tbl = pstate_set_str(
+ pstate, chunk.start, chunk.end_col - chunk.start.col, {
+ group = ffi.string(chunk.group),
+ })
+ chunk_str = ('%s:%u:%u:%s'):format(
+ chunk_tbl.group,
+ chunk_tbl.start.line,
+ chunk_tbl.start.col,
+ chunk_tbl.str)
+ ret[i + 1] = chunk_str
+ end
+ return ret
+end
+
+child_call_once(function()
+ assert:set_parameter('TableFormatLevel', 1000000)
+end)
+
+describe('Expressions parser', function()
+ itp('works', function()
+ local function check_parsing(str, flags, exp_ast, exp_highlighting_fs)
+ local pstate = new_pstate({str})
+ local east = lib.viml_pexpr_parse(pstate, flags)
+ local ast = east2lua(pstate, east)
+ eq(exp_ast, ast)
+ if exp_highlighting_fs then
+ local exp_highlighting = {}
+ local next_col = 0
+ for i, h in ipairs(exp_highlighting_fs) do
+ exp_highlighting[i], next_col = h(next_col)
+ end
+ eq(exp_highlighting, phl2lua(pstate))
+ end
+ end
+ local function hl(group, str, shift)
+ return function(next_col)
+ local col = next_col + (shift or 0)
+ return (('%s:%u:%u:%s'):format(
+ 'NVim' .. group,
+ 0,
+ col,
+ str)), (col + #str)
+ end
+ end
+ check_parsing('@a', 0, {
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ }, {
+ hl('Register', '@a'),
+ })
+ check_parsing('+@a', 0, {
+ ast = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a+@b', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a+@b+@c', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:5:+',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ 'Register(name=c):0:6:@c',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@c'),
+ })
+ check_parsing('+@a+@b', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:3:+',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('+@a++@b', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:3:+',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ 'UnaryPlus:0:4:+',
+ children = {
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a@b', 0, {
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:2:@b',
+ },
+ },
+ },
+ err = {
+ arg = '2:@b',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidRegister', '@b'),
+ })
+ check_parsing(' @a \t @b', 0, {
+ ast = {
+ {
+ 'OpMissing:0:3:',
+ children = {
+ 'Register(name=a):0:0: @a',
+ 'Register(name=b):0:3: \t @b',
+ },
+ },
+ },
+ err = {
+ arg = '2:@b',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a', 1),
+ hl('InvalidSpacing', ' \t '),
+ hl('Register', '@b'),
+ })
+ check_parsing('+', 0, {
+ ast = {
+ 'UnaryPlus:0:0:+',
+ },
+ err = {
+ arg = '0:',
+ msg = 'E15: Expected value: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ })
+ check_parsing(' +', 0, {
+ ast = {
+ 'UnaryPlus:0:0: +',
+ },
+ err = {
+ arg = '0:',
+ msg = 'E15: Expected value: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+', 1),
+ })
+ check_parsing('@a+ ', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = '0:',
+ msg = 'E15: Expected value: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ })
+ check_parsing('(@a)', 0, {
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('()', 0, {
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ err = {
+ arg = '1:)',
+ msg = 'E15: Expected value: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing(')', 0, {
+ ast = {
+ {
+ 'Nested:0:0:',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = '1:)',
+ msg = 'E15: Expected value: %.*s',
+ },
+ }, {
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('+)', 0, {
+ ast = {
+ {
+ 'Nested:0:1:',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '1:)',
+ msg = 'E15: Expected value: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('+@a(@b)', 0, {
+ ast = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ {
+ 'Call:0:3:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a+@b(@c)', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:5:(',
+ children = {
+ 'Register(name=b):0:3:@b',
+ 'Register(name=c):0:6:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a()', 0, {
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a ()', 0, {
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:2: (',
+ children = {
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '2:()',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidSpacing', ' '),
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing(
+ '@a + (@b)', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ 'Register(name=b):0:6:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing(
+ '@a + (+@b)', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ {
+ 'UnaryPlus:0:6:+',
+ children = {
+ 'Register(name=b):0:7:@b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing(
+ '@a + (@b + @c)', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ {
+ 'BinaryPlus:0:8: +',
+ children = {
+ 'Register(name=b):0:6:@b',
+ 'Register(name=c):0:10: @c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Register', '@b'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@c', 1),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('(@a)+@b', 0, {
+ ast = {
+ {
+ 'BinaryPlus:0:4:+',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a+(@b)(@c)', 0, {
+ -- 01234567890
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:7:(',
+ children = {
+ {
+ 'Nested:0:3:(',
+ children = { 'Register(name=b):0:4:@b' },
+ },
+ 'Register(name=c):0:8:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a+((@b))(@c)', 0, {
+ -- 01234567890123456890123456789
+ -- 0 1 2
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:9:(',
+ children = {
+ {
+ 'Nested:0:3:(',
+ children = {
+ {
+ 'Nested:0:4:(',
+ children = { 'Register(name=b):0:5:@b' }
+ },
+ },
+ },
+ 'Register(name=c):0:10:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('NestingParenthesis', '('),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a+((@b))+@c', 0, {
+ -- 01234567890123456890123456789
+ -- 0 1 2
+ ast = {
+ {
+ 'BinaryPlus:0:9:+',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:3:(',
+ children = {
+ {
+ 'Nested:0:4:(',
+ children = { 'Register(name=b):0:5:@b' }
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=c):0:10:@c',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('NestingParenthesis', '('),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@c'),
+ })
+ check_parsing(
+ '@a + (@b + @c) + @d(@e) + (+@f) + ((+@g(@h))(@j)(@k))(@l)', 0, {--[[
+ | | | | | | | | || | | || | | ||| || || || ||
+ 000000000011111111112222222222333333333344444444445555555
+ 012345678901234567890123456789012345678901234567890123456
+ ]]
+ ast = {{
+ 'BinaryPlus:0:31: +',
+ children = {
+ {
+ 'BinaryPlus:0:23: +',
+ children = {
+ {
+ 'BinaryPlus:0:14: +',
+ children = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ {
+ 'BinaryPlus:0:8: +',
+ children = {
+ 'Register(name=b):0:6:@b',
+ 'Register(name=c):0:10: @c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Call:0:19:(',
+ children = {
+ 'Register(name=d):0:16: @d',
+ 'Register(name=e):0:20:@e',
+ },
+ },
+ },
+ },
+ {
+ 'Nested:0:25: (',
+ children = {
+ {
+ 'UnaryPlus:0:27:+',
+ children = {
+ 'Register(name=f):0:28:@f',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Call:0:53:(',
+ children = {
+ {
+ 'Nested:0:33: (',
+ children = {
+ {
+ 'Call:0:48:(',
+ children = {
+ {
+ 'Call:0:44:(',
+ children = {
+ {
+ 'Nested:0:35:(',
+ children = {
+ {
+ 'UnaryPlus:0:36:+',
+ children = {
+ {
+ 'Call:0:39:(',
+ children = {
+ 'Register(name=g):0:37:@g',
+ 'Register(name=h):0:40:@h',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=j):0:45:@j',
+ },
+ },
+ 'Register(name=k):0:49:@k',
+ },
+ },
+ },
+ },
+ 'Register(name=l):0:54:@l',
+ },
+ },
+ },
+ }},
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Register', '@b'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@c', 1),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@d', 1),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@e'),
+ hl('CallingParenthesis', ')'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@f'),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('NestingParenthesis', '('),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@g'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@j'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@k'),
+ hl('CallingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@l'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a)', 0, {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = '1:)',
+ msg = 'E15: Unexpected closing parenthesis: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('(@a', 0, {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ err = {
+ arg = '3:(@a',
+ msg = 'E110: Missing closing parenthesis for nested expression: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a(@b', 0, {
+ -- 01234
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ err = {
+ arg = '3:(@b',
+ msg = 'E116: Missing closing parenthesis for function call: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ })
+ end)
+end)