190 lines
6.7 KiB
Plaintext
Executable File

-{ extension 'match' }
--------------------------------------------------------------------------------
--
-- TODO:
--
-- * Hygienize calls to pcall()
--
--------------------------------------------------------------------------------
-{ extension 'H' }
-{ extension 'log' }
-- Get match parsers and builder, for catch cases handling:
local match_alpha = require 'metalua.extension.match'
local H = H:new{side='inside', alpha = match_alpha }
-- We'll need to track rogue return statements:
require 'metalua.walk'
-- Put a block AST into a pcall():
local mkpcall = |block| +{pcall(function() -{block} end)}
-- The statement builder:
function trycatch_builder(x)
--$log ("trycatch_builder", x, 'nohash', 60)
local try_code, catch_cases, finally_code = unpack(x)
local insert_return_catcher = false
-- Can't be hygienize automatically by the current version of H, as
-- it must bridge from inside user code (hacjed return statements)
-- to outside macro code.
local caught_return = !mlp.gensym 'caught_return'
local saved_args
!try_code; !(finally_code or { })
-- FIXME: Am I sure there's no need to hygienize inside?
--[[if catch_cases then
for case in ivalues(catch_cases) do
--$log(case,'nohash')
local patterns, guard, block = unpack(case)
! block
end
end]]
----------------------------------------------------------------
-- Returns in the try-block must be transformed:
-- from the user's PoV, the code in the try-block isn't
-- a function, therefore a return in it must not merely
-- end the execution of the try block, but:
-- * not cause any error to be caught;
-- * let the finally-block be executed;
-- * only then, let the enclosing function return with the
-- appropraite values.
-- The way to handle that is that any returned value is stored
-- into the runtime variable caught_return, then a return with
-- no value is sent, to stop the execution of the try-code.
--
-- Similarly, a return in a catch case code must not prevent
-- the finally-code from being run.
--
-- This walker catches return statements and perform the relevant
-- transformation into caught_return setting + empty return.
--
-- There is an insert_return_catcher compile-time flag, which
-- allows to avoid inserting return-handling code in the result
-- when not needed.
----------------------------------------------------------------
local replace_returns_and_dots do
local function f(x)
match x with
| `Return{...} ->
insert_return_catcher = true
-- Setvar's 'caught_return' code can't be hygienize by H currently.
local setvar = `Set{ {caught_return}, { `Table{ unpack(x) } } }
x <- { setvar; `Return }; x.tag = nil;
--$log('transformed return stat:', x, 60)
return 'break'
| `Function{...} -> return 'break'
-- inside this, returns would be the nested function's, not ours.
| `Dots ->
if not saved_args then saved_args = mlp.gensym 'args' end
x <- `Call{ `Id 'unpack', saved_args }
| _ -> -- pass
end
end
local cfg = { stat = {down=f}, expr = {down=f} }
replace_returns_and_dots = |x| walk.block(cfg, x)
end
-- parse returns in the try-block:
replace_returns_and_dots (try_code)
-- code handling the error catching process:
local catch_result do
if catch_cases and #catch_cases>0 then
----------------------------------------------------------
-- Protect catch code against failures: they run in a pcall(), and
-- the result is kept in catch_* vars so that it can be used to
-- relaunch the error after the finally code has been executed.
----------------------------------------------------------
for x in ivalues (catch_cases) do
local case_code = x[3]
-- handle rogue returns:
replace_returns_and_dots (case_code)
-- in case of error in the catch, we still need to run "finally":
x[3] = +{block: catch_success, catch_error = -{mkpcall(case_code)}}
end
----------------------------------------------------------
-- Uncaught exceptions must not cause a mismatch,
-- so we introduce a catch-all do-nothing last case:
----------------------------------------------------------
table.insert (catch_cases, { { { `Id '_' } }, false, { } })
catch_result = spmatch.match_builder{ {+{user_error}}, catch_cases }
else
catch_result = { }
end
end
----------------------------------------------------------------
-- Build the bits of code that will handle return statements
-- in the user code (try-block and catch-blocks).
----------------------------------------------------------------
local caught_return_init, caught_return_rethrow do
if insert_return_catcher then
caught_return_init = `Local{{caught_return}}
caught_return_rethrow =
+{stat: if -{caught_return} then return unpack(-{caught_return}) end}
else
caught_return_init, caught_return_rethrow = { }, { }
end
end
local saved_args_init =
saved_args and `Local{ {saved_args}, { `Table{`Dots} } } or { }
-- The finally code, to execute no matter what:
local finally_result = finally_code or { }
-- And the whole statement, gluing all taht together:
local result = +{stat:
do
-{ saved_args_init }
-{ caught_return_init }
local user_success, user_error = -{mkpcall(try_code)}
local catch_success, catch_error = false, user_error
if not user_success then -{catch_result} end
-{finally_result}
if not user_success and not catch_success then error(catch_error) end
-{ caught_return_rethrow }
end }
H(result)
return result
end
function catch_case_builder(x)
--$log ("catch_case_builder", x, 'nohash', 60)
local patterns, guard, _, code = unpack(x)
-- patterns ought to be a pattern_group, but each expression must
-- be converted into a single-element pattern_seq.
for i = 1, #patterns do patterns[i] = {patterns[i]} end
return { patterns, guard, code }
end
mlp.lexer:add{ 'try', 'catch', 'finally', '->' }
mlp.block.terminators:add{ 'catch', 'finally' }
mlp.stat:add{
'try',
mlp.block,
gg.onkeyword{ 'catch',
gg.list{
gg.sequence{
mlp.expr_list,
gg.onkeyword{ 'if', mlp.expr },
gg.optkeyword 'then',
mlp.block,
builder = catch_case_builder },
separators = 'catch' } },
gg.onkeyword{ 'finally', mlp.block },
'end',
builder = trycatch_builder }
return H.alpha