Skip to content

Commit ef03288

Browse files
committed
[Tolk] throw interrupts control flow; never type
In FunC (and in Tolk before) throwing an exception is just calling a built-in function: > throw 123; // actually, __throw(123) Since it's a regular function, the compiler was not aware that execution will stop, and all following code is unreachable. For instance, `throw` in the end on function needed to be followed by `return` statement. Now, `throw` interrupts control flow, all statements after it are considered unreachable. At IR level, code Ops are also not produced. This works because a built-in __throw() now has `never` type. It can also be applied to custom functions: > fun alwaysThrow(): never { throw 123; } The code after alwaysThrow() call will also be unreachable.
1 parent 7bcb8b8 commit ef03288

File tree

10 files changed

+227
-25
lines changed

10 files changed

+227
-25
lines changed

tolk-tester/tests/inference-tests.tolk

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ fun test7() {
8686
// __expect_type(eq<(int, slice)>, "(int, slice) -> (int, slice)");
8787
}
8888

89+
fun alwaysThrows(): never { throw 123; }
90+
fun alwaysThrowsNotAnnotated() { throw 123; }
91+
fun alwaysThrowsNotAnnotated2() { alwaysThrows(); }
92+
93+
fun test9() {
94+
__expect_type(alwaysThrows(), "never");
95+
__expect_type(alwaysThrows, "() -> never");
96+
__expect_type(alwaysThrowsNotAnnotated(), "void");
97+
__expect_type(alwaysThrowsNotAnnotated2(), "void");
98+
}
99+
89100

