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

[Tolk] Change order of assignment evaluation, lhs first

In FunC (and in Tolk before), the assignment
> lhs = rhs
evaluation order (at IR level) was "rhs first, lhs second".
In practice, this did not matter, because lhs could only
be a primitive:
> (v1, v2) = getValue()
Left side of assignment actually has no "evaluation".
Since Tolk implemented indexed access, there could be
> getTensor().0 = getValue()
or (in the future)
> getObject().field = getValue()
where evaluation order becomes significant.

Now evaluation order will be to "lhs first, rhs second"
(more expected from user's point of view), which will become
significant when building control flow graph.
This commit is contained in:
tolk-vm 2025-02-10 12:57:25 +04:00
parent 2a68c8610b
commit 1389ff6789
No known key found for this signature in database
GPG key ID: 7905DD7FE0324B12
20 changed files with 602 additions and 456 deletions

View file

@ -30,158 +30,17 @@
* Up to this point, all types have been inferred, all validity checks have been passed, etc.
* All properties in AST nodes are assigned and can be safely used (fun_ref, etc.).
* So, if execution reaches this pass, the input is (almost) correct, and code generation should succeed.
* The only thing additionally checked during this pass is tricky lvalue, like one and the same variable
* assigned/mutated multiple times in same expression, e.g. `(t.0, t.0) = rhs` / `f(mutate x.1.2, mutate x)`.
* (previously, there was a check for one variable modified twice like `(t.0, t.0) = rhs`, but after changing
* execution order of assignment to "first lhs, then lhs", it was removed for several reasons)
*/
namespace tolk {
// fire error on cases like `(a, a) = rhs` / `f(mutate t.1.0, mutate t.1.0)`
GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD
static void fire_error_variable_modified_twice_inside_same_expression(SrcLocation loc) {
throw ParseError(loc, "one variable modified twice inside the same expression");
}
class LValContext;
std::vector<var_idx_t> pre_compile_expr(AnyExprV v, CodeBlob& code, LValContext* lval_ctx = nullptr);
std::vector<var_idx_t> pre_compile_symbol(SrcLocation loc, const Symbol* sym, CodeBlob& code, LValContext* lval_ctx);
void process_any_statement(AnyV v, CodeBlob& code);
// fire error on cases like `(m.1.0, m.1) = rhs` (m.1 inside m.1.0 is "rval inside lval")
GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD
static void fire_error_variable_modified_and_read_inside_same_expression(SrcLocation loc) {
throw ParseError(loc, "one variable both modified and read inside the same expression");
}
// Main goal of LValContext is to handle non-primitive lvalues. At IR level, a usual local variable
// exists, but on its change, something non-trivial should happen.
// Example: `globalVar = 9` actually does `Const $5 = 9` + `Let $6 = $5` + `SetGlob "globVar" = $6`
// Example: `tupleVar.0 = 9` actually does `Const $5 = 9` + `Let $6 = $5` + `Const $7 = 0` + `Call tupleSetAt($4, $6, $7)`
// Of course, mixing globals with tuples should also be supported.
// To achieve this, treat tupleObj inside "tupleObj.i" like "rvalue inside lvalue".
// For instance, `globalTuple.0 = 9` reads global (like rvalue), assigns 9 to tmp var, modifies tuple, writes global.
// A challenging thing is handling "unique" parts, to be read/updated only once.
// Example: `f(mutate globalTensor.0, mutate globalTensor.1)`, then globalTensor should be read/written once.
// Example: `(t.0.0, t.0.1) = rhs` (m is [[int, int]]), then t.0 should be read/updated once.
// Solving this by calculating hashes of every lvalue or rvalue inside lvalue automatically gives an ability
// to detect and fire "multiple writes inside expression", like `(a, a) = rhs` / `[t.0, (t.0.1, c)] = rhs`.
// Note, that tensors (not tuples) `tensorVar.0 = 9` do not emit anything special (unless global).
class LValContext {
// every global variable used as lvalue is registered here
// example: `globalInt = 9`, implicit var is created `$tmp = 9`, and `SetGlob "globalInt" $tmp` is done after
// global tensors are stored as tuples (unpacked on reading, packed on writing), then multiple tmp vars are created
struct ModifiedGlob {
const GlobalVarData* glob_ref;
std::vector<var_idx_t> local_ir_idx; // typically 1, generally calc_width_on_stack() of global var (tensors)
void apply(CodeBlob& code, SrcLocation loc) const {
Op& op = code.emplace_back(loc, Op::_SetGlob, std::vector<var_idx_t>{}, local_ir_idx, glob_ref);
op.set_impure_flag();
}
};
// every tuple index used as lvalue is registered here
// example: `t.0 = 9`, implicit var is created `$tmp = 9`, as well as `$tmp_idx = 0` and `tupleSetAt()` is done after
// for `t.0.0` if t is `[[int, ...]]`, `tupleAt()` for it is done since it's rvalue, and `tupleSetAt()` is done 2 times
struct ModifiedTupleIndex {
uint64_t hash;
var_idx_t tuple_ir_idx;
var_idx_t index_ir_idx;
var_idx_t field_ir_idx;
void apply(CodeBlob& code, SrcLocation loc) const {
const FunctionData* builtin_sym = lookup_global_symbol("tupleSetAt")->as<FunctionData>();
code.emplace_back(loc, Op::_Call, std::vector{tuple_ir_idx}, std::vector{tuple_ir_idx, field_ir_idx, index_ir_idx}, builtin_sym);
}
};
int level_rval_inside_lval = 0;
std::vector<std::variant<ModifiedGlob, ModifiedTupleIndex>> modifications;
std::unordered_set<uint64_t> all_modified_hashes;
void fire_if_one_variable_modified_twice(SrcLocation loc, uint64_t modified_hash) {
if (!is_rval_inside_lval()) {
if (!all_modified_hashes.insert(modified_hash).second) {
fire_error_variable_modified_twice_inside_same_expression(loc);
}
if (all_modified_hashes.contains(~modified_hash)) {
fire_error_variable_modified_and_read_inside_same_expression(loc);
}
} else {
all_modified_hashes.insert(~modified_hash);
if (all_modified_hashes.contains(modified_hash)) {
fire_error_variable_modified_and_read_inside_same_expression(loc);
}
}
}
public:
void enter_rval_inside_lval() { level_rval_inside_lval++; }
void exit_rval_inside_lval() { level_rval_inside_lval--; }
bool is_rval_inside_lval() const { return level_rval_inside_lval > 0; }
uint64_t register_lval(SrcLocation loc, const LocalVarData* var_ref) {
uint64_t hash = reinterpret_cast<uint64_t>(var_ref);
fire_if_one_variable_modified_twice(loc, hash);
return hash;
}
uint64_t register_lval(SrcLocation loc, const GlobalVarData* glob_ref) {
uint64_t hash = reinterpret_cast<uint64_t>(glob_ref);
fire_if_one_variable_modified_twice(loc, hash);
return hash;
}
uint64_t register_lval(SrcLocation loc, V<ast_dot_access> v) {
uint64_t hash = 7;
AnyExprV leftmost_obj = v;
while (auto v_dot = leftmost_obj->try_as<ast_dot_access>()) {
if (!v_dot->is_target_indexed_access()) {
break;
}
hash = hash * 1915239017 + std::get<int>(v_dot->target);
leftmost_obj = v_dot->get_obj();
}
if (auto v_ref = leftmost_obj->try_as<ast_reference>()) {
hash *= reinterpret_cast<uint64_t>(v_ref->sym); // `v.0` and `v.0` in 2 places is the same
} else {
hash *= reinterpret_cast<uint64_t>(leftmost_obj); // unlike `f().0` and `f().0` (pointers to AST nodes differ)
}
fire_if_one_variable_modified_twice(loc, hash);
return hash;
}
const std::vector<var_idx_t>* exists_already_known_global(const GlobalVarData* glob_ref) const {
for (const auto& m : modifications) {
if (const auto* m_glob = std::get_if<ModifiedGlob>(&m); m_glob && m_glob->glob_ref == glob_ref) {
return &m_glob->local_ir_idx;
}
}
return nullptr;
}
const var_idx_t* exists_already_known_tuple_index(uint64_t hash) const {
for (const auto& m : modifications) {
if (const auto* m_tup = std::get_if<ModifiedTupleIndex>(&m); m_tup && m_tup->hash == hash) {
return &m_tup->field_ir_idx;
}
}
return nullptr;
}
void register_modified_global(const GlobalVarData* glob_ref, std::vector<var_idx_t> local_ir_idx) {
modifications.emplace_back(ModifiedGlob{glob_ref, std::move(local_ir_idx)});
}
void register_modified_tuple_index(uint64_t hash, var_idx_t tuple_ir_idx, var_idx_t index_ir_idx, var_idx_t field_ir_idx) {
modifications.emplace_back(ModifiedTupleIndex{hash, tuple_ir_idx, index_ir_idx, field_ir_idx});
}
void gen_ops_if_nonempty(CodeBlob& code, SrcLocation loc) const {
for (auto it = modifications.rbegin(); it != modifications.rend(); ++it) { // reverse, it's important
if (const auto* m_glob = std::get_if<ModifiedGlob>(&*it)) {
m_glob->apply(code, loc);
} else if (const auto* m_tup = std::get_if<ModifiedTupleIndex>(&*it)) {
m_tup->apply(code, loc);
}
}
}
};
// The goal of VarsModificationWatcher is to detect such cases: `return (x, x += y, x)`.
// Without any changes, ops will be { _Call $2 = +($0_x, $1_y); _Return $0_x, $2, $0_x } - incorrect
@ -229,8 +88,176 @@ public:
static VarsModificationWatcher vars_modification_watcher;
std::vector<var_idx_t> pre_compile_expr(AnyExprV v, CodeBlob& code, LValContext* lval_ctx = nullptr);
void process_any_statement(AnyV v, CodeBlob& code);
// Main goal of LValContext is to handle non-primitive lvalues. At IR level, a usual local variable
// exists, but on its change, something non-trivial should happen.
// Example: `globalVar = 9` actually does `Const $5 = 9` + `Let $6 = $5` + `SetGlob "globVar" = $6`
// Example: `tupleVar.0 = 9` actually does `Const $5 = 9` + `Let $6 = $5` + `Const $7 = 0` + `Call tupleSetAt($4, $6, $7)`
// Of course, mixing globals with tuples should also be supported.
// To achieve this, treat tupleObj inside "tupleObj.i" like "rvalue inside lvalue".
// For instance, `globalTuple.0 = 9` reads global (like rvalue), assigns 9 to tmp var, modifies tuple, writes global.
// Note, that tensors (not tuples) `tensorVar.0 = 9` do not emit anything special (unless global).
class LValContext {
// every global variable used as lvalue is registered here
// example: `globalInt = 9`, implicit var is created `$tmp = 9`, and `SetGlob "globalInt" $tmp` is done after
struct ModifiedGlobal {
const GlobalVarData* glob_ref;
std::vector<var_idx_t> lval_ir_idx; // typically 1, generally calc_width_on_stack() of global var (tensors)
// for 1-slot globals int/cell/slice, assigning to them is just SETGLOB
// same for tensors, if they are fully rewritten in an expression: `gTensor = (5,6)`
void apply_fully_rewrite(CodeBlob& code, SrcLocation loc) const {
Op& op = code.emplace_back(loc, Op::_SetGlob, std::vector<var_idx_t>{}, lval_ir_idx, glob_ref);
op.set_impure_flag();
}
// for N-slot globals tensor/struct/union, assigning to their parts, like `gTensor.1 = 6`
// we need to read gTensor as a whole (0-th and 1-th component), rewrite 1-th component, and SETGLOB a whole back
void apply_partially_rewrite(CodeBlob& code, SrcLocation loc, std::vector<bool>&& was_modified_by_let) const {
LValContext local_lval;
local_lval.enter_rval_inside_lval();
std::vector<var_idx_t> local_ir_idx = pre_compile_symbol(loc, glob_ref, code, &local_lval);
for (size_t i = 0; i < local_ir_idx.size(); ++i) {
if (was_modified_by_let[i]) {
code.emplace_back(loc, Op::_Let, std::vector{local_ir_idx[i]}, std::vector{lval_ir_idx[i]});
}
}
Op& op = code.emplace_back(loc, Op::_SetGlob, std::vector<var_idx_t>{}, local_ir_idx, glob_ref);
op.set_impure_flag();
}
};
// every tensor index, when a tensor is a global, is registered here (same for structs and fields)
// example: `global v: (int, int); v.1 = 5`, implicit var is created `$tmp = 5`, and when it's modified,
// we need to partially update w; essentially, apply_partially_rewrite() above will be called
struct ModifiedFieldOfGlobal {
AnyExprV tensor_obj;
int index_at;
std::vector<var_idx_t> lval_ir_idx;
void apply(CodeBlob& code, SrcLocation loc) const {
LValContext local_lval;
local_lval.enter_rval_inside_lval();
std::vector<var_idx_t> obj_ir_idx = pre_compile_expr(tensor_obj, code, &local_lval);
const TypeDataTensor* t_tensor = tensor_obj->inferred_type->try_as<TypeDataTensor>();
tolk_assert(t_tensor);
int stack_width = t_tensor->items[index_at]->calc_width_on_stack();
int stack_offset = 0;
for (int i = 0; i < index_at; ++i) {
stack_offset += t_tensor->items[i]->calc_width_on_stack();
}
std::vector<var_idx_t> field_ir_idx = {obj_ir_idx.begin() + stack_offset, obj_ir_idx.begin() + stack_offset + stack_width};
tolk_assert(field_ir_idx.size() == lval_ir_idx.size());
vars_modification_watcher.trigger_callbacks(field_ir_idx, loc);
code.emplace_back(loc, Op::_Let, field_ir_idx, lval_ir_idx);
local_lval.after_let(std::move(field_ir_idx), code, loc);
}
};
// every tuple index used as lvalue is registered here
// example: `t.0 = 9`, implicit var is created `$tmp = 9`, as well as `$tmp_idx = 0` and `tupleSetAt()` is done after
// for `t.0.0` if t is `[[int, ...]]`, `tupleAt()` for it is done since it's rvalue, and `tupleSetAt()` is done 2 times
struct ModifiedTupleIndex {
AnyExprV tuple_obj;
int index_at;
std::vector<var_idx_t> lval_ir_idx;
void apply(CodeBlob& code, SrcLocation loc) const {
LValContext local_lval;
local_lval.enter_rval_inside_lval();
std::vector<var_idx_t> tuple_ir_idx = pre_compile_expr(tuple_obj, code, &local_lval);
std::vector<var_idx_t> index_ir_idx = code.create_tmp_var(TypeDataInt::create(), loc, "(tuple-idx)");
code.emplace_back(loc, Op::_IntConst, index_ir_idx, td::make_refint(index_at));
vars_modification_watcher.trigger_callbacks(tuple_ir_idx, loc);
const FunctionData* builtin_sym = lookup_global_symbol("tupleSetAt")->as<FunctionData>();
code.emplace_back(loc, Op::_Call, std::vector{tuple_ir_idx}, std::vector{tuple_ir_idx[0], lval_ir_idx[0], index_ir_idx[0]}, builtin_sym);
local_lval.after_let(std::move(tuple_ir_idx), code, loc);
}
};
int level_rval_inside_lval = 0;
std::vector<std::variant<ModifiedGlobal, ModifiedTupleIndex, ModifiedFieldOfGlobal>> modifications;
static bool vector_contains(const std::vector<var_idx_t>& ir_vars, var_idx_t ir_idx) {
for (var_idx_t var_in_vector : ir_vars) {
if (var_in_vector == ir_idx) {
return true;
}
}
return false;
}
public:
void enter_rval_inside_lval() { level_rval_inside_lval++; }
void exit_rval_inside_lval() { level_rval_inside_lval--; }
bool is_rval_inside_lval() const { return level_rval_inside_lval > 0; }
void capture_global_modification(const GlobalVarData* glob_ref, std::vector<var_idx_t> lval_ir_idx) {
modifications.emplace_back(ModifiedGlobal{glob_ref, std::move(lval_ir_idx)});
}
void capture_field_of_global_modification(AnyExprV tensor_obj, int index_at, std::vector<var_idx_t> lval_ir_idx) {
modifications.emplace_back(ModifiedFieldOfGlobal{tensor_obj, index_at, std::move(lval_ir_idx)});
}
void capture_tuple_index_modification(AnyExprV tuple_obj, int index_at, std::vector<var_idx_t> lval_ir_idx) {
modifications.emplace_back(ModifiedTupleIndex{tuple_obj, index_at, std::move(lval_ir_idx)});
}
void after_let(std::vector<var_idx_t>&& let_left_vars, CodeBlob& code, SrcLocation loc) const {
for (const auto& modification : modifications) {
if (const auto* m_glob = std::get_if<ModifiedGlobal>(&modification)) {
int n_modified_by_let = 0;
std::vector<bool> was_modified_by_let;
was_modified_by_let.resize(m_glob->lval_ir_idx.size());
for (size_t i = 0; i < m_glob->lval_ir_idx.size(); ++i) {
if (vector_contains(let_left_vars, m_glob->lval_ir_idx[i])) {
was_modified_by_let[i] = true;
n_modified_by_let++;
}
}
if (n_modified_by_let == static_cast<int>(m_glob->lval_ir_idx.size())) {
m_glob->apply_fully_rewrite(code, loc);
} else if (n_modified_by_let > 0) {
m_glob->apply_partially_rewrite(code, loc, std::move(was_modified_by_let));
}
} else if (const auto* m_tup = std::get_if<ModifiedTupleIndex>(&modification)) {
bool was_tuple_index_modified = false;
for (var_idx_t field_ir_idx : m_tup->lval_ir_idx) {
was_tuple_index_modified |= vector_contains(let_left_vars, field_ir_idx);
}
if (was_tuple_index_modified) {
m_tup->apply(code, loc);
}
} else if (const auto* m_tens = std::get_if<ModifiedFieldOfGlobal>(&modification)) {
bool was_tensor_index_modified = false;
for (var_idx_t field_ir_idx : m_tens->lval_ir_idx) {
was_tensor_index_modified |= vector_contains(let_left_vars, field_ir_idx);
}
if (was_tensor_index_modified) {
m_tens->apply(code, loc);
}
}
}
}
};
// given `{some_expr}.{i}`, check it for pattern `some_var.0` / `some_var.0.1` / etc.
// return some_var if satisfies (it may be a local or a global var, a tensor or a tuple)
// return nullptr otherwise: `f().0` / `(v = rhs).0` / `some_var.method().0` / etc.
static V<ast_reference> calc_sink_leftmost_obj(V<ast_dot_access> v) {
AnyExprV leftmost_obj = v->get_obj();
while (auto v_dot = leftmost_obj->try_as<ast_dot_access>()) {
if (!v_dot->is_target_indexed_access()) {
break;
}
leftmost_obj = v_dot->get_obj();
}
return leftmost_obj->type == ast_reference ? leftmost_obj->as<ast_reference>() : nullptr;
}
static std::vector<std::vector<var_idx_t>> pre_compile_tensor_inner(CodeBlob& code, const std::vector<AnyExprV>& args,
@ -313,43 +340,45 @@ static std::vector<var_idx_t> pre_compile_tensor(CodeBlob& code, const std::vect
static std::vector<var_idx_t> pre_compile_let(CodeBlob& code, AnyExprV lhs, AnyExprV rhs, SrcLocation loc) {
// [lhs] = [rhs]; since type checking is ok, it's the same as "lhs = rhs"
if (lhs->type == ast_typed_tuple && rhs->type == ast_typed_tuple) {
std::vector<var_idx_t> right = pre_compile_tensor(code, rhs->as<ast_typed_tuple>()->get_items());
LValContext local_lval;
std::vector<var_idx_t> left = pre_compile_tensor(code, lhs->as<ast_typed_tuple>()->get_items(), &local_lval);
vars_modification_watcher.trigger_callbacks(left, loc);
code.emplace_back(loc, Op::_Let, std::move(left), right);
local_lval.gen_ops_if_nonempty(code, loc);
std::vector<var_idx_t> rvect = pre_compile_tensor(code, rhs->as<ast_typed_tuple>()->get_items());
code.emplace_back(loc, Op::_Let, left, rvect);
local_lval.after_let(std::move(left), code, loc);
std::vector<var_idx_t> right = code.create_tmp_var(TypeDataTuple::create(), loc, "(tuple)");
code.emplace_back(lhs->loc, Op::_Tuple, right, std::move(rvect));
return right;
}
// [lhs] = rhs; it's un-tuple to N left vars
if (lhs->type == ast_typed_tuple) {
LValContext local_lval;
std::vector<var_idx_t> left = pre_compile_tensor(code, lhs->as<ast_typed_tuple>()->get_items(), &local_lval);
vars_modification_watcher.trigger_callbacks(left, loc);
std::vector<var_idx_t> right = pre_compile_expr(rhs, code);
const TypeDataTypedTuple* inferred_tuple = rhs->inferred_type->try_as<TypeDataTypedTuple>();
std::vector<TypePtr> types_list = inferred_tuple->items;
std::vector<var_idx_t> rvect = code.create_tmp_var(TypeDataTensor::create(std::move(types_list)), rhs->loc, "(unpack-tuple)");
code.emplace_back(lhs->loc, Op::_UnTuple, rvect, std::move(right));
LValContext local_lval;
std::vector<var_idx_t> left = pre_compile_tensor(code, lhs->as<ast_typed_tuple>()->get_items(), &local_lval);
vars_modification_watcher.trigger_callbacks(left, loc);
code.emplace_back(loc, Op::_Let, std::move(left), rvect);
local_lval.gen_ops_if_nonempty(code, loc);
return rvect;
code.emplace_back(loc, Op::_Let, left, rvect);
local_lval.after_let(std::move(left), code, loc);
return right;
}
// small optimization: `var x = rhs` or `local_var = rhs` (90% cases), LValContext not needed actually
if (lhs->type == ast_local_var_lhs || (lhs->type == ast_reference && lhs->as<ast_reference>()->sym->try_as<LocalVarData>())) {
std::vector<var_idx_t> right = pre_compile_expr(rhs, code);
std::vector<var_idx_t> left = pre_compile_expr(lhs, code); // effectively, local_var->ir_idx
vars_modification_watcher.trigger_callbacks(left, loc);
std::vector<var_idx_t> right = pre_compile_expr(rhs, code);
code.emplace_back(loc, Op::_Let, std::move(left), right);
return right;
}
// lhs = rhs
std::vector<var_idx_t> right = pre_compile_expr(rhs, code);
LValContext local_lval;
std::vector<var_idx_t> left = pre_compile_expr(lhs, code, &local_lval);
vars_modification_watcher.trigger_callbacks(left, loc);
code.emplace_back(loc, Op::_Let, std::move(left), right);
local_lval.gen_ops_if_nonempty(code, loc);
std::vector<var_idx_t> right = pre_compile_expr(rhs, code);
code.emplace_back(loc, Op::_Let, left, right);
local_lval.after_let(std::move(left), code, loc);
return right;
}
@ -364,28 +393,22 @@ static std::vector<var_idx_t> gen_op_call(CodeBlob& code, TypePtr ret_type, SrcL
}
static std::vector<var_idx_t> pre_compile_symbol(SrcLocation loc, const Symbol* sym, CodeBlob& code, LValContext* lval_ctx) {
std::vector<var_idx_t> pre_compile_symbol(SrcLocation loc, const Symbol* sym, CodeBlob& code, LValContext* lval_ctx) {
if (const auto* glob_ref = sym->try_as<GlobalVarData>()) {
if (!lval_ctx) {
// `globalVar` is used for reading, just create local IR var to represent its value, Op GlobVar will fill it
// note, that global tensors are stored as a tuple an unpacked to N vars on read, N determined by declared_type
std::vector<var_idx_t> local_ir_idx = code.create_tmp_var(glob_ref->declared_type, loc, "(glob-var)");
code.emplace_back(loc, Op::_GlobVar, local_ir_idx, std::vector<var_idx_t>{}, glob_ref);
return local_ir_idx;
} else {
// `globalVar = rhs` / `mutate globalVar` / `globalTuple.0 = rhs`
lval_ctx->register_lval(loc, glob_ref);
if (const std::vector<var_idx_t>* local_ir_idx = lval_ctx->exists_already_known_global(glob_ref)) {
return *local_ir_idx; // `f(mutate g.0, mutate g.1)`, then g will be read only once
}
std::vector<var_idx_t> local_ir_idx = code.create_tmp_var(glob_ref->declared_type, loc, "(glob-var)");
if (lval_ctx->is_rval_inside_lval()) { // for `globalVar.0` "globalVar" is rvalue inside lvalue
// for `globalVar = rhs` don't read a global actually, but for `globalVar.0 = rhs` do
code.emplace_back(loc, Op::_GlobVar, local_ir_idx, std::vector<var_idx_t>{}, glob_ref);
}
lval_ctx->register_modified_global(glob_ref, local_ir_idx);
return local_ir_idx;
// handle `globalVar = rhs` / `mutate globalVar`
if (lval_ctx && !lval_ctx->is_rval_inside_lval()) {
std::vector<var_idx_t> lval_ir_idx = code.create_tmp_var(glob_ref->declared_type, loc, "(lval-glob)");
lval_ctx->capture_global_modification(glob_ref, lval_ir_idx);
return lval_ir_idx;
}
// `globalVar` is used for reading, just create local IR var to represent its value, Op GlobVar will fill it
// note, that global tensors are stored as a tuple an unpacked to N vars on read, N determined by declared_type
std::vector<var_idx_t> local_ir_idx = code.create_var(glob_ref->declared_type, loc, "g_" + glob_ref->name);
code.emplace_back(loc, Op::_GlobVar, local_ir_idx, std::vector<var_idx_t>{}, glob_ref);
if (lval_ctx) { // `globalVar.0 = rhs`, globalVar is rval inside lval
lval_ctx->capture_global_modification(glob_ref, local_ir_idx);
}
return local_ir_idx;
}
if (const auto* const_ref = sym->try_as<GlobalConstData>()) {
if (const_ref->is_int_const()) {
@ -407,15 +430,12 @@ static std::vector<var_idx_t> pre_compile_symbol(SrcLocation loc, const Symbol*
#ifdef TOLK_DEBUG
tolk_assert(static_cast<int>(var_ref->ir_idx.size()) == var_ref->declared_type->calc_width_on_stack());
#endif
if (lval_ctx) {
lval_ctx->register_lval(loc, var_ref);
}
return var_ref->ir_idx;
}
throw Fatal("pre_compile_symbol");
}
static std::vector<var_idx_t> process_assign(V<ast_assign> v, CodeBlob& code) {
static std::vector<var_idx_t> process_assignment(V<ast_assign> v, CodeBlob& code) {
if (auto lhs_decl = v->get_lhs()->try_as<ast_local_vars_declaration>()) {
return pre_compile_let(code, lhs_decl->get_expr(), v->get_rhs(), v->loc);
} else {
@ -492,12 +512,18 @@ static std::vector<var_idx_t> process_dot_access(V<ast_dot_access> v, CodeBlob&
if (!v->is_target_fun_ref()) {
TypePtr obj_type = v->get_obj()->inferred_type;
int index_at = std::get<int>(v->target);
// `tensorVar.0`; since a tensor of N elems are N vars on a stack actually, calculate offset
// `tensorVar.0`
if (const auto* t_tensor = obj_type->try_as<TypeDataTensor>()) {
if (lval_ctx) lval_ctx->register_lval(v->loc, v);
if (lval_ctx) lval_ctx->enter_rval_inside_lval();
// handle `tensorVar.0 = rhs` if tensors is a global, special case, then the global will be read on demand
if (lval_ctx && !lval_ctx->is_rval_inside_lval()) {
if (auto sink = calc_sink_leftmost_obj(v); sink && sink->sym->try_as<GlobalVarData>()) {
std::vector<var_idx_t> lval_ir_idx = code.create_tmp_var(v->inferred_type, v->loc, "(lval-global-tensor)");
lval_ctx->capture_field_of_global_modification(v->get_obj(), index_at, lval_ir_idx);
return lval_ir_idx;
}
}
// since a tensor of N elems are N vars on a stack actually, calculate offset
std::vector<var_idx_t> lhs_vars = pre_compile_expr(v->get_obj(), code, lval_ctx);
if (lval_ctx) lval_ctx->exit_rval_inside_lval();
int stack_width = t_tensor->items[index_at]->calc_width_on_stack();
int stack_offset = 0;
for (int i = 0; i < index_at; ++i) {
@ -505,39 +531,26 @@ static std::vector<var_idx_t> process_dot_access(V<ast_dot_access> v, CodeBlob&
}
return {lhs_vars.begin() + stack_offset, lhs_vars.begin() + stack_offset + stack_width};
}
// `tupleVar.0`; not to mess up, separate rvalue and lvalue cases
// `tupleVar.0`
if (obj_type->try_as<TypeDataTypedTuple>() || obj_type->try_as<TypeDataTuple>()) {
if (!lval_ctx) {
// `tupleVar.0` as rvalue: the same as "tupleAt(tupleVar, 0)" written in terms of IR vars
std::vector<var_idx_t> tuple_ir_idx = pre_compile_expr(v->get_obj(), code);
std::vector<var_idx_t> index_ir_idx = code.create_tmp_var(TypeDataInt::create(), v->get_identifier()->loc, "(tuple-idx)");
code.emplace_back(v->loc, Op::_IntConst, index_ir_idx, td::make_refint(index_at));
std::vector<var_idx_t> field_ir_idx = code.create_tmp_var(v->inferred_type, v->loc, "(tuple-field)");
tolk_assert(tuple_ir_idx.size() == 1 && field_ir_idx.size() == 1); // tuples contain only 1-slot values
const FunctionData* builtin_sym = lookup_global_symbol("tupleAt")->as<FunctionData>();
code.emplace_back(v->loc, Op::_Call, field_ir_idx, std::vector{tuple_ir_idx[0], index_ir_idx[0]}, builtin_sym);
return field_ir_idx;
} else {
// `tupleVar.0 = rhs`: finally "tupleSetAt(tupleVar, rhs, 0)" will be done
uint64_t hash = lval_ctx->register_lval(v->loc, v);
if (const var_idx_t* field_ir_idx = lval_ctx->exists_already_known_tuple_index(hash)) {
return {*field_ir_idx}; // `(t.0.0, t.0.1) = rhs`, then "t.0" will be read (tupleAt) once
}
lval_ctx->enter_rval_inside_lval();
std::vector<var_idx_t> tuple_ir_idx = pre_compile_expr(v->get_obj(), code, lval_ctx);
lval_ctx->exit_rval_inside_lval();
std::vector<var_idx_t> index_ir_idx = code.create_tmp_var(TypeDataInt::create(), v->get_identifier()->loc, "(tuple-idx)");
code.emplace_back(v->loc, Op::_IntConst, index_ir_idx, td::make_refint(index_at));
std::vector<var_idx_t> field_ir_idx = code.create_tmp_var(v->inferred_type, v->loc, "(tuple-field)");
if (lval_ctx->is_rval_inside_lval()) { // for `t.0.1 = rhs` "t.0" is rvalue inside lvalue
// for `t.0 = rhs` don't call tupleAt, but for `t.0.1 = rhs` do for t.0 (still don't for t.0.1)
const FunctionData* builtin_sym = lookup_global_symbol("tupleAt")->as<FunctionData>();
code.emplace_back(v->loc, Op::_Call, field_ir_idx, std::vector{tuple_ir_idx[0], index_ir_idx[0]}, builtin_sym);
}
lval_ctx->register_modified_tuple_index(hash, tuple_ir_idx[0], index_ir_idx[0], field_ir_idx[0]);
vars_modification_watcher.trigger_callbacks(tuple_ir_idx, v->loc);
return field_ir_idx;
// handle `tupleVar.0 = rhs`, "0 SETINDEX" will be called when this was is modified
if (lval_ctx && !lval_ctx->is_rval_inside_lval() && calc_sink_leftmost_obj(v)) {
std::vector<var_idx_t> lval_ir_idx = code.create_tmp_var(v->inferred_type, v->loc, "(lval-tuple-field)");
lval_ctx->capture_tuple_index_modification(v->get_obj(), index_at, lval_ir_idx);
return lval_ir_idx;
}
// `tupleVar.0` as rvalue: the same as "tupleAt(tupleVar, 0)" written in terms of IR vars
std::vector<var_idx_t> tuple_ir_idx = pre_compile_expr(v->get_obj(), code);
std::vector<var_idx_t> index_ir_idx = code.create_tmp_var(TypeDataInt::create(), v->get_identifier()->loc, "(tuple-idx)");
code.emplace_back(v->loc, Op::_IntConst, index_ir_idx, td::make_refint(index_at));
std::vector<var_idx_t> field_ir_idx = code.create_tmp_var(v->inferred_type, v->loc, "(tuple-field)");
tolk_assert(tuple_ir_idx.size() == 1 && field_ir_idx.size() == 1); // tuples contain only 1-slot values
const FunctionData* builtin_sym = lookup_global_symbol("tupleAt")->as<FunctionData>();
code.emplace_back(v->loc, Op::_Call, field_ir_idx, std::vector{tuple_ir_idx[0], index_ir_idx[0]}, builtin_sym);
if (lval_ctx && calc_sink_leftmost_obj(v)) { // `tupleVar.0.1 = rhs`, then `tupleVar.0` is rval inside lval
lval_ctx->capture_tuple_index_modification(v->get_obj(), index_at, field_ir_idx);
}
return field_ir_idx;
}
tolk_assert(false);
}
@ -627,8 +640,8 @@ static std::vector<var_idx_t> process_function_call(V<ast_function_call> v, Code
std::vector<var_idx_t> rvect = code.create_tmp_var(real_ret_type, v->loc, "(fun-call)");
left.insert(left.end(), rvect.begin(), rvect.end());
vars_modification_watcher.trigger_callbacks(left, v->loc);
code.emplace_back(v->loc, Op::_Let, std::move(left), rvect_apply);
local_lval.gen_ops_if_nonempty(code, v->loc);
code.emplace_back(v->loc, Op::_Let, left, rvect_apply);
local_lval.after_let(std::move(left), code, v->loc);
rvect_apply = rvect;
}
@ -710,7 +723,7 @@ std::vector<var_idx_t> pre_compile_expr(AnyExprV v, CodeBlob& code, LValContext*
case ast_reference:
return pre_compile_symbol(v->loc, v->as<ast_reference>()->sym, code, lval_ctx);
case ast_assign:
return process_assign(v->as<ast_assign>(), code);
return process_assignment(v->as<ast_assign>(), code);
case ast_set_assign:
return process_set_assign(v->as<ast_set_assign>(), code);
case ast_binary_operator: