unittests,viml/parser/expressions: Start adding asgn parsing tests

This commit is contained in:
ZyX
2017-11-13 01:10:39 +03:00
parent 39c75d31be
commit 342239a9c5
4 changed files with 348 additions and 49 deletions

View File

@@ -1363,7 +1363,6 @@ static inline ParserPosition recol_pos(const ParserPosition pos,
} \ } \
} while (0) } while (0)
// TODO(ZyX-I): actual condition
/// Check whether it is possible to have next expression after current /// 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. /// For :echo: `:echo @a @a` is a valid expression. `:echo (@a @a)` is not.
@@ -1901,6 +1900,7 @@ ExprAST viml_pexpr_parse(ParserState *const pstate, const int flags)
bool highlighted_prev_spacing = false; bool highlighted_prev_spacing = false;
// Lambda node, valid when parsing lambda arguments only. // Lambda node, valid when parsing lambda arguments only.
ExprASTNode *lambda_node = NULL; ExprASTNode *lambda_node = NULL;
size_t asgn_level = 0;
do { do {
const bool is_concat_or_subscript = ( const bool is_concat_or_subscript = (
want_node == kENodeValue want_node == kENodeValue
@@ -2063,6 +2063,9 @@ viml_pexpr_parse_process_token:
&& tok_type != kExprLexDot && tok_type != kExprLexDot
&& (tok_type != kExprLexComma || !is_single_assignment) && (tok_type != kExprLexComma || !is_single_assignment)
&& tok_type != kExprLexAssignment) { && tok_type != kExprLexAssignment) {
if (flags & kExprFlagsMulti && MAY_HAVE_NEXT_EXPR) {
goto viml_pexpr_parse_end;
}
ERROR_FROM_TOKEN_AND_MSG( ERROR_FROM_TOKEN_AND_MSG(
cur_token, cur_token,
_("E15: Expected assignment operator or subscript: %.*s")); _("E15: Expected assignment operator or subscript: %.*s"));
@@ -2429,6 +2432,10 @@ viml_pexpr_parse_valid_colon:
ExprASTNode *new_top_node = *new_top_node_p; ExprASTNode *new_top_node = *new_top_node_p;
switch (new_top_node->type) { switch (new_top_node->type) {
case kExprNodeListLiteral: { case kExprNodeListLiteral: {
if (pt_is_assignment(cur_pt) && new_top_node->children == NULL) {
ERROR_FROM_TOKEN_AND_MSG(
cur_token, _("E475: Unable to assign to empty list: %.*s"));
}
HL_CUR_TOKEN(List); HL_CUR_TOKEN(List);
break; break;
} }
@@ -2447,14 +2454,18 @@ viml_pexpr_parse_bracket_closing_error:
} }
kvi_push(ast_stack, new_top_node_p); kvi_push(ast_stack, new_top_node_p);
want_node = kENodeOperator; want_node = kENodeOperator;
if (cur_pt == kEPTSingleAssignment) { if (kv_size(ast_stack) <= asgn_level) {
kv_drop(pt_stack, 1); assert(kv_size(ast_stack) == asgn_level);
} else if (cur_pt == kEPTAssignment) { asgn_level = 0;
assert(ast.err.msg); if (cur_pt == kEPTSingleAssignment) {
} else if (cur_pt == kEPTExpr kv_drop(pt_stack, 1);
&& kv_size(pt_stack) > 1 } else if (cur_pt == kEPTAssignment) {
&& pt_is_assignment(kv_Z(pt_stack, 1))) { assert(ast.err.msg);
kv_drop(pt_stack, 1); } else if (cur_pt == kEPTExpr
&& kv_size(pt_stack) > 1
&& pt_is_assignment(kv_Z(pt_stack, 1))) {
kv_drop(pt_stack, 1);
}
} }
} else { } else {
if (want_node == kENodeValue) { if (want_node == kENodeValue) {
@@ -2480,6 +2491,7 @@ viml_pexpr_parse_bracket_closing_error:
ADD_OP_NODE(cur_node); ADD_OP_NODE(cur_node);
HL_CUR_TOKEN(SubscriptBracket); HL_CUR_TOKEN(SubscriptBracket);
if (pt_is_assignment(cur_pt)) { if (pt_is_assignment(cur_pt)) {
asgn_level = kv_size(ast_stack);
kvi_push(pt_stack, kEPTExpr); kvi_push(pt_stack, kEPTExpr);
} }
} }
@@ -2586,10 +2598,14 @@ viml_pexpr_parse_figure_brace_closing_error:
} }
kvi_push(ast_stack, new_top_node_p); kvi_push(ast_stack, new_top_node_p);
want_node = kENodeOperator; want_node = kENodeOperator;
if (cur_pt == kEPTExpr if (kv_size(ast_stack) <= asgn_level) {
&& kv_size(pt_stack) > 1 assert(kv_size(ast_stack) == asgn_level);
&& pt_is_assignment(kv_Z(pt_stack, 1))) { if (cur_pt == kEPTExpr
kv_drop(pt_stack, 1); && kv_size(pt_stack) > 1
&& pt_is_assignment(kv_Z(pt_stack, 1))) {
kv_drop(pt_stack, 1);
asgn_level = 0;
}
} }
} else { } else {
if (want_node == kENodeValue) { if (want_node == kENodeValue) {
@@ -2635,6 +2651,10 @@ viml_pexpr_parse_figure_brace_closing_error:
} while (0), } while (0),
Curly); Curly);
} }
if (pt_is_assignment(cur_pt)
&& !pt_is_assignment(kv_last(pt_stack))) {
asgn_level = kv_size(ast_stack);
}
} }
break; break;
} }
@@ -2755,6 +2775,10 @@ viml_pexpr_parse_figure_brace_closing_error:
case kExprLexDot: { case kExprLexDot: {
ADD_VALUE_IF_MISSING(_("E15: Unexpected dot: %.*s")); ADD_VALUE_IF_MISSING(_("E15: Unexpected dot: %.*s"));
if (prev_token.type == kExprLexSpacing) { if (prev_token.type == kExprLexSpacing) {
if (cur_pt == kEPTAssignment) {
ERROR_FROM_TOKEN_AND_MSG(
cur_token, _("E15: Cannot concatenate in assignments: %.*s"));
}
NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeConcat); NEW_NODE_WITH_CUR_POS(cur_node, kExprNodeConcat);
HL_CUR_TOKEN(Concat); HL_CUR_TOKEN(Concat);
} else { } else {

View File

@@ -264,6 +264,9 @@ local function which(exe)
end end
local function shallowcopy(orig) local function shallowcopy(orig)
if type(orig) ~= 'table' then
return orig
end
local copy = {} local copy = {}
for orig_key, orig_value in pairs(orig) do for orig_key, orig_value in pairs(orig) do
copy[orig_key] = orig_value copy[orig_key] = orig_value
@@ -312,7 +315,7 @@ end
-- dictdiff: find a diff so that mergedicts_copy(d1, diff) is equal to d2 -- dictdiff: find a diff so that mergedicts_copy(d1, diff) is equal to d2
-- --
-- Note: does not copy values from d2 -- Note: does not do copies of d2 values used.
local function dictdiff(d1, d2) local function dictdiff(d1, d2)
local ret = {} local ret = {}
local hasdiff = false local hasdiff = false
@@ -321,7 +324,7 @@ local function dictdiff(d1, d2)
hasdiff = true hasdiff = true
ret[k] = REMOVE_THIS ret[k] = REMOVE_THIS
elseif type(v) == type(d2[k]) then elseif type(v) == type(d2[k]) then
if type(v) == 'table' and type(d2[k]) == 'table' then if type(v) == 'table' then
local subdiff = dictdiff(v, d2[k]) local subdiff = dictdiff(v, d2[k])
if subdiff ~= nil then if subdiff ~= nil then
hasdiff = true hasdiff = true
@@ -329,14 +332,16 @@ local function dictdiff(d1, d2)
end end
elseif v ~= d2[k] then elseif v ~= d2[k] then
ret[k] = d2[k] ret[k] = d2[k]
hasdiff = true
end end
else else
ret[k] = d2[k] ret[k] = d2[k]
hasdiff = true
end end
end end
for k, v in pairs(d2) do for k, v in pairs(d2) do
if d1[k] == nil then if d1[k] == nil then
ret[k] = v ret[k] = shallowcopy(v)
hasdiff = true hasdiff = true
end end
end end
@@ -406,13 +411,18 @@ format_luav = function(v, indent, opts)
opts = opts or {} opts = opts or {}
local linesep = '\n' local linesep = '\n'
local next_indent_arg = nil local next_indent_arg = nil
local indent_shift = opts.indent_shift or ' '
local next_indent
local nl = '\n'
if indent == nil then if indent == nil then
indent = '' indent = ''
linesep = '' linesep = ''
next_indent = ''
nl = ' '
else else
next_indent_arg = indent .. ' ' next_indent_arg = indent .. indent_shift
next_indent = indent .. indent_shift
end end
local next_indent = indent .. ' '
local ret = '' local ret = ''
if type(v) == 'string' then if type(v) == 'string' then
if opts.literal_strings then if opts.literal_strings then
@@ -430,10 +440,12 @@ format_luav = function(v, indent, opts)
else else
local processed_keys = {} local processed_keys = {}
ret = '{' .. linesep ret = '{' .. linesep
local non_empty = false
for i, subv in ipairs(v) do for i, subv in ipairs(v) do
ret = ('%s%s%s,\n'):format(ret, next_indent, ret = ('%s%s%s,%s'):format(ret, next_indent,
format_luav(subv, next_indent_arg, opts)) format_luav(subv, next_indent_arg, opts), nl)
processed_keys[i] = true processed_keys[i] = true
non_empty = true
end end
for k, subv in pairs(v) do for k, subv in pairs(v) do
if not processed_keys[k] then if not processed_keys[k] then
@@ -443,9 +455,13 @@ format_luav = function(v, indent, opts)
ret = ('%s%s[%s] = '):format(ret, next_indent, ret = ('%s%s[%s] = '):format(ret, next_indent,
format_luav(k, nil, opts)) format_luav(k, nil, opts))
end end
ret = ret .. format_luav(subv, next_indent_arg, opts) .. ',\n' ret = ret .. format_luav(subv, next_indent_arg, opts) .. ',' .. nl
non_empty = true
end end
end end
if nl == ' ' and non_empty then
ret = ret:sub(1, -3)
end
ret = ret .. indent .. '}' ret = ret .. indent .. '}'
end end
elseif type(v) == 'number' then elseif type(v) == 'number' then
@@ -495,6 +511,25 @@ local function intchar2lua(ch)
return (20 <= ch and ch < 127) and ('%c'):format(ch) or ch return (20 <= ch and ch < 127) and ('%c'):format(ch) or ch
end end
local fixtbl_metatable = {
__newindex = function()
assert(false)
end,
}
local function fixtbl(tbl)
return setmetatable(tbl, fixtbl_metatable)
end
local function fixtbl_rec(tbl)
for k, v in pairs(tbl) do
if type(v) == 'table' then
fixtbl_rec(v)
end
end
return fixtbl(tbl)
end
return { return {
eq = eq, eq = eq,
neq = neq, neq = neq,
@@ -519,4 +554,6 @@ return {
format_string = format_string, format_string = format_string,
intchar2lua = intchar2lua, intchar2lua = intchar2lua,
updated = updated, updated = updated,
fixtbl = fixtbl,
fixtbl_rec = fixtbl_rec,
} }

View File

@@ -93,6 +93,20 @@ int main(const int argc, const char *const *const argv,
const ExprAST ast = viml_pexpr_parse(&pstate, (int)flags); const ExprAST ast = viml_pexpr_parse(&pstate, (int)flags);
assert(ast.root != NULL || ast.err.msg); assert(ast.root != NULL || ast.err.msg);
if (flags & kExprFlagsParseLet) {
assert(ast.err.msg != NULL
|| ast.root->type == kExprNodeAssignment
|| (ast.root->type == kExprNodeListLiteral
&& ast.root->children != NULL)
|| ast.root->type == kExprNodeComplexIdentifier
|| ast.root->type == kExprNodeCurlyBracesIdentifier
|| ast.root->type == kExprNodePlainIdentifier
|| ast.root->type == kExprNodeRegister
|| ast.root->type == kExprNodeEnvironment
|| ast.root->type == kExprNodeOption
|| ast.root->type == kExprNodeSubscript
|| ast.root->type == kExprNodeConcatOrSubscript);
}
// Cant possibly have more highlight tokens then there are bytes in string. // Cant possibly have more highlight tokens then there are bytes in string.
assert(kv_size(colors) <= INPUT_SIZE - shift); assert(kv_size(colors) <= INPUT_SIZE - shift);
kvi_destroy(colors); kvi_destroy(colors);

View File

@@ -202,12 +202,20 @@ local function hls_to_hl_fs(hls)
return ret return ret
end end
local function format_check(expr, format_check_data) local function format_check(expr, format_check_data, opts)
-- That forces specific order. -- That forces specific order.
local zdata = format_check_data[0] local zflags = opts.flags[1]
print(format_string('\ncheck_parsing(%r, {', expr, flags)) local zdata = format_check_data[zflags]
local digits = ' -- ' local dig_len = 0
local digits2 = ' -- ' if opts.funcname then
print(format_string('\n%s(%r, {', opts.funcname, expr))
dig_len = #opts.funcname + 2
else
print(format_string('\n_check_parsing(%r, %r, {', opts, expr))
dig_len = #('_check_parsing(, \'') + #(format_string('%r', opts))
end
local digits = ' --' .. (' '):rep(dig_len - #(' --'))
local digits2 = digits:sub(1, -10)
for i = 0, #expr - 1 do for i = 0, #expr - 1 do
if i % 10 == 0 then if i % 10 == 0 then
digits2 = ('%s%10u'):format(digits2, i / 10) digits2 = ('%s%10u'):format(digits2, i / 10)
@@ -232,12 +240,15 @@ local function format_check(expr, format_check_data)
local diffs = {} local diffs = {}
local diffs_num = 0 local diffs_num = 0
for flags, v in pairs(format_check_data) do for flags, v in pairs(format_check_data) do
if flags ~= 0 then if flags ~= zflags then
diffs[flags] = dictdiff(zdata, v) diffs[flags] = dictdiff(zdata, v)
if diffs[flags] then if diffs[flags] then
if flags == 3 then if flags == 3 + zflags then
if (dictdiff(format_check_data[1], format_check_data[3]) == nil if (dictdiff(format_check_data[1 + zflags],
or dictdiff(format_check_data[2], format_check_data[3]) == nil) then format_check_data[3 + zflags]) == nil
or dictdiff(format_check_data[2 + zflags],
format_check_data[3 + zflags]) == nil)
then
diffs[flags] = nil diffs[flags] = nil
else else
diffs_num = diffs_num + 1 diffs_num = diffs_num + 1
@@ -437,10 +448,12 @@ child_call_once(function()
end) end)
describe('Expressions parser', function() describe('Expressions parser', function()
local function check_parsing(str, exp_ast, exp_highlighting_fs, nz_flags_exps) local function _check_parsing(opts, str, exp_ast, exp_highlighting_fs,
nz_flags_exps)
local zflags = opts.flags[1]
nz_flags_exps = nz_flags_exps or {} nz_flags_exps = nz_flags_exps or {}
local format_check_data = {} local format_check_data = {}
for _, flags in ipairs({0, 1, 2, 3}) do for _, flags in ipairs(opts.flags) do
debug_log(('Running test case (%s, %u)'):format(str, flags)) debug_log(('Running test case (%s, %u)'):format(str, flags))
local err, msg = pcall(function() local err, msg = pcall(function()
if os.getenv('NVIM_TEST_PARSER_SPEC_PRINT_TEST_CASE') == '1' then if os.getenv('NVIM_TEST_PARSER_SPEC_PRINT_TEST_CASE') == '1' then
@@ -452,25 +465,25 @@ describe('Expressions parser', function()
local east = lib.viml_pexpr_parse(pstate, flags) local east = lib.viml_pexpr_parse(pstate, flags)
local ast = east2lua(pstate, east) local ast = east2lua(pstate, east)
local hls = phl2lua(pstate) local hls = phl2lua(pstate)
local exps = {
ast = exp_ast,
hl_fs = exp_highlighting_fs,
}
local add_exps = nz_flags_exps[flags]
if not add_exps and flags == 3 then
add_exps = nz_flags_exps[1] or nz_flags_exps[2]
end
if add_exps then
if add_exps.ast then
exps.ast = mergedicts_copy(exps.ast, add_exps.ast)
end
if add_exps.hl_fs then
exps.hl_fs = mergedicts_copy(exps.hl_fs, add_exps.hl_fs)
end
end
if exp_ast == nil then if exp_ast == nil then
format_check_data[flags] = {ast=ast, hl_fs=hls_to_hl_fs(hls)} format_check_data[flags] = {ast=ast, hl_fs=hls_to_hl_fs(hls)}
else else
local exps = {
ast = exp_ast,
hl_fs = exp_highlighting_fs,
}
local add_exps = nz_flags_exps[flags]
if not add_exps and flags == 3 + zflags then
add_exps = nz_flags_exps[1 + zflags] or nz_flags_exps[2 + zflags]
end
if add_exps then
if add_exps.ast then
exps.ast = mergedicts_copy(exps.ast, add_exps.ast)
end
if add_exps.hl_fs then
exps.hl_fs = mergedicts_copy(exps.hl_fs, add_exps.hl_fs)
end
end
eq(exps.ast, ast) eq(exps.ast, ast)
if exp_highlighting_fs then if exp_highlighting_fs then
local exp_highlighting = {} local exp_highlighting = {}
@@ -493,9 +506,18 @@ describe('Expressions parser', function()
end end
end end
if exp_ast == nil then if exp_ast == nil then
format_check(str, format_check_data) format_check(str, format_check_data, opts)
end end
end end
local function check_parsing(...)
return _check_parsing({flags={0, 1, 2, 3}, funcname='check_parsing'}, ...)
end
local function check_asgn_parsing(...)
return _check_parsing({
flags={4, 5, 6, 7},
funcname='check_asgn_parsing',
}, ...)
end
local function hl(group, str, shift) local function hl(group, str, shift)
return function(next_col) return function(next_col)
if nvim_hl_defs['NVim' .. group] == nil then if nvim_hl_defs['NVim' .. group] == nil then
@@ -7694,6 +7716,208 @@ describe('Expressions parser', function()
}, },
}) })
end) end)
itp('works with assignments', function()
check_asgn_parsing('a=b', {
-- 012
ast = {
{
'Assignment(Plain):0:1:=',
children = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
'PlainIdentifier(scope=0,ident=b):0:2:b',
},
},
},
}, {
hl('IdentifierName', 'a'),
hl('PlainAssignment', '='),
hl('IdentifierName', 'b'),
})
check_asgn_parsing('a+=b', {
-- 0123
ast = {
{
'Assignment(Add):0:1:+=',
children = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
'PlainIdentifier(scope=0,ident=b):0:3:b',
},
},
},
}, {
hl('IdentifierName', 'a'),
hl('AssignmentWithAddition', '+='),
hl('IdentifierName', 'b'),
})
check_asgn_parsing('a-=b', {
-- 0123
ast = {
{
'Assignment(Subtract):0:1:-=',
children = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
'PlainIdentifier(scope=0,ident=b):0:3:b',
},
},
},
}, {
hl('IdentifierName', 'a'),
hl('AssignmentWithSubtraction', '-='),
hl('IdentifierName', 'b'),
})
check_asgn_parsing('a.=b', {
-- 0123
ast = {
{
'Assignment(Concat):0:1:.=',
children = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
'PlainIdentifier(scope=0,ident=b):0:3:b',
},
},
},
}, {
hl('IdentifierName', 'a'),
hl('AssignmentWithConcatenation', '.='),
hl('IdentifierName', 'b'),
})
check_asgn_parsing('a', {
-- 0
ast = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
},
}, {
hl('IdentifierName', 'a'),
})
check_asgn_parsing('a b', {
-- 012
ast = {
{
'OpMissing:0:1:',
children = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
'PlainIdentifier(scope=0,ident=b):0:1: b',
},
},
},
err = {
arg = 'b',
msg = 'E15: Expected assignment operator or subscript: %.*s',
},
}, {
hl('IdentifierName', 'a'),
hl('InvalidSpacing', ' '),
hl('IdentifierName', 'b'),
}, {
[5] = {
ast = {
ast = {
'PlainIdentifier(scope=0,ident=a):0:0:a',
},
err = REMOVE_THIS,
},
hl_fs = {
[2] = REMOVE_THIS,
[3] = REMOVE_THIS,
}
},
})
check_asgn_parsing('[a, b, c]', {
-- 012345678
ast = {
{
'ListLiteral:0:0:[',
children = {
{
'Comma:0:2:,',
children = {
'PlainIdentifier(scope=0,ident=a):0:1:a',
{
'Comma:0:5:,',
children = {
'PlainIdentifier(scope=0,ident=b):0:3: b',
'PlainIdentifier(scope=0,ident=c):0:6: c',
},
},
},
},
},
},
},
}, {
hl('List', '['),
hl('IdentifierName', 'a'),
hl('Comma', ','),
hl('IdentifierName', 'b', 1),
hl('Comma', ','),
hl('IdentifierName', 'c', 1),
hl('List', ']'),
})
check_asgn_parsing('[a, b]', {
-- 012345
ast = {
{
'ListLiteral:0:0:[',
children = {
{
'Comma:0:2:,',
children = {
'PlainIdentifier(scope=0,ident=a):0:1:a',
'PlainIdentifier(scope=0,ident=b):0:3: b',
},
},
},
},
},
}, {
hl('List', '['),
hl('IdentifierName', 'a'),
hl('Comma', ','),
hl('IdentifierName', 'b', 1),
hl('List', ']'),
})
check_asgn_parsing('[a]', {
-- 012
ast = {
{
'ListLiteral:0:0:[',
children = {
'PlainIdentifier(scope=0,ident=a):0:1:a',
},
},
},
}, {
hl('List', '['),
hl('IdentifierName', 'a'),
hl('List', ']'),
})
check_asgn_parsing('[]', {
-- 01
ast = {
'ListLiteral:0:0:[',
},
err = {
arg = ']',
msg = 'E475: Unable to assign to empty list: %.*s',
},
}, {
hl('List', '['),
hl('InvalidList', ']'),
})
-- check_asgn_parsing('a[1 + 2] += 3')
-- check_asgn_parsing('a[{-> {b{3}: 4}[5]}()] += 6')
-- check_asgn_parsing('a{1}.2[{-> {b{3}: 4}[5]}()]')
end)
-- FIXME: Test assignments thoroughly -- FIXME: Test assignments thoroughly
-- FIXME: Test that parsing assignments can be used for `:for` pre-`in` part. -- FIXME: Test that parsing assignments can be used for `:for` pre-`in` part.
-- FIXME: Somehow make functional tests use the same code. Or, at least, -- FIXME: Somehow make functional tests use the same code. Or, at least,