90101
fun main() {
91102
return 0;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
fun invalidNever(): never {
2+
if (random()) { throw 123; }
3+
}
4+
5+
/**
6+
@compilation_should_fail
7+
@stderr a function returning `never` can not have a reachable endpoint
8+
*/

tolk-tester/tests/try-func.tolk

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,78 @@ fun test109(): (int, int) {
164164
return (g_reg, l_reg);
165165
}
166166

167+
fun alwaysThrow123(): never {
168+
throw 123;
169+
}
170+
171+
fun alwaysThrowX(x: int): never {
172+
if (x > 10) { throw (x, beginCell()); }
173+
else { throw (x, null); }
174+
}
175+
176+
fun anotherNever(throw123: bool): never {
177+
if (throw123) { alwaysThrow123(); }
178+
alwaysThrowX(456);
179+
}
180+
181+
fun testCodegen1(x: int) {
182+
if (x > 10) {
183+
throw 123;
184+
anotherNever(true); // unreachable, will be dropped
185+
}
186+
else if (x < 10) {
187+
throw x;
188+
return -123; // unreachable, will be dropped
189+
}
190+
return 0;
191+
}
192+
193+
fun testCodegen2(x: int) {
194+
if (x > 10) {
195+
alwaysThrow123();
196+
anotherNever(true); // unreachable, will be dropped
197+
}
198+
else if (x < 10) {
199+
anotherNever(false);
200+
return -123; // unreachable, will be dropped
201+
}
202+
return 0;
203+
}
204+
205+
@method_id(110)
206+
fun test110(b: bool) {
207+
try {
208+
if (b == true) { testCodegen1(100); }
209+
testCodegen1(5);
210+
return -1;
211+
} catch (ex) {
212+
return ex;
213+
}
214+
}
215+
216+
@method_id(111)
217+
fun test111(b: bool) {
218+
try {
219+
if (b == true) { testCodegen2(100); }
220+
testCodegen2(5);
221+
return -1;
222+
} catch (ex) {
223+
return ex;
224+
}
225+
}
226+
227+
fun mySetCode(newCode: slice): void
228+
asm "SETCODE";
229+
230+
fun testCodegen3(numberId: int, paramVal: cell) {
231+
if (numberId == -1000) {
232+
var cs = paramVal.beginParse();
233+
mySetCode(cs);
234+
throw 0;
235+
}
236+
paramVal.beginParse();
237+
}
238+
167239
fun main() {
168240
}
169241

@@ -187,6 +259,65 @@ fun main() {
187259
@testcase | 107 | 5 | 5
188260
@testcase | 107 | 20 | 20
189261
@testcase | 108 | | 0
262+
@testcase | 109 | | 10 10
263+
@testcase | 110 | -1 | 123
264+
@testcase | 110 | 0 | 5
265+
@testcase | 111 | -1 | 123
266+
@testcase | 111 | 0 | 456
267+
268+
@code_hash 57361460846265694653029920796509802052573595128418810728101968091567195330515
269+
270+
@fif_codegen
271+
"""
272+
testCodegen1 PROC:<{
273+
// x
274+
DUP // x x
275+
10 GTINT // x '2
276+
IFJMP:<{ // x
277+
123 THROW
278+
}> // x
279+
DUP // x x
280+
10 LESSINT // x '6
281+
IFJMP:<{ // x
282+
THROWANY
283+
}> // x
284+
DROP //
285+
0 PUSHINT // '8=0
286+
}>
287+
"""
288+
289+
@fif_codegen
290+
"""
291+
testCodegen2 PROC:<{
292+
// x
293+
DUP // x x
294+
10 GTINT // x '2
295+
IFJMP:<{ // x
296+
DROP //
297+
alwaysThrow123 CALLDICT
298+
}> // x
299+
10 LESSINT // '5
300+
IFJMP:<{ //
301+
FALSE // '6
302+
anotherNever CALLDICT
303+
}> //
304+
0 PUSHINT // '8=0
305+
}>
306+
"""
190307

191-
@code_hash 39307974281105539319288356721945232226028429128341177951717392648324358675585
308+
@fif_codegen
309+
"""
310+
testCodegen3 PROC:<{
311+
// numberId paramVal
312+
SWAP
313+
-1000 PUSHINT // paramVal numberId '2=-1000
314+
EQUAL // paramVal '3
315+
IFJMP:<{ // paramVal
316+
CTOS // cs
317+
SETCODE
318+
0 THROW
319+
}> // paramVal
320+
DROP //
321+
}>
322+
"""
192323
*/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
fun alwaysThrows(): never {
2+
throw 456;
3+
}
4+
5+
fun testUnreachable(x: int) {
6+
if (x) { throw 123; }
7+
else { alwaysThrows(); }
8+
return 1;
9+
}
10+
11+
fun main() {
12+
try {
13+
testUnreachable(100);
14+
throw 80;
15+
} catch (excNo) {
16+
return excNo;
17+
}
18+
}
19+
20+
/**
21+
@testcase | 0 | | 123
22+
@stderr warning: unreachable code
23+
@stderr return 1;
24+
*/

tolk/analyzer.cpp

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020

2121
namespace tolk {
2222

23+
// functions returning "never" are assumed to interrupt flow
24+
// for instance, variables after their call aren't considered used
25+
// its main purpose is `throw` statement, it's a call to a built-in `__throw` function
26+
static bool does_function_always_throw(FunctionPtr fun_ref) {
27+
return fun_ref->declared_return_type == TypeDataNever::create();
28+
}
29+
2330
/*
2431
*
2532
* ANALYZE AND PREPROCESS ABSTRACT CODE
@@ -262,17 +269,6 @@ VarDescrList& VarDescrList::operator|=(const VarDescrList& y) {
262269
}
263270
}
264271

265-
VarDescrList& VarDescrList::operator&=(const VarDescrList& values) {
266-
for (const VarDescr& vd : values.list) {
267-
VarDescr* item = operator[](vd.idx);
268-
if (item) {
269-
*item &= vd;
270-
}
271-
}
272-
unreachable |= values.unreachable;
273-
return *this;
274-
}
275-
276272
VarDescrList& VarDescrList::import_values(const VarDescrList& values) {
277273
if (values.unreachable) {
278274
set_unreachable();
@@ -326,6 +322,17 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
326322
}
327323
return std_compute_used_vars(true);
328324
}
325+
if (cl == _Call && does_function_always_throw(f_sym)) {
326+
VarDescrList new_var_info; // empty, not next->var_info
327+
if (args.size() == right.size()) {
328+
for (const VarDescr& arg : args) {
329+
new_var_info.add_var(arg.idx, arg.is_unused());
330+
}
331+
} else {
332+
new_var_info.add_vars(right, false);
333+
}
334+
return set_var_info(std::move(new_var_info));
335+
}
329336
return std_compute_used_vars();
330337
}
331338
case _SetGlob: {
@@ -516,20 +523,19 @@ bool prune_unreachable(std::unique_ptr<Op>& ops) {
516523
case Op::_SliceConst:
517524
case Op::_GlobVar:
518525
case Op::_SetGlob:
519-
case Op::_Call:
520526
case Op::_CallInd:
521527
case Op::_Tuple:
522528
case Op::_UnTuple:
523529
case Op::_Import:
530+
case Op::_Let:
524531
reach = true;
525532
break;
526-
case Op::_Let: {
527-
reach = true;
528-
break;
529-
}
530533
case Op::_Return:
531534
reach = false;
532535
break;
536+
case Op::_Call:
537+
reach = !does_function_always_throw(op.f_sym);
538+
break;
533539
case Op::_If: {
534540
// if left then block0 else block1; ...
535541
VarDescr* c_var = op.var_info[op.left[0]];
@@ -712,6 +718,9 @@ VarDescrList Op::fwd_analyze(VarDescrList values) {
712718
values.add_newval(i);
713719
}
714720
}
721+
if (does_function_always_throw(f_sym)) {
722+
values.set_unreachable();
723+
}
715724
break;
716725
}
717726
case _Tuple:
@@ -860,10 +869,11 @@ bool Op::mark_noreturn() {
860869
case _SetGlob:
861870
case _GlobVar:
862871
case _CallInd:
863-
case _Call:
864872
return set_noreturn(next->mark_noreturn());
865873
case _Return:
866874
return set_noreturn();
875+
case _Call:
876+
return set_noreturn(next->mark_noreturn() || does_function_always_throw(f_sym));
867877
case _If:
868878
case _TryCatch:
869879
// note, that & | (not && ||) here and below is mandatory to invoke both left and right calls

tolk/builtins.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,7 @@ void define_builtins() {
10881088
TypePtr Slice = TypeDataSlice::create();
10891089
TypePtr Builder = TypeDataBuilder::create();
10901090
TypePtr Tuple = TypeDataTuple::create();
1091+
TypePtr Never = TypeDataNever::create();
10911092

10921093
std::vector<GenericsDeclaration::GenericsItem> itemsT;
10931094
itemsT.emplace_back("T");
@@ -1201,10 +1202,10 @@ void define_builtins() {
12011202
define_builtin_func("__isNull", {typeT}, Bool, declGenericT,
12021203
compile_is_null,
12031204
FunctionData::flagMarkedAsPure);
1204-
define_builtin_func("__throw", ParamsInt1, Unit, nullptr,
1205+
define_builtin_func("__throw", ParamsInt1, Never, nullptr,
12051206
compile_throw,
12061207
0);
1207-
define_builtin_func("__throw_arg", {typeT, Int}, Unit, declGenericT,
1208+
define_builtin_func("__throw_arg", {typeT, Int}, Never, declGenericT,
12081209
compile_throw_arg,
12091210
0);
12101211
define_builtin_func("__throw_if_unless", ParamsInt3, Unit, nullptr,

tolk/codegen.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,16 @@ void Stack::rearrange_top(var_idx_t top, bool last) {
274274

275275
bool Op::generate_code_step(Stack& stack) {
276276
stack.opt_show();
277-
stack.drop_vars_except(var_info);
278-
stack.opt_show();
277+
278+
// detect `throw 123` (actually _IntConst 123 + _Call __throw)
279+
// don't clear the stack, since dropping unused elements make no sense, an exception is thrown anyway
280+
bool will_now_immediate_throw = (cl == _Call && f_sym->is_builtin_function() && f_sym->name == "__throw")
281+
|| (cl == _IntConst && next->cl == _Call && next->f_sym->is_builtin_function() && next->f_sym->name == "__throw");
282+
if (!will_now_immediate_throw) {
283+
stack.drop_vars_except(var_info);
284+
stack.opt_show();
285+
}
286+
279287
bool inline_func = stack.mode & Stack::_InlineFunc;
280288
switch (cl) {
281289
case _Nop:
@@ -285,6 +293,7 @@ bool Op::generate_code_step(Stack& stack) {
285293
stack.enforce_state(left);
286294
if (stack.o.retalt_ && (stack.mode & Stack::_NeedRetAlt)) {
287295
stack.o << "RETALT";
296+
stack.o.retalt_inserted_ = true;
288297
}
289298
stack.opt_show();
290299
return false;
@@ -514,7 +523,7 @@ bool Op::generate_code_step(Stack& stack) {
514523
int j = ret_order ? ret_order->at(i) : i;
515524
stack.push_new_var(left.at(j));
516525
}
517-
return true;
526+
return !f_sym || f_sym->declared_return_type != TypeDataNever::create();
518527
}
519528
case _SetGlob: {
520529
tolk_assert(g_sym);

tolk/pipe-infer-types-and-calls.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,9 @@ class InferTypesAndCallsAndFieldsVisitor final {
10131013
TypePtr inferred_type = dot_obj && fun_ref->does_return_self() ? dot_obj->inferred_type : fun_ref->inferred_return_type;
10141014
assign_inferred_type(v, inferred_type);
10151015
assign_inferred_type(callee, fun_ref->inferred_full_type);
1016+
if (inferred_type == TypeDataNever::create()) {
1017+
flow.mark_unreachable(UnreachableKind::CallNeverReturnFunction);
1018+
}
10161019
// note, that mutate params don't affect typing, they are handled when converting to IR
10171020
return ExprFlow(std::move(flow), used_as_condition);
10181021
}
@@ -1139,6 +1142,7 @@ class InferTypesAndCallsAndFieldsVisitor final {
11391142
FlowContext process_throw_statement(V<ast_throw_statement> v, FlowContext&& flow) {
11401143
flow = infer_any_expr(v->get_thrown_code(), std::move(flow), false).out_flow;
11411144
flow = infer_any_expr(v->get_thrown_arg(), std::move(flow), false).out_flow;
1145+
flow.mark_unreachable(UnreachableKind::ThrowStatement);
11421146
return flow;
11431147
}
11441148

@@ -1209,6 +1213,9 @@ class InferTypesAndCallsAndFieldsVisitor final {
12091213

12101214
if (!body_end.is_unreachable()) {
12111215
fun_ref->mutate()->assign_is_implicit_return();
1216+
if (fun_ref->declared_return_type == TypeDataNever::create()) { // `never` can only be declared, it can't be inferred
1217+
fire(fun_ref, v_function->get_body()->as<ast_sequence>()->loc_end, "a function returning `never` can not have a reachable endpoint");
1218+
}
12121219
}
12131220

12141221
if (!fun_ref->declared_return_type) {

tolk/smart-casts-cfg.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ struct SinkExpression {
7777
enum class UnreachableKind {
7878
Unknown, // no definite info or not unreachable
7979
CantHappen,
80+
ThrowStatement,
8081
ReturnStatement,
8182
CallNeverReturnFunction,
8283
};

0 commit comments

Comments
 (0)