1
0
Fork 0
mirror of https://github.com/ton-blockchain/ton synced 2025-02-14 20:22:19 +00:00

[FunC] Forbid impure operations inside pure functions

In stdlib, all existing pure functions are asm-implemented.
But since we introduced a `pure` keyword applicable to user-defined functions,
we need to check that they won't have any side effects
(exceptions, globals modification, etc.)
This commit is contained in:
Aleksandr Kirsanov 2024-05-03 20:58:21 +03:00
parent 85c60d1263
commit ef5719d7e6
No known key found for this signature in database
GPG key ID: B758BBAA01FFB3D3
9 changed files with 181 additions and 51 deletions

View file

@ -223,15 +223,6 @@ void VarDescrList::show(std::ostream& os) const {
os << " ]\n";
}
void Op::flags_set_clear(int set, int clear) {
flags = (flags | set) & ~clear;
for (auto& op : block0) {
op.flags_set_clear(set, clear);
}
for (auto& op : block1) {
op.flags_set_clear(set, clear);
}
}
void Op::split_vars(const std::vector<TmpVar>& vars) {
split_var_list(left, vars);
split_var_list(right, vars);
@ -296,7 +287,7 @@ void Op::show(std::ostream& os, const std::vector<TmpVar>& vars, std::string pfx
if (noreturn()) {
dis += "<noret> ";
}
if (!is_pure()) {
if (impure()) {
dis += "<impure> ";
}
switch (cl) {
@ -469,12 +460,6 @@ void Op::show_block(std::ostream& os, const Op* block, const std::vector<TmpVar>
os << pfx << "}";
}
void CodeBlob::flags_set_clear(int set, int clear) {
for (auto& op : ops) {
op.flags_set_clear(set, clear);
}
}
std::ostream& operator<<(std::ostream& os, const CodeBlob& code) {
code.print(os);
return os;

View file

@ -360,10 +360,10 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
case _Tuple:
case _UnTuple: {
// left = EXEC right;
if (!next_var_info.count_used(left) && is_pure()) {
if (!next_var_info.count_used(left) && !impure()) {
// all variables in `left` are not needed
if (edit) {
disable();
set_disabled();
}
return std_compute_used_vars(true);
}
@ -372,7 +372,7 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
case _SetGlob: {
// GLOB = right
if (right.empty() && edit) {
disable();
set_disabled();
}
return std_compute_used_vars(right.empty());
}
@ -399,7 +399,7 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
}
if (!cnt && edit) {
// all variables in `left` are not needed
disable();
set_disabled();
}
return set_var_info(std::move(new_var_info));
}
@ -860,15 +860,45 @@ VarDescrList Op::fwd_analyze(VarDescrList values) {
}
}
bool Op::set_noreturn(bool nr) {
if (nr) {
void Op::set_disabled(bool flag) {
if (flag) {
flags |= _Disabled;
} else {
flags &= ~_Disabled;
}
}
bool Op::set_noreturn(bool flag) {
if (flag) {
flags |= _NoReturn;
} else {
flags &= ~_NoReturn;
}
return nr;
return flag;
}
void Op::set_impure(const CodeBlob &code) {
// todo calling this function with `code` is a bad design (flags are assigned after Op is constructed)
// later it's better to check this somewhere in code.emplace_back()
if (code.flags & CodeBlob::_ForbidImpure) {
throw src::ParseError(where, "An impure operation in a pure function");
}
flags |= _Impure;
}
void Op::set_impure(const CodeBlob &code, bool flag) {
if (flag) {
if (code.flags & CodeBlob::_ForbidImpure) {
throw src::ParseError(where, "An impure operation in a pure function");
}
flags |= _Impure;
} else {
flags &= ~_Impure;
}
}
bool Op::mark_noreturn() {
switch (cl) {
case _Nop:
@ -888,13 +918,14 @@ bool Op::mark_noreturn() {
case _Call:
return set_noreturn(next->mark_noreturn());
case _Return:
return set_noreturn(true);
return set_noreturn();
case _If:
case _TryCatch:
// note, that & | (not && ||) here and below is mandatory to invoke both left and right calls
return set_noreturn((block0->mark_noreturn() & (block1 && block1->mark_noreturn())) | next->mark_noreturn());
case _Again:
block0->mark_noreturn();
return set_noreturn(true);
return set_noreturn();
case _Until:
return set_noreturn(block0->mark_noreturn() | next->mark_noreturn());
case _While:

View file

@ -0,0 +1,18 @@
int f_impure();
int f_pure() pure {
return f_impure();
}
int main() {
return f_pure();
}
{-
@compilation_should_fail
@stderr
"""
An impure operation in a pure function
return f_impure();
"""
-}

View file

@ -0,0 +1,25 @@
builder begin_cell() pure asm "NEWC";
global int g;
(builder) f_pure() pure {
var g; // strange, but this doesn't make a variable local, it still refers to a global one
builder b = begin_cell();
g = g + 1;
return b;
}
int main() {
g = 0;
f_pure();
return g;
}
{-
@compilation_should_fail
@stderr
"""
An impure operation in a pure function
g = g + 1;
"""
-}

View file

@ -0,0 +1,23 @@
(int, int, int, int) compute_data_size?(cell c, int max_cells) pure asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
builder begin_cell() pure asm "NEWC";
cell end_cell(builder b) pure asm "ENDC";
(int, int) validate_input(cell input) pure {
var (x, y, z, correct) = compute_data_size?(input, 10);
throw_unless(102, correct);
}
int main() pure {
cell c = begin_cell().end_cell();
validate_input(c);
return 0;
}
{-
@compilation_should_fail
@stderr
"""
An impure operation in a pure function
throw_unless
"""
-}

View file

@ -0,0 +1,47 @@
cell get_data() pure asm "c4 PUSH";
slice begin_parse(cell c) pure asm "CTOS";
builder begin_cell() pure asm "NEWC";
cell end_cell(builder b) pure asm "ENDC";
() set_data(cell c) asm "c4 POP";
int f_pure2() pure;
int f_pure1() pure {
return f_pure2();
}
int f_pure2() pure {
return 2;
}
(int, int) get_contract_data() pure {
cell c = get_data();
slice cs = c.begin_parse();
cs~load_bits(32);
int value = cs~load_uint(16);
return (1, value);
}
() save_contract_data(int value) {
builder b = begin_cell().store_int(1, 32).store_uint(value, 16);
set_data(b.end_cell());
}
int test1() pure method_id(101) {
return f_pure1();
}
int test2(int value) method_id(102) {
save_contract_data(value);
(_, var restored) = get_contract_data();
return restored;
}
() main() { return (); }
{-
TESTCASE | 101 | | 2
TESTCASE | 102 | 44 | 44
-}

View file

@ -593,16 +593,19 @@ struct Op {
SymDef* _fun = nullptr)
: cl(_cl), flags(0), fun_ref(_fun), where(_where), left(std::move(_left)), right(std::move(_right)) {
}
bool disabled() const {
return flags & _Disabled;
}
bool enabled() const {
return !disabled();
}
void disable() {
flags |= _Disabled;
}
void flags_set_clear(int set, int clear);
bool disabled() const { return flags & _Disabled; }
void set_disabled() { flags |= _Disabled; }
void set_disabled(bool flag);
bool noreturn() const { return flags & _NoReturn; }
bool set_noreturn() { flags |= _NoReturn; return true; }
bool set_noreturn(bool flag);
bool impure() const { return flags & _Impure; }
void set_impure(const CodeBlob &code);
void set_impure(const CodeBlob &code, bool flag);
void show(std::ostream& os, const std::vector<TmpVar>& vars, std::string pfx = "", int mode = 0) const;
void show_var_list(std::ostream& os, const std::vector<var_idx_t>& idx_list, const std::vector<TmpVar>& vars) const;
void show_var_list(std::ostream& os, const std::vector<VarDescr>& list, const std::vector<TmpVar>& vars) const;
@ -618,17 +621,10 @@ struct Op {
bool set_var_info_except(VarDescrList&& new_var_info, const std::vector<var_idx_t>& var_list);
void prepare_args(VarDescrList values);
VarDescrList fwd_analyze(VarDescrList values);
bool set_noreturn(bool nr);
bool mark_noreturn();
bool noreturn() const {
return flags & _NoReturn;
}
bool is_empty() const {
return cl == _Nop && !next;
}
bool is_pure() const {
return !(flags & _Impure);
}
bool generate_code_step(Stack& stack);
void generate_code_all(Stack& stack);
Op& last() {
@ -689,7 +685,7 @@ typedef std::vector<FormalArg> FormalArgList;
struct AsmOpList;
struct CodeBlob {
enum { _AllowPostModification = 1, _ComputeAsmLtr = 2 };
enum { _AllowPostModification = 1, _ComputeAsmLtr = 2, _ForbidImpure = 4 };
int var_cnt, in_var_cnt, op_cnt;
TypeExpr* ret_type;
std::string name;
@ -733,7 +729,6 @@ struct CodeBlob {
pop_cur();
}
void simplify_var_types();
void flags_set_clear(int set, int clear);
void prune_unreachable_code();
void fwd_analyze();
void mark_noreturn();

View file

@ -229,7 +229,7 @@ var_idx_t Expr::new_tmp(CodeBlob& code) const {
void add_set_globs(CodeBlob& code, std::vector<std::pair<SymDef*, var_idx_t>>& globs, const SrcLocation& here) {
for (const auto& p : globs) {
auto& op = code.emplace_back(here, Op::_SetGlob, std::vector<var_idx_t>{}, std::vector<var_idx_t>{ p.second }, p.first);
op.flags |= Op::_Impure;
op.set_impure(code);
}
}
@ -289,7 +289,7 @@ std::vector<var_idx_t> pre_compile_tensor(const std::vector<Expr *> args, CodeBl
if (code.flags & CodeBlob::_AllowPostModification) {
if (!lval_globs && (var.cls & TmpVar::_Named)) {
Op *op = &code.emplace_back(nullptr, Op::_Let, std::vector<var_idx_t>(), std::vector<var_idx_t>());
op->flags |= Op::_Disabled;
op->set_disabled();
var.on_modification.push_back([modified_vars, i, j, op, done = false](const SrcLocation &here) mutable {
if (!done) {
done = true;
@ -319,7 +319,7 @@ std::vector<var_idx_t> pre_compile_tensor(const std::vector<Expr *> args, CodeBl
var_idx_t v2 = code.create_tmp_var(code.vars[v].v_type, code.vars[v].where.get());
m.op->left = {v2};
m.op->right = {v};
m.op->flags &= ~Op::_Disabled;
m.op->set_disabled(false);
v = v2;
}
std::vector<var_idx_t> res;
@ -371,7 +371,7 @@ std::vector<var_idx_t> Expr::pre_compile(CodeBlob& code, std::vector<std::pair<S
auto rvect = new_tmp_vect(code);
auto& op = code.emplace_back(here, Op::_Call, rvect, res, applied_sym);
if (flags & _IsImpure) {
op.flags |= Op::_Impure;
op.set_impure(code);
}
return rvect;
}
@ -389,7 +389,7 @@ std::vector<var_idx_t> Expr::pre_compile(CodeBlob& code, std::vector<std::pair<S
auto rvect = new_tmp_vect(code);
auto& op = code.emplace_back(here, Op::_Call, rvect, std::move(res), args[0]->sym);
if (args[0]->flags & _IsImpure) {
op.flags |= Op::_Impure;
op.set_impure(code);
}
return rvect;
} else {

View file

@ -1216,7 +1216,7 @@ blk_fl::val parse_stmt(Lexer& lex, CodeBlob& code) {
}
}
CodeBlob* parse_func_body(Lexer& lex, FormalArgList arg_list, TypeExpr* ret_type) {
CodeBlob* parse_func_body(Lexer& lex, FormalArgList arg_list, TypeExpr* ret_type, bool marked_as_pure) {
lex.expect('{');
CodeBlob* blob = new CodeBlob{ret_type};
if (pragma_allow_post_modification.enabled()) {
@ -1225,6 +1225,9 @@ CodeBlob* parse_func_body(Lexer& lex, FormalArgList arg_list, TypeExpr* ret_type
if (pragma_compute_asm_ltr.enabled()) {
blob->flags |= CodeBlob::_ComputeAsmLtr;
}
if (marked_as_pure) {
blob->flags |= CodeBlob::_ForbidImpure;
}
blob->import_params(std::move(arg_list));
blk_fl::val res = blk_fl::init;
bool warned = false;
@ -1604,7 +1607,10 @@ void parse_func_def(Lexer& lex) {
if (func_sym_code->code) {
lex.cur().error("redefinition of function `"s + func_name.str + "`");
}
CodeBlob* code = parse_func_body(lex, arg_list, ret_type);
if (marked_as_pure && ret_type->get_width() == 0) {
lex.cur().error("a pure function should return something, otherwise it will be optimized out anyway");
}
CodeBlob* code = parse_func_body(lex, arg_list, ret_type, marked_as_pure);
code->name = func_name.str;
code->loc = loc;
// code->print(std::cerr); // !!!DEBUG!!!