diff --git a/tolk-tester/tests/inference-tests.tolk b/tolk-tester/tests/inference-tests.tolk index 96bf8b1a..5020d0dd 100644 --- a/tolk-tester/tests/inference-tests.tolk +++ b/tolk-tester/tests/inference-tests.tolk @@ -86,6 +86,17 @@ fun test7() { // __expect_type(eq<(int, slice)>, "(int, slice) -> (int, slice)"); } +fun alwaysThrows(): never { throw 123; } +fun alwaysThrowsNotAnnotated() { throw 123; } +fun alwaysThrowsNotAnnotated2() { alwaysThrows(); } + +fun test9() { + __expect_type(alwaysThrows(), "never"); + __expect_type(alwaysThrows, "() -> never"); + __expect_type(alwaysThrowsNotAnnotated(), "void"); + __expect_type(alwaysThrowsNotAnnotated2(), "void"); +} + fun main() { return 0; diff --git a/tolk-tester/tests/invalid-never-1.tolk b/tolk-tester/tests/invalid-never-1.tolk new file mode 100644 index 00000000..68c6c804 --- /dev/null +++ b/tolk-tester/tests/invalid-never-1.tolk @@ -0,0 +1,8 @@ +fun invalidNever(): never { + if (random()) { throw 123; } +} + +/** +@compilation_should_fail +@stderr a function returning `never` can not have a reachable endpoint + */ diff --git a/tolk-tester/tests/try-func.tolk b/tolk-tester/tests/try-func.tolk index dfd72e9e..4ac86d96 100644 --- a/tolk-tester/tests/try-func.tolk +++ b/tolk-tester/tests/try-func.tolk @@ -164,6 +164,78 @@ fun test109(): (int, int) { return (g_reg, l_reg); } +fun alwaysThrow123(): never { + throw 123; +} + +fun alwaysThrowX(x: int): never { + if (x > 10) { throw (x, beginCell()); } + else { throw (x, null); } +} + +fun anotherNever(throw123: bool): never { + if (throw123) { alwaysThrow123(); } + alwaysThrowX(456); +} + +fun testCodegen1(x: int) { + if (x > 10) { + throw 123; + anotherNever(true); // unreachable, will be dropped + } + else if (x < 10) { + throw x; + return -123; // unreachable, will be dropped + } + return 0; +} + +fun testCodegen2(x: int) { + if (x > 10) { + alwaysThrow123(); + anotherNever(true); // unreachable, will be dropped + } + else if (x < 10) { + anotherNever(false); + return -123; // unreachable, will be dropped + } + return 0; +} + +@method_id(110) +fun test110(b: bool) { + try { + if (b == true) { testCodegen1(100); } + testCodegen1(5); + return -1; + } catch (ex) { + return ex; + } +} + +@method_id(111) +fun test111(b: bool) { + try { + if (b == true) { testCodegen2(100); } + testCodegen2(5); + return -1; + } catch (ex) { + return ex; + } +} + +fun mySetCode(newCode: slice): void + asm "SETCODE"; + +fun testCodegen3(numberId: int, paramVal: cell) { + if (numberId == -1000) { + var cs = paramVal.beginParse(); + mySetCode(cs); + throw 0; + } + paramVal.beginParse(); +} + fun main() { } @@ -187,6 +259,65 @@ fun main() { @testcase | 107 | 5 | 5 @testcase | 107 | 20 | 20 @testcase | 108 | | 0 +@testcase | 109 | | 10 10 +@testcase | 110 | -1 | 123 +@testcase | 110 | 0 | 5 +@testcase | 111 | -1 | 123 +@testcase | 111 | 0 | 456 -@code_hash 39307974281105539319288356721945232226028429128341177951717392648324358675585 +@code_hash 57361460846265694653029920796509802052573595128418810728101968091567195330515 + +@fif_codegen +""" + testCodegen1 PROC:<{ + // x + DUP // x x + 10 GTINT // x '2 + IFJMP:<{ // x + 123 THROW + }> // x + DUP // x x + 10 LESSINT // x '6 + IFJMP:<{ // x + THROWANY + }> // x + DROP // + 0 PUSHINT // '8=0 + }> +""" + +@fif_codegen +""" + testCodegen2 PROC:<{ + // x + DUP // x x + 10 GTINT // x '2 + IFJMP:<{ // x + DROP // + alwaysThrow123 CALLDICT + }> // x + 10 LESSINT // '5 + IFJMP:<{ // + FALSE // '6 + anotherNever CALLDICT + }> // + 0 PUSHINT // '8=0 + }> +""" + +@fif_codegen +""" + testCodegen3 PROC:<{ + // numberId paramVal + SWAP + -1000 PUSHINT // paramVal numberId '2=-1000 + EQUAL // paramVal '3 + IFJMP:<{ // paramVal + CTOS // cs + SETCODE + 0 THROW + }> // paramVal + DROP // + }> +""" */ diff --git a/tolk-tester/tests/unreachable-4.tolk b/tolk-tester/tests/unreachable-4.tolk new file mode 100644 index 00000000..6b25b3d9 --- /dev/null +++ b/tolk-tester/tests/unreachable-4.tolk @@ -0,0 +1,24 @@ +fun alwaysThrows(): never { + throw 456; +} + +fun testUnreachable(x: int) { + if (x) { throw 123; } + else { alwaysThrows(); } + return 1; +} + +fun main() { + try { + testUnreachable(100); + throw 80; + } catch (excNo) { + return excNo; + } +} + +/** +@testcase | 0 | | 123 +@stderr warning: unreachable code +@stderr return 1; + */ diff --git a/tolk/analyzer.cpp b/tolk/analyzer.cpp index 9303bc83..c38b0bfa 100644 --- a/tolk/analyzer.cpp +++ b/tolk/analyzer.cpp @@ -20,6 +20,13 @@ namespace tolk { +// functions returning "never" are assumed to interrupt flow +// for instance, variables after their call aren't considered used +// its main purpose is `throw` statement, it's a call to a built-in `__throw` function +static bool does_function_always_throw(FunctionPtr fun_ref) { + return fun_ref->declared_return_type == TypeDataNever::create(); +} + /* * * ANALYZE AND PREPROCESS ABSTRACT CODE @@ -262,17 +269,6 @@ VarDescrList& VarDescrList::operator|=(const VarDescrList& y) { } } -VarDescrList& VarDescrList::operator&=(const VarDescrList& values) { - for (const VarDescr& vd : values.list) { - VarDescr* item = operator[](vd.idx); - if (item) { - *item &= vd; - } - } - unreachable |= values.unreachable; - return *this; -} - VarDescrList& VarDescrList::import_values(const VarDescrList& values) { if (values.unreachable) { set_unreachable(); @@ -326,6 +322,17 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) { } return std_compute_used_vars(true); } + if (cl == _Call && does_function_always_throw(f_sym)) { + VarDescrList new_var_info; // empty, not next->var_info + if (args.size() == right.size()) { + for (const VarDescr& arg : args) { + new_var_info.add_var(arg.idx, arg.is_unused()); + } + } else { + new_var_info.add_vars(right, false); + } + return set_var_info(std::move(new_var_info)); + } return std_compute_used_vars(); } case _SetGlob: { @@ -516,20 +523,19 @@ bool prune_unreachable(std::unique_ptr& ops) { case Op::_SliceConst: case Op::_GlobVar: case Op::_SetGlob: - case Op::_Call: case Op::_CallInd: case Op::_Tuple: case Op::_UnTuple: case Op::_Import: + case Op::_Let: reach = true; break; - case Op::_Let: { - reach = true; - break; - } case Op::_Return: reach = false; break; + case Op::_Call: + reach = !does_function_always_throw(op.f_sym); + break; case Op::_If: { // if left then block0 else block1; ... VarDescr* c_var = op.var_info[op.left[0]]; @@ -712,6 +718,9 @@ VarDescrList Op::fwd_analyze(VarDescrList values) { values.add_newval(i); } } + if (does_function_always_throw(f_sym)) { + values.set_unreachable(); + } break; } case _Tuple: @@ -860,10 +869,11 @@ bool Op::mark_noreturn() { case _SetGlob: case _GlobVar: case _CallInd: - case _Call: return set_noreturn(next->mark_noreturn()); case _Return: return set_noreturn(); + case _Call: + return set_noreturn(next->mark_noreturn() || does_function_always_throw(f_sym)); case _If: case _TryCatch: // note, that & | (not && ||) here and below is mandatory to invoke both left and right calls diff --git a/tolk/builtins.cpp b/tolk/builtins.cpp index 2b207c25..cb89c984 100644 --- a/tolk/builtins.cpp +++ b/tolk/builtins.cpp @@ -1088,6 +1088,7 @@ void define_builtins() { TypePtr Slice = TypeDataSlice::create(); TypePtr Builder = TypeDataBuilder::create(); TypePtr Tuple = TypeDataTuple::create(); + TypePtr Never = TypeDataNever::create(); std::vector itemsT; itemsT.emplace_back("T"); @@ -1201,10 +1202,10 @@ void define_builtins() { define_builtin_func("__isNull", {typeT}, Bool, declGenericT, compile_is_null, FunctionData::flagMarkedAsPure); - define_builtin_func("__throw", ParamsInt1, Unit, nullptr, + define_builtin_func("__throw", ParamsInt1, Never, nullptr, compile_throw, 0); - define_builtin_func("__throw_arg", {typeT, Int}, Unit, declGenericT, + define_builtin_func("__throw_arg", {typeT, Int}, Never, declGenericT, compile_throw_arg, 0); define_builtin_func("__throw_if_unless", ParamsInt3, Unit, nullptr, diff --git a/tolk/codegen.cpp b/tolk/codegen.cpp index 5b2c50cc..ac1cf639 100644 --- a/tolk/codegen.cpp +++ b/tolk/codegen.cpp @@ -274,8 +274,16 @@ void Stack::rearrange_top(var_idx_t top, bool last) { bool Op::generate_code_step(Stack& stack) { stack.opt_show(); - stack.drop_vars_except(var_info); - stack.opt_show(); + + // detect `throw 123` (actually _IntConst 123 + _Call __throw) + // don't clear the stack, since dropping unused elements make no sense, an exception is thrown anyway + bool will_now_immediate_throw = (cl == _Call && f_sym->is_builtin_function() && f_sym->name == "__throw") + || (cl == _IntConst && next->cl == _Call && next->f_sym->is_builtin_function() && next->f_sym->name == "__throw"); + if (!will_now_immediate_throw) { + stack.drop_vars_except(var_info); + stack.opt_show(); + } + bool inline_func = stack.mode & Stack::_InlineFunc; switch (cl) { case _Nop: @@ -285,6 +293,7 @@ bool Op::generate_code_step(Stack& stack) { stack.enforce_state(left); if (stack.o.retalt_ && (stack.mode & Stack::_NeedRetAlt)) { stack.o << "RETALT"; + stack.o.retalt_inserted_ = true; } stack.opt_show(); return false; @@ -514,7 +523,7 @@ bool Op::generate_code_step(Stack& stack) { int j = ret_order ? ret_order->at(i) : i; stack.push_new_var(left.at(j)); } - return true; + return !f_sym || f_sym->declared_return_type != TypeDataNever::create(); } case _SetGlob: { tolk_assert(g_sym); diff --git a/tolk/pipe-infer-types-and-calls.cpp b/tolk/pipe-infer-types-and-calls.cpp index 7ab0aa1c..5fb12059 100644 --- a/tolk/pipe-infer-types-and-calls.cpp +++ b/tolk/pipe-infer-types-and-calls.cpp @@ -1013,6 +1013,9 @@ class InferTypesAndCallsAndFieldsVisitor final { TypePtr inferred_type = dot_obj && fun_ref->does_return_self() ? dot_obj->inferred_type : fun_ref->inferred_return_type; assign_inferred_type(v, inferred_type); assign_inferred_type(callee, fun_ref->inferred_full_type); + if (inferred_type == TypeDataNever::create()) { + flow.mark_unreachable(UnreachableKind::CallNeverReturnFunction); + } // note, that mutate params don't affect typing, they are handled when converting to IR return ExprFlow(std::move(flow), used_as_condition); } @@ -1139,6 +1142,7 @@ class InferTypesAndCallsAndFieldsVisitor final { FlowContext process_throw_statement(V v, FlowContext&& flow) { flow = infer_any_expr(v->get_thrown_code(), std::move(flow), false).out_flow; flow = infer_any_expr(v->get_thrown_arg(), std::move(flow), false).out_flow; + flow.mark_unreachable(UnreachableKind::ThrowStatement); return flow; } @@ -1209,6 +1213,9 @@ public: if (!body_end.is_unreachable()) { fun_ref->mutate()->assign_is_implicit_return(); + if (fun_ref->declared_return_type == TypeDataNever::create()) { // `never` can only be declared, it can't be inferred + fire(fun_ref, v_function->get_body()->as()->loc_end, "a function returning `never` can not have a reachable endpoint"); + } } if (!fun_ref->declared_return_type) { diff --git a/tolk/smart-casts-cfg.h b/tolk/smart-casts-cfg.h index 7321f952..b97c8864 100644 --- a/tolk/smart-casts-cfg.h +++ b/tolk/smart-casts-cfg.h @@ -77,6 +77,7 @@ struct SinkExpression { enum class UnreachableKind { Unknown, // no definite info or not unreachable CantHappen, + ThrowStatement, ReturnStatement, CallNeverReturnFunction, }; diff --git a/tolk/tolk.h b/tolk/tolk.h index d218d510..3f00d0d4 100644 --- a/tolk/tolk.h +++ b/tolk/tolk.h @@ -205,7 +205,6 @@ struct VarDescrList { std::size_t count_used(const std::vector idx_list) const; VarDescr& add(var_idx_t idx); VarDescr& add_newval(var_idx_t idx); - VarDescrList& operator&=(const VarDescrList& values); VarDescrList& import_values(const VarDescrList& values); VarDescrList operator|(const VarDescrList& y) const; VarDescrList& operator|=(const VarDescrList& values); @@ -575,6 +574,7 @@ struct AsmOpList { const std::vector* var_names_{nullptr}; std::vector constants_; bool retalt_{false}; + bool retalt_inserted_{false}; void out(std::ostream& os, int mode = 0) const; AsmOpList(int indent = 0, const std::vector* var_names = nullptr) : indent_(indent), var_names_(var_names) { } @@ -1030,7 +1030,7 @@ struct Stack { } void apply_wrappers(int callxargs_count) { bool is_inline = mode & _InlineFunc; - if (o.retalt_) { + if (o.retalt_inserted_) { o.insert(0, "SAMEALTSAVE"); o.insert(0, "c2 SAVE"); }