mirror of
https://github.com/ton-blockchain/ton
synced 2025-02-15 04:32:21 +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:
parent
85c60d1263
commit
ef5719d7e6
9 changed files with 181 additions and 51 deletions
|
@ -223,15 +223,6 @@ void VarDescrList::show(std::ostream& os) const {
|
||||||
os << " ]\n";
|
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) {
|
void Op::split_vars(const std::vector<TmpVar>& vars) {
|
||||||
split_var_list(left, vars);
|
split_var_list(left, vars);
|
||||||
split_var_list(right, 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()) {
|
if (noreturn()) {
|
||||||
dis += "<noret> ";
|
dis += "<noret> ";
|
||||||
}
|
}
|
||||||
if (!is_pure()) {
|
if (impure()) {
|
||||||
dis += "<impure> ";
|
dis += "<impure> ";
|
||||||
}
|
}
|
||||||
switch (cl) {
|
switch (cl) {
|
||||||
|
@ -469,12 +460,6 @@ void Op::show_block(std::ostream& os, const Op* block, const std::vector<TmpVar>
|
||||||
os << pfx << "}";
|
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) {
|
std::ostream& operator<<(std::ostream& os, const CodeBlob& code) {
|
||||||
code.print(os);
|
code.print(os);
|
||||||
return os;
|
return os;
|
||||||
|
|
|
@ -360,10 +360,10 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
|
||||||
case _Tuple:
|
case _Tuple:
|
||||||
case _UnTuple: {
|
case _UnTuple: {
|
||||||
// left = EXEC right;
|
// 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
|
// all variables in `left` are not needed
|
||||||
if (edit) {
|
if (edit) {
|
||||||
disable();
|
set_disabled();
|
||||||
}
|
}
|
||||||
return std_compute_used_vars(true);
|
return std_compute_used_vars(true);
|
||||||
}
|
}
|
||||||
|
@ -372,7 +372,7 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
|
||||||
case _SetGlob: {
|
case _SetGlob: {
|
||||||
// GLOB = right
|
// GLOB = right
|
||||||
if (right.empty() && edit) {
|
if (right.empty() && edit) {
|
||||||
disable();
|
set_disabled();
|
||||||
}
|
}
|
||||||
return std_compute_used_vars(right.empty());
|
return std_compute_used_vars(right.empty());
|
||||||
}
|
}
|
||||||
|
@ -399,7 +399,7 @@ bool Op::compute_used_vars(const CodeBlob& code, bool edit) {
|
||||||
}
|
}
|
||||||
if (!cnt && edit) {
|
if (!cnt && edit) {
|
||||||
// all variables in `left` are not needed
|
// all variables in `left` are not needed
|
||||||
disable();
|
set_disabled();
|
||||||
}
|
}
|
||||||
return set_var_info(std::move(new_var_info));
|
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) {
|
void Op::set_disabled(bool flag) {
|
||||||
if (nr) {
|
if (flag) {
|
||||||
|
flags |= _Disabled;
|
||||||
|
} else {
|
||||||
|
flags &= ~_Disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool Op::set_noreturn(bool flag) {
|
||||||
|
if (flag) {
|
||||||
flags |= _NoReturn;
|
flags |= _NoReturn;
|
||||||
} else {
|
} else {
|
||||||
flags &= ~_NoReturn;
|
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() {
|
bool Op::mark_noreturn() {
|
||||||
switch (cl) {
|
switch (cl) {
|
||||||
case _Nop:
|
case _Nop:
|
||||||
|
@ -888,13 +918,14 @@ bool Op::mark_noreturn() {
|
||||||
case _Call:
|
case _Call:
|
||||||
return set_noreturn(next->mark_noreturn());
|
return set_noreturn(next->mark_noreturn());
|
||||||
case _Return:
|
case _Return:
|
||||||
return set_noreturn(true);
|
return set_noreturn();
|
||||||
case _If:
|
case _If:
|
||||||
case _TryCatch:
|
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());
|
return set_noreturn((block0->mark_noreturn() & (block1 && block1->mark_noreturn())) | next->mark_noreturn());
|
||||||
case _Again:
|
case _Again:
|
||||||
block0->mark_noreturn();
|
block0->mark_noreturn();
|
||||||
return set_noreturn(true);
|
return set_noreturn();
|
||||||
case _Until:
|
case _Until:
|
||||||
return set_noreturn(block0->mark_noreturn() | next->mark_noreturn());
|
return set_noreturn(block0->mark_noreturn() | next->mark_noreturn());
|
||||||
case _While:
|
case _While:
|
||||||
|
|
18
crypto/func/auto-tests/tests/invalid-pure-1.fc
Normal file
18
crypto/func/auto-tests/tests/invalid-pure-1.fc
Normal 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();
|
||||||
|
"""
|
||||||
|
-}
|
25
crypto/func/auto-tests/tests/invalid-pure-2.fc
Normal file
25
crypto/func/auto-tests/tests/invalid-pure-2.fc
Normal 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;
|
||||||
|
"""
|
||||||
|
-}
|
23
crypto/func/auto-tests/tests/invalid-pure-3.fc
Normal file
23
crypto/func/auto-tests/tests/invalid-pure-3.fc
Normal 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
|
||||||
|
"""
|
||||||
|
-}
|
47
crypto/func/auto-tests/tests/pure-functions.fc
Normal file
47
crypto/func/auto-tests/tests/pure-functions.fc
Normal 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
|
||||||
|
|
||||||
|
-}
|
|
@ -593,16 +593,19 @@ struct Op {
|
||||||
SymDef* _fun = nullptr)
|
SymDef* _fun = nullptr)
|
||||||
: cl(_cl), flags(0), fun_ref(_fun), where(_where), left(std::move(_left)), right(std::move(_right)) {
|
: cl(_cl), flags(0), fun_ref(_fun), where(_where), left(std::move(_left)), right(std::move(_right)) {
|
||||||
}
|
}
|
||||||
bool disabled() const {
|
|
||||||
return flags & _Disabled;
|
bool disabled() const { return flags & _Disabled; }
|
||||||
}
|
void set_disabled() { flags |= _Disabled; }
|
||||||
bool enabled() const {
|
void set_disabled(bool flag);
|
||||||
return !disabled();
|
|
||||||
}
|
bool noreturn() const { return flags & _NoReturn; }
|
||||||
void disable() {
|
bool set_noreturn() { flags |= _NoReturn; return true; }
|
||||||
flags |= _Disabled;
|
bool set_noreturn(bool flag);
|
||||||
}
|
|
||||||
void flags_set_clear(int set, int clear);
|
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(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<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;
|
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);
|
bool set_var_info_except(VarDescrList&& new_var_info, const std::vector<var_idx_t>& var_list);
|
||||||
void prepare_args(VarDescrList values);
|
void prepare_args(VarDescrList values);
|
||||||
VarDescrList fwd_analyze(VarDescrList values);
|
VarDescrList fwd_analyze(VarDescrList values);
|
||||||
bool set_noreturn(bool nr);
|
|
||||||
bool mark_noreturn();
|
bool mark_noreturn();
|
||||||
bool noreturn() const {
|
|
||||||
return flags & _NoReturn;
|
|
||||||
}
|
|
||||||
bool is_empty() const {
|
bool is_empty() const {
|
||||||
return cl == _Nop && !next;
|
return cl == _Nop && !next;
|
||||||
}
|
}
|
||||||
bool is_pure() const {
|
|
||||||
return !(flags & _Impure);
|
|
||||||
}
|
|
||||||
bool generate_code_step(Stack& stack);
|
bool generate_code_step(Stack& stack);
|
||||||
void generate_code_all(Stack& stack);
|
void generate_code_all(Stack& stack);
|
||||||
Op& last() {
|
Op& last() {
|
||||||
|
@ -689,7 +685,7 @@ typedef std::vector<FormalArg> FormalArgList;
|
||||||
struct AsmOpList;
|
struct AsmOpList;
|
||||||
|
|
||||||
struct CodeBlob {
|
struct CodeBlob {
|
||||||
enum { _AllowPostModification = 1, _ComputeAsmLtr = 2 };
|
enum { _AllowPostModification = 1, _ComputeAsmLtr = 2, _ForbidImpure = 4 };
|
||||||
int var_cnt, in_var_cnt, op_cnt;
|
int var_cnt, in_var_cnt, op_cnt;
|
||||||
TypeExpr* ret_type;
|
TypeExpr* ret_type;
|
||||||
std::string name;
|
std::string name;
|
||||||
|
@ -733,7 +729,6 @@ struct CodeBlob {
|
||||||
pop_cur();
|
pop_cur();
|
||||||
}
|
}
|
||||||
void simplify_var_types();
|
void simplify_var_types();
|
||||||
void flags_set_clear(int set, int clear);
|
|
||||||
void prune_unreachable_code();
|
void prune_unreachable_code();
|
||||||
void fwd_analyze();
|
void fwd_analyze();
|
||||||
void mark_noreturn();
|
void mark_noreturn();
|
||||||
|
|
|
@ -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) {
|
void add_set_globs(CodeBlob& code, std::vector<std::pair<SymDef*, var_idx_t>>& globs, const SrcLocation& here) {
|
||||||
for (const auto& p : globs) {
|
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);
|
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 (code.flags & CodeBlob::_AllowPostModification) {
|
||||||
if (!lval_globs && (var.cls & TmpVar::_Named)) {
|
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 *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 {
|
var.on_modification.push_back([modified_vars, i, j, op, done = false](const SrcLocation &here) mutable {
|
||||||
if (!done) {
|
if (!done) {
|
||||||
done = true;
|
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());
|
var_idx_t v2 = code.create_tmp_var(code.vars[v].v_type, code.vars[v].where.get());
|
||||||
m.op->left = {v2};
|
m.op->left = {v2};
|
||||||
m.op->right = {v};
|
m.op->right = {v};
|
||||||
m.op->flags &= ~Op::_Disabled;
|
m.op->set_disabled(false);
|
||||||
v = v2;
|
v = v2;
|
||||||
}
|
}
|
||||||
std::vector<var_idx_t> res;
|
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 rvect = new_tmp_vect(code);
|
||||||
auto& op = code.emplace_back(here, Op::_Call, rvect, res, applied_sym);
|
auto& op = code.emplace_back(here, Op::_Call, rvect, res, applied_sym);
|
||||||
if (flags & _IsImpure) {
|
if (flags & _IsImpure) {
|
||||||
op.flags |= Op::_Impure;
|
op.set_impure(code);
|
||||||
}
|
}
|
||||||
return rvect;
|
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 rvect = new_tmp_vect(code);
|
||||||
auto& op = code.emplace_back(here, Op::_Call, rvect, std::move(res), args[0]->sym);
|
auto& op = code.emplace_back(here, Op::_Call, rvect, std::move(res), args[0]->sym);
|
||||||
if (args[0]->flags & _IsImpure) {
|
if (args[0]->flags & _IsImpure) {
|
||||||
op.flags |= Op::_Impure;
|
op.set_impure(code);
|
||||||
}
|
}
|
||||||
return rvect;
|
return rvect;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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('{');
|
lex.expect('{');
|
||||||
CodeBlob* blob = new CodeBlob{ret_type};
|
CodeBlob* blob = new CodeBlob{ret_type};
|
||||||
if (pragma_allow_post_modification.enabled()) {
|
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()) {
|
if (pragma_compute_asm_ltr.enabled()) {
|
||||||
blob->flags |= CodeBlob::_ComputeAsmLtr;
|
blob->flags |= CodeBlob::_ComputeAsmLtr;
|
||||||
}
|
}
|
||||||
|
if (marked_as_pure) {
|
||||||
|
blob->flags |= CodeBlob::_ForbidImpure;
|
||||||
|
}
|
||||||
blob->import_params(std::move(arg_list));
|
blob->import_params(std::move(arg_list));
|
||||||
blk_fl::val res = blk_fl::init;
|
blk_fl::val res = blk_fl::init;
|
||||||
bool warned = false;
|
bool warned = false;
|
||||||
|
@ -1604,7 +1607,10 @@ void parse_func_def(Lexer& lex) {
|
||||||
if (func_sym_code->code) {
|
if (func_sym_code->code) {
|
||||||
lex.cur().error("redefinition of function `"s + func_name.str + "`");
|
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->name = func_name.str;
|
||||||
code->loc = loc;
|
code->loc = loc;
|
||||||
// code->print(std::cerr); // !!!DEBUG!!!
|
// code->print(std::cerr); // !!!DEBUG!!!
|
||||||
|
|
Loading…
Reference in a new issue