aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/README.md3
-rw-r--r--test/functional/api/vim_spec.lua196
-rw-r--r--test/functional/ui/cmdline_highlight_spec.lua30
-rw-r--r--test/functional/ui/cmdline_spec.lua16
-rw-r--r--test/helpers.lua244
-rw-r--r--test/symbolic/klee/nvim/charset.c177
-rw-r--r--test/symbolic/klee/nvim/garray.c195
-rw-r--r--test/symbolic/klee/nvim/gettext.c4
-rw-r--r--test/symbolic/klee/nvim/keymap.c538
-rw-r--r--test/symbolic/klee/nvim/mbyte.c266
-rw-r--r--test/symbolic/klee/nvim/memory.c101
-rwxr-xr-xtest/symbolic/klee/run.sh102
-rw-r--r--test/symbolic/klee/viml_expressions_lexer.c105
-rw-r--r--test/symbolic/klee/viml_expressions_parser.c117
-rw-r--r--test/unit/charset/vim_str2nr_spec.lua303
-rw-r--r--test/unit/eval/helpers.lua5
-rw-r--r--test/unit/formatc.lua7
-rw-r--r--test/unit/helpers.lua109
-rw-r--r--test/unit/viml/expressions/lexer_spec.lua428
-rw-r--r--test/unit/viml/expressions/parser_spec.lua540
-rw-r--r--test/unit/viml/expressions/parser_tests.lua8185
-rw-r--r--test/unit/viml/helpers.lua130
22 files changed, 11769 insertions, 32 deletions
diff --git a/test/README.md b/test/README.md
index 010a2c9c12..6ad70e2f45 100644
--- a/test/README.md
+++ b/test/README.md
@@ -341,3 +341,6 @@ disables tracing (the fastest, but you get no data if tests crash and there was
no core dump generated), `1` or empty/undefined leaves only C function cals and
returns in the trace (faster then recording everything), `2` records all
function calls, returns and lua source lines exuecuted.
+
+`NVIM_TEST_TRACE_ON_ERROR` (U) (1): makes unit tests yield trace on error in
+addition to regular error message.
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
index b849304d45..5b5340d9e2 100644
--- a/test/functional/api/vim_spec.lua
+++ b/test/functional/api/vim_spec.lua
@@ -1,5 +1,7 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
+local global_helpers = require('test.helpers')
+
local NIL = helpers.NIL
local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq
local ok, nvim_async, feed = helpers.ok, helpers.nvim_async, helpers.feed
@@ -10,6 +12,10 @@ local request = helpers.request
local meth_pcall = helpers.meth_pcall
local command = helpers.command
+local intchar2lua = global_helpers.intchar2lua
+local format_string = global_helpers.format_string
+local mergedicts_copy = global_helpers.mergedicts_copy
+
describe('api', function()
before_each(clear)
@@ -710,4 +716,194 @@ describe('api', function()
ok(err:match(': Wrong type for argument 1, expecting String') ~= nil)
end)
+ describe('nvim_parse_expression', function()
+ local function simplify_east_api_node(line, east_api_node)
+ if east_api_node == NIL then
+ return nil
+ end
+ if east_api_node.children then
+ for k, v in pairs(east_api_node.children) do
+ east_api_node.children[k] = simplify_east_api_node(line, v)
+ end
+ end
+ local typ = east_api_node.type
+ if typ == 'Register' then
+ typ = typ .. ('(name=%s)'):format(
+ tostring(intchar2lua(east_api_node.name)))
+ east_api_node.name = nil
+ elseif typ == 'PlainIdentifier' then
+ typ = typ .. ('(scope=%s,ident=%s)'):format(
+ tostring(intchar2lua(east_api_node.scope)), east_api_node.ident)
+ east_api_node.scope = nil
+ east_api_node.ident = nil
+ elseif typ == 'PlainKey' then
+ typ = typ .. ('(key=%s)'):format(east_api_node.ident)
+ east_api_node.ident = nil
+ elseif typ == 'Comparison' then
+ typ = typ .. ('(type=%s,inv=%u,ccs=%s)'):format(
+ east_api_node.cmp_type, east_api_node.invert and 1 or 0,
+ east_api_node.ccs_strategy)
+ east_api_node.ccs_strategy = nil
+ east_api_node.cmp_type = nil
+ east_api_node.invert = nil
+ elseif typ == 'Integer' then
+ typ = typ .. ('(val=%u)'):format(east_api_node.ivalue)
+ east_api_node.ivalue = nil
+ elseif typ == 'Float' then
+ typ = typ .. format_string('(val=%e)', east_api_node.fvalue)
+ east_api_node.fvalue = nil
+ elseif typ == 'SingleQuotedString' or typ == 'DoubleQuotedString' then
+ typ = format_string('%s(val=%q)', typ, east_api_node.svalue)
+ east_api_node.svalue = nil
+ elseif typ == 'Option' then
+ typ = ('%s(scope=%s,ident=%s)'):format(
+ typ,
+ tostring(intchar2lua(east_api_node.scope)),
+ east_api_node.ident)
+ east_api_node.ident = nil
+ east_api_node.scope = nil
+ elseif typ == 'Environment' then
+ typ = ('%s(ident=%s)'):format(typ, east_api_node.ident)
+ east_api_node.ident = nil
+ elseif typ == 'Assignment' then
+ local aug = east_api_node.augmentation
+ if aug == '' then aug = 'Plain' end
+ typ = ('%s(%s)'):format(typ, aug)
+ east_api_node.augmentation = nil
+ end
+ typ = ('%s:%u:%u:%s'):format(
+ typ, east_api_node.start[1], east_api_node.start[2],
+ line:sub(east_api_node.start[2] + 1,
+ east_api_node.start[2] + 1 + east_api_node.len - 1))
+ assert(east_api_node.start[2] + east_api_node.len - 1 <= #line)
+ for k, _ in pairs(east_api_node.start) do
+ assert(({true, true})[k])
+ end
+ east_api_node.start = nil
+ east_api_node.type = nil
+ east_api_node.len = nil
+ local can_simplify = true
+ for _, _ in pairs(east_api_node) do
+ if can_simplify then can_simplify = false end
+ end
+ if can_simplify then
+ return typ
+ else
+ east_api_node[1] = typ
+ return east_api_node
+ end
+ end
+ local function simplify_east_api(line, east_api)
+ if east_api.error then
+ east_api.err = east_api.error
+ east_api.error = nil
+ east_api.err.msg = east_api.err.message
+ east_api.err.message = nil
+ end
+ if east_api.ast then
+ east_api.ast = {simplify_east_api_node(line, east_api.ast)}
+ if #east_api.ast == 0 then
+ east_api.ast = nil
+ end
+ end
+ if east_api.len == #line then
+ east_api.len = nil
+ end
+ return east_api
+ end
+ local function simplify_east_hl(line, east_hl)
+ for i, v in ipairs(east_hl) do
+ east_hl[i] = ('%s:%u:%u:%s'):format(
+ v[4],
+ v[1],
+ v[2],
+ line:sub(v[2] + 1, v[3]))
+ end
+ return east_hl
+ end
+ local FLAGS_TO_STR = {
+ [0] = "",
+ [1] = "m",
+ [2] = "E",
+ [3] = "mE",
+ [4] = "l",
+ [5] = "lm",
+ [6] = "lE",
+ [7] = "lmE",
+ }
+ local function _check_parsing(opts, str, exp_ast, exp_highlighting_fs,
+ nz_flags_exps)
+ if type(str) ~= 'string' then
+ return
+ end
+ local zflags = opts.flags[1]
+ nz_flags_exps = nz_flags_exps or {}
+ for _, flags in ipairs(opts.flags) do
+ local err, msg = pcall(function()
+ local east_api = meths.parse_expression(str, FLAGS_TO_STR[flags], true)
+ local east_hl = east_api.highlight
+ east_api.highlight = nil
+ local ast = simplify_east_api(str, east_api)
+ local hls = simplify_east_hl(str, east_hl)
+ 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)
+ if exp_highlighting_fs then
+ local exp_highlighting = {}
+ local next_col = 0
+ for i, h in ipairs(exps.hl_fs) do
+ exp_highlighting[i], next_col = h(next_col)
+ end
+ eq(exp_highlighting, hls)
+ end
+ end)
+ if not err then
+ msg = format_string('Error while processing test (%r, %s):\n%s',
+ str, FLAGS_TO_STR[flags], msg)
+ error(msg)
+ end
+ 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
+ local function fmtn(typ, args, rest)
+ if (typ == 'UnknownFigure'
+ or typ == 'DictLiteral'
+ or typ == 'CurlyBracesIdentifier'
+ or typ == 'Lambda') then
+ return ('%s%s'):format(typ, rest)
+ elseif typ == 'DoubleQuotedString' or typ == 'SingleQuotedString' then
+ if args:sub(-4) == 'NULL' then
+ args = args:sub(1, -5) .. '""'
+ end
+ return ('%s(%s)%s'):format(typ, args, rest)
+ end
+ end
+ assert:set_parameter('TableFormatLevel', 1000000)
+ require('test.unit.viml.expressions.parser_tests')(
+ it, _check_parsing, hl, fmtn)
+ end)
+
end)
diff --git a/test/functional/ui/cmdline_highlight_spec.lua b/test/functional/ui/cmdline_highlight_spec.lua
index d87ce72599..023673738d 100644
--- a/test/functional/ui/cmdline_highlight_spec.lua
+++ b/test/functional/ui/cmdline_highlight_spec.lua
@@ -144,7 +144,9 @@ before_each(function()
EOB={bold = true, foreground = Screen.colors.Blue1},
ERR={foreground = Screen.colors.Grey100, background = Screen.colors.Red},
SK={foreground = Screen.colors.Blue},
- PE={bold = true, foreground = Screen.colors.SeaGreen4}
+ PE={bold = true, foreground = Screen.colors.SeaGreen4},
+ NUM={foreground = Screen.colors.Blue2},
+ NPAR={foreground = Screen.colors.Yellow},
})
end)
@@ -863,7 +865,10 @@ describe('Ex commands coloring support', function()
end)
describe('Expressions coloring support', function()
it('works', function()
- meths.set_var('Nvim_color_expr', 'RainBowParens')
+ meths.command('hi clear NVimNumber')
+ meths.command('hi clear NVimNestingParenthesis')
+ meths.command('hi NVimNumber guifg=Blue2')
+ meths.command('hi NVimNestingParenthesis guifg=Yellow')
feed(':echo <C-r>=(((1)))')
screen:expect([[
|
@@ -873,21 +878,28 @@ describe('Expressions coloring support', function()
{EOB:~ }|
{EOB:~ }|
{EOB:~ }|
- ={RBP1:(}{RBP2:(}{RBP3:(}1{RBP3:)}{RBP2:)}{RBP1:)}^ |
+ ={NPAR:(((}{NUM:1}{NPAR:)))}^ |
]])
end)
- it('errors out when failing to get callback', function()
+ it('does not use Nvim_color_expr', function()
meths.set_var('Nvim_color_expr', 42)
+ -- Used to error out due to failing to get callback.
+ meths.command('hi clear NVimNumber')
+ meths.command('hi NVimNumber guifg=Blue2')
feed(':<C-r>=1')
screen:expect([[
+ |
+ {EOB:~ }|
+ {EOB:~ }|
+ {EOB:~ }|
{EOB:~ }|
{EOB:~ }|
{EOB:~ }|
- = |
- {ERR:E5409: Unable to get g:Nvim_color_expr c}|
- {ERR:allback: Vim:E6000: Argument is not a fu}|
- {ERR:nction or function name} |
- =1^ |
+ ={NUM:1}^ |
]])
end)
+ -- FIXME: Test expr coloring when using -u NORC and -u NONE.
+ -- FIXME: Test different ways of triggering expression highlighting (:<C-r>=,
+ -- i<C-r>=, :<C-\>e, "=).
+ -- FIXME: Test with various invalid unicode and multibyte characters.
end)
diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua
index 0f8302b036..136ab09d4f 100644
--- a/test/functional/ui/cmdline_spec.lua
+++ b/test/functional/ui/cmdline_spec.lua
@@ -226,7 +226,11 @@ describe('external cmdline', function()
prompt = "",
special = {'"', true},
},{
- content = { { {}, "1+2" } },
+ content = {
+ { {}, "1" },
+ { {}, "+" },
+ { {}, "2" },
+ },
firstc = "=",
indent = 0,
pos = 3,
@@ -303,7 +307,7 @@ describe('external cmdline', function()
pos = 0,
prompt = "",
}}, cmdline)
- eq({{{{}, 'function Foo()'}}}, block)
+ eq({ { { {}, 'function Foo()'} } }, block)
end)
feed('line1<cr>')
@@ -314,8 +318,8 @@ describe('external cmdline', function()
~ |
|
]], nil, nil, function()
- eq({{{{}, 'function Foo()'}},
- {{{}, ' line1'}}}, block)
+ eq({ { { {}, 'function Foo()'} },
+ { { {}, ' line1'} } }, block)
end)
block = {}
@@ -327,8 +331,8 @@ describe('external cmdline', function()
~ |
^ |
]], nil, nil, function()
- eq({{{{}, 'function Foo()'}},
- {{{}, ' line1'}}}, block)
+ eq({ { { {}, 'function Foo()'} },
+ { { {}, ' line1'} } }, block)
end)
diff --git a/test/helpers.lua b/test/helpers.lua
index 123dd111ee..3ec4aa511f 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -251,6 +251,9 @@ local function which(exe)
end
local function shallowcopy(orig)
+ if type(orig) ~= 'table' then
+ return orig
+ end
local copy = {}
for orig_key, orig_value in pairs(orig) do
copy[orig_key] = orig_value
@@ -258,6 +261,91 @@ local function shallowcopy(orig)
return copy
end
+local deepcopy
+
+local function id(v)
+ return v
+end
+
+local deepcopy_funcs = {
+ table = function(orig)
+ local copy = {}
+ for k, v in pairs(orig) do
+ copy[deepcopy(k)] = deepcopy(v)
+ end
+ end,
+ number = id,
+ string = id,
+ ['nil'] = id,
+ boolean = id,
+}
+
+deepcopy = function(orig)
+ return deepcopy_funcs[type(orig)](orig)
+end
+
+local REMOVE_THIS = {}
+
+local function mergedicts_copy(d1, d2)
+ local ret = shallowcopy(d1)
+ for k, v in pairs(d2) do
+ if d2[k] == REMOVE_THIS then
+ ret[k] = nil
+ elseif type(d1[k]) == 'table' and type(v) == 'table' then
+ ret[k] = mergedicts_copy(d1[k], v)
+ else
+ ret[k] = v
+ end
+ end
+ return ret
+end
+
+-- dictdiff: find a diff so that mergedicts_copy(d1, diff) is equal to d2
+--
+-- Note: does not do copies of d2 values used.
+local function dictdiff(d1, d2)
+ local ret = {}
+ local hasdiff = false
+ for k, v in pairs(d1) do
+ if d2[k] == nil then
+ hasdiff = true
+ ret[k] = REMOVE_THIS
+ elseif type(v) == type(d2[k]) then
+ if type(v) == 'table' then
+ local subdiff = dictdiff(v, d2[k])
+ if subdiff ~= nil then
+ hasdiff = true
+ ret[k] = subdiff
+ end
+ elseif v ~= d2[k] then
+ ret[k] = d2[k]
+ hasdiff = true
+ end
+ else
+ ret[k] = d2[k]
+ hasdiff = true
+ end
+ end
+ for k, v in pairs(d2) do
+ if d1[k] == nil then
+ ret[k] = shallowcopy(v)
+ hasdiff = true
+ end
+ end
+ if hasdiff then
+ return ret
+ else
+ return nil
+ end
+end
+
+local function updated(d, d2)
+ for k, v in pairs(d2) do
+ d[k] = v
+ end
+ return d
+end
+
local function concat_tables(...)
local ret = {}
for i = 1, select('#', ...) do
@@ -294,6 +382,152 @@ local function dedent(str, leave_indent)
return str
end
+local function format_float(v)
+ -- On windows exponent appears to have three digits and not two
+ local ret = ('%.6e'):format(v)
+ local l, f, es, e = ret:match('^(%-?%d)%.(%d+)e([+%-])0*(%d%d+)$')
+ return l .. '.' .. f .. 'e' .. es .. e
+end
+
+local SUBTBL = {
+ '\\000', '\\001', '\\002', '\\003', '\\004',
+ '\\005', '\\006', '\\007', '\\008', '\\t',
+ '\\n', '\\011', '\\012', '\\r', '\\014',
+ '\\015', '\\016', '\\017', '\\018', '\\019',
+ '\\020', '\\021', '\\022', '\\023', '\\024',
+ '\\025', '\\026', '\\027', '\\028', '\\029',
+ '\\030', '\\031',
+}
+
+local format_luav
+
+format_luav = function(v, indent, opts)
+ opts = opts or {}
+ local linesep = '\n'
+ local next_indent_arg = nil
+ local indent_shift = opts.indent_shift or ' '
+ local next_indent
+ local nl = '\n'
+ if indent == nil then
+ indent = ''
+ linesep = ''
+ next_indent = ''
+ nl = ' '
+ else
+ next_indent_arg = indent .. indent_shift
+ next_indent = indent .. indent_shift
+ end
+ local ret = ''
+ if type(v) == 'string' then
+ if opts.literal_strings then
+ ret = v
+ else
+ ret = tostring(v):gsub('[\'\\]', '\\%0'):gsub(
+ '[%z\1-\31]', function(match)
+ return SUBTBL[match:byte() + 1]
+ end)
+ ret = '\'' .. ret .. '\''
+ end
+ elseif type(v) == 'table' then
+ if v == REMOVE_THIS then
+ ret = 'REMOVE_THIS'
+ else
+ local processed_keys = {}
+ ret = '{' .. linesep
+ local non_empty = false
+ for i, subv in ipairs(v) do
+ ret = ('%s%s%s,%s'):format(ret, next_indent,
+ format_luav(subv, next_indent_arg, opts), nl)
+ processed_keys[i] = true
+ non_empty = true
+ end
+ for k, subv in pairs(v) do
+ if not processed_keys[k] then
+ if type(k) == 'string' and k:match('^[a-zA-Z_][a-zA-Z0-9_]*$') then
+ ret = ret .. next_indent .. k .. ' = '
+ else
+ ret = ('%s%s[%s] = '):format(ret, next_indent,
+ format_luav(k, nil, opts))
+ end
+ ret = ret .. format_luav(subv, next_indent_arg, opts) .. ',' .. nl
+ non_empty = true
+ end
+ end
+ if nl == ' ' and non_empty then
+ ret = ret:sub(1, -3)
+ end
+ ret = ret .. indent .. '}'
+ end
+ elseif type(v) == 'number' then
+ if v % 1 == 0 then
+ ret = ('%d'):format(v)
+ else
+ ret = format_float(v)
+ end
+ elseif type(v) == 'nil' then
+ ret = 'nil'
+ else
+ print(type(v))
+ -- Not implemented yet
+ assert(false)
+ end
+ return ret
+end
+
+local function format_string(fmt, ...)
+ local i = 0
+ local args = {...}
+ local function getarg()
+ i = i + 1
+ return args[i]
+ end
+ local ret = fmt:gsub('%%[0-9*]*%.?[0-9*]*[cdEefgGiouXxqsr%%]', function(match)
+ local subfmt = match:gsub('%*', function()
+ return tostring(getarg())
+ end)
+ local arg = nil
+ if subfmt:sub(-1) ~= '%' then
+ arg = getarg()
+ end
+ if subfmt:sub(-1) == 'r' then
+ -- %r is like %q, but it is supposed to single-quote strings and not
+ -- double-quote them, and also work not only for strings.
+ subfmt = subfmt:sub(1, -2) .. 's'
+ arg = format_luav(arg)
+ end
+ if subfmt == '%e' then
+ return format_float(arg)
+ else
+ return subfmt:format(arg)
+ end
+ end)
+ return ret
+end
+
+local function intchar2lua(ch)
+ ch = tonumber(ch)
+ return (20 <= ch and ch < 127) and ('%c'):format(ch) or ch
+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 _, v in pairs(tbl) do
+ if type(v) == 'table' then
+ fixtbl_rec(v)
+ end
+ end
+ return fixtbl(tbl)
+end
+
return {
eq = eq,
neq = neq,
@@ -308,6 +542,16 @@ return {
hasenv = hasenv,
which = which,
shallowcopy = shallowcopy,
+ deepcopy = deepcopy,
+ mergedicts_copy = mergedicts_copy,
+ dictdiff = dictdiff,
+ REMOVE_THIS = REMOVE_THIS,
concat_tables = concat_tables,
dedent = dedent,
+ format_luav = format_luav,
+ format_string = format_string,
+ intchar2lua = intchar2lua,
+ updated = updated,
+ fixtbl = fixtbl,
+ fixtbl_rec = fixtbl_rec,
}
diff --git a/test/symbolic/klee/nvim/charset.c b/test/symbolic/klee/nvim/charset.c
new file mode 100644
index 0000000000..77f690f08d
--- /dev/null
+++ b/test/symbolic/klee/nvim/charset.c
@@ -0,0 +1,177 @@
+#include <stdbool.h>
+
+#include "nvim/ascii.h"
+#include "nvim/macros.h"
+#include "nvim/charset.h"
+#include "nvim/eval/typval.h"
+#include "nvim/vim.h"
+
+bool vim_isIDc(int c)
+{
+ return ASCII_ISALNUM(c);
+}
+
+int hex2nr(int c)
+{
+ if ((c >= 'a') && (c <= 'f')) {
+ return c - 'a' + 10;
+ }
+
+ if ((c >= 'A') && (c <= 'F')) {
+ return c - 'A' + 10;
+ }
+ return c - '0';
+}
+
+void vim_str2nr(const char_u *const start, int *const prep, int *const len,
+ const int what, varnumber_T *const nptr,
+ uvarnumber_T *const unptr, const int maxlen)
+{
+ const char *ptr = (const char *)start;
+#define STRING_ENDED(ptr) \
+ (!(maxlen == 0 || (int)((ptr) - (const char *)start) < maxlen))
+ int pre = 0; // default is decimal
+ const bool negative = (ptr[0] == '-');
+ uvarnumber_T un = 0;
+
+ if (negative) {
+ ptr++;
+ }
+
+ if (what & STR2NR_FORCE) {
+ // When forcing main consideration is skipping the prefix. Octal and decimal
+ // numbers have no prefixes to skip. pre is not set.
+ switch ((unsigned)what & (~(unsigned)STR2NR_FORCE)) {
+ case STR2NR_HEX: {
+ if (!STRING_ENDED(ptr + 2)
+ && ptr[0] == '0'
+ && (ptr[1] == 'x' || ptr[1] == 'X')
+ && ascii_isxdigit(ptr[2])) {
+ ptr += 2;
+ }
+ goto vim_str2nr_hex;
+ }
+ case STR2NR_BIN: {
+ if (!STRING_ENDED(ptr + 2)
+ && ptr[0] == '0'
+ && (ptr[1] == 'b' || ptr[1] == 'B')
+ && ascii_isbdigit(ptr[2])) {
+ ptr += 2;
+ }
+ goto vim_str2nr_bin;
+ }
+ case STR2NR_OCT: {
+ goto vim_str2nr_oct;
+ }
+ case 0: {
+ goto vim_str2nr_dec;
+ }
+ default: {
+ assert(false);
+ }
+ }
+ } else if ((what & (STR2NR_HEX|STR2NR_OCT|STR2NR_BIN))
+ && !STRING_ENDED(ptr + 1)
+ && ptr[0] == '0' && ptr[1] != '8' && ptr[1] != '9') {
+ pre = ptr[1];
+ // Detect hexadecimal: 0x or 0X follwed by hex digit
+ if ((what & STR2NR_HEX)
+ && !STRING_ENDED(ptr + 2)
+ && (pre == 'X' || pre == 'x')
+ && ascii_isxdigit(ptr[2])) {
+ ptr += 2;
+ goto vim_str2nr_hex;
+ }
+ // Detect binary: 0b or 0B follwed by 0 or 1
+ if ((what & STR2NR_BIN)
+ && !STRING_ENDED(ptr + 2)
+ && (pre == 'B' || pre == 'b')
+ && ascii_isbdigit(ptr[2])) {
+ ptr += 2;
+ goto vim_str2nr_bin;
+ }
+ // Detect octal number: zero followed by octal digits without '8' or '9'
+ pre = 0;
+ if (!(what & STR2NR_OCT)) {
+ goto vim_str2nr_dec;
+ }
+ for (int i = 2; !STRING_ENDED(ptr + i) && ascii_isdigit(ptr[i]); i++) {
+ if (ptr[i] > '7') {
+ goto vim_str2nr_dec;
+ }
+ }
+ pre = '0';
+ goto vim_str2nr_oct;
+ } else {
+ goto vim_str2nr_dec;
+ }
+
+ // Do the string-to-numeric conversion "manually" to avoid sscanf quirks.
+ assert(false); // Should’ve used goto earlier.
+#define PARSE_NUMBER(base, cond, conv) \
+ do { \
+ while (!STRING_ENDED(ptr) && (cond)) { \
+ /* avoid ubsan error for overflow */ \
+ if (un < UVARNUMBER_MAX / base) { \
+ un = base * un + (uvarnumber_T)(conv); \
+ } else { \
+ un = UVARNUMBER_MAX; \
+ } \
+ ptr++; \
+ } \
+ } while (0)
+ switch (pre) {
+ case 'b':
+ case 'B': {
+vim_str2nr_bin:
+ PARSE_NUMBER(2, (*ptr == '0' || *ptr == '1'), (*ptr - '0'));
+ break;
+ }
+ case '0': {
+vim_str2nr_oct:
+ PARSE_NUMBER(8, ('0' <= *ptr && *ptr <= '7'), (*ptr - '0'));
+ break;
+ }
+ case 0: {
+vim_str2nr_dec:
+ PARSE_NUMBER(10, (ascii_isdigit(*ptr)), (*ptr - '0'));
+ break;
+ }
+ case 'x':
+ case 'X': {
+vim_str2nr_hex:
+ PARSE_NUMBER(16, (ascii_isxdigit(*ptr)), (hex2nr(*ptr)));
+ break;
+ }
+ }
+#undef PARSE_NUMBER
+
+ if (prep != NULL) {
+ *prep = pre;
+ }
+
+ if (len != NULL) {
+ *len = (int)(ptr - (const char *)start);
+ }
+
+ if (nptr != NULL) {
+ if (negative) { // account for leading '-' for decimal numbers
+ // avoid ubsan error for overflow
+ if (un > VARNUMBER_MAX) {
+ *nptr = VARNUMBER_MIN;
+ } else {
+ *nptr = -(varnumber_T)un;
+ }
+ } else {
+ if (un > VARNUMBER_MAX) {
+ un = VARNUMBER_MAX;
+ }
+ *nptr = (varnumber_T)un;
+ }
+ }
+
+ if (unptr != NULL) {
+ *unptr = un;
+ }
+#undef STRING_ENDED
+}
diff --git a/test/symbolic/klee/nvim/garray.c b/test/symbolic/klee/nvim/garray.c
new file mode 100644
index 0000000000..260870c3c2
--- /dev/null
+++ b/test/symbolic/klee/nvim/garray.c
@@ -0,0 +1,195 @@
+// This is an open source non-commercial project. Dear PVS-Studio, please check
+// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+
+/// @file garray.c
+///
+/// Functions for handling growing arrays.
+
+#include <string.h>
+#include <inttypes.h>
+
+#include "nvim/vim.h"
+#include "nvim/ascii.h"
+#include "nvim/log.h"
+#include "nvim/memory.h"
+#include "nvim/path.h"
+#include "nvim/garray.h"
+#include "nvim/strings.h"
+
+// #include "nvim/globals.h"
+#include "nvim/memline.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "garray.c.generated.h"
+#endif
+
+/// Clear an allocated growing array.
+void ga_clear(garray_T *gap)
+{
+ xfree(gap->ga_data);
+
+ // Initialize growing array without resetting itemsize or growsize
+ gap->ga_data = NULL;
+ gap->ga_maxlen = 0;
+ gap->ga_len = 0;
+}
+
+/// Clear a growing array that contains a list of strings.
+///
+/// @param gap
+void ga_clear_strings(garray_T *gap)
+{
+ GA_DEEP_CLEAR_PTR(gap);
+}
+
+/// Initialize a growing array.
+///
+/// @param gap
+/// @param itemsize
+/// @param growsize
+void ga_init(garray_T *gap, int itemsize, int growsize)
+{
+ gap->ga_data = NULL;
+ gap->ga_maxlen = 0;
+ gap->ga_len = 0;
+ gap->ga_itemsize = itemsize;
+ ga_set_growsize(gap, growsize);
+}
+
+/// A setter for the growsize that guarantees it will be at least 1.
+///
+/// @param gap
+/// @param growsize
+void ga_set_growsize(garray_T *gap, int growsize)
+{
+ if (growsize < 1) {
+ WLOG("trying to set an invalid ga_growsize: %d", growsize);
+ gap->ga_growsize = 1;
+ } else {
+ gap->ga_growsize = growsize;
+ }
+}
+
+/// Make room in growing array "gap" for at least "n" items.
+///
+/// @param gap
+/// @param n
+void ga_grow(garray_T *gap, int n)
+{
+ if (gap->ga_maxlen - gap->ga_len >= n) {
+ // the garray still has enough space, do nothing
+ return;
+ }
+
+ if (gap->ga_growsize < 1) {
+ WLOG("ga_growsize(%d) is less than 1", gap->ga_growsize);
+ }
+
+ // the garray grows by at least growsize
+ if (n < gap->ga_growsize) {
+ n = gap->ga_growsize;
+ }
+ int new_maxlen = gap->ga_len + n;
+
+ size_t new_size = (size_t)(gap->ga_itemsize * new_maxlen);
+ size_t old_size = (size_t)(gap->ga_itemsize * gap->ga_maxlen);
+
+ // reallocate and clear the new memory
+ char *pp = xrealloc(gap->ga_data, new_size);
+ memset(pp + old_size, 0, new_size - old_size);
+
+ gap->ga_maxlen = new_maxlen;
+ gap->ga_data = pp;
+}
+
+/// For a growing array that contains a list of strings: concatenate all the
+/// strings with sep as separator.
+///
+/// @param gap
+/// @param sep
+///
+/// @returns the concatenated strings
+char_u *ga_concat_strings_sep(const garray_T *gap, const char *sep)
+ FUNC_ATTR_NONNULL_RET
+{
+ const size_t nelem = (size_t) gap->ga_len;
+ const char **strings = gap->ga_data;
+
+ if (nelem == 0) {
+ return (char_u *) xstrdup("");
+ }
+
+ size_t len = 0;
+ for (size_t i = 0; i < nelem; i++) {
+ len += strlen(strings[i]);
+ }
+
+ // add some space for the (num - 1) separators
+ len += (nelem - 1) * strlen(sep);
+ char *const ret = xmallocz(len);
+
+ char *s = ret;
+ for (size_t i = 0; i < nelem - 1; i++) {
+ s = xstpcpy(s, strings[i]);
+ s = xstpcpy(s, sep);
+ }
+ strcpy(s, strings[nelem - 1]);
+
+ return (char_u *) ret;
+}
+
+/// For a growing array that contains a list of strings: concatenate all the
+/// strings with a separating comma.
+///
+/// @param gap
+///
+/// @returns the concatenated strings
+char_u* ga_concat_strings(const garray_T *gap) FUNC_ATTR_NONNULL_RET
+{
+ return ga_concat_strings_sep(gap, ",");
+}
+
+/// Concatenate a string to a growarray which contains characters.
+/// When "s" is NULL does not do anything.
+///
+/// WARNING:
+/// - Does NOT copy the NUL at the end!
+/// - The parameter may not overlap with the growing array
+///
+/// @param gap
+/// @param s
+void ga_concat(garray_T *gap, const char_u *restrict s)
+{
+ if (s == NULL) {
+ return;
+ }
+
+ ga_concat_len(gap, (const char *restrict) s, strlen((char *) s));
+}
+
+/// Concatenate a string to a growarray which contains characters
+///
+/// @param[out] gap Growarray to modify.
+/// @param[in] s String to concatenate.
+/// @param[in] len String length.
+void ga_concat_len(garray_T *const gap, const char *restrict s,
+ const size_t len)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (len) {
+ ga_grow(gap, (int) len);
+ char *data = gap->ga_data;
+ memcpy(data + gap->ga_len, s, len);
+ gap->ga_len += (int) len;
+ }
+}
+
+/// Append one byte to a growarray which contains bytes.
+///
+/// @param gap
+/// @param c
+void ga_append(garray_T *gap, char c)
+{
+ GA_APPEND(char, gap, c);
+}
+
diff --git a/test/symbolic/klee/nvim/gettext.c b/test/symbolic/klee/nvim/gettext.c
new file mode 100644
index 0000000000..b9cc98d770
--- /dev/null
+++ b/test/symbolic/klee/nvim/gettext.c
@@ -0,0 +1,4 @@
+char *gettext(const char *s)
+{
+ return (char *)s;
+}
diff --git a/test/symbolic/klee/nvim/keymap.c b/test/symbolic/klee/nvim/keymap.c
new file mode 100644
index 0000000000..ff5e46e75b
--- /dev/null
+++ b/test/symbolic/klee/nvim/keymap.c
@@ -0,0 +1,538 @@
+#include <stdbool.h>
+
+#include "nvim/types.h"
+#include "nvim/keymap.h"
+#include "nvim/eval/typval.h"
+
+#define MOD_KEYS_ENTRY_SIZE 5
+
+static char_u modifier_keys_table[] =
+{
+ MOD_MASK_SHIFT, '&', '9', '@', '1',
+ MOD_MASK_SHIFT, '&', '0', '@', '2',
+ MOD_MASK_SHIFT, '*', '1', '@', '4',
+ MOD_MASK_SHIFT, '*', '2', '@', '5',
+ MOD_MASK_SHIFT, '*', '3', '@', '6',
+ MOD_MASK_SHIFT, '*', '4', 'k', 'D',
+ MOD_MASK_SHIFT, '*', '5', 'k', 'L',
+ MOD_MASK_SHIFT, '*', '7', '@', '7',
+ MOD_MASK_CTRL, KS_EXTRA, (int)KE_C_END, '@', '7',
+ MOD_MASK_SHIFT, '*', '9', '@', '9',
+ MOD_MASK_SHIFT, '*', '0', '@', '0',
+ MOD_MASK_SHIFT, '#', '1', '%', '1',
+ MOD_MASK_SHIFT, '#', '2', 'k', 'h',
+ MOD_MASK_CTRL, KS_EXTRA, (int)KE_C_HOME, 'k', 'h',
+ MOD_MASK_SHIFT, '#', '3', 'k', 'I',
+ MOD_MASK_SHIFT, '#', '4', 'k', 'l',
+ MOD_MASK_CTRL, KS_EXTRA, (int)KE_C_LEFT, 'k', 'l',
+ MOD_MASK_SHIFT, '%', 'a', '%', '3',
+ MOD_MASK_SHIFT, '%', 'b', '%', '4',
+ MOD_MASK_SHIFT, '%', 'c', '%', '5',
+ MOD_MASK_SHIFT, '%', 'd', '%', '7',
+ MOD_MASK_SHIFT, '%', 'e', '%', '8',
+ MOD_MASK_SHIFT, '%', 'f', '%', '9',
+ MOD_MASK_SHIFT, '%', 'g', '%', '0',
+ MOD_MASK_SHIFT, '%', 'h', '&', '3',
+ MOD_MASK_SHIFT, '%', 'i', 'k', 'r',
+ MOD_MASK_CTRL, KS_EXTRA, (int)KE_C_RIGHT, 'k', 'r',
+ MOD_MASK_SHIFT, '%', 'j', '&', '5',
+ MOD_MASK_SHIFT, '!', '1', '&', '6',
+ MOD_MASK_SHIFT, '!', '2', '&', '7',
+ MOD_MASK_SHIFT, '!', '3', '&', '8',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_UP, 'k', 'u',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_DOWN, 'k', 'd',
+
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_XF1, KS_EXTRA, (int)KE_XF1,
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_XF2, KS_EXTRA, (int)KE_XF2,
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_XF3, KS_EXTRA, (int)KE_XF3,
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_XF4, KS_EXTRA, (int)KE_XF4,
+
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F1, 'k', '1',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F2, 'k', '2',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F3, 'k', '3',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F4, 'k', '4',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F5, 'k', '5',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F6, 'k', '6',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F7, 'k', '7',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F8, 'k', '8',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F9, 'k', '9',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F10, 'k', ';',
+
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F11, 'F', '1',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F12, 'F', '2',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F13, 'F', '3',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F14, 'F', '4',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F15, 'F', '5',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F16, 'F', '6',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F17, 'F', '7',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F18, 'F', '8',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F19, 'F', '9',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F20, 'F', 'A',
+
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F21, 'F', 'B',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F22, 'F', 'C',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F23, 'F', 'D',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F24, 'F', 'E',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F25, 'F', 'F',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F26, 'F', 'G',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F27, 'F', 'H',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F28, 'F', 'I',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F29, 'F', 'J',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F30, 'F', 'K',
+
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F31, 'F', 'L',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F32, 'F', 'M',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F33, 'F', 'N',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F34, 'F', 'O',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F35, 'F', 'P',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F36, 'F', 'Q',
+ MOD_MASK_SHIFT, KS_EXTRA, (int)KE_S_F37, 'F', 'R',
+
+ MOD_MASK_SHIFT, 'k', 'B', KS_EXTRA, (int)KE_TAB,
+
+ NUL
+};
+
+int simplify_key(const int key, int *modifiers)
+{
+ if (*modifiers & (MOD_MASK_SHIFT | MOD_MASK_CTRL | MOD_MASK_ALT)) {
+ // TAB is a special case.
+ if (key == TAB && (*modifiers & MOD_MASK_SHIFT)) {
+ *modifiers &= ~MOD_MASK_SHIFT;
+ return K_S_TAB;
+ }
+ const int key0 = KEY2TERMCAP0(key);
+ const int key1 = KEY2TERMCAP1(key);
+ for (int i = 0; modifier_keys_table[i] != NUL; i += MOD_KEYS_ENTRY_SIZE) {
+ if (key0 == modifier_keys_table[i + 3]
+ && key1 == modifier_keys_table[i + 4]
+ && (*modifiers & modifier_keys_table[i])) {
+ *modifiers &= ~modifier_keys_table[i];
+ return TERMCAP2KEY(modifier_keys_table[i + 1],
+ modifier_keys_table[i + 2]);
+ }
+ }
+ }
+ return key;
+}
+
+int handle_x_keys(const int key)
+ FUNC_ATTR_CONST FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ switch (key) {
+ case K_XUP: return K_UP;
+ case K_XDOWN: return K_DOWN;
+ case K_XLEFT: return K_LEFT;
+ case K_XRIGHT: return K_RIGHT;
+ case K_XHOME: return K_HOME;
+ case K_ZHOME: return K_HOME;
+ case K_XEND: return K_END;
+ case K_ZEND: return K_END;
+ case K_XF1: return K_F1;
+ case K_XF2: return K_F2;
+ case K_XF3: return K_F3;
+ case K_XF4: return K_F4;
+ case K_S_XF1: return K_S_F1;
+ case K_S_XF2: return K_S_F2;
+ case K_S_XF3: return K_S_F3;
+ case K_S_XF4: return K_S_F4;
+ }
+ return key;
+}
+
+static const struct key_name_entry {
+ int key; // Special key code or ascii value
+ const char *name; // Name of key
+} key_names_table[] = {
+ { ' ', "Space" },
+ { TAB, "Tab" },
+ { K_TAB, "Tab" },
+ { NL, "NL" },
+ { NL, "NewLine" }, // Alternative name
+ { NL, "LineFeed" }, // Alternative name
+ { NL, "LF" }, // Alternative name
+ { CAR, "CR" },
+ { CAR, "Return" }, // Alternative name
+ { CAR, "Enter" }, // Alternative name
+ { K_BS, "BS" },
+ { K_BS, "BackSpace" }, // Alternative name
+ { ESC, "Esc" },
+ { CSI, "CSI" },
+ { K_CSI, "xCSI" },
+ { '|', "Bar" },
+ { '\\', "Bslash" },
+ { K_DEL, "Del" },
+ { K_DEL, "Delete" }, // Alternative name
+ { K_KDEL, "kDel" },
+ { K_UP, "Up" },
+ { K_DOWN, "Down" },
+ { K_LEFT, "Left" },
+ { K_RIGHT, "Right" },
+ { K_XUP, "xUp" },
+ { K_XDOWN, "xDown" },
+ { K_XLEFT, "xLeft" },
+ { K_XRIGHT, "xRight" },
+
+ { K_F1, "F1" },
+ { K_F2, "F2" },
+ { K_F3, "F3" },
+ { K_F4, "F4" },
+ { K_F5, "F5" },
+ { K_F6, "F6" },
+ { K_F7, "F7" },
+ { K_F8, "F8" },
+ { K_F9, "F9" },
+ { K_F10, "F10" },
+
+ { K_F11, "F11" },
+ { K_F12, "F12" },
+ { K_F13, "F13" },
+ { K_F14, "F14" },
+ { K_F15, "F15" },
+ { K_F16, "F16" },
+ { K_F17, "F17" },
+ { K_F18, "F18" },
+ { K_F19, "F19" },
+ { K_F20, "F20" },
+
+ { K_F21, "F21" },
+ { K_F22, "F22" },
+ { K_F23, "F23" },
+ { K_F24, "F24" },
+ { K_F25, "F25" },
+ { K_F26, "F26" },
+ { K_F27, "F27" },
+ { K_F28, "F28" },
+ { K_F29, "F29" },
+ { K_F30, "F30" },
+
+ { K_F31, "F31" },
+ { K_F32, "F32" },
+ { K_F33, "F33" },
+ { K_F34, "F34" },
+ { K_F35, "F35" },
+ { K_F36, "F36" },
+ { K_F37, "F37" },
+
+ { K_XF1, "xF1" },
+ { K_XF2, "xF2" },
+ { K_XF3, "xF3" },
+ { K_XF4, "xF4" },
+
+ { K_HELP, "Help" },
+ { K_UNDO, "Undo" },
+ { K_INS, "Insert" },
+ { K_INS, "Ins" }, // Alternative name
+ { K_KINS, "kInsert" },
+ { K_HOME, "Home" },
+ { K_KHOME, "kHome" },
+ { K_XHOME, "xHome" },
+ { K_ZHOME, "zHome" },
+ { K_END, "End" },
+ { K_KEND, "kEnd" },
+ { K_XEND, "xEnd" },
+ { K_ZEND, "zEnd" },
+ { K_PAGEUP, "PageUp" },
+ { K_PAGEDOWN, "PageDown" },
+ { K_KPAGEUP, "kPageUp" },
+ { K_KPAGEDOWN, "kPageDown" },
+
+ { K_KPLUS, "kPlus" },
+ { K_KMINUS, "kMinus" },
+ { K_KDIVIDE, "kDivide" },
+ { K_KMULTIPLY, "kMultiply" },
+ { K_KENTER, "kEnter" },
+ { K_KPOINT, "kPoint" },
+
+ { K_K0, "k0" },
+ { K_K1, "k1" },
+ { K_K2, "k2" },
+ { K_K3, "k3" },
+ { K_K4, "k4" },
+ { K_K5, "k5" },
+ { K_K6, "k6" },
+ { K_K7, "k7" },
+ { K_K8, "k8" },
+ { K_K9, "k9" },
+
+ { '<', "lt" },
+
+ { K_MOUSE, "Mouse" },
+ { K_LEFTMOUSE, "LeftMouse" },
+ { K_LEFTMOUSE_NM, "LeftMouseNM" },
+ { K_LEFTDRAG, "LeftDrag" },
+ { K_LEFTRELEASE, "LeftRelease" },
+ { K_LEFTRELEASE_NM, "LeftReleaseNM" },
+ { K_MIDDLEMOUSE, "MiddleMouse" },
+ { K_MIDDLEDRAG, "MiddleDrag" },
+ { K_MIDDLERELEASE, "MiddleRelease" },
+ { K_RIGHTMOUSE, "RightMouse" },
+ { K_RIGHTDRAG, "RightDrag" },
+ { K_RIGHTRELEASE, "RightRelease" },
+ { K_MOUSEDOWN, "ScrollWheelUp" },
+ { K_MOUSEUP, "ScrollWheelDown" },
+ { K_MOUSELEFT, "ScrollWheelRight" },
+ { K_MOUSERIGHT, "ScrollWheelLeft" },
+ { K_MOUSEDOWN, "MouseDown" }, // OBSOLETE: Use
+ { K_MOUSEUP, "MouseUp" }, // ScrollWheelXXX instead
+ { K_X1MOUSE, "X1Mouse" },
+ { K_X1DRAG, "X1Drag" },
+ { K_X1RELEASE, "X1Release" },
+ { K_X2MOUSE, "X2Mouse" },
+ { K_X2DRAG, "X2Drag" },
+ { K_X2RELEASE, "X2Release" },
+ { K_DROP, "Drop" },
+ { K_ZERO, "Nul" },
+ { K_SNR, "SNR" },
+ { K_PLUG, "Plug" },
+ { K_PASTE, "Paste" },
+ { 0, NULL }
+};
+
+int get_special_key_code(const char_u *name)
+{
+ for (int i = 0; key_names_table[i].name != NULL; i++) {
+ const char *const table_name = key_names_table[i].name;
+ int j;
+ for (j = 0; vim_isIDc(name[j]) && table_name[j] != NUL; j++) {
+ if (TOLOWER_ASC(table_name[j]) != TOLOWER_ASC(name[j])) {
+ break;
+ }
+ }
+ if (!vim_isIDc(name[j]) && table_name[j] == NUL) {
+ return key_names_table[i].key;
+ }
+ }
+
+ return 0;
+}
+
+
+static const struct modmasktable {
+ short mod_mask; ///< Bit-mask for particular key modifier.
+ short mod_flag; ///< Bit(s) for particular key modifier.
+ char_u name; ///< Single letter name of modifier.
+} mod_mask_table[] = {
+ {MOD_MASK_ALT, MOD_MASK_ALT, (char_u)'M'},
+ {MOD_MASK_META, MOD_MASK_META, (char_u)'T'},
+ {MOD_MASK_CTRL, MOD_MASK_CTRL, (char_u)'C'},
+ {MOD_MASK_SHIFT, MOD_MASK_SHIFT, (char_u)'S'},
+ {MOD_MASK_MULTI_CLICK, MOD_MASK_2CLICK, (char_u)'2'},
+ {MOD_MASK_MULTI_CLICK, MOD_MASK_3CLICK, (char_u)'3'},
+ {MOD_MASK_MULTI_CLICK, MOD_MASK_4CLICK, (char_u)'4'},
+ {MOD_MASK_CMD, MOD_MASK_CMD, (char_u)'D'},
+ // 'A' must be the last one
+ {MOD_MASK_ALT, MOD_MASK_ALT, (char_u)'A'},
+ {0, 0, NUL}
+};
+
+int name_to_mod_mask(int c)
+{
+ c = TOUPPER_ASC(c);
+ for (size_t i = 0; mod_mask_table[i].mod_mask != 0; i++) {
+ if (c == mod_mask_table[i].name) {
+ return mod_mask_table[i].mod_flag;
+ }
+ }
+ return 0;
+}
+
+static int extract_modifiers(int key, int *modp)
+{
+ int modifiers = *modp;
+
+ if (!(modifiers & MOD_MASK_CMD)) { // Command-key is special
+ if ((modifiers & MOD_MASK_SHIFT) && ASCII_ISALPHA(key)) {
+ key = TOUPPER_ASC(key);
+ modifiers &= ~MOD_MASK_SHIFT;
+ }
+ }
+ if ((modifiers & MOD_MASK_CTRL)
+ && ((key >= '?' && key <= '_') || ASCII_ISALPHA(key))) {
+ key = Ctrl_chr(key);
+ modifiers &= ~MOD_MASK_CTRL;
+ if (key == 0) { // <C-@> is <Nul>
+ key = K_ZERO;
+ }
+ }
+
+ *modp = modifiers;
+ return key;
+}
+
+int find_special_key(const char_u **srcp, const size_t src_len, int *const modp,
+ const bool keycode, const bool keep_x_key,
+ const bool in_string)
+{
+ const char_u *last_dash;
+ const char_u *end_of_name;
+ const char_u *src;
+ const char_u *bp;
+ const char_u *const end = *srcp + src_len - 1;
+ int modifiers;
+ int bit;
+ int key;
+ uvarnumber_T n;
+ int l;
+
+ if (src_len == 0) {
+ return 0;
+ }
+
+ src = *srcp;
+ if (src[0] != '<') {
+ return 0;
+ }
+
+ // Find end of modifier list
+ last_dash = src;
+ for (bp = src + 1; bp <= end && (*bp == '-' || vim_isIDc(*bp)); bp++) {
+ if (*bp == '-') {
+ last_dash = bp;
+ if (bp + 1 <= end) {
+ l = utfc_ptr2len_len(bp + 1, (int)(end - bp) + 1);
+ // Anything accepted, like <C-?>.
+ // <C-"> or <M-"> are not special in strings as " is
+ // the string delimiter. With a backslash it works: <M-\">
+ if (end - bp > l && !(in_string && bp[1] == '"') && bp[2] == '>') {
+ bp += l;
+ } else if (end - bp > 2 && in_string && bp[1] == '\\'
+ && bp[2] == '"' && bp[3] == '>') {
+ bp += 2;
+ }
+ }
+ }
+ if (end - bp > 3 && bp[0] == 't' && bp[1] == '_') {
+ bp += 3; // skip t_xx, xx may be '-' or '>'
+ } else if (end - bp > 4 && STRNICMP(bp, "char-", 5) == 0) {
+ vim_str2nr(bp + 5, NULL, &l, STR2NR_ALL, NULL, NULL, 0);
+ bp += l + 5;
+ break;
+ }
+ }
+
+ if (bp <= end && *bp == '>') { // found matching '>'
+ end_of_name = bp + 1;
+
+ /* Which modifiers are given? */
+ modifiers = 0x0;
+ for (bp = src + 1; bp < last_dash; bp++) {
+ if (*bp != '-') {
+ bit = name_to_mod_mask(*bp);
+ if (bit == 0x0) {
+ break; // Illegal modifier name
+ }
+ modifiers |= bit;
+ }
+ }
+
+ // Legal modifier name.
+ if (bp >= last_dash) {
+ if (STRNICMP(last_dash + 1, "char-", 5) == 0
+ && ascii_isdigit(last_dash[6])) {
+ // <Char-123> or <Char-033> or <Char-0x33>
+ vim_str2nr(last_dash + 6, NULL, NULL, STR2NR_ALL, NULL, &n, 0);
+ key = (int)n;
+ } else {
+ int off = 1;
+
+ // Modifier with single letter, or special key name.
+ if (in_string && last_dash[1] == '\\' && last_dash[2] == '"') {
+ off = 2;
+ }
+ l = mb_ptr2len(last_dash + 1);
+ if (modifiers != 0 && last_dash[l + 1] == '>') {
+ key = PTR2CHAR(last_dash + off);
+ } else {
+ key = get_special_key_code(last_dash + off);
+ if (!keep_x_key) {
+ key = handle_x_keys(key);
+ }
+ }
+ }
+
+ // get_special_key_code() may return NUL for invalid
+ // special key name.
+ if (key != NUL) {
+ // Only use a modifier when there is no special key code that
+ // includes the modifier.
+ key = simplify_key(key, &modifiers);
+
+ if (!keycode) {
+ // don't want keycode, use single byte code
+ if (key == K_BS) {
+ key = BS;
+ } else if (key == K_DEL || key == K_KDEL) {
+ key = DEL;
+ }
+ }
+
+ // Normal Key with modifier:
+ // Try to make a single byte code (except for Alt/Meta modifiers).
+ if (!IS_SPECIAL(key)) {
+ key = extract_modifiers(key, &modifiers);
+ }
+
+ *modp = modifiers;
+ *srcp = end_of_name;
+ return key;
+ }
+ }
+ }
+ return 0;
+}
+
+char_u *add_char2buf(int c, char_u *s)
+{
+ char_u temp[MB_MAXBYTES + 1];
+ const int len = utf_char2bytes(c, temp);
+ for (int i = 0; i < len; ++i) {
+ c = temp[i];
+ // Need to escape K_SPECIAL and CSI like in the typeahead buffer.
+ if (c == K_SPECIAL) {
+ *s++ = K_SPECIAL;
+ *s++ = KS_SPECIAL;
+ *s++ = KE_FILLER;
+ } else {
+ *s++ = c;
+ }
+ }
+ return s;
+}
+
+unsigned int trans_special(const char_u **srcp, const size_t src_len,
+ char_u *const dst, const bool keycode,
+ const bool in_string)
+{
+ int modifiers = 0;
+ int key;
+ unsigned int dlen = 0;
+
+ key = find_special_key(srcp, src_len, &modifiers, keycode, false, in_string);
+ if (key == 0) {
+ return 0;
+ }
+
+ // Put the appropriate modifier in a string.
+ if (modifiers != 0) {
+ dst[dlen++] = K_SPECIAL;
+ dst[dlen++] = KS_MODIFIER;
+ dst[dlen++] = (char_u)modifiers;
+ }
+
+ if (IS_SPECIAL(key)) {
+ dst[dlen++] = K_SPECIAL;
+ dst[dlen++] = (char_u)KEY2TERMCAP0(key);
+ dst[dlen++] = KEY2TERMCAP1(key);
+ } else if (has_mbyte && !keycode) {
+ dlen += (unsigned int)(*mb_char2bytes)(key, dst + dlen);
+ } else if (keycode) {
+ char_u *after = add_char2buf(key, dst + dlen);
+ assert(after >= dst && (uintmax_t)(after - dst) <= UINT_MAX);
+ dlen = (unsigned int)(after - dst);
+ } else {
+ dst[dlen++] = (char_u)key;
+ }
+
+ return dlen;
+}
diff --git a/test/symbolic/klee/nvim/mbyte.c b/test/symbolic/klee/nvim/mbyte.c
new file mode 100644
index 0000000000..f98a531206
--- /dev/null
+++ b/test/symbolic/klee/nvim/mbyte.c
@@ -0,0 +1,266 @@
+#include <stddef.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <stdbool.h>
+
+#include "nvim/types.h"
+#include "nvim/mbyte.h"
+#include "nvim/ascii.h"
+
+const uint8_t utf8len_tab_zero[] = {
+ //1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 2
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 4
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 6
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 8
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // A
+ 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // C
+ 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,6,6,0,0, // E
+};
+
+const uint8_t utf8len_tab[] = {
+ // ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9 ?A ?B ?C ?D ?E ?F
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A?
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B?
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C?
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D?
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E?
+ 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1, // F?
+};
+
+int utf_ptr2char(const char_u *const p)
+{
+ if (p[0] < 0x80) { // Be quick for ASCII.
+ return p[0];
+ }
+
+ const uint8_t len = utf8len_tab_zero[p[0]];
+ if (len > 1 && (p[1] & 0xc0) == 0x80) {
+ if (len == 2) {
+ return ((p[0] & 0x1f) << 6) + (p[1] & 0x3f);
+ }
+ if ((p[2] & 0xc0) == 0x80) {
+ if (len == 3) {
+ return (((p[0] & 0x0f) << 12) + ((p[1] & 0x3f) << 6)
+ + (p[2] & 0x3f));
+ }
+ if ((p[3] & 0xc0) == 0x80) {
+ if (len == 4) {
+ return (((p[0] & 0x07) << 18) + ((p[1] & 0x3f) << 12)
+ + ((p[2] & 0x3f) << 6) + (p[3] & 0x3f));
+ }
+ if ((p[4] & 0xc0) == 0x80) {
+ if (len == 5) {
+ return (((p[0] & 0x03) << 24) + ((p[1] & 0x3f) << 18)
+ + ((p[2] & 0x3f) << 12) + ((p[3] & 0x3f) << 6)
+ + (p[4] & 0x3f));
+ }
+ if ((p[5] & 0xc0) == 0x80 && len == 6) {
+ return (((p[0] & 0x01) << 30) + ((p[1] & 0x3f) << 24)
+ + ((p[2] & 0x3f) << 18) + ((p[3] & 0x3f) << 12)
+ + ((p[4] & 0x3f) << 6) + (p[5] & 0x3f));
+ }
+ }
+ }
+ }
+ }
+ // Illegal value: just return the first byte.
+ return p[0];
+}
+
+bool utf_composinglike(const char_u *p1, const char_u *p2)
+{
+ return false;
+}
+
+char_u *string_convert(const vimconv_T *conv, char_u *data, size_t *size)
+{
+ return NULL;
+}
+
+int utf_ptr2len_len(const char_u *p, int size)
+{
+ int len;
+ int i;
+ int m;
+
+ len = utf8len_tab[*p];
+ if (len == 1)
+ return 1; /* NUL, ascii or illegal lead byte */
+ if (len > size)
+ m = size; /* incomplete byte sequence. */
+ else
+ m = len;
+ for (i = 1; i < m; ++i)
+ if ((p[i] & 0xc0) != 0x80)
+ return 1;
+ return len;
+}
+
+int utfc_ptr2len_len(const char_u *p, int size)
+{
+ int len;
+ int prevlen;
+
+ if (size < 1 || *p == NUL)
+ return 0;
+ if (p[0] < 0x80 && (size == 1 || p[1] < 0x80)) /* be quick for ASCII */
+ return 1;
+
+ /* Skip over first UTF-8 char, stopping at a NUL byte. */
+ len = utf_ptr2len_len(p, size);
+
+ /* Check for illegal byte and incomplete byte sequence. */
+ if ((len == 1 && p[0] >= 0x80) || len > size)
+ return 1;
+
+ /*
+ * Check for composing characters. We can handle only the first six, but
+ * skip all of them (otherwise the cursor would get stuck).
+ */
+ prevlen = 0;
+ while (len < size) {
+ int len_next_char;
+
+ if (p[len] < 0x80)
+ break;
+
+ /*
+ * Next character length should not go beyond size to ensure that
+ * UTF_COMPOSINGLIKE(...) does not read beyond size.
+ */
+ len_next_char = utf_ptr2len_len(p + len, size - len);
+ if (len_next_char > size - len)
+ break;
+
+ if (!UTF_COMPOSINGLIKE(p + prevlen, p + len))
+ break;
+
+ /* Skip over composing char */
+ prevlen = len;
+ len += len_next_char;
+ }
+ return len;
+}
+
+int utf_char2len(const int c)
+{
+ if (c < 0x80) {
+ return 1;
+ } else if (c < 0x800) {
+ return 2;
+ } else if (c < 0x10000) {
+ return 3;
+ } else if (c < 0x200000) {
+ return 4;
+ } else if (c < 0x4000000) {
+ return 5;
+ } else {
+ return 6;
+ }
+}
+
+int utf_char2bytes(const int c, char_u *const buf)
+{
+ if (c < 0x80) { // 7 bits
+ buf[0] = c;
+ return 1;
+ } else if (c < 0x800) { // 11 bits
+ buf[0] = 0xc0 + ((unsigned)c >> 6);
+ buf[1] = 0x80 + (c & 0x3f);
+ return 2;
+ } else if (c < 0x10000) { // 16 bits
+ buf[0] = 0xe0 + ((unsigned)c >> 12);
+ buf[1] = 0x80 + (((unsigned)c >> 6) & 0x3f);
+ buf[2] = 0x80 + (c & 0x3f);
+ return 3;
+ } else if (c < 0x200000) { // 21 bits
+ buf[0] = 0xf0 + ((unsigned)c >> 18);
+ buf[1] = 0x80 + (((unsigned)c >> 12) & 0x3f);
+ buf[2] = 0x80 + (((unsigned)c >> 6) & 0x3f);
+ buf[3] = 0x80 + (c & 0x3f);
+ return 4;
+ } else if (c < 0x4000000) { // 26 bits
+ buf[0] = 0xf8 + ((unsigned)c >> 24);
+ buf[1] = 0x80 + (((unsigned)c >> 18) & 0x3f);
+ buf[2] = 0x80 + (((unsigned)c >> 12) & 0x3f);
+ buf[3] = 0x80 + (((unsigned)c >> 6) & 0x3f);
+ buf[4] = 0x80 + (c & 0x3f);
+ return 5;
+ } else { // 31 bits
+ buf[0] = 0xfc + ((unsigned)c >> 30);
+ buf[1] = 0x80 + (((unsigned)c >> 24) & 0x3f);
+ buf[2] = 0x80 + (((unsigned)c >> 18) & 0x3f);
+ buf[3] = 0x80 + (((unsigned)c >> 12) & 0x3f);
+ buf[4] = 0x80 + (((unsigned)c >> 6) & 0x3f);
+ buf[5] = 0x80 + (c & 0x3f);
+ return 6;
+ }
+}
+
+int utf_ptr2len(const char_u *const p)
+{
+ if (*p == NUL) {
+ return 0;
+ }
+ const int len = utf8len_tab[*p];
+ for (int i = 1; i < len; i++) {
+ if ((p[i] & 0xc0) != 0x80) {
+ return 1;
+ }
+ }
+ return len;
+}
+
+int utfc_ptr2len(const char_u *const p)
+{
+ uint8_t b0 = (uint8_t)(*p);
+
+ if (b0 == NUL) {
+ return 0;
+ }
+ if (b0 < 0x80 && p[1] < 0x80) { // be quick for ASCII
+ return 1;
+ }
+
+ // Skip over first UTF-8 char, stopping at a NUL byte.
+ int len = utf_ptr2len(p);
+
+ // Check for illegal byte.
+ if (len == 1 && b0 >= 0x80) {
+ return 1;
+ }
+
+ // Check for composing characters. We can handle only the first six, but
+ // skip all of them (otherwise the cursor would get stuck).
+ int prevlen = 0;
+ for (;;) {
+ if (p[len] < 0x80 || !UTF_COMPOSINGLIKE(p + prevlen, p + len)) {
+ return len;
+ }
+
+ // Skip over composing char.
+ prevlen = len;
+ len += utf_ptr2len(p + len);
+ }
+}
+
+void mb_copy_char(const char_u **fp, char_u **tp)
+{
+ const size_t l = utfc_ptr2len(*fp);
+
+ memmove(*tp, *fp, (size_t)l);
+ *tp += l;
+ *fp += l;
+}
diff --git a/test/symbolic/klee/nvim/memory.c b/test/symbolic/klee/nvim/memory.c
new file mode 100644
index 0000000000..df422cea3e
--- /dev/null
+++ b/test/symbolic/klee/nvim/memory.c
@@ -0,0 +1,101 @@
+#include <stdlib.h>
+#include <assert.h>
+
+#include "nvim/lib/ringbuf.h"
+
+enum { RB_SIZE = 1024 };
+
+typedef struct {
+ void *ptr;
+ size_t size;
+} AllocRecord;
+
+RINGBUF_TYPEDEF(AllocRecords, AllocRecord)
+RINGBUF_INIT(AllocRecords, arecs, AllocRecord, RINGBUF_DUMMY_FREE)
+RINGBUF_STATIC(static, AllocRecords, AllocRecord, arecs, RB_SIZE)
+
+size_t allocated_memory = 0;
+size_t ever_allocated_memory = 0;
+
+size_t allocated_memory_limit = SIZE_MAX;
+
+void *xmalloc(const size_t size)
+{
+ void *ret = malloc(size);
+ allocated_memory += size;
+ ever_allocated_memory += size;
+ assert(allocated_memory <= allocated_memory_limit);
+ assert(arecs_rb_length(&arecs) < RB_SIZE);
+ arecs_rb_push(&arecs, (AllocRecord) {
+ .ptr = ret,
+ .size = size,
+ });
+ return ret;
+}
+
+void xfree(void *const p)
+{
+ if (p == NULL) {
+ return;
+ }
+ RINGBUF_FORALL(&arecs, AllocRecord, arec) {
+ if (arec->ptr == p) {
+ allocated_memory -= arec->size;
+ arecs_rb_remove(&arecs, arecs_rb_find_idx(&arecs, arec));
+ return;
+ }
+ }
+ assert(false);
+}
+
+void *xrealloc(void *const p, size_t new_size)
+{
+ void *ret = realloc(p, new_size);
+ RINGBUF_FORALL(&arecs, AllocRecord, arec) {
+ if (arec->ptr == p) {
+ allocated_memory -= arec->size;
+ allocated_memory += new_size;
+ if (new_size > arec->size) {
+ ever_allocated_memory += (new_size - arec->size);
+ }
+ arec->ptr = ret;
+ arec->size = new_size;
+ return ret;
+ }
+ }
+ assert(false);
+ return (void *)(intptr_t)1;
+}
+
+char *xstrdup(const char *str)
+ FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET
+ FUNC_ATTR_NONNULL_ALL
+{
+ return xmemdupz(str, strlen(str));
+}
+
+void *xmallocz(size_t size)
+ FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ size_t total_size = size + 1;
+ assert(total_size > size);
+
+ void *ret = xmalloc(total_size);
+ ((char *)ret)[size] = 0;
+
+ return ret;
+}
+
+char *xstpcpy(char *restrict dst, const char *restrict src)
+ FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
+{
+ const size_t len = strlen(src);
+ return (char *)memcpy(dst, src, len + 1) + len;
+}
+
+void *xmemdupz(const void *data, size_t len)
+ FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT
+ FUNC_ATTR_NONNULL_ALL
+{
+ return memcpy(xmallocz(len), data, len);
+}
diff --git a/test/symbolic/klee/run.sh b/test/symbolic/klee/run.sh
new file mode 100755
index 0000000000..0234a935b5
--- /dev/null
+++ b/test/symbolic/klee/run.sh
@@ -0,0 +1,102 @@
+#!/bin/sh
+
+set -e
+set -x
+test -z "$POSH_VERSION" && set -u
+
+PROJECT_SOURCE_DIR=.
+PROJECT_BINARY_DIR="$PROJECT_SOURCE_DIR/build"
+KLEE_TEST_DIR="$PROJECT_SOURCE_DIR/test/symbolic/klee"
+KLEE_BIN_DIR="$PROJECT_BINARY_DIR/klee"
+KLEE_OUT_DIR="$KLEE_BIN_DIR/out"
+
+help() {
+ echo "Usage:"
+ echo
+ echo " $0 -c fname"
+ echo " $0 fname"
+ echo " $0 -s"
+ echo
+ echo "First form compiles executable out of test/symbolic/klee/{fname}.c."
+ echo "Compiled executable is placed into build/klee/{fname}. Must first"
+ echo "successfully compile Neovim in order to generate declarations."
+ echo
+ echo "Second form runs KLEE in a docker container using file "
+ echo "test/symbolic/klee/{fname.c}. Bitcode is placed into build/klee/a.bc,"
+ echo "results are placed into build/klee/out/. The latter is first deleted if"
+ echo "it exists."
+ echo
+ echo "Third form runs ktest-tool which prints errors found by KLEE via "
+ echo "the same container used to run KLEE."
+}
+
+main() {
+ local compile=
+ local print_errs=
+ if test "$1" = "--help" ; then
+ help
+ return
+ fi
+ if test "$1" = "-s" ; then
+ print_errs=1
+ shift
+ elif test "$1" = "-c" ; then
+ compile=1
+ shift
+ fi
+ if test -z "$print_errs" ; then
+ local test="$1" ; shift
+ fi
+
+ local includes=
+ includes="$includes -I$KLEE_TEST_DIR"
+ includes="$includes -I/home/klee/klee_src/include"
+ includes="$includes -I$PROJECT_SOURCE_DIR/src"
+ includes="$includes -I$PROJECT_BINARY_DIR/src/nvim/auto"
+ includes="$includes -I$PROJECT_BINARY_DIR/include"
+ includes="$includes -I$PROJECT_BINARY_DIR/config"
+ includes="$includes -I/host-includes"
+
+ local defines=
+ defines="$defines -DMIN_LOG_LEVEL=9999"
+ defines="$defines -DINCLUDE_GENERATED_DECLARATIONS"
+
+ test -z "$compile" && defines="$defines -DUSE_KLEE"
+
+ test -d "$KLEE_BIN_DIR" || mkdir -p "$KLEE_BIN_DIR"
+
+ if test -z "$compile" ; then
+ local line1='cd /image'
+ if test -z "$print_errs" ; then
+ test -d "$KLEE_OUT_DIR" && rm -r "$KLEE_OUT_DIR"
+
+ line1="$line1 && $(echo clang \
+ $includes $defines \
+ -o "$KLEE_BIN_DIR/a.bc" -emit-llvm -g -c \
+ "$KLEE_TEST_DIR/$test.c")"
+ line1="$line1 && klee --libc=uclibc --posix-runtime "
+ line1="$line1 '--output-dir=$KLEE_OUT_DIR' '$KLEE_BIN_DIR/a.bc'"
+ fi
+ local line2="for t in '$KLEE_OUT_DIR'/*.err"
+ line2="$line2 ; do ktest-tool --write-ints"
+ line2="$line2 \"\$(printf '%s' \"\$t\" | sed -e 's@\\.[^/]*\$@.ktest@')\""
+ line2="$line2 ; done"
+ printf '%s\n%s\n' "$line1" "$line2" | \
+ docker run \
+ --volume "$(cd "$PROJECT_SOURCE_DIR" && pwd)":/image \
+ --volume "/usr/include":/host-includes \
+ --interactive \
+ --rm \
+ --ulimit='stack=-1:-1' \
+ klee/klee \
+ /bin/sh -x
+ else
+ clang \
+ $includes $defines \
+ -o "$KLEE_BIN_DIR/$test" \
+ -O0 -g \
+ "$KLEE_TEST_DIR/$test.c"
+ fi
+}
+
+main "$@"
diff --git a/test/symbolic/klee/viml_expressions_lexer.c b/test/symbolic/klee/viml_expressions_lexer.c
new file mode 100644
index 0000000000..ee7dc312e9
--- /dev/null
+++ b/test/symbolic/klee/viml_expressions_lexer.c
@@ -0,0 +1,105 @@
+#ifdef USE_KLEE
+# include <klee/klee.h>
+#else
+# include <string.h>
+# include <stdio.h>
+#endif
+#include <stddef.h>
+#include <stdint.h>
+#include <assert.h>
+
+#include "nvim/viml/parser/expressions.h"
+#include "nvim/viml/parser/parser.h"
+#include "nvim/mbyte.h"
+
+#include "nvim/memory.c"
+#include "nvim/mbyte.c"
+#include "nvim/charset.c"
+#include "nvim/garray.c"
+#include "nvim/gettext.c"
+#include "nvim/keymap.c"
+#include "nvim/viml/parser/expressions.c"
+
+#define INPUT_SIZE 7
+
+uint8_t avoid_optimizing_out;
+
+void simple_get_line(void *cookie, ParserLine *ret_pline)
+{
+ ParserLine **plines_p = (ParserLine **)cookie;
+ *ret_pline = **plines_p;
+ (*plines_p)++;
+}
+
+int main(const int argc, const char *const *const argv,
+ const char *const *const environ)
+{
+ char input[INPUT_SIZE];
+ uint8_t shift;
+ int flags;
+ avoid_optimizing_out = argc;
+
+#ifndef USE_KLEE
+ sscanf(argv[2], "%d", &flags);
+#endif
+
+#ifdef USE_KLEE
+ klee_make_symbolic(input, sizeof(input), "input");
+ klee_make_symbolic(&shift, sizeof(shift), "shift");
+ klee_make_symbolic(&flags, sizeof(flags), "flags");
+ klee_assume(shift < INPUT_SIZE);
+ klee_assume(flags <= (kELFlagPeek|kELFlagAllowFloat|kELFlagForbidEOC
+ |kELFlagForbidScope|kELFlagIsNotCmp));
+#endif
+
+ ParserLine plines[] = {
+ {
+#ifdef USE_KLEE
+ .data = &input[shift],
+ .size = sizeof(input) - shift,
+#else
+ .data = (const char *)argv[1],
+ .size = strlen(argv[1]),
+#endif
+ .allocated = false,
+ },
+ {
+ .data = NULL,
+ .size = 0,
+ .allocated = false,
+ },
+ };
+#ifdef USE_KLEE
+ assert(plines[0].size <= INPUT_SIZE);
+ assert((plines[0].data[0] != 5) | (plines[0].data[0] != argc));
+#endif
+ ParserLine *cur_pline = &plines[0];
+
+ ParserState pstate = {
+ .reader = {
+ .get_line = simple_get_line,
+ .cookie = &cur_pline,
+ .lines = KV_INITIAL_VALUE,
+ .conv.vc_type = CONV_NONE,
+ },
+ .pos = { 0, 0 },
+ .colors = NULL,
+ .can_continuate = false,
+ };
+ kvi_init(pstate.reader.lines);
+
+ allocated_memory_limit = 0;
+ LexExprToken token = viml_pexpr_next_token(&pstate, flags);
+ if (flags & kELFlagPeek) {
+ assert(pstate.pos.line == 0 && pstate.pos.col == 0);
+ } else {
+ assert((pstate.pos.line == 0)
+ ? (pstate.pos.col > 0)
+ : (pstate.pos.line == 1 && pstate.pos.col == 0));
+ }
+ assert(allocated_memory == 0);
+ assert(ever_allocated_memory == 0);
+#ifndef USE_KLEE
+ fprintf(stderr, "tkn: %s\n", viml_pexpr_repr_token(&pstate, token, NULL));
+#endif
+}
diff --git a/test/symbolic/klee/viml_expressions_parser.c b/test/symbolic/klee/viml_expressions_parser.c
new file mode 100644
index 0000000000..9a876ed3fa
--- /dev/null
+++ b/test/symbolic/klee/viml_expressions_parser.c
@@ -0,0 +1,117 @@
+#ifdef USE_KLEE
+# include <klee/klee.h>
+#else
+# include <string.h>
+#endif
+#include <stddef.h>
+#include <stdint.h>
+#include <assert.h>
+
+#include "nvim/viml/parser/expressions.h"
+#include "nvim/viml/parser/parser.h"
+#include "nvim/mbyte.h"
+
+#include "nvim/memory.c"
+#include "nvim/mbyte.c"
+#include "nvim/charset.c"
+#include "nvim/garray.c"
+#include "nvim/gettext.c"
+#include "nvim/viml/parser/expressions.c"
+#include "nvim/keymap.c"
+
+#define INPUT_SIZE 50
+
+uint8_t avoid_optimizing_out;
+
+void simple_get_line(void *cookie, ParserLine *ret_pline)
+{
+ ParserLine **plines_p = (ParserLine **)cookie;
+ *ret_pline = **plines_p;
+ (*plines_p)++;
+}
+
+int main(const int argc, const char *const *const argv,
+ const char *const *const environ)
+{
+ char input[INPUT_SIZE];
+ uint8_t shift;
+ unsigned flags;
+ const bool peek = false;
+ avoid_optimizing_out = argc;
+
+#ifndef USE_KLEE
+ sscanf(argv[2], "%d", &flags);
+#endif
+
+#ifdef USE_KLEE
+ klee_make_symbolic(input, sizeof(input), "input");
+ klee_make_symbolic(&shift, sizeof(shift), "shift");
+ klee_make_symbolic(&flags, sizeof(flags), "flags");
+ klee_assume(shift < INPUT_SIZE);
+ klee_assume(
+ flags <= (kExprFlagsMulti|kExprFlagsDisallowEOC|kExprFlagsParseLet));
+#endif
+
+ ParserLine plines[] = {
+ {
+#ifdef USE_KLEE
+ .data = &input[shift],
+ .size = sizeof(input) - shift,
+#else
+ .data = argv[1],
+ .size = strlen(argv[1]),
+#endif
+ .allocated = false,
+ },
+ {
+ .data = NULL,
+ .size = 0,
+ .allocated = false,
+ },
+ };
+#ifdef USE_KLEE
+ assert(plines[0].size <= INPUT_SIZE);
+ assert((plines[0].data[0] != 5) | (plines[0].data[0] != argc));
+#endif
+ ParserLine *cur_pline = &plines[0];
+
+ ParserHighlight colors;
+ kvi_init(colors);
+
+ ParserState pstate = {
+ .reader = {
+ .get_line = simple_get_line,
+ .cookie = &cur_pline,
+ .lines = KV_INITIAL_VALUE,
+ .conv.vc_type = CONV_NONE,
+ },
+ .pos = { 0, 0 },
+ .colors = &colors,
+ .can_continuate = false,
+ };
+ kvi_init(pstate.reader.lines);
+
+ const ExprAST ast = viml_pexpr_parse(&pstate, (int)flags);
+ 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);
+ }
+ // Can’t possibly have more highlight tokens then there are bytes in string.
+ assert(kv_size(colors) <= INPUT_SIZE - shift);
+ kvi_destroy(colors);
+ // Not destroying pstate.reader.lines because there is no way it could exceed
+ // its limits in the current circumstances.
+ viml_pexpr_free_ast(ast);
+ assert(allocated_memory == 0);
+}
diff --git a/test/unit/charset/vim_str2nr_spec.lua b/test/unit/charset/vim_str2nr_spec.lua
new file mode 100644
index 0000000000..394934c339
--- /dev/null
+++ b/test/unit/charset/vim_str2nr_spec.lua
@@ -0,0 +1,303 @@
+local helpers = require("test.unit.helpers")(after_each)
+local global_helpers = require('test.helpers')
+
+local itp = helpers.gen_itp(it)
+
+local child_call_once = helpers.child_call_once
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+
+local shallowcopy = global_helpers.shallowcopy
+local updated = global_helpers.updated
+
+local lib = cimport('./src/nvim/charset.h')
+
+local ARGTYPES
+
+child_call_once(function()
+ ARGTYPES = {
+ num = ffi.typeof('varnumber_T[1]'),
+ unum = ffi.typeof('uvarnumber_T[1]'),
+ pre = ffi.typeof('int[1]'),
+ len = ffi.typeof('int[1]'),
+ }
+end)
+
+local icnt = -42
+local ucnt = 4242
+
+local function arginit(arg)
+ if arg == 'unum' then
+ ucnt = ucnt + 1
+ return ARGTYPES[arg]({ucnt})
+ else
+ icnt = icnt - 1
+ return ARGTYPES[arg]({icnt})
+ end
+end
+
+local function test_vim_str2nr(s, what, exp, maxlen)
+ local comb = {[''] = {}}
+ for k, _ in pairs(exp) do
+ for ck, cv in pairs(comb) do
+ comb[ck .. ',' .. k] = updated(shallowcopy(cv), { [k] = arginit(k) })
+ end
+ end
+ maxlen = maxlen or #s
+ for _, cv in pairs(comb) do
+ lib.vim_str2nr(s, cv.pre, cv.len, what, cv.num, cv.unum, maxlen)
+ for cck, ccv in pairs(cv) do
+ if exp[cck] ~= tonumber(ccv[0]) then
+ error(('Failed check (%s = %d) in test (s=%s, w=%u, m=%d): %d'):format(
+ cck, exp[cck], s, tonumber(what), maxlen, tonumber(ccv[0])
+ ))
+ end
+ end
+ end
+end
+
+local _itp = itp
+itp = function(...)
+ collectgarbage('restart')
+ _itp(...)
+end
+
+describe('vim_str2nr()', function()
+ itp('works fine when it has nothing to do', function()
+ test_vim_str2nr('', 0, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_ALL, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_BIN, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_OCT, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_HEX, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_DEC, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_BIN, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_OCT, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_HEX, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ end)
+ itp('works with decimal numbers', function()
+ for _, flags in ipairs({
+ 0,
+ lib.STR2NR_BIN,
+ lib.STR2NR_OCT,
+ lib.STR2NR_HEX,
+ lib.STR2NR_BIN + lib.STR2NR_OCT,
+ lib.STR2NR_BIN + lib.STR2NR_HEX,
+ lib.STR2NR_OCT + lib.STR2NR_HEX,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_DEC,
+ }) do
+ -- Check that all digits are recognized
+ test_vim_str2nr( '12345', flags, {len = 5, num = 12345, unum = 12345, pre = 0}, 0)
+ test_vim_str2nr( '67890', flags, {len = 5, num = 67890, unum = 67890, pre = 0}, 0)
+ test_vim_str2nr( '12345A', flags, {len = 5, num = 12345, unum = 12345, pre = 0}, 0)
+ test_vim_str2nr( '67890A', flags, {len = 5, num = 67890, unum = 67890, pre = 0}, 0)
+
+ test_vim_str2nr( '42', flags, {len = 2, num = 42, unum = 42, pre = 0}, 0)
+ test_vim_str2nr( '42', flags, {len = 1, num = 4, unum = 4, pre = 0}, 1)
+ test_vim_str2nr( '42', flags, {len = 2, num = 42, unum = 42, pre = 0}, 2)
+ test_vim_str2nr( '42', flags, {len = 2, num = 42, unum = 42, pre = 0}, 3) -- includes NUL byte in maxlen
+
+ test_vim_str2nr( '42x', flags, {len = 2, num = 42, unum = 42, pre = 0}, 0)
+ test_vim_str2nr( '42x', flags, {len = 2, num = 42, unum = 42, pre = 0}, 3)
+
+ test_vim_str2nr('-42', flags, {len = 3, num = -42, unum = 42, pre = 0}, 3)
+ test_vim_str2nr('-42', flags, {len = 1, num = 0, unum = 0, pre = 0}, 1)
+
+ test_vim_str2nr('-42x', flags, {len = 3, num = -42, unum = 42, pre = 0}, 0)
+ test_vim_str2nr('-42x', flags, {len = 3, num = -42, unum = 42, pre = 0}, 4)
+ end
+ end)
+ itp('works with binary numbers', function()
+ for _, flags in ipairs({
+ lib.STR2NR_BIN,
+ lib.STR2NR_BIN + lib.STR2NR_OCT,
+ lib.STR2NR_BIN + lib.STR2NR_HEX,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_BIN,
+ }) do
+ local bin
+ local BIN
+ if flags > lib.STR2NR_FORCE then
+ bin = 0
+ BIN = 0
+ else
+ bin = ('b'):byte()
+ BIN = ('B'):byte()
+ end
+
+ test_vim_str2nr( '0b101', flags, {len = 5, num = 5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr( '0b101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0b101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0b101', flags, {len = 3, num = 1, unum = 1, pre = bin}, 3)
+ test_vim_str2nr( '0b101', flags, {len = 4, num = 2, unum = 2, pre = bin}, 4)
+ test_vim_str2nr( '0b101', flags, {len = 5, num = 5, unum = 5, pre = bin}, 5)
+ test_vim_str2nr( '0b101', flags, {len = 5, num = 5, unum = 5, pre = bin}, 6)
+
+ test_vim_str2nr( '0b1012', flags, {len = 5, num = 5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr( '0b1012', flags, {len = 5, num = 5, unum = 5, pre = bin}, 6)
+
+ test_vim_str2nr('-0b101', flags, {len = 6, num = -5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr('-0b101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0b101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0b101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0b101', flags, {len = 4, num = -1, unum = 1, pre = bin}, 4)
+ test_vim_str2nr('-0b101', flags, {len = 5, num = -2, unum = 2, pre = bin}, 5)
+ test_vim_str2nr('-0b101', flags, {len = 6, num = -5, unum = 5, pre = bin}, 6)
+ test_vim_str2nr('-0b101', flags, {len = 6, num = -5, unum = 5, pre = bin}, 7)
+
+ test_vim_str2nr('-0b1012', flags, {len = 6, num = -5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr('-0b1012', flags, {len = 6, num = -5, unum = 5, pre = bin}, 7)
+
+ test_vim_str2nr( '0B101', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr( '0B101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0B101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0B101', flags, {len = 3, num = 1, unum = 1, pre = BIN}, 3)
+ test_vim_str2nr( '0B101', flags, {len = 4, num = 2, unum = 2, pre = BIN}, 4)
+ test_vim_str2nr( '0B101', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 5)
+ test_vim_str2nr( '0B101', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 6)
+
+ test_vim_str2nr( '0B1012', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr( '0B1012', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 6)
+
+ test_vim_str2nr('-0B101', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr('-0B101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0B101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0B101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0B101', flags, {len = 4, num = -1, unum = 1, pre = BIN}, 4)
+ test_vim_str2nr('-0B101', flags, {len = 5, num = -2, unum = 2, pre = BIN}, 5)
+ test_vim_str2nr('-0B101', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 6)
+ test_vim_str2nr('-0B101', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 7)
+
+ test_vim_str2nr('-0B1012', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr('-0B1012', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 7)
+
+ if flags > lib.STR2NR_FORCE then
+ test_vim_str2nr('-101', flags, {len = 4, num = -5, unum = 5, pre = 0}, 0)
+ end
+ end
+ end)
+ itp('works with octal numbers', function()
+ for _, flags in ipairs({
+ lib.STR2NR_OCT,
+ lib.STR2NR_OCT + lib.STR2NR_BIN,
+ lib.STR2NR_OCT + lib.STR2NR_HEX,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_OCT,
+ }) do
+ local oct
+ if flags > lib.STR2NR_FORCE then
+ oct = 0
+ else
+ oct = ('0'):byte()
+ end
+
+ -- Check that all digits are recognized
+ test_vim_str2nr( '012345670', flags, {len = 9, num = 2739128, unum = 2739128, pre = oct}, 0)
+
+ test_vim_str2nr( '054', flags, {len = 3, num = 44, unum = 44, pre = oct}, 0)
+ test_vim_str2nr( '054', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '054', flags, {len = 2, num = 5, unum = 5, pre = oct}, 2)
+ test_vim_str2nr( '054', flags, {len = 3, num = 44, unum = 44, pre = oct}, 3)
+ test_vim_str2nr( '0548', flags, {len = 3, num = 44, unum = 44, pre = oct}, 3)
+ test_vim_str2nr( '054', flags, {len = 3, num = 44, unum = 44, pre = oct}, 4)
+
+ test_vim_str2nr( '054x', flags, {len = 3, num = 44, unum = 44, pre = oct}, 4)
+ test_vim_str2nr( '054x', flags, {len = 3, num = 44, unum = 44, pre = oct}, 0)
+
+ test_vim_str2nr('-054', flags, {len = 4, num = -44, unum = 44, pre = oct}, 0)
+ test_vim_str2nr('-054', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-054', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-054', flags, {len = 3, num = -5, unum = 5, pre = oct}, 3)
+ test_vim_str2nr('-054', flags, {len = 4, num = -44, unum = 44, pre = oct}, 4)
+ test_vim_str2nr('-0548', flags, {len = 4, num = -44, unum = 44, pre = oct}, 4)
+ test_vim_str2nr('-054', flags, {len = 4, num = -44, unum = 44, pre = oct}, 5)
+
+ test_vim_str2nr('-054x', flags, {len = 4, num = -44, unum = 44, pre = oct}, 5)
+ test_vim_str2nr('-054x', flags, {len = 4, num = -44, unum = 44, pre = oct}, 0)
+
+ if flags > lib.STR2NR_FORCE then
+ test_vim_str2nr('-54', flags, {len = 3, num = -44, unum = 44, pre = 0}, 0)
+ test_vim_str2nr('-0548', flags, {len = 4, num = -44, unum = 44, pre = 0}, 5)
+ test_vim_str2nr('-0548', flags, {len = 4, num = -44, unum = 44, pre = 0}, 0)
+ else
+ test_vim_str2nr('-0548', flags, {len = 5, num = -548, unum = 548, pre = 0}, 5)
+ test_vim_str2nr('-0548', flags, {len = 5, num = -548, unum = 548, pre = 0}, 0)
+ end
+ end
+ end)
+ itp('works with hexadecimal numbers', function()
+ for _, flags in ipairs({
+ lib.STR2NR_HEX,
+ lib.STR2NR_HEX + lib.STR2NR_BIN,
+ lib.STR2NR_HEX + lib.STR2NR_OCT,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_HEX,
+ }) do
+ local hex
+ local HEX
+ if flags > lib.STR2NR_FORCE then
+ hex = 0
+ HEX = 0
+ else
+ hex = ('x'):byte()
+ HEX = ('X'):byte()
+ end
+
+ -- Check that all digits are recognized
+ test_vim_str2nr('0x12345', flags, {len = 7, num = 74565, unum = 74565, pre = hex}, 0)
+ test_vim_str2nr('0x67890', flags, {len = 7, num = 424080, unum = 424080, pre = hex}, 0)
+ test_vim_str2nr('0xABCDEF', flags, {len = 8, num = 11259375, unum = 11259375, pre = hex}, 0)
+ test_vim_str2nr('0xabcdef', flags, {len = 8, num = 11259375, unum = 11259375, pre = hex}, 0)
+
+ test_vim_str2nr( '0x101', flags, {len = 5, num = 257, unum =257, pre = hex}, 0)
+ test_vim_str2nr( '0x101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0x101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0x101', flags, {len = 3, num = 1, unum = 1, pre = hex}, 3)
+ test_vim_str2nr( '0x101', flags, {len = 4, num = 16, unum = 16, pre = hex}, 4)
+ test_vim_str2nr( '0x101', flags, {len = 5, num = 257, unum =257, pre = hex}, 5)
+ test_vim_str2nr( '0x101', flags, {len = 5, num = 257, unum =257, pre = hex}, 6)
+
+ test_vim_str2nr( '0x101G', flags, {len = 5, num = 257, unum =257, pre = hex}, 0)
+ test_vim_str2nr( '0x101G', flags, {len = 5, num = 257, unum =257, pre = hex}, 6)
+
+ test_vim_str2nr('-0x101', flags, {len = 6, num =-257, unum =257, pre = hex}, 0)
+ test_vim_str2nr('-0x101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0x101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0x101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0x101', flags, {len = 4, num = -1, unum = 1, pre = hex}, 4)
+ test_vim_str2nr('-0x101', flags, {len = 5, num = -16, unum = 16, pre = hex}, 5)
+ test_vim_str2nr('-0x101', flags, {len = 6, num =-257, unum =257, pre = hex}, 6)
+ test_vim_str2nr('-0x101', flags, {len = 6, num =-257, unum =257, pre = hex}, 7)
+
+ test_vim_str2nr('-0x101G', flags, {len = 6, num =-257, unum =257, pre = hex}, 0)
+ test_vim_str2nr('-0x101G', flags, {len = 6, num =-257, unum =257, pre = hex}, 7)
+
+ test_vim_str2nr( '0X101', flags, {len = 5, num = 257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr( '0X101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0X101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0X101', flags, {len = 3, num = 1, unum = 1, pre = HEX}, 3)
+ test_vim_str2nr( '0X101', flags, {len = 4, num = 16, unum = 16, pre = HEX}, 4)
+ test_vim_str2nr( '0X101', flags, {len = 5, num = 257, unum =257, pre = HEX}, 5)
+ test_vim_str2nr( '0X101', flags, {len = 5, num = 257, unum =257, pre = HEX}, 6)
+
+ test_vim_str2nr( '0X101G', flags, {len = 5, num = 257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr( '0X101G', flags, {len = 5, num = 257, unum =257, pre = HEX}, 6)
+
+ test_vim_str2nr('-0X101', flags, {len = 6, num =-257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr('-0X101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0X101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0X101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0X101', flags, {len = 4, num = -1, unum = 1, pre = HEX}, 4)
+ test_vim_str2nr('-0X101', flags, {len = 5, num = -16, unum = 16, pre = HEX}, 5)
+ test_vim_str2nr('-0X101', flags, {len = 6, num =-257, unum =257, pre = HEX}, 6)
+ test_vim_str2nr('-0X101', flags, {len = 6, num =-257, unum =257, pre = HEX}, 7)
+
+ test_vim_str2nr('-0X101G', flags, {len = 6, num =-257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr('-0X101G', flags, {len = 6, num =-257, unum =257, pre = HEX}, 7)
+
+ if flags > lib.STR2NR_FORCE then
+ test_vim_str2nr('-101', flags, {len = 4, num = -257, unum = 257, pre = 0}, 0)
+ end
+ end
+ end)
+end)
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/formatc.lua b/test/unit/formatc.lua
index e288081960..2fd37c599a 100644
--- a/test/unit/formatc.lua
+++ b/test/unit/formatc.lua
@@ -65,11 +65,12 @@ local tokens = P { "tokens";
identifier = Ct(C(R("az","AZ","__") * R("09","az","AZ","__")^0) * Cc"identifier"),
-- Single character in a string
- string_char = R("az","AZ","09") + S"$%^&*()_-+={[}]:;@~#<,>.!?/ \t" + (P"\\" * S[[ntvbrfa\?'"0x]]),
+ sstring_char = R("\001&","([","]\255") + (P"\\" * S[[ntvbrfa\?'"0x]]),
+ dstring_char = R("\001!","#[","]\255") + (P"\\" * S[[ntvbrfa\?'"0x]]),
-- String literal
- string = Ct(C(P"'" * (V"string_char" + P'"')^0 * P"'" +
- P'"' * (V"string_char" + P"'")^0 * P'"') * Cc"string"),
+ string = Ct(C(P"'" * (V"sstring_char" + P'"')^0 * P"'" +
+ P'"' * (V"dstring_char" + P"'")^0 * P'"') * Cc"string"),
-- Operator
operator = Ct(C(P">>=" + P"<<=" + P"..." +
diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua
index 4b9f185156..7689c1824b 100644
--- a/test/unit/helpers.lua
+++ b/test/unit/helpers.lua
@@ -315,7 +315,7 @@ local function alloc_log_new()
eq(exp, self.log)
self:clear()
end
- function log:clear_tmp_allocs()
+ function log:clear_tmp_allocs(clear_null_frees)
local toremove = {}
local allocs = {}
for i, v in ipairs(self.log) do
@@ -327,6 +327,8 @@ local function alloc_log_new()
if v.func == 'free' then
toremove[#toremove + 1] = i
end
+ elseif clear_null_frees and v.args[1] == self.null then
+ toremove[#toremove + 1] = i
end
if v.func == 'realloc' then
allocs[tostring(v.ret)] = i
@@ -527,9 +529,13 @@ local hook_numlen = 5
local hook_msglen = 1 + 1 + 1 + (1 + hook_fnamelen) + (1 + hook_sfnamelen) + (1 + hook_numlen) + 1
local tracehelp = dedent([[
+ Trace: either in the format described below or custom debug output starting
+ with `>`. Latter lines still have the same width in byte.
+
┌ Trace type: _r_eturn from function , function _c_all, _l_ine executed,
│ _t_ail return, _C_ount (should not actually appear),
- │ _s_aved from previous run for reference.
+ │ _s_aved from previous run for reference, _>_ for custom debug
+ │ output.
│┏ Function type: _L_ua function, _C_ function, _m_ain part of chunk,
│┃ function that did _t_ail call.
│┃┌ Function name type: _g_lobal, _l_ocal, _m_ethod, _f_ield, _u_pvalue,
@@ -627,14 +633,26 @@ end
local trace_end_msg = ('E%s\n'):format((' '):rep(hook_msglen - 2))
+local _debug_log
+
+local debug_log = only_separate(function(...)
+ return _debug_log(...)
+end)
+
local function itp_child(wr, func)
- init()
- collectgarbage('stop')
- child_sethook(wr)
- local err, emsg = pcall(func)
- collectgarbage('restart')
- collectgarbage()
- debug.sethook()
+ _debug_log = function(s)
+ s = s:sub(1, hook_msglen - 2)
+ sc.write(wr, '>' .. s .. (' '):rep(hook_msglen - 2 - #s) .. '\n')
+ end
+ local err, emsg = pcall(init)
+ if err then
+ collectgarbage('stop')
+ child_sethook(wr)
+ err, emsg = pcall(func)
+ collectgarbage('restart')
+ collectgarbage()
+ debug.sethook()
+ end
emsg = tostring(emsg)
sc.write(wr, trace_end_msg)
if not err then
@@ -698,7 +716,14 @@ local function check_child_err(rd)
local len_s = sc.read(rd, 5)
local len = tonumber(len_s)
neq(0, len)
- local err = sc.read(rd, len + 1)
+ local err = ''
+ if os.getenv('NVIM_TEST_TRACE_ON_ERROR') == '1' and #trace ~= 0 then
+ err = '\nTest failed, trace:\n' .. tracehelp
+ for _, traceline in ipairs(trace) do
+ err = err .. traceline
+ end
+ end
+ err = err .. sc.read(rd, len + 1)
assert.just_fail(err)
end
@@ -753,6 +778,60 @@ end
cimport('./src/nvim/types.h', './src/nvim/main.h', './src/nvim/os/time.h')
+local function conv_enum(etab, eval)
+ local n = tonumber(eval)
+ return etab[n] or n
+end
+
+local function array_size(arr)
+ return ffi.sizeof(arr) / ffi.sizeof(arr[0])
+end
+
+local function kvi_size(kvi)
+ return array_size(kvi.init_array)
+end
+
+local function kvi_init(kvi)
+ kvi.capacity = kvi_size(kvi)
+ kvi.items = kvi.init_array
+ return kvi
+end
+
+local function kvi_destroy(kvi)
+ if kvi.items ~= kvi.init_array then
+ lib.xfree(kvi.items)
+ end
+end
+
+local function kvi_new(ct)
+ return kvi_init(ffi.new(ct))
+end
+
+local function make_enum_conv_tab(m, 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(m[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,
@@ -773,6 +852,16 @@ local module = {
child_call_once = child_call_once,
child_cleanup_once = child_cleanup_once,
sc = sc,
+ conv_enum = conv_enum,
+ array_size = array_size,
+ kvi_destroy = kvi_destroy,
+ kvi_size = kvi_size,
+ kvi_init = kvi_init,
+ kvi_new = kvi_new,
+ make_enum_conv_tab = make_enum_conv_tab,
+ ptr2addr = ptr2addr,
+ ptr2key = ptr2key,
+ debug_log = debug_log,
}
return function()
return module
diff --git a/test/unit/viml/expressions/lexer_spec.lua b/test/unit/viml/expressions/lexer_spec.lua
new file mode 100644
index 0000000000..1b57a24ad5
--- /dev/null
+++ b/test/unit/viml/expressions/lexer_spec.lua
@@ -0,0 +1,428 @@
+local helpers = require('test.unit.helpers')(after_each)
+local global_helpers = require('test.helpers')
+local itp = helpers.gen_itp(it)
+local viml_helpers = require('test.unit.viml.helpers')
+
+local child_call_once = helpers.child_call_once
+local conv_enum = helpers.conv_enum
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local eq = helpers.eq
+
+local conv_ccs = viml_helpers.conv_ccs
+local new_pstate = viml_helpers.new_pstate
+local conv_cmp_type = viml_helpers.conv_cmp_type
+local pstate_set_str = viml_helpers.pstate_set_str
+local conv_expr_asgn_type = viml_helpers.conv_expr_asgn_type
+
+local shallowcopy = global_helpers.shallowcopy
+local intchar2lua = global_helpers.intchar2lua
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h')
+
+local eltkn_type_tab, eltkn_mul_type_tab, eltkn_opt_scope_tab
+child_call_once(function()
+ eltkn_type_tab = {
+ [tonumber(lib.kExprLexInvalid)] = 'Invalid',
+ [tonumber(lib.kExprLexMissing)] = 'Missing',
+ [tonumber(lib.kExprLexSpacing)] = 'Spacing',
+ [tonumber(lib.kExprLexEOC)] = 'EOC',
+
+ [tonumber(lib.kExprLexQuestion)] = 'Question',
+ [tonumber(lib.kExprLexColon)] = 'Colon',
+ [tonumber(lib.kExprLexOr)] = 'Or',
+ [tonumber(lib.kExprLexAnd)] = 'And',
+ [tonumber(lib.kExprLexComparison)] = 'Comparison',
+ [tonumber(lib.kExprLexPlus)] = 'Plus',
+ [tonumber(lib.kExprLexMinus)] = 'Minus',
+ [tonumber(lib.kExprLexDot)] = 'Dot',
+ [tonumber(lib.kExprLexMultiplication)] = 'Multiplication',
+
+ [tonumber(lib.kExprLexNot)] = 'Not',
+
+ [tonumber(lib.kExprLexNumber)] = 'Number',
+ [tonumber(lib.kExprLexSingleQuotedString)] = 'SingleQuotedString',
+ [tonumber(lib.kExprLexDoubleQuotedString)] = 'DoubleQuotedString',
+ [tonumber(lib.kExprLexOption)] = 'Option',
+ [tonumber(lib.kExprLexRegister)] = 'Register',
+ [tonumber(lib.kExprLexEnv)] = 'Env',
+ [tonumber(lib.kExprLexPlainIdentifier)] = 'PlainIdentifier',
+
+ [tonumber(lib.kExprLexBracket)] = 'Bracket',
+ [tonumber(lib.kExprLexFigureBrace)] = 'FigureBrace',
+ [tonumber(lib.kExprLexParenthesis)] = 'Parenthesis',
+ [tonumber(lib.kExprLexComma)] = 'Comma',
+ [tonumber(lib.kExprLexArrow)] = 'Arrow',
+
+ [tonumber(lib.kExprLexAssignment)] = 'Assignment',
+ }
+
+ eltkn_mul_type_tab = {
+ [tonumber(lib.kExprLexMulMul)] = 'Mul',
+ [tonumber(lib.kExprLexMulDiv)] = 'Div',
+ [tonumber(lib.kExprLexMulMod)] = 'Mod',
+ }
+
+ eltkn_opt_scope_tab = {
+ [tonumber(lib.kExprOptScopeUnspecified)] = 'Unspecified',
+ [tonumber(lib.kExprOptScopeGlobal)] = 'Global',
+ [tonumber(lib.kExprOptScopeLocal)] = 'Local',
+ }
+end)
+
+local function conv_eltkn_type(typ)
+ return conv_enum(eltkn_type_tab, typ)
+end
+
+local bracket_types = {
+ Bracket = true,
+ FigureBrace = true,
+ Parenthesis = true,
+}
+
+local function eltkn2lua(pstate, tkn)
+ local ret = {
+ type = conv_eltkn_type(tkn.type),
+ }
+ pstate_set_str(pstate, tkn.start, tkn.len, ret)
+ if not ret.error and (#(ret.str) ~= ret.len) then
+ ret.error = '#str /= len'
+ end
+ if ret.type == 'Comparison' then
+ ret.data = {
+ type = conv_cmp_type(tkn.data.cmp.type),
+ ccs = conv_ccs(tkn.data.cmp.ccs),
+ inv = (not not tkn.data.cmp.inv),
+ }
+ elseif ret.type == 'Multiplication' then
+ ret.data = { type = conv_enum(eltkn_mul_type_tab, tkn.data.mul.type) }
+ elseif bracket_types[ret.type] then
+ ret.data = { closing = (not not tkn.data.brc.closing) }
+ elseif ret.type == 'Register' then
+ ret.data = { name = intchar2lua(tkn.data.reg.name) }
+ elseif (ret.type == 'SingleQuotedString'
+ or ret.type == 'DoubleQuotedString') then
+ ret.data = { closed = (not not tkn.data.str.closed) }
+ elseif ret.type == 'Option' then
+ ret.data = {
+ scope = conv_enum(eltkn_opt_scope_tab, tkn.data.opt.scope),
+ name = ffi.string(tkn.data.opt.name, tkn.data.opt.len),
+ }
+ elseif ret.type == 'PlainIdentifier' then
+ ret.data = {
+ scope = intchar2lua(tkn.data.var.scope),
+ autoload = (not not tkn.data.var.autoload),
+ }
+ elseif ret.type == 'Number' then
+ ret.data = {
+ is_float = (not not tkn.data.num.is_float),
+ base = tonumber(tkn.data.num.base),
+ }
+ ret.data.val = tonumber(tkn.data.num.is_float
+ and tkn.data.num.val.floating
+ or tkn.data.num.val.integer)
+ elseif ret.type == 'Assignment' then
+ ret.data = { type = conv_expr_asgn_type(tkn.data.ass.type) }
+ elseif ret.type == 'Invalid' then
+ ret.data = { error = ffi.string(tkn.data.err.msg) }
+ end
+ return ret, tkn
+end
+
+local function next_eltkn(pstate, flags)
+ return eltkn2lua(pstate, lib.viml_pexpr_next_token(pstate, flags))
+end
+
+describe('Expressions lexer', function()
+ local flags = 0
+ local should_advance = true
+ local function check_advance(pstate, bytes_to_advance, initial_col)
+ local tgt = initial_col + bytes_to_advance
+ if should_advance then
+ if pstate.reader.lines.items[0].size == tgt then
+ eq(1, pstate.pos.line)
+ eq(0, pstate.pos.col)
+ else
+ eq(0, pstate.pos.line)
+ eq(tgt, pstate.pos.col)
+ end
+ else
+ eq(0, pstate.pos.line)
+ eq(initial_col, pstate.pos.col)
+ end
+ end
+ local function singl_eltkn_test(typ, str, data)
+ local pstate = new_pstate({str})
+ eq({data=data, len=#str, start={col=0, line=0}, str=str, type=typ},
+ next_eltkn(pstate, flags))
+ check_advance(pstate, #str, 0)
+ if not (
+ typ == 'Spacing'
+ or (typ == 'Register' and str == '@')
+ or ((typ == 'SingleQuotedString' or typ == 'DoubleQuotedString')
+ and not data.closed)
+ ) then
+ pstate = new_pstate({str .. ' '})
+ eq({data=data, len=#str, start={col=0, line=0}, str=str, type=typ},
+ next_eltkn(pstate, flags))
+ check_advance(pstate, #str, 0)
+ end
+ pstate = new_pstate({'x' .. str})
+ pstate.pos.col = 1
+ eq({data=data, len=#str, start={col=1, line=0}, str=str, type=typ},
+ next_eltkn(pstate, flags))
+ check_advance(pstate, #str, 1)
+ end
+ local function scope_test(scope)
+ singl_eltkn_test('PlainIdentifier', scope .. ':test#var', {autoload=true, scope=scope})
+ singl_eltkn_test('PlainIdentifier', scope .. ':', {autoload=false, scope=scope})
+ end
+ local function comparison_test(op, inv_op, cmp_type)
+ singl_eltkn_test('Comparison', op, {type=cmp_type, inv=false, ccs='UseOption'})
+ singl_eltkn_test('Comparison', inv_op, {type=cmp_type, inv=true, ccs='UseOption'})
+ singl_eltkn_test('Comparison', op .. '#', {type=cmp_type, inv=false, ccs='MatchCase'})
+ singl_eltkn_test('Comparison', inv_op .. '#', {type=cmp_type, inv=true, ccs='MatchCase'})
+ singl_eltkn_test('Comparison', op .. '?', {type=cmp_type, inv=false, ccs='IgnoreCase'})
+ singl_eltkn_test('Comparison', inv_op .. '?', {type=cmp_type, inv=true, ccs='IgnoreCase'})
+ end
+ local function simple_test(pstate_arg, exp_type, exp_len, exp)
+ local pstate = new_pstate(pstate_arg)
+ exp = shallowcopy(exp)
+ exp.type = exp_type
+ exp.len = exp_len or #(pstate_arg[0])
+ exp.start = { col = 0, line = 0 }
+ eq(exp, next_eltkn(pstate, flags))
+ end
+ local function stable_tests()
+ singl_eltkn_test('Parenthesis', '(', {closing=false})
+ singl_eltkn_test('Parenthesis', ')', {closing=true})
+ singl_eltkn_test('Bracket', '[', {closing=false})
+ singl_eltkn_test('Bracket', ']', {closing=true})
+ singl_eltkn_test('FigureBrace', '{', {closing=false})
+ singl_eltkn_test('FigureBrace', '}', {closing=true})
+ singl_eltkn_test('Question', '?')
+ singl_eltkn_test('Colon', ':')
+ singl_eltkn_test('Dot', '.')
+ singl_eltkn_test('Assignment', '.=', {type='Concat'})
+ singl_eltkn_test('Plus', '+')
+ singl_eltkn_test('Assignment', '+=', {type='Add'})
+ singl_eltkn_test('Comma', ',')
+ singl_eltkn_test('Multiplication', '*', {type='Mul'})
+ singl_eltkn_test('Multiplication', '/', {type='Div'})
+ singl_eltkn_test('Multiplication', '%', {type='Mod'})
+ singl_eltkn_test('Spacing', ' \t\t \t\t')
+ singl_eltkn_test('Spacing', ' ')
+ singl_eltkn_test('Spacing', '\t')
+ singl_eltkn_test('Invalid', '\x01\x02\x03', {error='E15: Invalid control character present in input: %.*s'})
+ singl_eltkn_test('Number', '0123', {is_float=false, base=8, val=83})
+ singl_eltkn_test('Number', '01234567', {is_float=false, base=8, val=342391})
+ singl_eltkn_test('Number', '012345678', {is_float=false, base=10, val=12345678})
+ singl_eltkn_test('Number', '0x123', {is_float=false, base=16, val=291})
+ singl_eltkn_test('Number', '0x56FF', {is_float=false, base=16, val=22271})
+ singl_eltkn_test('Number', '0xabcdef', {is_float=false, base=16, val=11259375})
+ singl_eltkn_test('Number', '0xABCDEF', {is_float=false, base=16, val=11259375})
+ singl_eltkn_test('Number', '0x0', {is_float=false, base=16, val=0})
+ singl_eltkn_test('Number', '00', {is_float=false, base=8, val=0})
+ singl_eltkn_test('Number', '0b0', {is_float=false, base=2, val=0})
+ singl_eltkn_test('Number', '0b010111', {is_float=false, base=2, val=23})
+ singl_eltkn_test('Number', '0b100111', {is_float=false, base=2, val=39})
+ singl_eltkn_test('Number', '0', {is_float=false, base=10, val=0})
+ singl_eltkn_test('Number', '9', {is_float=false, base=10, val=9})
+ singl_eltkn_test('Env', '$abc')
+ singl_eltkn_test('Env', '$')
+ singl_eltkn_test('PlainIdentifier', 'test', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', '_test', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', '_test_foo', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 't', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 'test5', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 't0', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 'test#var', {autoload=true, scope=0})
+ singl_eltkn_test('PlainIdentifier', 'test#var#val###', {autoload=true, scope=0})
+ singl_eltkn_test('PlainIdentifier', 't#####', {autoload=true, scope=0})
+ singl_eltkn_test('And', '&&')
+ singl_eltkn_test('Or', '||')
+ singl_eltkn_test('Invalid', '&', {error='E112: Option name missing: %.*s'})
+ singl_eltkn_test('Option', '&opt', {scope='Unspecified', name='opt'})
+ singl_eltkn_test('Option', '&t_xx', {scope='Unspecified', name='t_xx'})
+ singl_eltkn_test('Option', '&t_\r\r', {scope='Unspecified', name='t_\r\r'})
+ singl_eltkn_test('Option', '&t_\t\t', {scope='Unspecified', name='t_\t\t'})
+ singl_eltkn_test('Option', '&t_ ', {scope='Unspecified', name='t_ '})
+ singl_eltkn_test('Option', '&g:opt', {scope='Global', name='opt'})
+ singl_eltkn_test('Option', '&l:opt', {scope='Local', name='opt'})
+ singl_eltkn_test('Invalid', '&l:', {error='E112: Option name missing: %.*s'})
+ singl_eltkn_test('Invalid', '&g:', {error='E112: Option name missing: %.*s'})
+ singl_eltkn_test('Register', '@', {name=-1})
+ singl_eltkn_test('Register', '@a', {name='a'})
+ singl_eltkn_test('Register', '@\r', {name=13})
+ singl_eltkn_test('Register', '@ ', {name=' '})
+ singl_eltkn_test('Register', '@\t', {name=9})
+ singl_eltkn_test('SingleQuotedString', '\'test', {closed=false})
+ singl_eltkn_test('SingleQuotedString', '\'test\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'x\'\'\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'x\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'', {closed=false})
+ singl_eltkn_test('SingleQuotedString', '\'x\'\'', {closed=false})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'x', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"test', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"test"', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"\\""', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"x\\""', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"\\"x"', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"\\"', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"x\\"', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"\\"x', {closed=false})
+ singl_eltkn_test('Not', '!')
+ singl_eltkn_test('Assignment', '=', {type='Plain'})
+ comparison_test('==', '!=', 'Equal')
+ comparison_test('=~', '!~', 'Matches')
+ comparison_test('>', '<=', 'Greater')
+ comparison_test('>=', '<', 'GreaterOrEqual')
+ singl_eltkn_test('Minus', '-')
+ singl_eltkn_test('Assignment', '-=', {type='Subtract'})
+ singl_eltkn_test('Arrow', '->')
+ singl_eltkn_test('Invalid', '~', {error='E15: Unidentified character: %.*s'})
+ simple_test({{data=nil, size=0}}, 'EOC', 0, {error='start.col >= #pstr'})
+ simple_test({''}, 'EOC', 0, {error='start.col >= #pstr'})
+ simple_test({'2.'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2e5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.2.'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+1a'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-1a'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'0b102'}, 'Number', 4, {data={is_float=false, base=2, val=2}, str='0b10'})
+ simple_test({'10F'}, 'Number', 2, {data={is_float=false, base=10, val=10}, str='10'})
+ simple_test({'0x0123456789ABCDEFG'}, 'Number', 18, {data={is_float=false, base=16, val=81985529216486895}, str='0x0123456789ABCDEF'})
+ simple_test({{data='00', size=2}}, 'Number', 2, {data={is_float=false, base=8, val=0}, str='00'})
+ simple_test({{data='009', size=2}}, 'Number', 2, {data={is_float=false, base=8, val=0}, str='00'})
+ simple_test({{data='01', size=1}}, 'Number', 1, {data={is_float=false, base=10, val=0}, str='0'})
+ end
+
+ local function regular_scope_tests()
+ scope_test('s')
+ scope_test('g')
+ scope_test('v')
+ scope_test('b')
+ scope_test('w')
+ scope_test('t')
+ scope_test('l')
+ scope_test('a')
+
+ simple_test({'g:'}, 'PlainIdentifier', 2, {data={scope='g', autoload=false}, str='g:'})
+ simple_test({'g:is#foo'}, 'PlainIdentifier', 8, {data={scope='g', autoload=true}, str='g:is#foo'})
+ simple_test({'g:isnot#foo'}, 'PlainIdentifier', 11, {data={scope='g', autoload=true}, str='g:isnot#foo'})
+ end
+
+ local function regular_is_tests()
+ comparison_test('is', 'isnot', 'Identical')
+
+ simple_test({'is'}, 'Comparison', 2, {data={type='Identical', inv=false, ccs='UseOption'}, str='is'})
+ simple_test({'isnot'}, 'Comparison', 5, {data={type='Identical', inv=true, ccs='UseOption'}, str='isnot'})
+ simple_test({'is?'}, 'Comparison', 3, {data={type='Identical', inv=false, ccs='IgnoreCase'}, str='is?'})
+ simple_test({'isnot?'}, 'Comparison', 6, {data={type='Identical', inv=true, ccs='IgnoreCase'}, str='isnot?'})
+ simple_test({'is#'}, 'Comparison', 3, {data={type='Identical', inv=false, ccs='MatchCase'}, str='is#'})
+ simple_test({'isnot#'}, 'Comparison', 6, {data={type='Identical', inv=true, ccs='MatchCase'}, str='isnot#'})
+ simple_test({'is#foo'}, 'Comparison', 3, {data={type='Identical', inv=false, ccs='MatchCase'}, str='is#'})
+ simple_test({'isnot#foo'}, 'Comparison', 6, {data={type='Identical', inv=true, ccs='MatchCase'}, str='isnot#'})
+ end
+
+ local function regular_number_tests()
+ simple_test({'2.0'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ end
+
+ local function regular_eoc_tests()
+ singl_eltkn_test('EOC', '|')
+ singl_eltkn_test('EOC', '\0')
+ singl_eltkn_test('EOC', '\n')
+ end
+
+ itp('works (single tokens, zero flags)', function()
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_is_tests()
+ regular_number_tests()
+ end)
+ itp('peeks', function()
+ flags = tonumber(lib.kELFlagPeek)
+ should_advance = false
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_is_tests()
+ regular_number_tests()
+ end)
+ itp('forbids scope', function()
+ flags = tonumber(lib.kELFlagForbidScope)
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_is_tests()
+ regular_number_tests()
+
+ simple_test({'g:'}, 'PlainIdentifier', 1, {data={scope=0, autoload=false}, str='g'})
+ end)
+ itp('allows floats', function()
+ flags = tonumber(lib.kELFlagAllowFloat)
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_is_tests()
+
+ simple_test({'2.2'}, 'Number', 3, {data={is_float=true, base=10, val=2.2}, str='2.2'})
+ simple_test({'2.0e5'}, 'Number', 5, {data={is_float=true, base=10, val=2e5}, str='2.0e5'})
+ simple_test({'2.0e+5'}, 'Number', 6, {data={is_float=true, base=10, val=2e5}, str='2.0e+5'})
+ simple_test({'2.0e-5'}, 'Number', 6, {data={is_float=true, base=10, val=2e-5}, str='2.0e-5'})
+ simple_test({'2.500000e-5'}, 'Number', 11, {data={is_float=true, base=10, val=2.5e-5}, str='2.500000e-5'})
+ simple_test({'2.5555e2'}, 'Number', 8, {data={is_float=true, base=10, val=2.5555e2}, str='2.5555e2'})
+ simple_test({'2.5555e+2'}, 'Number', 9, {data={is_float=true, base=10, val=2.5555e2}, str='2.5555e+2'})
+ simple_test({'2.5555e-2'}, 'Number', 9, {data={is_float=true, base=10, val=2.5555e-2}, str='2.5555e-2'})
+ simple_test({{data='2.5e-5', size=3}},
+ 'Number', 3, {data={is_float=true, base=10, val=2.5}, str='2.5'})
+ simple_test({{data='2.5e5', size=4}},
+ 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({{data='2.5e-50', size=6}},
+ 'Number', 6, {data={is_float=true, base=10, val=2.5e-5}, str='2.5e-5'})
+ end)
+ itp('treats `is` as an identifier', function()
+ flags = tonumber(lib.kELFlagIsNotCmp)
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_number_tests()
+
+ simple_test({'is'}, 'PlainIdentifier', 2, {data={scope=0, autoload=false}, str='is'})
+ simple_test({'isnot'}, 'PlainIdentifier', 5, {data={scope=0, autoload=false}, str='isnot'})
+ simple_test({'is?'}, 'PlainIdentifier', 2, {data={scope=0, autoload=false}, str='is'})
+ simple_test({'isnot?'}, 'PlainIdentifier', 5, {data={scope=0, autoload=false}, str='isnot'})
+ simple_test({'is#'}, 'PlainIdentifier', 3, {data={scope=0, autoload=true}, str='is#'})
+ simple_test({'isnot#'}, 'PlainIdentifier', 6, {data={scope=0, autoload=true}, str='isnot#'})
+ simple_test({'is#foo'}, 'PlainIdentifier', 6, {data={scope=0, autoload=true}, str='is#foo'})
+ simple_test({'isnot#foo'}, 'PlainIdentifier', 9, {data={scope=0, autoload=true}, str='isnot#foo'})
+ end)
+ itp('forbids EOC', function()
+ flags = tonumber(lib.kELFlagForbidEOC)
+ stable_tests()
+
+ regular_scope_tests()
+ regular_is_tests()
+ regular_number_tests()
+
+ singl_eltkn_test('Invalid', '|', {error='E15: Unexpected EOC character: %.*s'})
+ singl_eltkn_test('Invalid', '\0', {error='E15: Unexpected EOC character: %.*s'})
+ singl_eltkn_test('Invalid', '\n', {error='E15: Unexpected EOC character: %.*s'})
+ end)
+end)
diff --git a/test/unit/viml/expressions/parser_spec.lua b/test/unit/viml/expressions/parser_spec.lua
new file mode 100644
index 0000000000..79b19de833
--- /dev/null
+++ b/test/unit/viml/expressions/parser_spec.lua
@@ -0,0 +1,540 @@
+local helpers = require('test.unit.helpers')(after_each)
+local global_helpers = require('test.helpers')
+local itp = helpers.gen_itp(it)
+local viml_helpers = require('test.unit.viml.helpers')
+
+local make_enum_conv_tab = helpers.make_enum_conv_tab
+local child_call_once = helpers.child_call_once
+local alloc_log_new = helpers.alloc_log_new
+local kvi_destroy = helpers.kvi_destroy
+local conv_enum = helpers.conv_enum
+local debug_log = helpers.debug_log
+local ptr2key = helpers.ptr2key
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local neq = helpers.neq
+local eq = helpers.eq
+
+local conv_ccs = viml_helpers.conv_ccs
+local new_pstate = viml_helpers.new_pstate
+local conv_cmp_type = viml_helpers.conv_cmp_type
+local pstate_set_str = viml_helpers.pstate_set_str
+local conv_expr_asgn_type = viml_helpers.conv_expr_asgn_type
+
+local mergedicts_copy = global_helpers.mergedicts_copy
+local format_string = global_helpers.format_string
+local format_luav = global_helpers.format_luav
+local intchar2lua = global_helpers.intchar2lua
+local dictdiff = global_helpers.dictdiff
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h',
+ './src/nvim/syntax.h')
+
+local alloc_log = alloc_log_new()
+
+local predefined_hl_defs = {
+ -- From highlight_init_both
+ Conceal=true,
+ Cursor=true,
+ lCursor=true,
+ DiffText=true,
+ ErrorMsg=true,
+ IncSearch=true,
+ ModeMsg=true,
+ NonText=true,
+ PmenuSbar=true,
+ StatusLine=true,
+ StatusLineNC=true,
+ TabLineFill=true,
+ TabLineSel=true,
+ TermCursor=true,
+ VertSplit=true,
+ WildMenu=true,
+ EndOfBuffer=true,
+ QuickFixLine=true,
+ Substitute=true,
+ Whitespace=true,
+
+ -- From highlight_init_(dark|light)
+ ColorColumn=true,
+ CursorColumn=true,
+ CursorLine=true,
+ CursorLineNr=true,
+ DiffAdd=true,
+ DiffChange=true,
+ DiffDelete=true,
+ Directory=true,
+ FoldColumn=true,
+ Folded=true,
+ LineNr=true,
+ MatchParen=true,
+ MoreMsg=true,
+ Pmenu=true,
+ PmenuSel=true,
+ PmenuThumb=true,
+ Question=true,
+ Search=true,
+ SignColumn=true,
+ SpecialKey=true,
+ SpellBad=true,
+ SpellCap=true,
+ SpellLocal=true,
+ SpellRare=true,
+ TabLine=true,
+ Title=true,
+ Visual=true,
+ WarningMsg=true,
+ Normal=true,
+
+ -- From syncolor.vim, if &background
+ Comment=true,
+ Constant=true,
+ Special=true,
+ Identifier=true,
+ Statement=true,
+ PreProc=true,
+ Type=true,
+ Underlined=true,
+ Ignore=true,
+
+ -- From syncolor.vim, below if &background
+ Error=true,
+ Todo=true,
+
+ -- From syncolor.vim, links at the bottom
+ String=true,
+ Character=true,
+ Number=true,
+ Boolean=true,
+ Float=true,
+ Function=true,
+ Conditional=true,
+ Repeat=true,
+ Label=true,
+ Operator=true,
+ Keyword=true,
+ Exception=true,
+ Include=true,
+ Define=true,
+ Macro=true,
+ PreCondit=true,
+ StorageClass=true,
+ Structure=true,
+ Typedef=true,
+ Tag=true,
+ SpecialChar=true,
+ Delimiter=true,
+ SpecialComment=true,
+ Debug=true,
+}
+
+local nvim_hl_defs = {}
+
+child_call_once(function()
+ local i = 0
+ while lib.highlight_init_cmdline[i] ~= nil do
+ local hl_args = lib.highlight_init_cmdline[i]
+ local s = ffi.string(hl_args)
+ local err, msg = pcall(function()
+ if s:sub(1, 13) == 'default link ' then
+ local new_grp, grp_link = s:match('^default link (%w+) (%w+)$')
+ neq(nil, new_grp)
+ -- Note: group to link to must be already defined at the time of
+ -- linking, otherwise it will be created as cleared. So existence
+ -- of the group is checked here and not in the next pass over
+ -- nvim_hl_defs.
+ eq(true, not not (nvim_hl_defs[grp_link]
+ or predefined_hl_defs[grp_link]))
+ eq(false, not not (nvim_hl_defs[new_grp]
+ or predefined_hl_defs[new_grp]))
+ nvim_hl_defs[new_grp] = {'link', grp_link}
+ else
+ local new_grp, grp_args = s:match('^(%w+) (.*)')
+ neq(nil, new_grp)
+ eq(false, not not (nvim_hl_defs[new_grp]
+ or predefined_hl_defs[new_grp]))
+ nvim_hl_defs[new_grp] = {'definition', grp_args}
+ end
+ end)
+ if not err then
+ msg = format_string(
+ 'Error while processing string %s at position %u:\n%s', s, i, msg)
+ error(msg)
+ end
+ i = i + 1
+ end
+ for k, _ in ipairs(nvim_hl_defs) do
+ eq('NVim', k:sub(1, 4))
+ -- NVimInvalid
+ -- 12345678901
+ local err, msg = pcall(function()
+ if k:sub(5, 11) == 'Invalid' then
+ neq(nil, nvim_hl_defs['NVim' .. k:sub(12)])
+ else
+ neq(nil, nvim_hl_defs['NVimInvalid' .. k:sub(5)])
+ end
+ end)
+ if not err then
+ msg = format_string('Error while processing group %s:\n%s', k, msg)
+ error(msg)
+ end
+ end
+end)
+
+local function hls_to_hl_fs(hls)
+ local ret = {}
+ local next_col = 0
+ for i, v in ipairs(hls) do
+ local group, line, col, str = v:match('^NVim([a-zA-Z]+):(%d+):(%d+):(.*)$')
+ col = tonumber(col)
+ line = tonumber(line)
+ assert(line == 0)
+ local col_shift = col - next_col
+ assert(col_shift >= 0)
+ next_col = col + #str
+ ret[i] = format_string('hl(%r, %r%s)',
+ group,
+ str,
+ (col_shift == 0
+ and ''
+ or (', %u'):format(col_shift)))
+ end
+ return ret
+end
+
+local function format_check(expr, format_check_data, opts)
+ -- That forces specific order.
+ local zflags = opts.flags[1]
+ local zdata = format_check_data[zflags]
+ local dig_len
+ 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
+ if i % 10 == 0 then
+ digits2 = ('%s%10u'):format(digits2, i / 10)
+ end
+ digits = ('%s%u'):format(digits, i % 10)
+ end
+ print(digits)
+ if #expr > 10 then
+ print(digits2)
+ end
+ if zdata.ast.len then
+ print((' len = %u,'):format(zdata.ast.len))
+ end
+ print(' ast = ' .. format_luav(zdata.ast.ast, ' ') .. ',')
+ if zdata.ast.err then
+ print(' err = {')
+ print(' arg = ' .. format_luav(zdata.ast.err.arg) .. ',')
+ print(' msg = ' .. format_luav(zdata.ast.err.msg) .. ',')
+ print(' },')
+ end
+ print('}, {')
+ for _, v in ipairs(zdata.hl_fs) do
+ print(' ' .. v .. ',')
+ end
+ local diffs = {}
+ local diffs_num = 0
+ for flags, v in pairs(format_check_data) do
+ if flags ~= zflags then
+ diffs[flags] = dictdiff(zdata, v)
+ if diffs[flags] then
+ if flags == 3 + zflags then
+ if (dictdiff(format_check_data[1 + zflags],
+ format_check_data[3 + zflags]) == nil
+ or dictdiff(format_check_data[2 + zflags],
+ format_check_data[3 + zflags]) == nil)
+ then
+ diffs[flags] = nil
+ else
+ diffs_num = diffs_num + 1
+ end
+ else
+ diffs_num = diffs_num + 1
+ end
+ end
+ end
+ end
+ if diffs_num ~= 0 then
+ print('}, {')
+ local flags = 1
+ while diffs_num ~= 0 do
+ if diffs[flags] then
+ diffs_num = diffs_num - 1
+ local diff = diffs[flags]
+ print((' [%u] = {'):format(flags))
+ if diff.ast then
+ print(' ast = ' .. format_luav(diff.ast, ' ') .. ',')
+ end
+ if diff.hl_fs then
+ print(' hl_fs = ' .. format_luav(diff.hl_fs, ' ', {
+ literal_strings=true
+ }) .. ',')
+ end
+ print(' },')
+ end
+ flags = flags + 1
+ end
+ end
+ print('})')
+end
+
+local east_node_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprNodeMissing',
+ 'kExprNodeOpMissing',
+ 'kExprNodeTernary',
+ 'kExprNodeTernaryValue',
+ 'kExprNodeRegister',
+ 'kExprNodeSubscript',
+ 'kExprNodeListLiteral',
+ 'kExprNodeUnaryPlus',
+ 'kExprNodeBinaryPlus',
+ 'kExprNodeNested',
+ 'kExprNodeCall',
+ 'kExprNodePlainIdentifier',
+ 'kExprNodePlainKey',
+ 'kExprNodeComplexIdentifier',
+ 'kExprNodeUnknownFigure',
+ 'kExprNodeLambda',
+ 'kExprNodeDictLiteral',
+ 'kExprNodeCurlyBracesIdentifier',
+ 'kExprNodeComma',
+ 'kExprNodeColon',
+ 'kExprNodeArrow',
+ 'kExprNodeComparison',
+ 'kExprNodeConcat',
+ 'kExprNodeConcatOrSubscript',
+ 'kExprNodeInteger',
+ 'kExprNodeFloat',
+ 'kExprNodeSingleQuotedString',
+ 'kExprNodeDoubleQuotedString',
+ 'kExprNodeOr',
+ 'kExprNodeAnd',
+ 'kExprNodeUnaryMinus',
+ 'kExprNodeBinaryMinus',
+ 'kExprNodeNot',
+ 'kExprNodeMultiplication',
+ 'kExprNodeDivision',
+ 'kExprNodeMod',
+ 'kExprNodeOption',
+ 'kExprNodeEnvironment',
+ 'kExprNodeAssignment',
+}, '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)))
+ elseif typ == 'PlainIdentifier' then
+ typ = typ .. ('(scope=%s,ident=%s)'):format(
+ tostring(intchar2lua(eastnode.data.var.scope)),
+ ffi.string(eastnode.data.var.ident, eastnode.data.var.ident_len))
+ elseif typ == 'PlainKey' then
+ typ = typ .. ('(key=%s)'):format(
+ ffi.string(eastnode.data.var.ident, eastnode.data.var.ident_len))
+ elseif (typ == 'UnknownFigure' or typ == 'DictLiteral'
+ or typ == 'CurlyBracesIdentifier' or typ == 'Lambda') then
+ typ = typ .. ('(%s)'):format(
+ (eastnode.data.fig.type_guesses.allow_lambda and '\\' or '-')
+ .. (eastnode.data.fig.type_guesses.allow_dict and 'd' or '-')
+ .. (eastnode.data.fig.type_guesses.allow_ident and 'i' or '-'))
+ elseif typ == 'Comparison' then
+ typ = typ .. ('(type=%s,inv=%u,ccs=%s)'):format(
+ conv_cmp_type(eastnode.data.cmp.type), eastnode.data.cmp.inv and 1 or 0,
+ conv_ccs(eastnode.data.cmp.ccs))
+ elseif typ == 'Integer' then
+ typ = typ .. ('(val=%u)'):format(tonumber(eastnode.data.num.value))
+ elseif typ == 'Float' then
+ typ = typ .. format_string('(val=%e)', tonumber(eastnode.data.flt.value))
+ elseif typ == 'SingleQuotedString' or typ == 'DoubleQuotedString' then
+ if eastnode.data.str.value == nil then
+ typ = typ .. '(val=NULL)'
+ else
+ local s = ffi.string(eastnode.data.str.value, eastnode.data.str.size)
+ typ = format_string('%s(val=%q)', typ, s)
+ end
+ elseif typ == 'Option' then
+ typ = ('%s(scope=%s,ident=%s)'):format(
+ typ,
+ tostring(intchar2lua(eastnode.data.opt.scope)),
+ ffi.string(eastnode.data.opt.ident, eastnode.data.opt.ident_len))
+ elseif typ == 'Environment' then
+ typ = ('%s(ident=%s)'):format(
+ typ,
+ ffi.string(eastnode.data.env.ident, eastnode.data.env.ident_len))
+ elseif typ == 'Assignment' then
+ typ = ('%s(%s)'):format(typ, conv_expr_asgn_type(eastnode.data.ass.type))
+ end
+ ret_str = typ .. ':' .. ret_str
+ local can_simplify = not ret.children
+ 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(str, pstate, east)
+ local checked_nodes = {}
+ local len = tonumber(pstate.pos.col)
+ if pstate.pos.line == 1 then
+ len = tonumber(pstate.reader.lines.items[0].size)
+ end
+ if type(str) == 'string' and len == #str then
+ len = nil
+ end
+ return {
+ err = east.err.msg ~= nil and {
+ msg = ffi.string(east.err.msg),
+ arg = ffi.string(east.err.arg, east.err.arg_len),
+ } or nil,
+ len = len,
+ 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),
+ })
+ ret[i + 1] = ('%s:%u:%u:%s'):format(
+ chunk_tbl.group,
+ chunk_tbl.start.line,
+ chunk_tbl.start.col,
+ chunk_tbl.str)
+ end
+ return ret
+end
+
+child_call_once(function()
+ assert:set_parameter('TableFormatLevel', 1000000)
+end)
+
+describe('Expressions parser', function()
+ 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 {}
+ local format_check_data = {}
+ for _, flags in ipairs(opts.flags) do
+ debug_log(('Running test case (%s, %u)'):format(str, flags))
+ local err, msg = pcall(function()
+ if os.getenv('NVIM_TEST_PARSER_SPEC_PRINT_TEST_CASE') == '1' then
+ print(str, flags)
+ end
+ alloc_log:check({})
+
+ local pstate = new_pstate({str})
+ local east = lib.viml_pexpr_parse(pstate, flags)
+ local ast = east2lua(str, pstate, east)
+ local hls = phl2lua(pstate)
+ if exp_ast == nil then
+ format_check_data[flags] = {ast=ast, hl_fs=hls_to_hl_fs(hls)}
+ 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)
+ if exp_highlighting_fs then
+ local exp_highlighting = {}
+ local next_col = 0
+ for i, h in ipairs(exps.hl_fs) do
+ exp_highlighting[i], next_col = h(next_col)
+ end
+ eq(exp_highlighting, hls)
+ end
+ end
+ lib.viml_pexpr_free_ast(east)
+ kvi_destroy(pstate.colors)
+ alloc_log:clear_tmp_allocs(true)
+ alloc_log:check({})
+ end)
+ if not err then
+ msg = format_string('Error while processing test (%r, %u):\n%s',
+ str, flags, msg)
+ error(msg)
+ end
+ end
+ if exp_ast == nil then
+ format_check(str, format_check_data, opts)
+ end
+ end
+ local function hl(group, str, shift)
+ return function(next_col)
+ if nvim_hl_defs['NVim' .. group] == nil then
+ error(('Unknown group: NVim%s'):format(group))
+ end
+ local col = next_col + (shift or 0)
+ return (('%s:%u:%u:%s'):format(
+ 'NVim' .. group,
+ 0,
+ col,
+ str)), (col + #str)
+ end
+ end
+ local function fmtn(typ, args, rest)
+ return ('%s(%s)%s'):format(typ, args, rest)
+ end
+ require('test.unit.viml.expressions.parser_tests')(
+ itp, _check_parsing, hl, fmtn)
+end)
diff --git a/test/unit/viml/expressions/parser_tests.lua b/test/unit/viml/expressions/parser_tests.lua
new file mode 100644
index 0000000000..f0c2723a38
--- /dev/null
+++ b/test/unit/viml/expressions/parser_tests.lua
@@ -0,0 +1,8185 @@
+local global_helpers = require('test.helpers')
+
+local REMOVE_THIS = global_helpers.REMOVE_THIS
+
+return function(itp, _check_parsing, hl, fmtn)
+ 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
+ itp('works with + and @a', function()
+ check_parsing('@a', {
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ }, {
+ hl('Register', '@a'),
+ })
+ check_parsing('+@a', {
+ ast = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a+@b', {
+ 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', {
+ 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', {
+ 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', {
+ 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', {
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:2:@b',
+ },
+ },
+ },
+ err = {
+ arg = '@b',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidRegister', '@b'),
+ }, {
+ [1] = {
+ ast = {
+ len = 2,
+ err = REMOVE_THIS,
+ ast = {
+ 'Register(name=a):0:0:@a'
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing(' @a \t @b', {
+ ast = {
+ {
+ 'OpMissing:0:3:',
+ children = {
+ 'Register(name=a):0:0: @a',
+ 'Register(name=b):0:3: \t @b',
+ },
+ },
+ },
+ err = {
+ arg = '@b',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a', 1),
+ hl('InvalidSpacing', ' \t '),
+ hl('Register', '@b'),
+ }, {
+ [1] = {
+ ast = {
+ len = 6,
+ err = REMOVE_THIS,
+ ast = {
+ 'Register(name=a):0:0: @a'
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing('+', {
+ ast = {
+ 'UnaryPlus:0:0:+',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ })
+ check_parsing(' +', {
+ ast = {
+ 'UnaryPlus:0:0: +',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+', 1),
+ })
+ check_parsing('@a+ ', {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ })
+ end)
+ itp('works with @a, + and parenthesis', function()
+ check_parsing('(@a)', {
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('()', {
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing(')', {
+ ast = {
+ {
+ 'Nested:0:0:',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('+)', {
+ ast = {
+ {
+ 'Nested:0:1:',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('+@a(@b)', {
+ 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)', {
+ 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()', {
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a ()', {
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:2: (',
+ children = {
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '()',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidSpacing', ' '),
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ }, {
+ [1] = {
+ ast = {
+ len = 3,
+ err = REMOVE_THIS,
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ [4] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing('@a + (@b)', {
+ 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)', {
+ 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)', {
+ 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', {
+ 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)', {
+ -- 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)', {
+ -- 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', {
+ -- 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)', {--[[
+ | | | | | | | | || | | || | | ||| || || || ||
+ 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)', {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Unexpected closing parenthesis: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('(@a', {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ err = {
+ arg = '(@a',
+ msg = 'E110: Missing closing parenthesis for nested expression: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a(@b', {
+ -- 01234
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ err = {
+ arg = '(@b',
+ msg = 'E116: Missing closing parenthesis for function call: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a(@b, @c, @d, @e)', {
+ -- 012345678901234567
+ -- 0 1
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'Comma:0:9:,',
+ children = {
+ 'Register(name=c):0:6: @c',
+ {
+ 'Comma:0:13:,',
+ children = {
+ 'Register(name=d):0:10: @d',
+ 'Register(name=e):0:14: @e',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c', 1),
+ hl('Comma', ','),
+ hl('Register', '@d', 1),
+ hl('Comma', ','),
+ hl('Register', '@e', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a(@b(@c))', {
+ -- 01234567890123456789012345678901234567
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Call: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('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a(@b(@c(@d(@e), @f(@g(@h), @i(@j)))))', {
+ -- 01234567890123456789012345678901234567
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:5:(',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'Call:0:8:(',
+ children = {
+ 'Register(name=c):0:6:@c',
+ {
+ 'Comma:0:15:,',
+ children = {
+ {
+ 'Call:0:11:(',
+ children = {
+ 'Register(name=d):0:9:@d',
+ 'Register(name=e):0:12:@e',
+ },
+ },
+ {
+ 'Call:0:19:(',
+ children = {
+ 'Register(name=f):0:16: @f',
+ {
+ 'Comma:0:26:,',
+ children = {
+ {
+ 'Call:0:22:(',
+ children = {
+ 'Register(name=g):0:20:@g',
+ 'Register(name=h):0:23:@h',
+ },
+ },
+ {
+ 'Call:0:30:(',
+ children = {
+ 'Register(name=i):0:27: @i',
+ 'Register(name=j):0:31:@j',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@d'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@e'),
+ hl('CallingParenthesis', ')'),
+ hl('Comma', ','),
+ hl('Register', '@f', 1),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@g'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('Comma', ','),
+ hl('Register', '@i', 1),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@j'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('()()', {
+ -- 0123
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')()',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('(@a)()', {
+ -- 012345
+ ast = {
+ {
+ 'Call:0:4:(',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('(@a)(@b)', {
+ -- 01234567
+ ast = {
+ {
+ 'Call: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('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('(@a) (@b)', {
+ -- 012345678
+ ast = {
+ {
+ 'OpMissing:0:4:',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ 'Nested:0:4: (',
+ children = {
+ 'Register(name=b):0:6:@b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '(@b)',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('InvalidSpacing', ' '),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ }, {
+ [1] = {
+ ast = {
+ len = 5,
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ REMOVE_THIS,
+ },
+ },
+ },
+ err = REMOVE_THIS,
+ },
+ hl_fs = {
+ [4] = REMOVE_THIS,
+ [5] = REMOVE_THIS,
+ [6] = REMOVE_THIS,
+ [7] = REMOVE_THIS,
+ },
+ },
+ })
+ end)
+ itp('works with variable names, including curly braces ones', function()
+ check_parsing('var', {
+ ast = {
+ 'PlainIdentifier(scope=0,ident=var):0:0:var',
+ },
+ }, {
+ hl('IdentifierName', 'var'),
+ })
+ check_parsing('g:var', {
+ ast = {
+ 'PlainIdentifier(scope=g,ident=var):0:0:g:var',
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'var'),
+ })
+ check_parsing('g:', {
+ ast = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ })
+ check_parsing('{a}', {
+ -- 012
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{a:b}', {
+ -- 012
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=a,ident=b):0:1:a:b',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{a:@b}', {
+ -- 012345
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'OpMissing:0:3:',
+ children={
+ 'PlainIdentifier(scope=a,ident=):0:1:a:',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '@b}',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('InvalidRegister', '@b'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{@a}', {
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{@a}{@b}', {
+ -- 01234567
+ ast = {
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:4:{'),
+ children = {
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('Register', '@b'),
+ hl('Curly', '}'),
+ })
+ check_parsing('g:{@a}', {
+ -- 01234567
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{@a}_test', {
+ -- 012345678
+ ast = {
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=_test):0:4:_test',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', '_test'),
+ })
+ check_parsing('g:{@a}_test', {
+ -- 01234567890
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=_test):0:6:_test',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', '_test'),
+ })
+ check_parsing('g:{@a}_test()', {
+ -- 0123456789012
+ ast = {
+ {
+ 'Call:0:11:(',
+ children = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=_test):0:6:_test',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', '_test'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('{@a} ()', {
+ -- 0123456789012
+ ast = {
+ {
+ 'Call:0:4: (',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('CallingParenthesis', '(', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('g:{@a} ()', {
+ -- 0123456789012
+ ast = {
+ {
+ 'Call:0:6: (',
+ children = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('CallingParenthesis', '(', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('{@a', {
+ -- 012
+ ast = {
+ {
+ fmtn('UnknownFigure', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ err = {
+ arg = '{@a',
+ msg = 'E15: Missing closing figure brace: %.*s',
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('Register', '@a'),
+ })
+ check_parsing('a ()', {
+ -- 0123
+ ast = {
+ {
+ 'Call:0:1: (',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('CallingParenthesis', '(', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ end)
+ itp('works with lambdas and dictionaries', function()
+ check_parsing('{}', {
+ ast = {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Dict', '}'),
+ })
+ check_parsing('{->@a}', {
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:1:->',
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{->@a+@b}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:1:->',
+ children = {
+ {
+ 'BinaryPlus:0:5:+',
+ children = {
+ 'Register(name=a):0:3:@a',
+ 'Register(name=b):0:6:@b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a->@a}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Arrow:0:2:->',
+ children = {
+ 'Register(name=a):0:4:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b->@a}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ 'Register(name=a):0:6:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b,c->@a}', {
+ -- 01234567890
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:4:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ },
+ },
+ {
+ 'Arrow:0:6:->',
+ children = {
+ 'Register(name=a):0:8:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b,c,d->@a}', {
+ -- 0123456789012
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:4:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ {
+ 'Comma:0:6:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ 'PlainIdentifier(scope=0,ident=d):0:7:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Arrow:0:8:->',
+ children = {
+ 'Register(name=a):0:10:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b,c,d,->@a}', {
+ -- 01234567890123
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:4:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ {
+ 'Comma:0:6:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ {
+ 'Comma:0:8:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:7:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Arrow:0:9:->',
+ children = {
+ 'Register(name=a):0:11:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Comma', ','),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b->{c,d->{e,f->@a}}}', {
+ -- 01234567890123456789012
+ -- 0 1 2
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:6:{'),
+ children = {
+ {
+ 'Comma:0:8:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:7:c',
+ 'PlainIdentifier(scope=0,ident=d):0:9:d',
+ },
+ },
+ {
+ 'Arrow:0:10:->',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:12:{'),
+ children = {
+ {
+ 'Comma:0:14:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=e):0:13:e',
+ 'PlainIdentifier(scope=0,ident=f):0:15:f',
+ },
+ },
+ {
+ 'Arrow:0:16:->',
+ children = {
+ 'Register(name=a):0:18:@a',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Arrow', '->'),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'e'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'f'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ hl('Lambda', '}'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b->c,d}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ {
+ 'Comma:0:7:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:6:c',
+ 'PlainIdentifier(scope=0,ident=d):0:8:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',d}',
+ msg = 'E15: Comma outside of call, lambda or literal: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('IdentifierName', 'c'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('a,b,c,d', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comma:0:1:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comma:0:3:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ 'PlainIdentifier(scope=0,ident=d):0:6:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',b,c,d',
+ msg = 'E15: Comma outside of call, lambda or literal: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'b'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'c'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'd'),
+ })
+ check_parsing('a,b,c,d,', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comma:0:1:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comma:0:3:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ {
+ 'Comma:0:7:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:6:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',b,c,d,',
+ msg = 'E15: Comma outside of call, lambda or literal: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'b'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'c'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'd'),
+ hl('InvalidComma', ','),
+ })
+ check_parsing(',', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comma:0:0:,',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = ',',
+ msg = 'E15: Expected value, got comma: %.*s',
+ },
+ }, {
+ hl('InvalidComma', ','),
+ })
+ check_parsing('{,a->@a}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:3:->',
+ children = {
+ {
+ 'Comma:0:1:,',
+ children = {
+ 'Missing:0:1:',
+ 'PlainIdentifier(scope=0,ident=a):0:2:a',
+ },
+ },
+ 'Register(name=a):0:5:@a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',a->@a}',
+ msg = 'E15: Expected value, got comma: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'a'),
+ hl('InvalidArrow', '->'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('}', {
+ -- 0123456789
+ ast = {
+ fmtn('UnknownFigure', '---', ':0:0:'),
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('InvalidFigureBrace', '}'),
+ })
+ check_parsing('{->}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ 'Arrow:0:1:->',
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected value, got closing figure brace: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('InvalidLambda', '}'),
+ })
+ check_parsing('{a,b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected lambda arguments list or arrow: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('InvalidLambda', '}'),
+ })
+ check_parsing('{a,}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected lambda arguments list or arrow: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('InvalidLambda', '}'),
+ })
+ check_parsing('{@a:@b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Dict', '}'),
+ })
+ check_parsing('{@a:@b,@c:@d}', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ {
+ 'Colon:0:9::',
+ children = {
+ 'Register(name=c):0:7:@c',
+ 'Register(name=d):0:10:@d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c'),
+ hl('Colon', ':'),
+ hl('Register', '@d'),
+ hl('Dict', '}'),
+ })
+ check_parsing('{@a:@b,@c:@d,@e:@f,}', {
+ -- 01234567890123456789
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ {
+ 'Comma:0:12:,',
+ children = {
+ {
+ 'Colon:0:9::',
+ children = {
+ 'Register(name=c):0:7:@c',
+ 'Register(name=d):0:10:@d',
+ },
+ },
+ {
+ 'Comma:0:18:,',
+ children = {
+ {
+ 'Colon:0:15::',
+ children = {
+ 'Register(name=e):0:13:@e',
+ 'Register(name=f):0:16:@f',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c'),
+ hl('Colon', ':'),
+ hl('Register', '@d'),
+ hl('Comma', ','),
+ hl('Register', '@e'),
+ hl('Colon', ':'),
+ hl('Register', '@f'),
+ hl('Comma', ','),
+ hl('Dict', '}'),
+ })
+ check_parsing('{@a:@b,@c:@d,@e:@f,@g:}', {
+ -- 01234567890123456789012
+ -- 0 1 2
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ {
+ 'Comma:0:12:,',
+ children = {
+ {
+ 'Colon:0:9::',
+ children = {
+ 'Register(name=c):0:7:@c',
+ 'Register(name=d):0:10:@d',
+ },
+ },
+ {
+ 'Comma:0:18:,',
+ children = {
+ {
+ 'Colon:0:15::',
+ children = {
+ 'Register(name=e):0:13:@e',
+ 'Register(name=f):0:16:@f',
+ },
+ },
+ {
+ 'Colon:0:21::',
+ children = {
+ 'Register(name=g):0:19:@g',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected value, got closing figure brace: %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c'),
+ hl('Colon', ':'),
+ hl('Register', '@d'),
+ hl('Comma', ','),
+ hl('Register', '@e'),
+ hl('Colon', ':'),
+ hl('Register', '@f'),
+ hl('Comma', ','),
+ hl('Register', '@g'),
+ hl('Colon', ':'),
+ hl('InvalidDict', '}'),
+ })
+ check_parsing('{@a:@b,}', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Dict', '}'),
+ })
+ check_parsing('{({f -> g})(@h)(@i)}', {
+ -- 01234567890123456789
+ -- 0 1
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Call:0:15:(',
+ children = {
+ {
+ 'Call:0:11:(',
+ children = {
+ {
+ 'Nested:0:1:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:2:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=f):0:3:f',
+ {
+ 'Arrow:0:4: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=g):0:7: g',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=h):0:12:@h',
+ },
+ },
+ 'Register(name=i):0:16:@i',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('NestingParenthesis', '('),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'f'),
+ hl('Arrow', '->', 1),
+ hl('IdentifierName', 'g', 1),
+ hl('Lambda', '}'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@i'),
+ hl('CallingParenthesis', ')'),
+ hl('Curly', '}'),
+ })
+ check_parsing('a:{b()}c', {
+ -- 01234567
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=a,ident=):0:0:a:',
+ {
+ 'ComplexIdentifier:0:7:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ {
+ 'Call:0:4:(',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:7:c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ })
+ check_parsing('a:{{b, c -> @d + @e + ({f -> g})(@h)}(@i)}j', {
+ -- 01234567890123456789012345678901234567890123456
+ -- 0 1 2 3 4
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=a,ident=):0:0:a:',
+ {
+ 'ComplexIdentifier:0:42:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ {
+ 'Call:0:37:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:3:{'),
+ children = {
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4:b',
+ 'PlainIdentifier(scope=0,ident=c):0:6: c',
+ },
+ },
+ {
+ 'Arrow:0:8: ->',
+ children = {
+ {
+ 'BinaryPlus:0:19: +',
+ children = {
+ {
+ 'BinaryPlus:0:14: +',
+ children = {
+ 'Register(name=d):0:11: @d',
+ 'Register(name=e):0:16: @e',
+ },
+ },
+ {
+ 'Call:0:32:(',
+ children = {
+ {
+ 'Nested:0:21: (',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:23:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=f):0:24:f',
+ {
+ 'Arrow:0:25: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=g):0:28: g',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=h):0:33:@h',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=i):0:38:@i',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=j):0:42:j',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('Arrow', '->', 1),
+ hl('Register', '@d', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@e', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'f'),
+ hl('Arrow', '->', 1),
+ hl('IdentifierName', 'g', 1),
+ hl('Lambda', '}'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('Lambda', '}'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@i'),
+ hl('CallingParenthesis', ')'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'j'),
+ })
+ check_parsing('{@a + @b : @c + @d, @e + @f : @g + @i}', {
+ -- 01234567890123456789012345678901234567
+ -- 0 1 2 3
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:18:,',
+ children = {
+ {
+ 'Colon:0:8: :',
+ children = {
+ {
+ 'BinaryPlus:0:3: +',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:5: @b',
+ },
+ },
+ {
+ 'BinaryPlus:0:13: +',
+ children = {
+ 'Register(name=c):0:10: @c',
+ 'Register(name=d):0:15: @d',
+ },
+ },
+ },
+ },
+ {
+ 'Colon:0:27: :',
+ children = {
+ {
+ 'BinaryPlus:0:22: +',
+ children = {
+ 'Register(name=e):0:19: @e',
+ 'Register(name=f):0:24: @f',
+ },
+ },
+ {
+ 'BinaryPlus:0:32: +',
+ children = {
+ 'Register(name=g):0:29: @g',
+ 'Register(name=i):0:34: @i',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@b', 1),
+ hl('Colon', ':', 1),
+ hl('Register', '@c', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@d', 1),
+ hl('Comma', ','),
+ hl('Register', '@e', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@f', 1),
+ hl('Colon', ':', 1),
+ hl('Register', '@g', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@i', 1),
+ hl('Dict', '}'),
+ })
+ check_parsing('-> -> ->', {
+ -- 01234567
+ ast = {
+ {
+ 'Arrow:0:0:->',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'Arrow:0:2: ->',
+ children = {
+ 'Missing:0:2:',
+ {
+ 'Arrow:0:5: ->',
+ children = {
+ 'Missing:0:5:',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> -> ->',
+ msg = 'E15: Unexpected arrow: %.*s',
+ },
+ }, {
+ hl('InvalidArrow', '->'),
+ hl('InvalidArrow', '->', 1),
+ hl('InvalidArrow', '->', 1),
+ })
+ check_parsing('a -> b -> c -> d', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Arrow:0:1: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Arrow:0:6: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ {
+ 'Arrow:0:11: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:9: c',
+ 'PlainIdentifier(scope=0,ident=d):0:14: d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b -> c -> d',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+ check_parsing('{a -> b -> c}', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Arrow:0:2: ->',
+ children = {
+ {
+ 'Arrow:0:7: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ 'PlainIdentifier(scope=0,ident=c):0:10: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> c}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Arrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a: -> b}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:3: ->',
+ children = {
+ 'PlainIdentifier(scope=a,ident=):0:1:a:',
+ 'PlainIdentifier(scope=0,ident=b):0:6: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Curly', '}'),
+ })
+
+ check_parsing('{a:b -> b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:4: ->',
+ children = {
+ 'PlainIdentifier(scope=a,ident=b):0:1:a:b',
+ 'PlainIdentifier(scope=0,ident=b):0:7: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'b'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Curly', '}'),
+ })
+
+ check_parsing('{a#b -> b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:4: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a#b):0:1:a#b',
+ 'PlainIdentifier(scope=0,ident=b):0:7: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierName', 'a#b'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Curly', '}'),
+ })
+ check_parsing('{a : b : c}', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Colon:0:6: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ 'PlainIdentifier(scope=0,ident=c):0:8: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ': c}',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Colon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('Dict', '}'),
+ })
+ check_parsing('{', {
+ -- 0
+ ast = {
+ fmtn('UnknownFigure', '\\di', ':0:0:{'),
+ },
+ err = {
+ arg = '{',
+ msg = 'E15: Missing closing figure brace: %.*s',
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ })
+ check_parsing('{a', {
+ -- 01
+ ast = {
+ {
+ fmtn('UnknownFigure', '\\di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ err = {
+ arg = '{a',
+ msg = 'E15: Missing closing figure brace: %.*s',
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ })
+ check_parsing('{a,b', {
+ -- 0123
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{a,b',
+ msg = 'E15: Missing closing figure brace for lambda: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ })
+ check_parsing('{a,b->', {
+ -- 012345
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ 'Arrow:0:4:->',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ })
+ check_parsing('{a,b->c', {
+ -- 0123456
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:6:c',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{a,b->c',
+ msg = 'E15: Missing closing figure brace for lambda: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('IdentifierName', 'c'),
+ })
+ check_parsing('{a : b', {
+ -- 012345
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{a : b',
+ msg = 'E723: Missing end of Dictionary \'}\': %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Colon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+ check_parsing('{a : b,', {
+ -- 0123456
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Colon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ })
+ end)
+ itp('works with ternary operator', function()
+ check_parsing('a ? b : c', {
+ -- 012345678
+ ast = {
+ {
+ 'Ternary:0:1: ?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:5: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('TernaryColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ })
+ check_parsing('@a?@b?@c:@d:@e', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:2:?',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'TernaryValue:0:11::',
+ children = {
+ {
+ 'Ternary:0:5:?',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'TernaryValue:0:8::',
+ children = {
+ 'Register(name=c):0:6:@c',
+ 'Register(name=d):0:9:@d',
+ },
+ },
+ },
+ },
+ 'Register(name=e):0:12:@e',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('Ternary', '?'),
+ hl('Register', '@c'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ })
+ check_parsing('@a?@b:@c?@d:@e', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:2:?',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'TernaryValue:0:5::',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'Ternary:0:8:?',
+ children = {
+ 'Register(name=c):0:6:@c',
+ {
+ 'TernaryValue:0:11::',
+ children = {
+ 'Register(name=d):0:9:@d',
+ 'Register(name=e):0:12:@e',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ })
+ check_parsing('@a?@b?@c?@d:@e?@f:@g:@h?@i:@j:@k', {
+ -- 01234567890123456789012345678901
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Ternary:0:2:?',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'TernaryValue:0:29::',
+ children = {
+ {
+ 'Ternary:0:5:?',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'TernaryValue:0:20::',
+ children = {
+ {
+ 'Ternary:0:8:?',
+ children = {
+ 'Register(name=c):0:6:@c',
+ {
+ 'TernaryValue:0:11::',
+ children = {
+ 'Register(name=d):0:9:@d',
+ {
+ 'Ternary:0:14:?',
+ children = {
+ 'Register(name=e):0:12:@e',
+ {
+ 'TernaryValue:0:17::',
+ children = {
+ 'Register(name=f):0:15:@f',
+ 'Register(name=g):0:18:@g',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Ternary:0:23:?',
+ children = {
+ 'Register(name=h):0:21:@h',
+ {
+ 'TernaryValue:0:26::',
+ children = {
+ 'Register(name=i):0:24:@i',
+ 'Register(name=j):0:27:@j',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=k):0:30:@k',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('Ternary', '?'),
+ hl('Register', '@c'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ hl('Ternary', '?'),
+ hl('Register', '@f'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@g'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@h'),
+ hl('Ternary', '?'),
+ hl('Register', '@i'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@j'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@k'),
+ })
+ check_parsing('?', {
+ -- 0
+ ast = {
+ {
+ 'Ternary:0:0:?',
+ children = {
+ 'Missing:0:0:',
+ 'TernaryValue:0:0:?',
+ },
+ },
+ },
+ err = {
+ arg = '?',
+ msg = 'E15: Expected value, got question mark: %.*s',
+ },
+ }, {
+ hl('InvalidTernary', '?'),
+ })
+
+ check_parsing('?:', {
+ -- 01
+ ast = {
+ {
+ 'Ternary:0:0:?',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'TernaryValue:0:1::',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?:',
+ msg = 'E15: Expected value, got question mark: %.*s',
+ },
+ }, {
+ hl('InvalidTernary', '?'),
+ hl('InvalidTernaryColon', ':'),
+ })
+
+ check_parsing('?::', {
+ -- 012
+ ast = {
+ {
+ 'Colon:0:2::',
+ children = {
+ {
+ 'Ternary:0:0:?',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'TernaryValue:0:1::',
+ children = {
+ 'Missing:0:1:',
+ 'Missing:0:2:',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?::',
+ msg = 'E15: Expected value, got question mark: %.*s',
+ },
+ }, {
+ hl('InvalidTernary', '?'),
+ hl('InvalidTernaryColon', ':'),
+ hl('InvalidColon', ':'),
+ })
+
+ check_parsing('a?b', {
+ -- 012
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?b',
+ msg = 'E109: Missing \':\' after \'?\': %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierName', 'b'),
+ })
+ check_parsing('a?b:', {
+ -- 0123
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=b,ident=):0:2:b:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?b:',
+ msg = 'E109: Missing \':\' after \'?\': %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ })
+
+ check_parsing('a?b::c', {
+ -- 012345
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:4::',
+ children = {
+ 'PlainIdentifier(scope=b,ident=):0:2:b:',
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('TernaryColon', ':'),
+ hl('IdentifierName', 'c'),
+ })
+
+ check_parsing('a?b :', {
+ -- 01234
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:3: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierName', 'b'),
+ hl('TernaryColon', ':', 1),
+ })
+
+ check_parsing('(@a?@b:@c)?@d:@e', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:10:?',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Ternary:0:3:?',
+ children = {
+ 'Register(name=a):0:1:@a',
+ {
+ 'TernaryValue:0:6::',
+ children = {
+ 'Register(name=b):0:4:@b',
+ 'Register(name=c):0:7:@c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'TernaryValue:0:13::',
+ children = {
+ 'Register(name=d):0:11:@d',
+ 'Register(name=e):0:14:@e',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('NestingParenthesis', ')'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ })
+
+ check_parsing('(@a?@b:@c)?(@d?@e:@f):(@g?@h:@i)', {
+ -- 01234567890123456789012345678901
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Ternary:0:10:?',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Ternary:0:3:?',
+ children = {
+ 'Register(name=a):0:1:@a',
+ {
+ 'TernaryValue:0:6::',
+ children = {
+ 'Register(name=b):0:4:@b',
+ 'Register(name=c):0:7:@c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'TernaryValue:0:21::',
+ children = {
+ {
+ 'Nested:0:11:(',
+ children = {
+ {
+ 'Ternary:0:14:?',
+ children = {
+ 'Register(name=d):0:12:@d',
+ {
+ 'TernaryValue:0:17::',
+ children = {
+ 'Register(name=e):0:15:@e',
+ 'Register(name=f):0:18:@f',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Nested:0:22:(',
+ children = {
+ {
+ 'Ternary:0:25:?',
+ children = {
+ 'Register(name=g):0:23:@g',
+ {
+ 'TernaryValue:0:28::',
+ children = {
+ 'Register(name=h):0:26:@h',
+ 'Register(name=i):0:29:@i',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('NestingParenthesis', ')'),
+ hl('Ternary', '?'),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@d'),
+ hl('Ternary', '?'),
+ hl('Register', '@e'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@f'),
+ hl('NestingParenthesis', ')'),
+ hl('TernaryColon', ':'),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@g'),
+ hl('Ternary', '?'),
+ hl('Register', '@h'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@i'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(@a?@b:@c)?@d?@e:@f:@g?@h:@i', {
+ -- 0123456789012345678901234567
+ -- 0 1 2
+ ast = {
+ {
+ 'Ternary:0:10:?',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Ternary:0:3:?',
+ children = {
+ 'Register(name=a):0:1:@a',
+ {
+ 'TernaryValue:0:6::',
+ children = {
+ 'Register(name=b):0:4:@b',
+ 'Register(name=c):0:7:@c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'TernaryValue:0:19::',
+ children = {
+ {
+ 'Ternary:0:13:?',
+ children = {
+ 'Register(name=d):0:11:@d',
+ {
+ 'TernaryValue:0:16::',
+ children = {
+ 'Register(name=e):0:14:@e',
+ 'Register(name=f):0:17:@f',
+ },
+ },
+ },
+ },
+ {
+ 'Ternary:0:22:?',
+ children = {
+ 'Register(name=g):0:20:@g',
+ {
+ 'TernaryValue:0:25::',
+ children = {
+ 'Register(name=h):0:23:@h',
+ 'Register(name=i):0:26:@i',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('NestingParenthesis', ')'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('Ternary', '?'),
+ hl('Register', '@e'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@f'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@g'),
+ hl('Ternary', '?'),
+ hl('Register', '@h'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@i'),
+ })
+ check_parsing('a?b{cdef}g:h', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:10::',
+ children = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ {
+ 'ComplexIdentifier:0:9:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=cdef):0:4:cdef',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=g):0:9:g',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=h):0:11:h',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'cdef'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'g'),
+ hl('TernaryColon', ':'),
+ hl('IdentifierName', 'h'),
+ })
+ check_parsing('a ? b : c : d', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ 'Colon:0:9: :',
+ children = {
+ {
+ 'Ternary:0:1: ?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:5: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=d):0:11: d',
+ },
+ },
+ },
+ err = {
+ arg = ': d',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('TernaryColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+ end)
+ itp('works with comparison operators', function()
+ check_parsing('a == b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=UseOption):0:1: ==',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '==', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a ==? b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=IgnoreCase):0:1: ==?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '==', 1),
+ hl('ComparisonModifier', '?'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a ==# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=MatchCase):0:1: ==#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '==', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a !=# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=1,ccs=MatchCase):0:1: !=#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '!=', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a <=# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Greater,inv=1,ccs=MatchCase):0:1: <=#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<=', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a >=# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=0,ccs=MatchCase):0:1: >=#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '>=', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a ># b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Greater,inv=0,ccs=MatchCase):0:1: >#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '>', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a <# b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=MatchCase):0:1: <#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a is#b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Identical,inv=0,ccs=MatchCase):0:1: is#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', 'is', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_parsing('a is?b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Identical,inv=0,ccs=IgnoreCase):0:1: is?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', 'is', 1),
+ hl('ComparisonModifier', '?'),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_parsing('a isnot b', {
+ -- 012345678
+ ast = {
+ {
+ 'Comparison(type=Identical,inv=1,ccs=UseOption):0:1: isnot',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:7: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', 'isnot', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a < b < c', {
+ -- 012345678
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=UseOption):0:1: <',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=UseOption):0:5: <',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ' < c',
+ msg = 'E15: Operator is not associative: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidComparison', '<', 1),
+ hl('IdentifierName', 'c', 1),
+ })
+
+ check_parsing('a < b <# c', {
+ -- 012345678
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=UseOption):0:1: <',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=MatchCase):0:5: <#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:8: c',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ' <# c',
+ msg = 'E15: Operator is not associative: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidComparison', '<', 1),
+ hl('InvalidComparisonModifier', '#'),
+ hl('IdentifierName', 'c', 1),
+ })
+
+ check_parsing('a += b', {
+ -- 012345
+ ast = {
+ {
+ 'Assignment(Add):0:1: +=',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ err = {
+ arg = '+= b',
+ msg = 'E15: Misplaced assignment: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidAssignmentWithAddition', '+=', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+ check_parsing('a + b == c + d', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=UseOption):0:5: ==',
+ children = {
+ {
+ 'BinaryPlus:0:1: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ },
+ },
+ {
+ 'BinaryPlus:0:10: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:8: c',
+ 'PlainIdentifier(scope=0,ident=d):0:12: d',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('BinaryPlus', '+', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Comparison', '==', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+ check_parsing('+ a == + b', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=UseOption):0:3: ==',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1: a',
+ },
+ },
+ {
+ 'UnaryPlus:0:6: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:8: b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('IdentifierName', 'a', 1),
+ hl('Comparison', '==', 1),
+ hl('UnaryPlus', '+', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+ end)
+ itp('works with concat/subscript', function()
+ check_parsing('.', {
+ -- 0
+ ast = {
+ {
+ 'ConcatOrSubscript:0:0:.',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = '.',
+ msg = 'E15: Unexpected dot: %.*s',
+ },
+ }, {
+ hl('InvalidConcatOrSubscript', '.'),
+ })
+
+ check_parsing('a.', {
+ -- 01
+ ast = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ })
+
+ check_parsing('a.b', {
+ -- 012
+ ast = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainKey(key=b):0:2:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', 'b'),
+ })
+
+ check_parsing('1.2', {
+ -- 012
+ ast = {
+ 'Float(val=1.200000e+00):0:0:1.2',
+ },
+ }, {
+ hl('Float', '1.2'),
+ })
+
+ check_parsing('1.2 + 1.3e-5', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'BinaryPlus:0:3: +',
+ children = {
+ 'Float(val=1.200000e+00):0:0:1.2',
+ 'Float(val=1.300000e-05):0:5: 1.3e-5',
+ },
+ },
+ },
+ }, {
+ hl('Float', '1.2'),
+ hl('BinaryPlus', '+', 1),
+ hl('Float', '1.3e-5', 1),
+ })
+
+ check_parsing('a . 1.2 + 1.3e-5', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'BinaryPlus:0:7: +',
+ children = {
+ {
+ 'Concat:0:1: .',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ConcatOrSubscript:0:5:.',
+ children = {
+ 'Integer(val=1):0:3: 1',
+ 'PlainKey(key=2):0:6:2',
+ },
+ },
+ },
+ },
+ 'Float(val=1.300000e-05):0:9: 1.3e-5',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Concat', '.', 1),
+ hl('Number', '1', 1),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ hl('BinaryPlus', '+', 1),
+ hl('Float', '1.3e-5', 1),
+ })
+
+ check_parsing('1.3e-5 + 1.2 . a', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Concat:0:12: .',
+ children = {
+ {
+ 'BinaryPlus:0:6: +',
+ children = {
+ 'Float(val=1.300000e-05):0:0:1.3e-5',
+ 'Float(val=1.200000e+00):0:8: 1.2',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=a):0:14: a',
+ },
+ },
+ },
+ }, {
+ hl('Float', '1.3e-5'),
+ hl('BinaryPlus', '+', 1),
+ hl('Float', '1.2', 1),
+ hl('Concat', '.', 1),
+ hl('IdentifierName', 'a', 1),
+ })
+
+ check_parsing('1.3e-5 + a . 1.2', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Concat:0:10: .',
+ children = {
+ {
+ 'BinaryPlus:0:6: +',
+ children = {
+ 'Float(val=1.300000e-05):0:0:1.3e-5',
+ 'PlainIdentifier(scope=0,ident=a):0:8: a',
+ },
+ },
+ {
+ 'ConcatOrSubscript:0:14:.',
+ children = {
+ 'Integer(val=1):0:12: 1',
+ 'PlainKey(key=2):0:15:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Float', '1.3e-5'),
+ hl('BinaryPlus', '+', 1),
+ hl('IdentifierName', 'a', 1),
+ hl('Concat', '.', 1),
+ hl('Number', '1', 1),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ })
+
+ check_parsing('1.2.3', {
+ -- 01234
+ ast = {
+ {
+ 'ConcatOrSubscript:0:3:.',
+ children = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'Integer(val=1):0:0:1',
+ 'PlainKey(key=2):0:2:2',
+ },
+ },
+ 'PlainKey(key=3):0:4:3',
+ },
+ },
+ },
+ }, {
+ hl('Number', '1'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '3'),
+ })
+
+ check_parsing('a.1.2', {
+ -- 01234
+ ast = {
+ {
+ 'ConcatOrSubscript:0:3:.',
+ children = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainKey(key=1):0:2:1',
+ },
+ },
+ 'PlainKey(key=2):0:4:2',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '1'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ })
+
+ check_parsing('a . 1.2', {
+ -- 0123456
+ ast = {
+ {
+ 'Concat:0:1: .',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ConcatOrSubscript:0:5:.',
+ children = {
+ 'Integer(val=1):0:3: 1',
+ 'PlainKey(key=2):0:6:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Concat', '.', 1),
+ hl('Number', '1', 1),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ })
+
+ check_parsing('+a . +b', {
+ -- 0123456
+ ast = {
+ {
+ 'Concat:0:2: .',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ {
+ 'UnaryPlus:0:4: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:6:b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('IdentifierName', 'a'),
+ hl('Concat', '.', 1),
+ hl('UnaryPlus', '+', 1),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_parsing('a. b', {
+ -- 0123
+ ast = {
+ {
+ 'Concat:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:2: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a. 1', {
+ -- 0123
+ ast = {
+ {
+ 'Concat:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'Integer(val=1):0:2: 1',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('Number', '1', 1),
+ })
+ end)
+ itp('works with bracket subscripts', function()
+ check_parsing(':', {
+ -- 0
+ ast = {
+ {
+ 'Colon:0:0::',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = ':',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('InvalidColon', ':'),
+ })
+ check_parsing('a[]', {
+ -- 012
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Expected value, got closing bracket: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('InvalidSubscriptBracket', ']'),
+ })
+ check_parsing('a[b:]', {
+ -- 01234
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=b,ident=):0:2:b:',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_parsing('a[b:c]', {
+ -- 012345
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=b,ident=c):0:2:b:c',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', ']'),
+ })
+ check_parsing('a[b : c]', {
+ -- 01234567
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Colon:0:3: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ 'PlainIdentifier(scope=0,ident=c):0:5: c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_parsing('a[: b]', {
+ -- 012345
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Colon:0:2::',
+ children = {
+ 'Missing:0:2:',
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('SubscriptColon', ':'),
+ hl('IdentifierName', 'b', 1),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_parsing('a[b :]', {
+ -- 012345
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Colon:0:3: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptColon', ':', 1),
+ hl('SubscriptBracket', ']'),
+ })
+ check_parsing('a[b][c][d](e)(f)(g)', {
+ -- 0123456789012345678
+ -- 0 1
+ ast = {
+ {
+ 'Call:0:16:(',
+ children = {
+ {
+ 'Call:0:13:(',
+ children = {
+ {
+ 'Call:0:10:(',
+ children = {
+ {
+ 'Subscript:0:7:[',
+ children = {
+ {
+ 'Subscript:0:4:[',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=d):0:8:d',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=e):0:11:e',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=f):0:14:f',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=g):0:17:g',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'd'),
+ hl('SubscriptBracket', ']'),
+ hl('CallingParenthesis', '('),
+ hl('IdentifierName', 'e'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('IdentifierName', 'f'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('IdentifierName', 'g'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('{a}{b}{c}[d][e][f]', {
+ -- 012345678901234567
+ -- 0 1
+ ast = {
+ {
+ 'Subscript:0:15:[',
+ children = {
+ {
+ 'Subscript:0:12:[',
+ children = {
+ {
+ 'Subscript:0:9:[',
+ children = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4:b',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:6:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:7:c',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=d):0:10:d',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=e):0:13:e',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=f):0:16:f',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'c'),
+ hl('Curly', '}'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'd'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'e'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'f'),
+ hl('SubscriptBracket', ']'),
+ })
+ end)
+ itp('supports list literals', function()
+ check_parsing('[]', {
+ -- 01
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ }, {
+ hl('List', '['),
+ hl('List', ']'),
+ })
+
+ check_parsing('[a]', {
+ -- 012
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('List', ']'),
+ })
+
+ check_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_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_parsing('[a, b, c, ]', {
+ -- 01234567890
+ -- 0 1
+ 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',
+ {
+ 'Comma:0:8:,',
+ children = {
+ '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('Comma', ','),
+ hl('List', ']', 1),
+ })
+
+ check_parsing('[a : b, c : d]', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ {
+ 'Colon:0:9: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ 'PlainIdentifier(scope=0,ident=d):0:11: d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ': b, c : d]',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'd', 1),
+ hl('List', ']'),
+ })
+
+ check_parsing(']', {
+ -- 0
+ ast = {
+ 'ListLiteral:0:0:',
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('InvalidList', ']'),
+ })
+
+ check_parsing('a]', {
+ -- 01
+ ast = {
+ {
+ 'ListLiteral:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidList', ']'),
+ })
+
+ check_parsing('[] []', {
+ -- 01234
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'ListLiteral:0:0:[',
+ 'ListLiteral:0:2: [',
+ },
+ },
+ },
+ err = {
+ arg = '[]',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('List', ']'),
+ hl('InvalidSpacing', ' '),
+ hl('List', '['),
+ hl('List', ']'),
+ }, {
+ [1] = {
+ ast = {
+ len = 3,
+ err = REMOVE_THIS,
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ },
+ hl_fs = {
+ [3] = REMOVE_THIS,
+ [4] = REMOVE_THIS,
+ [5] = REMOVE_THIS,
+ },
+ },
+ })
+
+ check_parsing('[][]', {
+ -- 0123
+ ast = {
+ {
+ 'Subscript:0:2:[',
+ children = {
+ 'ListLiteral:0:0:[',
+ },
+ },
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Expected value, got closing bracket: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('List', ']'),
+ hl('SubscriptBracket', '['),
+ hl('InvalidSubscriptBracket', ']'),
+ })
+
+ check_parsing('[', {
+ -- 0
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ })
+
+ check_parsing('[1', {
+ -- 01
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'Integer(val=1):0:1:1',
+ },
+ },
+ },
+ err = {
+ arg = '[1',
+ msg = 'E697: Missing end of List \']\': %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('Number', '1'),
+ })
+ end)
+ itp('works with strings', function()
+ check_parsing('\'abc\'', {
+ -- 01234
+ ast = {
+ fmtn('SingleQuotedString', 'val="abc"', ':0:0:\'abc\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedBody', 'abc'),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('"abc"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="abc"', ':0:0:"abc"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedBody', 'abc'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('\'\'', {
+ -- 01
+ ast = {
+ fmtn('SingleQuotedString', 'val=NULL', ':0:0:\'\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('""', {
+ -- 01
+ ast = {
+ fmtn('DoubleQuotedString', 'val=NULL', ':0:0:""'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"', {
+ -- 0
+ ast = {
+ fmtn('DoubleQuotedString', 'val=NULL', ':0:0:"'),
+ },
+ err = {
+ arg = '"',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ })
+ check_parsing('\'', {
+ -- 0
+ ast = {
+ fmtn('SingleQuotedString', 'val=NULL', ':0:0:\''),
+ },
+ err = {
+ arg = '\'',
+ msg = 'E115: Missing single quote: %.*s',
+ },
+ }, {
+ hl('InvalidSingleQuote', '\''),
+ })
+ check_parsing('"a', {
+ -- 01
+ ast = {
+ fmtn('DoubleQuotedString', 'val="a"', ':0:0:"a'),
+ },
+ err = {
+ arg = '"a',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedBody', 'a'),
+ })
+ check_parsing('\'a', {
+ -- 01
+ ast = {
+ fmtn('SingleQuotedString', 'val="a"', ':0:0:\'a'),
+ },
+ err = {
+ arg = '\'a',
+ msg = 'E115: Missing single quote: %.*s',
+ },
+ }, {
+ hl('InvalidSingleQuote', '\''),
+ hl('InvalidSingleQuotedBody', 'a'),
+ })
+ check_parsing('\'abc\'\'def\'', {
+ -- 0123456789
+ ast = {
+ fmtn('SingleQuotedString', 'val="abc\'def"', ':0:0:\'abc\'\'def\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedBody', 'abc'),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedBody', 'def'),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('\'abc\'\'', {
+ -- 012345
+ ast = {
+ fmtn('SingleQuotedString', 'val="abc\'"', ':0:0:\'abc\'\''),
+ },
+ err = {
+ arg = '\'abc\'\'',
+ msg = 'E115: Missing single quote: %.*s',
+ },
+ }, {
+ hl('InvalidSingleQuote', '\''),
+ hl('InvalidSingleQuotedBody', 'abc'),
+ hl('InvalidSingleQuotedQuote', '\'\''),
+ })
+ check_parsing('\'\'\'\'\'\'\'\'', {
+ -- 01234567
+ ast = {
+ fmtn('SingleQuotedString', 'val="\'\'\'"', ':0:0:\'\'\'\'\'\'\'\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('\'\'\'a\'\'\'\'bc\'', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ fmtn('SingleQuotedString', 'val="\'a\'\'bc"', ':0:0:\'\'\'a\'\'\'\'bc\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedBody', 'a'),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedBody', 'bc'),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('"\\"\\"\\"\\""', {
+ -- 0123456789
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\"\\"\\"\\""', ':0:0:"\\"\\"\\"\\""'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"abc\\"def\\"ghi\\"jkl\\"mno"', {
+ -- 0123456789012345678901234
+ -- 0 1 2
+ ast = {
+ fmtn('DoubleQuotedString', 'val="abc\\"def\\"ghi\\"jkl\\"mno"', ':0:0:"abc\\"def\\"ghi\\"jkl\\"mno"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedBody', 'abc'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'def'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'ghi'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'jkl'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'mno'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\b\\e\\f\\r\\t\\\\"', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ [[DoubleQuotedString(val="\8\27\12\13\9\\"):0:0:"\b\e\f\r\t\\"]],
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\b'),
+ hl('DoubleQuotedEscape', '\\e'),
+ hl('DoubleQuotedEscape', '\\f'),
+ hl('DoubleQuotedEscape', '\\r'),
+ hl('DoubleQuotedEscape', '\\t'),
+ hl('DoubleQuotedEscape', '\\\\'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\n\n"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\\n\\\n"', ':0:0:"\\n\n"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\n'),
+ hl('DoubleQuotedBody', '\n'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\x00"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0"', ':0:0:"\\x00"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x00'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\xFF"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\255"', ':0:0:"\\xFF"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\xFF'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\xF"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\15"', ':0:0:"\\xF"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\xF'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\u00AB"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="«"', ':0:0:"\\u00AB"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u00AB'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\U000000AB"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="«"', ':0:0:"\\U000000AB"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U000000AB'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\x"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="x"', ':0:0:"\\x"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\x'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="x"', ':0:0:"\\x'),
+ },
+ err = {
+ arg = '"\\x',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\x'),
+ })
+
+ check_parsing('"\\xF', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\15"', ':0:0:"\\xF'),
+ },
+ err = {
+ arg = '"\\xF',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedEscape', '\\xF'),
+ })
+
+ check_parsing('"\\u"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="u"', ':0:0:"\\u"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\u'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="u"', ':0:0:"\\u'),
+ },
+ err = {
+ arg = '"\\u',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\u'),
+ })
+
+ check_parsing('"\\U', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="U"', ':0:0:"\\U'),
+ },
+ err = {
+ arg = '"\\U',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ })
+
+ check_parsing('"\\U"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="U"', ':0:0:"\\U"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\U'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\xFX"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\15X"', ':0:0:"\\xFX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\xF'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\XFX"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\15X"', ':0:0:"\\XFX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\XF'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\xX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="xX"', ':0:0:"\\xX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\x'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\XX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="XX"', ':0:0:"\\XX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\X'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\uX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="uX"', ':0:0:"\\uX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\u'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\UX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="UX"', ':0:0:"\\UX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\U'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\x0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\X0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\X0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\X0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\u0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\x00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\X00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\X00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\X00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\u00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\u000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u0000X"', {
+ -- 012345678
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\u0000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u0000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U0000X"', {
+ -- 012345678
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U0000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U0000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U00000X"', {
+ -- 0123456789
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U00000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U000000X"', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U000000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U0000000X"', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U0000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U0000000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U00000000X"', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0X"', ':0:0:"\\U00000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00000000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\x000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x00'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\X000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\X000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\X00'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u00000X"', {
+ -- 0123456789
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\u00000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u0000'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U000000000X"', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\U000000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00000000'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\0"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0"', ':0:0:"\\0"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\0'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\00"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0"', ':0:0:"\\00"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\00'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\000"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0"', ':0:0:"\\000"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\000'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\0000"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000"', ':0:0:"\\0000"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\000'),
+ hl('DoubleQuotedBody', '0'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\8"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="8"', ':0:0:"\\8"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\08"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0008"', ':0:0:"\\08"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\0'),
+ hl('DoubleQuotedBody', '8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\008"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0008"', ':0:0:"\\008"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\00'),
+ hl('DoubleQuotedBody', '8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\0008"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0008"', ':0:0:"\\0008"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\000'),
+ hl('DoubleQuotedBody', '8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\777"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\255"', ':0:0:"\\777"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\777'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\050"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\40"', ':0:0:"\\050"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\050'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\<C-u>"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\21"', ':0:0:"\\<C-u>"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\<C-u>'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\<', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<"', ':0:0:"\\<'),
+ },
+ err = {
+ arg = '"\\<',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\<'),
+ })
+
+ check_parsing('"\\<"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<"', ':0:0:"\\<"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\<'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\<C-u"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<C-u"', ':0:0:"\\<C-u"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\<'),
+ hl('DoubleQuotedBody', 'C-u'),
+ hl('DoubleQuote', '"'),
+ })
+ end)
+ itp('works with multiplication-like operators', function()
+ check_parsing('2+2*2', {
+ -- 01234
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Multiplication:0:3:*',
+ children = {
+ 'Integer(val=2):0:2:2',
+ 'Integer(val=2):0:4:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Multiplication', '*'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2*', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Multiplication:0:3:*',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Multiplication', '*'),
+ })
+
+ check_parsing('2+*2', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Multiplication:0:2:*',
+ children = {
+ 'Missing:0:2:',
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '*2',
+ msg = 'E15: Unexpected multiplication-like operator: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidMultiplication', '*'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2/2', {
+ -- 01234
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Division:0:3:/',
+ children = {
+ 'Integer(val=2):0:2:2',
+ 'Integer(val=2):0:4:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Division', '/'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2/', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Division:0:3:/',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Division', '/'),
+ })
+
+ check_parsing('2+/2', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Division:0:2:/',
+ children = {
+ 'Missing:0:2:',
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '/2',
+ msg = 'E15: Unexpected multiplication-like operator: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidDivision', '/'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2%2', {
+ -- 01234
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Mod:0:3:%',
+ children = {
+ 'Integer(val=2):0:2:2',
+ 'Integer(val=2):0:4:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Mod', '%'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2%', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Mod:0:3:%',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Mod', '%'),
+ })
+
+ check_parsing('2+%2', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Mod:0:2:%',
+ children = {
+ 'Missing:0:2:',
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '%2',
+ msg = 'E15: Unexpected multiplication-like operator: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidMod', '%'),
+ hl('Number', '2'),
+ })
+ end)
+ itp('works with -', function()
+ check_parsing('@a', {
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ }, {
+ hl('Register', '@a'),
+ })
+ check_parsing('-@a', {
+ ast = {
+ {
+ 'UnaryMinus:0:0:-',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a-@b', {
+ ast = {
+ {
+ 'BinaryMinus:0:2:-',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a-@b-@c', {
+ ast = {
+ {
+ 'BinaryMinus:0:5:-',
+ children = {
+ {
+ 'BinaryMinus: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('BinaryMinus', '-'),
+ hl('Register', '@b'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@c'),
+ })
+ check_parsing('-@a-@b', {
+ ast = {
+ {
+ 'BinaryMinus:0:3:-',
+ children = {
+ {
+ 'UnaryMinus:0:0:-',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@b'),
+ })
+ check_parsing('-@a--@b', {
+ ast = {
+ {
+ 'BinaryMinus:0:3:-',
+ children = {
+ {
+ 'UnaryMinus:0:0:-',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ 'UnaryMinus:0:4:-',
+ children = {
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('UnaryMinus', '-'),
+ hl('Register', '@b'),
+ })
+ check_parsing('-', {
+ ast = {
+ 'UnaryMinus:0:0:-',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ })
+ check_parsing(' -', {
+ ast = {
+ 'UnaryMinus:0:0: -',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryMinus', '-', 1),
+ })
+ check_parsing('@a- ', {
+ ast = {
+ {
+ 'BinaryMinus:0:2:-',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ })
+ end)
+ itp('works with logical operators', function()
+ check_parsing('a && b || c && d', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Or:0:6: ||',
+ children = {
+ {
+ 'And:0:1: &&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ {
+ 'And:0:11: &&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:9: c',
+ 'PlainIdentifier(scope=0,ident=d):0:14: d',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('And', '&&', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Or', '||', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('And', '&&', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+
+ check_parsing('&& a', {
+ -- 0123
+ ast = {
+ {
+ 'And:0:0:&&',
+ children = {
+ 'Missing:0:0:',
+ 'PlainIdentifier(scope=0,ident=a):0:2: a',
+ },
+ },
+ },
+ err = {
+ arg = '&& a',
+ msg = 'E15: Unexpected and operator: %.*s',
+ },
+ }, {
+ hl('InvalidAnd', '&&'),
+ hl('IdentifierName', 'a', 1),
+ })
+
+ check_parsing('|| a', {
+ -- 0123
+ ast = {
+ {
+ 'Or:0:0:||',
+ children = {
+ 'Missing:0:0:',
+ 'PlainIdentifier(scope=0,ident=a):0:2: a',
+ },
+ },
+ },
+ err = {
+ arg = '|| a',
+ msg = 'E15: Unexpected or operator: %.*s',
+ },
+ }, {
+ hl('InvalidOr', '||'),
+ hl('IdentifierName', 'a', 1),
+ })
+
+ check_parsing('a||', {
+ -- 012
+ ast = {
+ {
+ 'Or:0:1:||',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Or', '||'),
+ })
+
+ check_parsing('a&&', {
+ -- 012
+ ast = {
+ {
+ 'And:0:1:&&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('And', '&&'),
+ })
+
+ check_parsing('(&&)', {
+ -- 0123
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'And:0:1:&&',
+ children = {
+ 'Missing:0:1:',
+ 'Missing:0:3:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&&)',
+ msg = 'E15: Unexpected and operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidAnd', '&&'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(||)', {
+ -- 0123
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Or:0:1:||',
+ children = {
+ 'Missing:0:1:',
+ 'Missing:0:3:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '||)',
+ msg = 'E15: Unexpected or operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidOr', '||'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(a||)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Or:0:2:||',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('IdentifierName', 'a'),
+ hl('Or', '||'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(a&&)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'And:0:2:&&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('IdentifierName', 'a'),
+ hl('And', '&&'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(&&a)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'And:0:1:&&',
+ children = {
+ 'Missing:0:1:',
+ 'PlainIdentifier(scope=0,ident=a):0:3:a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&&a)',
+ msg = 'E15: Unexpected and operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidAnd', '&&'),
+ hl('IdentifierName', 'a'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(||a)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Or:0:1:||',
+ children = {
+ 'Missing:0:1:',
+ 'PlainIdentifier(scope=0,ident=a):0:3:a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '||a)',
+ msg = 'E15: Unexpected or operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidOr', '||'),
+ hl('IdentifierName', 'a'),
+ hl('NestingParenthesis', ')'),
+ })
+ end)
+ itp('works with &opt', function()
+ check_parsing('&', {
+ -- 0
+ ast = {
+ 'Option(scope=0,ident=):0:0:&',
+ },
+ err = {
+ arg = '&',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('InvalidOptionSigil', '&'),
+ })
+
+ check_parsing('&opt', {
+ -- 0123
+ ast = {
+ 'Option(scope=0,ident=opt):0:0:&opt',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'opt'),
+ })
+
+ check_parsing('&l:opt', {
+ -- 012345
+ ast = {
+ 'Option(scope=l,ident=opt):0:0:&l:opt',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionScope', 'l'),
+ hl('OptionScopeDelimiter', ':'),
+ hl('OptionName', 'opt'),
+ })
+
+ check_parsing('&g:opt', {
+ -- 012345
+ ast = {
+ 'Option(scope=g,ident=opt):0:0:&g:opt',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionScope', 'g'),
+ hl('OptionScopeDelimiter', ':'),
+ hl('OptionName', 'opt'),
+ })
+
+ check_parsing('&s:opt', {
+ -- 012345
+ ast = {
+ {
+ 'Colon:0:2::',
+ children = {
+ 'Option(scope=0,ident=s):0:0:&s',
+ 'PlainIdentifier(scope=0,ident=opt):0:3:opt',
+ },
+ },
+ },
+ err = {
+ arg = ':opt',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 's'),
+ hl('InvalidColon', ':'),
+ hl('IdentifierName', 'opt'),
+ })
+
+ check_parsing('& ', {
+ -- 01
+ ast = {
+ 'Option(scope=0,ident=):0:0:&',
+ },
+ err = {
+ arg = '& ',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('InvalidOptionSigil', '&'),
+ })
+
+ check_parsing('&-', {
+ -- 01
+ ast = {
+ {
+ 'BinaryMinus:0:1:-',
+ children = {
+ 'Option(scope=0,ident=):0:0:&',
+ },
+ },
+ },
+ err = {
+ arg = '&-',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('InvalidOptionSigil', '&'),
+ hl('BinaryMinus', '-'),
+ })
+
+ check_parsing('&A', {
+ -- 01
+ ast = {
+ 'Option(scope=0,ident=A):0:0:&A',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'A'),
+ })
+
+ check_parsing('&xxx_yyy', {
+ -- 01234567
+ ast = {
+ {
+ 'OpMissing:0:4:',
+ children = {
+ 'Option(scope=0,ident=xxx):0:0:&xxx',
+ 'PlainIdentifier(scope=0,ident=_yyy):0:4:_yyy',
+ },
+ },
+ },
+ err = {
+ arg = '_yyy',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'xxx'),
+ hl('InvalidIdentifierName', '_yyy'),
+ }, {
+ [1] = {
+ ast = {
+ len = 4,
+ err = REMOVE_THIS,
+ ast = {
+ 'Option(scope=0,ident=xxx):0:0:&xxx',
+ },
+ },
+ hl_fs = {
+ [3] = REMOVE_THIS,
+ },
+ },
+ })
+
+ check_parsing('(1+&)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Integer(val=1):0:1:1',
+ 'Option(scope=0,ident=):0:3:&',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&)',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Number', '1'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidOptionSigil', '&'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(&+1)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Option(scope=0,ident=):0:1:&',
+ 'Integer(val=1):0:3:1',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&+1)',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidOptionSigil', '&'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '1'),
+ hl('NestingParenthesis', ')'),
+ })
+ end)
+ itp('works with $ENV', function()
+ check_parsing('$', {
+ -- 0
+ ast = {
+ 'Environment(ident=):0:0:$',
+ },
+ err = {
+ arg = '$',
+ msg = 'E15: Environment variable name missing',
+ },
+ }, {
+ hl('InvalidEnvironmentSigil', '$'),
+ })
+
+ check_parsing('$g:A', {
+ -- 0123
+ ast = {
+ {
+ 'Colon:0:2::',
+ children = {
+ 'Environment(ident=g):0:0:$g',
+ 'PlainIdentifier(scope=0,ident=A):0:3:A',
+ },
+ },
+ },
+ err = {
+ arg = ':A',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'g'),
+ hl('InvalidColon', ':'),
+ hl('IdentifierName', 'A'),
+ })
+
+ check_parsing('$A', {
+ -- 01
+ ast = {
+ 'Environment(ident=A):0:0:$A',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'A'),
+ })
+
+ check_parsing('$ABC', {
+ -- 0123
+ ast = {
+ 'Environment(ident=ABC):0:0:$ABC',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'ABC'),
+ })
+
+ check_parsing('(1+$)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Integer(val=1):0:1:1',
+ 'Environment(ident=):0:3:$',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '$)',
+ msg = 'E15: Environment variable name missing',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Number', '1'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidEnvironmentSigil', '$'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('($+1)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Environment(ident=):0:1:$',
+ 'Integer(val=1):0:3:1',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '$+1)',
+ msg = 'E15: Environment variable name missing',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidEnvironmentSigil', '$'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '1'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('$_ABC', {
+ -- 01234
+ ast = {
+ 'Environment(ident=_ABC):0:0:$_ABC',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', '_ABC'),
+ })
+
+ check_parsing('$_', {
+ -- 01
+ ast = {
+ 'Environment(ident=_):0:0:$_',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', '_'),
+ })
+
+ check_parsing('$ABC_DEF', {
+ -- 01234567
+ ast = {
+ 'Environment(ident=ABC_DEF):0:0:$ABC_DEF',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'ABC_DEF'),
+ })
+ end)
+ itp('works with unary !', function()
+ check_parsing('!', {
+ -- 0
+ ast = {
+ 'Not:0:0:!',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Not', '!'),
+ })
+
+ check_parsing('!!', {
+ -- 01
+ ast = {
+ {
+ 'Not:0:0:!',
+ children = {
+ 'Not:0:1:!',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Not', '!'),
+ hl('Not', '!'),
+ })
+
+ check_parsing('!!1', {
+ -- 012
+ ast = {
+ {
+ 'Not:0:0:!',
+ children = {
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Not', '!'),
+ hl('Not', '!'),
+ hl('Number', '1'),
+ })
+
+ check_parsing('!1', {
+ -- 01
+ ast = {
+ {
+ 'Not:0:0:!',
+ children = {
+ 'Integer(val=1):0:1:1',
+ },
+ },
+ },
+ }, {
+ hl('Not', '!'),
+ hl('Number', '1'),
+ })
+
+ check_parsing('(!1)', {
+ -- 0123
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Not', '!'),
+ hl('Number', '1'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(!)', {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Missing:0:2:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Not', '!'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(1!2)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Integer(val=1):0:1:1',
+ {
+ 'Not:0:2:!',
+ children = {
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '!2)',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Number', '1'),
+ hl('InvalidNot', '!'),
+ hl('Number', '2'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('1!2', {
+ -- 012
+ ast = {
+ {
+ 'OpMissing:0:1:',
+ children = {
+ 'Integer(val=1):0:0:1',
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '!2',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Number', '1'),
+ hl('InvalidNot', '!'),
+ hl('Number', '2'),
+ }, {
+ [1] = {
+ ast = {
+ len = 1,
+ err = REMOVE_THIS,
+ ast = {
+ 'Integer(val=1):0:0:1',
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ },
+ },
+ })
+ end)
+ itp('highlights numbers with prefix', function()
+ check_parsing('0xABCDEF', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0xABCDEF',
+ },
+ }, {
+ hl('NumberPrefix', '0x'),
+ hl('Number', 'ABCDEF'),
+ })
+
+ check_parsing('0Xabcdef', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0Xabcdef',
+ },
+ }, {
+ hl('NumberPrefix', '0X'),
+ hl('Number', 'abcdef'),
+ })
+
+ check_parsing('0XABCDEF', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0XABCDEF',
+ },
+ }, {
+ hl('NumberPrefix', '0X'),
+ hl('Number', 'ABCDEF'),
+ })
+
+ check_parsing('0xabcdef', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0xabcdef',
+ },
+ }, {
+ hl('NumberPrefix', '0x'),
+ hl('Number', 'abcdef'),
+ })
+
+ check_parsing('0b001', {
+ -- 01234
+ ast = {
+ 'Integer(val=1):0:0:0b001',
+ },
+ }, {
+ hl('NumberPrefix', '0b'),
+ hl('Number', '001'),
+ })
+
+ check_parsing('0B001', {
+ -- 01234
+ ast = {
+ 'Integer(val=1):0:0:0B001',
+ },
+ }, {
+ hl('NumberPrefix', '0B'),
+ hl('Number', '001'),
+ })
+
+ check_parsing('0B00', {
+ -- 0123
+ ast = {
+ 'Integer(val=0):0:0:0B00',
+ },
+ }, {
+ hl('NumberPrefix', '0B'),
+ hl('Number', '00'),
+ })
+
+ check_parsing('00', {
+ -- 01
+ ast = {
+ 'Integer(val=0):0:0:00',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '0'),
+ })
+
+ check_parsing('001', {
+ -- 012
+ ast = {
+ 'Integer(val=1):0:0:001',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '01'),
+ })
+
+ check_parsing('01', {
+ -- 01
+ ast = {
+ 'Integer(val=1):0:0:01',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '1'),
+ })
+
+ check_parsing('1', {
+ -- 0
+ ast = {
+ 'Integer(val=1):0:0:1',
+ },
+ }, {
+ hl('Number', '1'),
+ })
+ end)
+ itp('works (KLEE tests)', function()
+ check_parsing('\0002&A:\000', {
+ len = 0,
+ ast = nil,
+ err = {
+ arg = '\0002&A:\000',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Colon:0:4::',
+ children = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Integer(val=2):0:1:2',
+ 'Option(scope=0,ident=A):0:2:&A',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidSpacing', '\0'),
+ hl('Number', '2'),
+ hl('InvalidOptionSigil', '&'),
+ hl('InvalidOptionName', 'A'),
+ hl('InvalidColon', ':'),
+ hl('InvalidSpacing', '\0'),
+ },
+ },
+ [3] = {
+ ast = {
+ len = 2,
+ ast = {
+ 'Integer(val=2):0:1:2',
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidSpacing', '\0'),
+ hl('Number', '2'),
+ },
+ },
+ })
+ check_parsing({data='01', size=1}, {
+ len = 1,
+ ast = {
+ 'Integer(val=0):0:0:0',
+ },
+ }, {
+ hl('Number', '0'),
+ })
+ check_parsing({data='001', size=2}, {
+ len = 2,
+ ast = {
+ 'Integer(val=0):0:0:00',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '0'),
+ })
+ check_parsing('"\\U\\', {
+ -- 0123
+ ast = {
+ [[DoubleQuotedString(val="U\\"):0:0:"\U\]],
+ },
+ err = {
+ arg = '"\\U\\',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ hl('InvalidDoubleQuotedBody', '\\'),
+ })
+ check_parsing('"\\U', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="U"', ':0:0:"\\U'),
+ },
+ err = {
+ arg = '"\\U',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ })
+ check_parsing('|"\\U\\', {
+ -- 01234
+ len = 0,
+ err = {
+ arg = '|"\\U\\',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Or:0:0:|',
+ children = {
+ 'Missing:0:0:',
+ fmtn('DoubleQuotedString', 'val="U\\\\"', ':0:1:"\\U\\'),
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidOr', '|'),
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ hl('InvalidDoubleQuotedBody', '\\'),
+ },
+ },
+ })
+ check_parsing('|"\\e"', {
+ -- 01234
+ len = 0,
+ err = {
+ arg = '|"\\e"',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Or:0:0:|',
+ children = {
+ 'Missing:0:0:',
+ fmtn('DoubleQuotedString', 'val="\\27"', ':0:1:"\\e"'),
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidOr', '|'),
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\e'),
+ hl('DoubleQuote', '"'),
+ },
+ },
+ })
+ check_parsing('|\029', {
+ -- 01
+ len = 0,
+ err = {
+ arg = '|\029',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Or:0:0:|',
+ children = {
+ 'Missing:0:0:',
+ 'PlainIdentifier(scope=0,ident=\029):0:1:\029',
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidOr', '|'),
+ hl('InvalidIdentifierName', '\029'),
+ },
+ },
+ })
+ check_parsing('"\\<', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<"', ':0:0:"\\<'),
+ },
+ err = {
+ arg = '"\\<',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\<'),
+ })
+ check_parsing('"\\1', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\1"', ':0:0:"\\1'),
+ },
+ err = {
+ arg = '"\\1',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedEscape', '\\1'),
+ })
+ check_parsing('}l', {
+ -- 01
+ ast = {
+ {
+ 'OpMissing:0:1:',
+ children = {
+ fmtn('UnknownFigure', '---', ':0:0:'),
+ 'PlainIdentifier(scope=0,ident=l):0:1:l',
+ },
+ },
+ },
+ err = {
+ arg = '}l',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('InvalidFigureBrace', '}'),
+ hl('InvalidIdentifierName', 'l'),
+ }, {
+ [1] = {
+ ast = {
+ len = 1,
+ ast = {
+ fmtn('UnknownFigure', '---', ':0:0:'),
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing(':?\000\000\000\000\000\000\000', {
+ len = 2,
+ ast = {
+ {
+ 'Colon:0:0::',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'Missing:0:1:',
+ 'TernaryValue:0:1:?',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ':?\000\000\000\000\000\000\000',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('InvalidColon', ':'),
+ hl('InvalidTernary', '?'),
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ },
+ hl_fs = {
+ [3] = hl('InvalidSpacing', '\0'),
+ [4] = hl('InvalidSpacing', '\0'),
+ [5] = hl('InvalidSpacing', '\0'),
+ [6] = hl('InvalidSpacing', '\0'),
+ [7] = hl('InvalidSpacing', '\0'),
+ [8] = hl('InvalidSpacing', '\0'),
+ [9] = hl('InvalidSpacing', '\0'),
+ },
+ },
+ })
+ 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 = {
+ len = 2,
+ 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] += 3', {
+ -- 012345678
+ ast = {
+ {
+ 'Assignment(Add):0:4: +=',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ 'Integer(val=3):0:7: 3',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '1'),
+ hl('SubscriptBracket', ']'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '3', 1),
+ })
+
+ check_asgn_parsing('a[1 + 2] += 3', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ 'Assignment(Add):0:8: +=',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'BinaryPlus:0:3: +',
+ children = {
+ 'Integer(val=1):0:2:1',
+ 'Integer(val=2):0:5: 2',
+ },
+ },
+ },
+ },
+ 'Integer(val=3):0:11: 3',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '1'),
+ hl('BinaryPlus', '+', 1),
+ hl('Number', '2', 1),
+ hl('SubscriptBracket', ']'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '3', 1),
+ })
+
+ check_asgn_parsing('a[{-> {b{3}: 4}[5]}()] += 6', {
+ -- 012345678901234567890123456
+ -- 0 1 2
+ ast = {
+ {
+ 'Assignment(Add):0:22: +=',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Call:0:19:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:2:{'),
+ children = {
+ {
+ 'Arrow:0:3:->',
+ children = {
+ {
+ 'Subscript:0:15:[',
+ children = {
+ {
+ fmtn('DictLiteral', '-di', ':0:5: {'),
+ children = {
+ {
+ 'Colon:0:11::',
+ children = {
+ {
+ 'ComplexIdentifier:0:8:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:7:b',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:8:{'),
+ children = {
+ 'Integer(val=3):0:9:3',
+ },
+ },
+ },
+ },
+ 'Integer(val=4):0:12: 4',
+ },
+ },
+ },
+ },
+ 'Integer(val=5):0:16:5',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Integer(val=6):0:25: 6',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Dict', '{', 1),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '{'),
+ hl('Number', '3'),
+ hl('Curly', '}'),
+ hl('Colon', ':'),
+ hl('Number', '4', 1),
+ hl('Dict', '}'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '5'),
+ hl('SubscriptBracket', ']'),
+ hl('Lambda', '}'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ hl('SubscriptBracket', ']'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '6', 1),
+ })
+
+ check_asgn_parsing('a{1}.2[{-> {b{3}: 4}[5]}()]', {
+ -- 012345678901234567890123456
+ -- 0 1 2
+ ast = {
+ {
+ 'Subscript:0:6:[',
+ children = {
+ {
+ 'ConcatOrSubscript:0:4:.',
+ children = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ },
+ },
+ 'PlainKey(key=2):0:5:2',
+ },
+ },
+ {
+ 'Call:0:24:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:7:{'),
+ children = {
+ {
+ 'Arrow:0:8:->',
+ children = {
+ {
+ 'Subscript:0:20:[',
+ children = {
+ {
+ fmtn('DictLiteral', '-di', ':0:10: {'),
+ children = {
+ {
+ 'Colon:0:16::',
+ children = {
+ {
+ 'ComplexIdentifier:0:13:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:12:b',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:13:{'),
+ children = {
+ 'Integer(val=3):0:14:3',
+ },
+ },
+ },
+ },
+ 'Integer(val=4):0:17: 4',
+ },
+ },
+ },
+ },
+ 'Integer(val=5):0:21:5',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('Number', '1'),
+ hl('Curly', '}'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ hl('SubscriptBracket', '['),
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Dict', '{', 1),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '{'),
+ hl('Number', '3'),
+ hl('Curly', '}'),
+ hl('Colon', ':'),
+ hl('Number', '4', 1),
+ hl('Dict', '}'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '5'),
+ hl('SubscriptBracket', ']'),
+ hl('Lambda', '}'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_asgn_parsing('a', {
+ -- 0
+ ast = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ })
+
+ check_asgn_parsing('{a}', {
+ -- 012
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ })
+
+ check_asgn_parsing('{a}b', {
+ -- 0123
+ ast = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_asgn_parsing('a{b}c', {
+ -- 01234
+ ast = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ })
+
+ check_asgn_parsing('a{b}c[0]', {
+ -- 01234567
+ ast = {
+ {
+ 'Subscript:0:5:[',
+ children = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ },
+ },
+ 'Integer(val=0):0:6:0',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '0'),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_asgn_parsing('a{b}c.0', {
+ -- 0123456
+ ast = {
+ {
+ 'ConcatOrSubscript:0:5:.',
+ children = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ },
+ },
+ 'PlainKey(key=0):0:6:0',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '0'),
+ })
+
+ check_asgn_parsing('[a{b}c[0].0]', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'ConcatOrSubscript:0:9:.',
+ children = {
+ {
+ 'Subscript:0:6:[',
+ children = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'ComplexIdentifier:0:5:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ },
+ },
+ 'Integer(val=0):0:7:0',
+ },
+ },
+ 'PlainKey(key=0):0:10:0',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '0'),
+ hl('SubscriptBracket', ']'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '0'),
+ hl('List', ']'),
+ })
+
+ check_asgn_parsing('{a}{b}', {
+ -- 012345
+ ast = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4:b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ })
+
+ check_asgn_parsing('a.b{c}{d}', {
+ -- 012345678
+ ast = {
+ {
+ 'OpMissing:0:3:',
+ children = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainKey(key=b):0:2:b',
+ },
+ },
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:6:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:7:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{c}{d}',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', 'b'),
+ hl('InvalidFigureBrace', '{'),
+ hl('IdentifierName', 'c'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'd'),
+ hl('Curly', '}'),
+ })
+
+ check_asgn_parsing('[a] = 1', {
+ -- 0123456
+ ast = {
+ {
+ 'Assignment(Plain):0:3: =',
+ children = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ 'Integer(val=1):0:5: 1',
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('List', ']'),
+ hl('PlainAssignment', '=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('[a[b], [c, [d, [e]]]] = 1', {
+ -- 0123456789012345678901234
+ -- 0 1 2
+ ast = {
+ {
+ 'Assignment(Plain):0:21: =',
+ children = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:5:,',
+ children = {
+ {
+ 'Subscript:0:2:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'ListLiteral:0:6: [',
+ children = {
+ {
+ 'Comma:0:9:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:8:c',
+ {
+ 'ListLiteral:0:10: [',
+ children = {
+ {
+ 'Comma:0:13:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:12:d',
+ {
+ 'ListLiteral:0:14: [',
+ children = {
+ 'PlainIdentifier(scope=0,ident=e):0:16:e',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Integer(val=1):0:23: 1',
+ },
+ },
+ },
+ err = {
+ arg = '[c, [d, [e]]]] = 1',
+ msg = 'E475: Nested lists not allowed when assigning: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptBracket', ']'),
+ hl('Comma', ','),
+ hl('InvalidList', '[', 1),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('InvalidList', '[', 1),
+ hl('IdentifierName', 'd'),
+ hl('Comma', ','),
+ hl('InvalidList', '[', 1),
+ hl('IdentifierName', 'e'),
+ hl('List', ']'),
+ hl('List', ']'),
+ hl('List', ']'),
+ hl('List', ']'),
+ hl('PlainAssignment', '=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('$X += 1', {
+ -- 0123456
+ ast = {
+ {
+ 'Assignment(Add):0:2: +=',
+ children = {
+ 'Environment(ident=X):0:0:$X',
+ 'Integer(val=1):0:5: 1',
+ },
+ },
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'X'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('@a .= 1', {
+ -- 0123456
+ ast = {
+ {
+ 'Assignment(Concat):0:2: .=',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Integer(val=1):0:5: 1',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('AssignmentWithConcatenation', '.=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('&option -= 1', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'Assignment(Subtract):0:7: -=',
+ children = {
+ 'Option(scope=0,ident=option):0:0:&option',
+ 'Integer(val=1):0:10: 1',
+ },
+ },
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'option'),
+ hl('AssignmentWithSubtraction', '-=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('[$X, @a, &l:option] = [1, 2, 3]', {
+ -- 0123456789012345678901234567890
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Assignment(Plain):0:19: =',
+ children = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:3:,',
+ children = {
+ 'Environment(ident=X):0:1:$X',
+ {
+ 'Comma:0:7:,',
+ children = {
+ 'Register(name=a):0:4: @a',
+ 'Option(scope=l,ident=option):0:8: &l:option',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'ListLiteral:0:21: [',
+ children = {
+ {
+ 'Comma:0:24:,',
+ children = {
+ 'Integer(val=1):0:23:1',
+ {
+ 'Comma:0:27:,',
+ children = {
+ 'Integer(val=2):0:25: 2',
+ 'Integer(val=3):0:28: 3',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'X'),
+ hl('Comma', ','),
+ hl('Register', '@a', 1),
+ hl('Comma', ','),
+ hl('OptionSigil', '&', 1),
+ hl('OptionScope', 'l'),
+ hl('OptionScopeDelimiter', ':'),
+ hl('OptionName', 'option'),
+ hl('List', ']'),
+ hl('PlainAssignment', '=', 1),
+ hl('List', '[', 1),
+ hl('Number', '1'),
+ hl('Comma', ','),
+ hl('Number', '2', 1),
+ hl('Comma', ','),
+ hl('Number', '3', 1),
+ hl('List', ']'),
+ })
+ end)
+end
diff --git a/test/unit/viml/helpers.lua b/test/unit/viml/helpers.lua
new file mode 100644
index 0000000000..9d8102e023
--- /dev/null
+++ b/test/unit/viml/helpers.lua
@@ -0,0 +1,130 @@
+local helpers = require('test.unit.helpers')(nil)
+
+local ffi = helpers.ffi
+local cimport = helpers.cimport
+local kvi_new = helpers.kvi_new
+local kvi_init = helpers.kvi_init
+local conv_enum = helpers.conv_enum
+local make_enum_conv_tab = helpers.make_enum_conv_tab
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h')
+
+local function new_pstate(strings)
+ local strings_idx = 0
+ local function get_line(_, ret_pline)
+ strings_idx = strings_idx + 1
+ local str = strings[strings_idx]
+ local data, size
+ if type(str) == 'string' then
+ data = str
+ size = #str
+ elseif type(str) == 'nil' then
+ data = nil
+ size = 0
+ elseif type(str) == 'table' then
+ data = str.data
+ size = str.size
+ elseif type(str) == 'function' then
+ data, size = str()
+ size = size or 0
+ end
+ ret_pline.data = data
+ ret_pline.size = size
+ ret_pline.allocated = false
+ end
+ local state = {
+ reader = {
+ get_line = get_line,
+ cookie = nil,
+ conv = {
+ vc_type = 0,
+ vc_factor = 1,
+ vc_fail = false,
+ },
+ },
+ pos = { line = 0, col = 0 },
+ colors = kvi_new('ParserHighlight'),
+ can_continuate = false,
+ }
+ local ret = ffi.new('ParserState', state)
+ kvi_init(ret.reader.lines)
+ kvi_init(ret.stack)
+ return ret
+end
+
+local function pline2lua(pline)
+ return ffi.string(pline.data, pline.size)
+end
+
+local function pstate_str(pstate, start, len)
+ local str = nil
+ local err = nil
+ if start.line < pstate.reader.lines.size then
+ local pstr = pline2lua(pstate.reader.lines.items[start.line])
+ if start.col >= #pstr then
+ err = 'start.col >= #pstr'
+ else
+ str = pstr:sub(tonumber(start.col) + 1, tonumber(start.col + len))
+ end
+ else
+ err = 'start.line >= pstate.reader.lines.size'
+ end
+ return str, err
+end
+
+local function pstate_set_str(pstate, start, len, ret)
+ ret = ret or {}
+ ret.start = {
+ line = tonumber(start.line),
+ col = tonumber(start.col)
+ }
+ ret.len = tonumber(len)
+ ret.str, ret.error = pstate_str(pstate, start, len)
+ return ret
+end
+
+local eltkn_cmp_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprCmpEqual',
+ 'kExprCmpMatches',
+ 'kExprCmpGreater',
+ 'kExprCmpGreaterOrEqual',
+ 'kExprCmpIdentical',
+}, 'kExprCmp', function(ret) eltkn_cmp_type_tab = ret end)
+
+local function conv_cmp_type(typ)
+ return conv_enum(eltkn_cmp_type_tab, typ)
+end
+
+local ccs_tab
+make_enum_conv_tab(lib, {
+ 'kCCStrategyUseOption',
+ 'kCCStrategyMatchCase',
+ 'kCCStrategyIgnoreCase',
+}, 'kCCStrategy', function(ret) ccs_tab = ret end)
+
+local function conv_ccs(ccs)
+ return conv_enum(ccs_tab, ccs)
+end
+
+local expr_asgn_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprAsgnPlain',
+ 'kExprAsgnAdd',
+ 'kExprAsgnSubtract',
+ 'kExprAsgnConcat',
+}, 'kExprAsgn', function(ret) expr_asgn_type_tab = ret end)
+
+local function conv_expr_asgn_type(expr_asgn_type)
+ return conv_enum(expr_asgn_type_tab, expr_asgn_type)
+end
+
+return {
+ conv_ccs = conv_ccs,
+ pline2lua = pline2lua,
+ pstate_str = pstate_str,
+ new_pstate = new_pstate,
+ conv_cmp_type = conv_cmp_type,
+ pstate_set_str = pstate_set_str,
+ conv_expr_asgn_type = conv_expr_asgn_type,
+}