vimlrs 0.2.3

Faithful Rust port of the Vimscript (VimL) interpreter, from the Neovim C eval engine
Documentation
" error_exceptions.vim — a runtime error inside `:try` is a catchable exception.
"
" Vim does not merely *print* an E-error: inside a `:try` it converts it into an
" exception (`emsg` → `cause_errthrow`, ex_eval.c), aborts the rest of the
" protected block, and lets `:catch` see it as `Vim(<cmd>):E<nnn>: …`. Plugins
" lean on this constantly (`try | call Foo() | catch /E117/ | endtry`), so it is
" compat-floor behavior, not a nicety.
"
" Outside a `:try` the error is printed and the script keeps going, as before.
" Self-tests with assert_*; exits non-zero on any failure.
"
"   vimlrs examples/error_exceptions.vim

" --- an error in the protected block is caught, and the block is abandoned
let s:reached = 0
try
  echo [1] . 'x'
  let s:reached = 1
catch
  let s:caught = v:exception
endtry
call assert_equal(0, s:reached)
call assert_match('E730', s:caught)

" --- the exception is tagged with the ex-command that raised it
call assert_match('^Vim(echo):', s:caught)
try
  call nosuchfn()
catch
  call assert_match('^Vim(call):E117:', v:exception)
endtry

" --- `:catch /pat/` matches on the error number, the usual plugin idiom
try
  echo [1] . 'x'
  call assert_report('E730 should have been thrown')
catch /E730/
  call assert_true(v:true)
endtry

" --- a non-matching `:catch` does not swallow it; an outer `:try` sees it
let s:outer = ''
try
  try
    echo {'a': 1} . 'x'
  catch /E999/
    call assert_report('E999 must not match E731')
  endtry
catch
  let s:outer = v:exception
endtry
call assert_match('E731', s:outer)

" --- the FIRST error wins: `eval5` type-checks the left operand of `-` before it
"     even evaluates the right one, so the Blob (E974) is reported, not the
"     failing remove() on the right
try
  let s:x = 0z - remove({'a': 1}, 'nokey')
catch
  call assert_match('E974', v:exception)
endtry

" --- `:finally` still runs, and a caught error does not leak into it
let s:fin = 0
try
  echo [1] . 'x'
catch
  " swallowed
finally
  let s:fin = 1
endtry
call assert_equal(1, s:fin)

" --- `:silent!` suppresses the error message; the command still fails, and the
"     script neither reports it nor exits non-zero because of it
let s:after = 0
silent! call nosuchfn()
let s:after = 1
call assert_equal(1, s:after)

" --- outside a `:try` an error is not thrown, so execution simply continues
"     (assert_fails runs the command and captures the error it raises)
call assert_fails('echo [1] . "x"', 'E730')

" --- a command whose expression errored is ABANDONED: its effect does not happen.
"     A failed `:let` therefore leaves the variable exactly as it was, rather than
"     storing whatever value the evaluator recovered with.
let s:keep = 'orig'
silent! let s:keep = [1] . 'x'
call assert_equal('orig', s:keep)

" --- an error abandons the REST OF THE COMMAND LINE: the `|`-separated commands
"     after the failing one do not run, and execution resumes at the next line
let s:ran = 0
silent! echo [1] . 'x' | let s:ran = 1
call assert_equal(0, s:ran)

" --- ...but a line whose commands all succeed runs every one of them
let s:n = 0
let s:n = 1 | let s:n = s:n + 1
call assert_equal(2, s:n)

" Run {cmd} inside a ONE-LINE try and report whether its `:catch` saw the error.
" The inner `:execute` builds the one-liner so the whole thing stays on one line.
func! s:CatchInline(cmd) abort
  let s:got = 'escaped'
  try
    execute 'try | ' . a:cmd . ' | catch | let s:got = "caught" . matchstr(v:exception, "E\\d\\+") | endtry'
  catch
    " the error escaped the inline :catch and reached this (multi-line) one
  endtry
  return s:got
endfunc

" --- a one-line `try | … | catch | … | endtry` DOES catch an ordinary runtime error
"     (and a `:throw`), exactly like a multi-line one
let s:inline = ''
try | call nosuchfn() | catch | let s:inline = v:exception | endtry
call assert_match('E117', s:inline)

let s:inline_throw = ''
try | throw 'boom' | catch | let s:inline_throw = v:exception | endtry
call assert_equal('boom', s:inline_throw)

" --- ...but NOT an error raised by the expression evaluator's OWN type checks.
"     Coercing a condition, indexing something unindexable, and the `eval5` operand
"     pre-check all make Vim's eval1() return FAIL, and a command whose argument
"     failed to evaluate takes the whole command line with it — so the `:catch` on
"     that line never runs. An error raised *inside a called builtin* (E117 above,
"     E684 from insert()) does not fail the evaluator and IS caught. A multi-line
"     `:try`, whose `:catch` is on another line, catches both; see the block above.
call assert_match('^caught', s:CatchInline('call nosuchfn()')) " builtin → soft
call assert_match('^caught', s:CatchInline('echo insert([1],{},100000)')) " builtin → soft
call assert_match('^caught', s:CatchInline('echo deepcopy({})[2]')) " E716 → soft
call assert_equal('escaped', s:CatchInline('echo [1] . "x"')) " eval5 pre-check → HARD
call assert_equal('escaped', s:CatchInline('echo 0z11 - 1')) " eval5 pre-check → HARD
call assert_equal('escaped', s:CatchInline('echo log10(-3.25)[-5:0]')) " unindexable → HARD
call assert_equal('escaped', s:CatchInline('echo (sort([3,1,2],"n") ? v:true : [1])')) " condition → HARD
let s:esc = ''
try
  try | echo [1] . 'x' | catch | call assert_report('a hard failure must not be caught inline') | endtry
catch
  let s:esc = v:exception
endtry
call assert_match('E730', s:esc)

" --- and a hard failure abandons the rest of the line even under `:silent!`, while an
"     ordinary silenced error lets the line continue
let s:hard = 'not-run'
silent! echo [1] . 'x' | let s:hard = 'ran'
call assert_equal('not-run', s:hard)

let s:soft = 'not-run'
silent! call nosuchfn() | let s:soft = 'ran'
call assert_equal('ran', s:soft)

" --- a builtin call with the WRONG NUMBER OF ARGUMENTS is an error Vim raises when it
"     runs the command, not when it loads the script. So an unreachable bad call is
"     harmless — the script still loads — and a reachable one is a catchable error.
"     (Rejecting it at load time would break every script whose dead branch guards a
"     call for another Vim version.)
let s:unreached = 'ok'
if 0
  echo strlen('a', 'b')
endif
call assert_equal('ok', s:unreached)

call assert_match('E118', s:CatchInline('echo strlen("a","b")'))
call assert_match('E119', s:CatchInline('echo strlen()'))

if len(v:errors) > 0
  for err in v:errors
    echo err
  endfor
  throw 'error_exceptions.vim: ' . len(v:errors) . ' assertion(s) failed'
endif