]> git.lizzy.rs Git - metalua.git/blob - src/lib/extension/trycatch.mlua
fixing CRLF
[metalua.git] / src / lib / extension / trycatch.mlua
1 -{ extension 'match' }\r
2 \r
3 --------------------------------------------------------------------------------\r
4 --\r
5 -- TODO:\r
6 --\r
7 -- * Hygienize calls to pcall()\r
8 --\r
9 --------------------------------------------------------------------------------\r
10 \r
11 -{ extension 'H' }\r
12 -{ extension 'log' }\r
13 \r
14 require 'extension.match'\r
15 \r
16 -- Get match parsers and builder, for catch cases handling:\r
17 local match_alpha = require 'extension.match' \r
18 local H = H:new{side='inside', alpha = match_alpha }\r
19 \r
20 -- We'll need to track rogue return statements:\r
21 require 'walk'\r
22 \r
23 -- Put a block AST into a pcall():\r
24 local mkpcall = |block| +{pcall(function() -{block} end)}\r
25 \r
26 -- The statement builder:\r
27 function trycatch_builder(x)\r
28    --$log ("trycatch_builder", x, 'nohash', 60)\r
29    local try_code, catch_cases, finally_code = unpack(x)\r
30    local insert_return_catcher = false   \r
31 \r
32    -- Can't be hygienize automatically by the current version of H, as\r
33    -- it must bridge from inside user code (hacjed return statements)\r
34    -- to outside macro code.\r
35    local caught_return = !mlp.gensym 'caught_return'\r
36    local saved_args\r
37 \r
38    !try_code; !(finally_code or { })\r
39    -- FIXME: Am I sure there's no need to hygienize inside?\r
40    --[[if catch_cases then\r
41       for case in ivalues(catch_cases) do\r
42          --$log(case,'nohash')\r
43          local patterns, guard, block = unpack(case)\r
44          ! block\r
45       end\r
46    end]]\r
47 \r
48 \r
49    ----------------------------------------------------------------\r
50    -- Returns in the try-block must be transformed:\r
51    -- from the user's PoV, the code in the try-block isn't \r
52    -- a function, therefore a return in it must not merely\r
53    -- end the execution of the try block, but:\r
54    --  * not cause any error to be caught;\r
55    --  * let the finally-block be executed;\r
56    --  * only then, let the enclosing function return with the\r
57    --    appropraite values.\r
58    -- The way to handle that is that any returned value is stored \r
59    -- into the runtime variable caught_return, then a return with\r
60    -- no value is sent, to stop the execution of the try-code.\r
61    --\r
62    -- Similarly, a return in a catch case code must not prevent\r
63    -- the finally-code from being run.\r
64    --\r
65    -- This walker catches return statements and perform the relevant\r
66    -- transformation into caught_return setting + empty return.\r
67    --\r
68    -- There is an insert_return_catcher compile-time flag, which\r
69    -- allows to avoid inserting return-handling code in the result\r
70    -- when not needed.\r
71    ----------------------------------------------------------------\r
72    local replace_returns_and_dots do\r
73       local function f(x)\r
74          match x with \r
75          | `Return{...} -> \r
76             insert_return_catcher = true\r
77             -- Setvar's 'caught_return' code can't be hygienize by H currently.\r
78             local setvar = `Set{ {caught_return}, { `Table{ unpack(x) } } }\r
79             x <- { setvar; `Return }; x.tag = nil;\r
80             --$log('transformed return stat:', x, 60)\r
81             return 'break'\r
82          | `Function{...} -> return 'break' \r
83             -- inside this, returns would be the nested function's, not ours.\r
84          | `Dots ->\r
85             if not saved_args then saved_args = mlp.gensym 'args' end\r
86             x <- `Call{ `Id 'unpack', saved_args }               \r
87          | _ -> -- pass\r
88          end\r
89       end\r
90       local cfg = { stat = {down=f}, expr = {down=f} }\r
91       replace_returns_and_dots = |x| walk.block(cfg, x)\r
92    end\r
93 \r
94    -- parse returns in the try-block:\r
95    replace_returns_and_dots (try_code)\r
96 \r
97    -- code handling the error catching process:\r
98    local catch_result do\r
99       if catch_cases and #catch_cases>0 then\r
100          ----------------------------------------------------------\r
101          -- Protect catch code against failures: they run in a pcall(), and\r
102          -- the result is kept in catch_* vars so that it can be used to\r
103          -- relaunch the error after the finally code has been executed.\r
104          ----------------------------------------------------------\r
105          for x in ivalues (catch_cases) do\r
106             local case_code = x[3]\r
107             -- handle rogue returns:\r
108             replace_returns_and_dots (case_code)\r
109             -- in case of error in the catch, we still need to run "finally":\r
110             x[3] = +{block: catch_success, catch_error = -{mkpcall(case_code)}}\r
111          end\r
112          ----------------------------------------------------------\r
113          -- Uncaught exceptions must not cause a mismatch,\r
114          -- so we introduce a catch-all do-nothing last case:\r
115          ----------------------------------------------------------\r
116          table.insert (catch_cases, { { { `Id '_' } }, false, { } }) \r
117          catch_result = spmatch.match_builder{ {+{user_error}}, catch_cases }\r
118       else\r
119          catch_result = { }\r
120       end\r
121    end\r
122 \r
123    ----------------------------------------------------------------\r
124    -- Build the bits of code that will handle return statements\r
125    -- in the user code (try-block and catch-blocks).\r
126    ----------------------------------------------------------------\r
127    local caught_return_init, caught_return_rethrow do\r
128       if insert_return_catcher then\r
129          caught_return_init    = `Local{{caught_return}}\r
130          caught_return_rethrow =\r
131             +{stat: if -{caught_return} then return unpack(-{caught_return}) end}\r
132       else\r
133          caught_return_init, caught_return_rethrow = { }, { }\r
134       end\r
135    end\r
136    \r
137    local saved_args_init =\r
138       saved_args and `Local{ {saved_args}, { `Table{`Dots} } } or { }\r
139 \r
140    -- The finally code, to execute no matter what:\r
141    local finally_result = finally_code or { }\r
142 \r
143    -- And the whole statement, gluing all taht together:\r
144    local result = +{stat: \r
145       do\r
146          -{ saved_args_init }\r
147          -{ caught_return_init }\r
148          local user_success,  user_error  = -{mkpcall(try_code)}\r
149          local catch_success, catch_error = false, user_error\r
150          if not user_success then -{catch_result} end\r
151          -{finally_result}\r
152          if not user_success and not catch_success then error(catch_error) end \r
153          -{ caught_return_rethrow }\r
154       end }\r
155 \r
156    H(result)\r
157 \r
158    return result\r
159 end\r
160 \r
161 function catch_case_builder(x)\r
162    --$log ("catch_case_builder", x, 'nohash', 60)\r
163    local patterns, guard, _, code = unpack(x)\r
164    -- patterns ought to be a pattern_group, but each expression must\r
165    -- be converted into a single-element pattern_seq.\r
166    for i = 1, #patterns do patterns[i] = {patterns[i]} end\r
167    return { patterns, guard, code }\r
168 end\r
169 \r
170 mlp.lexer:add{ 'try', 'catch', 'finally', '->' }\r
171 mlp.block.terminators:add{ 'catch', 'finally' }\r
172 \r
173 mlp.stat:add{\r
174    'try', \r
175    mlp.block, \r
176    gg.onkeyword{ 'catch', \r
177       gg.list{\r
178          gg.sequence{ \r
179             mlp.expr_list,\r
180             gg.onkeyword{ 'if', mlp.expr },\r
181             gg.optkeyword 'then', \r
182             mlp.block,\r
183             builder = catch_case_builder },\r
184          separators = 'catch' } },\r
185    gg.onkeyword{ 'finally', mlp.block },\r
186    'end',\r
187    builder = trycatch_builder }\r
188 \r
189 return H.alpha\r
190 \r
191 \r