1
0
Fork 0
mirror of https://github.com/ton-blockchain/ton synced 2025-03-09 15:40:10 +00:00

[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.
This commit is contained in:
tolk-vm 2025-02-24 20:15:24 +03:00
parent 7bcb8b895f
commit ef0328837f
No known key found for this signature in database
GPG key ID: 7905DD7FE0324B12
10 changed files with 227 additions and 25 deletions

View file

@ -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<Op>& 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

View file

@ -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<GenericsDeclaration::GenericsItem> 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,

View file

@ -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);

View file

@ -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<ast_throw_statement> 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<ast_sequence>()->loc_end, "a function returning `never` can not have a reachable endpoint");
}
}
if (!fun_ref->declared_return_type) {

View file

@ -77,6 +77,7 @@ struct SinkExpression {
enum class UnreachableKind {
Unknown, // no definite info or not unreachable
CantHappen,
ThrowStatement,
ReturnStatement,
CallNeverReturnFunction,
};

View file

@ -205,7 +205,6 @@ struct VarDescrList {
std::size_t count_used(const std::vector<var_idx_t> 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<TmpVar>* var_names_{nullptr};
std::vector<Const> constants_;
bool retalt_{false};
bool retalt_inserted_{false};
void out(std::ostream& os, int mode = 0) const;
AsmOpList(int indent = 0, const std::vector<TmpVar>* 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");
}