/* This file is part of TON Blockchain Library. TON Blockchain Library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. TON Blockchain Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with TON Blockchain Library. If not, see . */ #include "tolk.h" #include "ast.h" #include "ast-visitor.h" #include "type-system.h" namespace tolk { GNU_ATTRIBUTE_NOINLINE static std::string to_string(TypePtr type) { return "`" + type->as_human_readable() + "`"; } GNU_ATTRIBUTE_NOINLINE static std::string to_string(AnyExprV v_with_type) { return "`" + v_with_type->inferred_type->as_human_readable() + "`"; } GNU_ATTRIBUTE_NOINLINE static std::string expression_as_string(AnyExprV v) { if (auto v_ref = v->try_as()) { if (v_ref->sym->try_as() || v_ref->sym->try_as()) { return "variable `" + static_cast(v_ref->get_identifier()->name) + "`"; } } if (auto v_par = v->try_as()) { return expression_as_string(v_par->get_expr()); } return "expression"; } // fire a general "type mismatch" error, just a wrapper over `throw` GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD static void fire(FunctionPtr cur_f, SrcLocation loc, const std::string& message) { throw ParseError(cur_f, loc, message); } // fire an error on `!cell` / `+slice` GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD static void fire_error_cannot_apply_operator(FunctionPtr cur_f, SrcLocation loc, std::string_view operator_name, AnyExprV unary_expr) { std::string op = static_cast(operator_name); fire(cur_f, loc, "can not apply operator `" + op + "` to " + to_string(unary_expr->inferred_type)); } // fire an error on `int + cell` / `slice & int` GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD static void fire_error_cannot_apply_operator(FunctionPtr cur_f, SrcLocation loc, std::string_view operator_name, AnyExprV lhs, AnyExprV rhs) { std::string op = static_cast(operator_name); fire(cur_f, loc, "can not apply operator `" + op + "` to " + to_string(lhs->inferred_type) + " and " + to_string(rhs->inferred_type)); } GNU_ATTRIBUTE_NOINLINE static void warning_condition_always_true_or_false(FunctionPtr cur_f, SrcLocation loc, AnyExprV cond, const char* operator_name) { loc.show_warning("condition of " + static_cast(operator_name) + " is always " + (cond->is_always_true ? "true" : "false")); } // given `f(x: int)` and a call `f(expr)`, check that expr_type is assignable to `int` static void check_function_argument_passed(FunctionPtr cur_f, TypePtr param_type, AnyExprV ith_arg, bool is_obj_of_dot_call) { if (!param_type->can_rhs_be_assigned(ith_arg->inferred_type)) { if (is_obj_of_dot_call) { fire(cur_f, ith_arg->loc, "can not call method for " + to_string(param_type) + " with object of type " + to_string(ith_arg)); } else { fire(cur_f, ith_arg->loc, "can not pass " + to_string(ith_arg) + " to " + to_string(param_type)); } } } // given `f(x: mutate int?)` and a call `f(expr)`, check that `int?` is assignable to expr_type // (for instance, can't call `f(mutate intVal)`, since f can potentially assign null to it) static void check_function_argument_mutate_back(FunctionPtr cur_f, TypePtr param_type, AnyExprV ith_arg, bool is_obj_of_dot_call) { if (!ith_arg->inferred_type->can_rhs_be_assigned(param_type)) { if (is_obj_of_dot_call) { fire(cur_f, ith_arg->loc,"can not call method for mutate " + to_string(param_type) + " with object of type " + to_string(ith_arg) + ", because mutation is not type compatible"); } else { fire(cur_f, ith_arg->loc,"can not pass " + to_string(ith_arg) + " to mutate " + to_string(param_type) + ", because mutation is not type compatible"); } } } // fire an error on `var n = null` // technically it's correct, type of `n` is TypeDataNullLiteral, but it's not what the user wanted // so, it's better to see an error on assignment, that later, on `n` usage and types mismatch // (most common is situation above, but generally, `var (x,n) = xn` where xn is a tensor with 2-nd always-null, can be) GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD static void fire_error_assign_always_null_to_variable(FunctionPtr cur_f, SrcLocation loc, LocalVarPtr assigned_var, bool is_assigned_null_literal) { std::string var_name = assigned_var->name; fire(cur_f, loc, "can not infer type of `" + var_name + "`, it's always null; specify its type with `" + var_name + ": `" + (is_assigned_null_literal ? " or use `null as `" : "")); } // fire an error on `untypedTupleVar.0` when inferred as (int,int), or `[int, (int,int)]`, or other non-1 width in a tuple GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD static void fire_error_cannot_put_non1_stack_width_arg_to_tuple(FunctionPtr cur_f, SrcLocation loc, TypePtr inferred_type) { fire(cur_f, loc, "a tuple can not have " + to_string(inferred_type) + " inside, because it occupies " + std::to_string(inferred_type->get_width_on_stack()) + " stack slots in TVM, not 1"); } // handle __expect_type(expr, "type") call // this is used in compiler tests GNU_ATTRIBUTE_NOINLINE GNU_ATTRIBUTE_COLD static void handle_possible_compiler_internal_call(FunctionPtr cur_f, V v) { FunctionPtr fun_ref = v->fun_maybe; tolk_assert(fun_ref && fun_ref->is_builtin_function()); if (fun_ref->name == "__expect_type") { tolk_assert(v->get_num_args() == 2); TypePtr expected_type = parse_type_from_string(v->get_arg(1)->get_expr()->as()->str_val); TypePtr expr_type = v->get_arg(0)->inferred_type; if (expected_type != expr_type) { fire(cur_f, v->loc, "__expect_type failed: expected " + to_string(expected_type) + ", got " + to_string(expr_type)); } } } static bool expect_integer(AnyExprV v_inferred) { return v_inferred->inferred_type == TypeDataInt::create(); } static bool expect_boolean(AnyExprV v_inferred) { return v_inferred->inferred_type == TypeDataBool::create(); } class CheckInferredTypesVisitor final : public ASTVisitorFunctionBody { FunctionPtr cur_f = nullptr; // may be nullptr if checking `const a = ...` init_value protected: void visit(V v) override { AnyExprV lhs = v->get_lhs(); AnyExprV rhs = v->get_rhs(); parent::visit(lhs); parent::visit(rhs); // all operators (+=, etc.) can work for integers (if both sides are integers) bool types_ok = expect_integer(lhs) && expect_integer(rhs); // bitwise operators &= |= ^= are "overloaded" for booleans also (if both sides are booleans) if (!types_ok && (v->tok == tok_set_bitwise_and || v->tok == tok_set_bitwise_or || v->tok == tok_set_bitwise_xor)) { types_ok = expect_boolean(lhs) && expect_boolean(rhs); } // using += for other types (e.g. `tensorVar += tensorVar`) is not allowed if (!types_ok) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, lhs, rhs); } } void visit(V v) override { AnyExprV rhs = v->get_rhs(); parent::visit(rhs); switch (v->tok) { case tok_logical_not: if (!expect_integer(rhs) && !expect_boolean(rhs)) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, rhs); } break; default: if (!expect_integer(rhs)) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, rhs); } } } void visit(V v) override { AnyExprV lhs = v->get_lhs(); AnyExprV rhs = v->get_rhs(); parent::visit(lhs); parent::visit(rhs); switch (v->tok) { // == != can compare both integers and booleans, (int == bool) is NOT allowed // note, that `int?` and `int?` can't be compared, since Fift `EQUAL` works with integers only // (if to allow `int?` in the future, `==` must be expressed in a complicated Fift code considering TVM NULL) case tok_eq: case tok_neq: { bool both_int = expect_integer(lhs) && expect_integer(rhs); bool both_bool = expect_boolean(lhs) && expect_boolean(rhs); if (!both_int && !both_bool) { if (lhs->inferred_type == rhs->inferred_type) { // compare slice with slice, int? with int? fire(cur_f, v->loc, "type " + to_string(lhs) + " can not be compared with `== !=`"); } else { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, lhs, rhs); } } break; } // < > can compare only strict integers case tok_lt: case tok_gt: case tok_leq: case tok_geq: case tok_spaceship: if (!expect_integer(lhs) || !expect_integer(rhs)) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, lhs, rhs); } break; // & | ^ are "overloaded" both for integers and booleans, (int & bool) is NOT allowed case tok_bitwise_and: case tok_bitwise_or: case tok_bitwise_xor: { bool both_int = expect_integer(lhs) && expect_integer(rhs); bool both_bool = expect_boolean(lhs) && expect_boolean(rhs); if (!both_int && !both_bool) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, lhs, rhs); } break; } // && || can work with integers and booleans, (int && bool) is allowed, (int16 && int32) also case tok_logical_and: case tok_logical_or: { bool lhs_ok = expect_integer(lhs) || expect_boolean(lhs); bool rhs_ok = expect_integer(rhs) || expect_boolean(rhs); if (!lhs_ok || !rhs_ok) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, lhs, rhs); } break; } // others are mathematical: + * ... default: if (!expect_integer(lhs) || !expect_integer(rhs)) { fire_error_cannot_apply_operator(cur_f, v->loc, v->operator_name, lhs, rhs); } } } void visit(V v) override { parent::visit(v->get_expr()); if (!v->get_expr()->inferred_type->can_be_casted_with_as_operator(v->cast_to_type)) { fire(cur_f, v->loc, "type " + to_string(v->get_expr()) + " can not be cast to " + to_string(v->cast_to_type)); } } void visit(V v) override { parent::visit(v->get_expr()); if (v->get_expr()->inferred_type == TypeDataNullLiteral::create()) { // operator `!` used for always-null (proven by smart casts, for example), it's an error fire(cur_f, v->loc, "operator `!` used for always null expression"); } // if operator `!` used for non-nullable, probably a warning should be printed } void visit(V v) override { parent::visit(v->get_expr()); if ((v->is_always_true && !v->is_negated) || (v->is_always_false && v->is_negated)) { v->loc.show_warning(expression_as_string(v->get_expr()) + " is always null, this condition is always " + (v->is_always_true ? "true" : "false")); } if ((v->is_always_false && !v->is_negated) || (v->is_always_true && v->is_negated)) { v->loc.show_warning(expression_as_string(v->get_expr()) + " of type " + to_string(v->get_expr()) + " is always not null, this condition is always " + (v->is_always_true ? "true" : "false")); } } void visit(V v) override { parent::visit(v); for (int i = 0; i < v->size(); ++i) { AnyExprV item = v->get_item(i); if (item->inferred_type->get_width_on_stack() != 1) { fire_error_cannot_put_non1_stack_width_arg_to_tuple(cur_f, v->get_item(i)->loc, item->inferred_type); } } } void visit(V v) override { parent::visit(v); TypePtr obj_type = v->get_obj()->inferred_type; if (v->is_target_indexed_access()) { if (obj_type->try_as() && v->inferred_type->get_width_on_stack() != 1) { fire_error_cannot_put_non1_stack_width_arg_to_tuple(cur_f, v->loc, v->inferred_type); } } } void visit(V v) override { parent::visit(v); // check against type mismatch inside nested arguments FunctionPtr fun_ref = v->fun_maybe; if (!fun_ref) { // `local_var(args)` and similar const TypeDataFunCallable* f_callable = v->get_callee()->inferred_type->try_as(); tolk_assert(f_callable && f_callable->params_size() == v->get_num_args()); for (int i = 0; i < v->get_num_args(); ++i) { auto arg_i = v->get_arg(i)->get_expr(); TypePtr param_type = f_callable->params_types[i]; if (!param_type->can_rhs_be_assigned(arg_i->inferred_type)) { fire(cur_f, arg_i->loc, "can not pass " + to_string(arg_i) + " to " + to_string(param_type)); } } return; } // so, we have a call `f(args)` or `obj.f(args)`, f is a global function (fun_ref) (code / asm / builtin) int delta_self = 0; AnyExprV dot_obj = nullptr; if (auto v_dot = v->get_callee()->try_as()) { delta_self = 1; dot_obj = v_dot->get_obj(); } if (dot_obj) { const LocalVarData& param_0 = fun_ref->parameters[0]; TypePtr param_type = param_0.declared_type; check_function_argument_passed(cur_f, param_type, dot_obj, true); if (param_0.is_mutate_parameter()) { check_function_argument_mutate_back(cur_f, param_type, dot_obj, true); } } for (int i = 0; i < v->get_num_args(); ++i) { const LocalVarData& param_i = fun_ref->parameters[delta_self + i]; AnyExprV arg_i = v->get_arg(i)->get_expr(); TypePtr param_type = param_i.declared_type; check_function_argument_passed(cur_f, param_type, arg_i, false); if (param_i.is_mutate_parameter()) { check_function_argument_mutate_back(cur_f, param_type, arg_i, false); } } if (fun_ref->is_builtin_function() && fun_ref->name[0] == '_') { handle_possible_compiler_internal_call(cur_f, v); } } void visit(V v) override { parent::visit(v->get_lhs()); parent::visit(v->get_rhs()); process_assignment_lhs(v->get_lhs(), v->get_rhs()->inferred_type, v->get_rhs()); } // handle (and dig recursively) into `var lhs = rhs` // examples: `var z = 5`, `var (x, [y]) = (2, [3])`, `var (x, [y]) = xy` // while recursing, keep track of rhs if lhs and rhs have common shape (5 for z, 2 for x, [3] for [y], 3 for y) // (so that on type mismatch, point to corresponding rhs, example: `var (x, y:slice) = (1, 2)` point to 2 void process_assignment_lhs(AnyExprV lhs, TypePtr rhs_type, AnyExprV corresponding_maybe_rhs) { AnyExprV err_loc = corresponding_maybe_rhs ? corresponding_maybe_rhs : lhs; // `var ... = rhs` - dig into left part if (auto lhs_decl = lhs->try_as()) { process_assignment_lhs(lhs_decl->get_expr(), rhs_type, corresponding_maybe_rhs); return; } // inside `var v: int = rhs` / `var _ = rhs` / `var v redef = rhs` (lhs is "v" / "_" / "v") if (auto lhs_var = lhs->try_as()) { TypePtr declared_type = lhs_var->declared_type; // `var v: int = rhs` (otherwise, nullptr) if (lhs_var->marked_as_redef) { tolk_assert(lhs_var->var_ref && lhs_var->var_ref->declared_type); declared_type = lhs_var->var_ref->declared_type; } if (declared_type) { if (!declared_type->can_rhs_be_assigned(rhs_type)) { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + " to variable of type " + to_string(declared_type)); } } else { if (rhs_type == TypeDataNullLiteral::create()) { fire_error_assign_always_null_to_variable(cur_f, err_loc->loc, lhs_var->var_ref->try_as(), corresponding_maybe_rhs && corresponding_maybe_rhs->type == ast_null_keyword); } } return; } // `(v1, v2) = rhs` / `var (v1, v2) = rhs` (rhs may be `(1,2)` or `tensorVar` or `someF()`, doesn't matter) // dig recursively into v1 and v2 with corresponding rhs i-th item of a tensor if (auto lhs_tensor = lhs->try_as()) { const TypeDataTensor* rhs_type_tensor = rhs_type->try_as(); if (!rhs_type_tensor) { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + " to a tensor"); } if (lhs_tensor->size() != rhs_type_tensor->size()) { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + ", sizes mismatch"); } V rhs_tensor_maybe = corresponding_maybe_rhs ? corresponding_maybe_rhs->try_as() : nullptr; for (int i = 0; i < lhs_tensor->size(); ++i) { process_assignment_lhs(lhs_tensor->get_item(i), rhs_type_tensor->items[i], rhs_tensor_maybe ? rhs_tensor_maybe->get_item(i) : nullptr); } return; } // `[v1, v2] = rhs` / `var [v1, v2] = rhs` (rhs may be `[1,2]` or `tupleVar` or `someF()`, doesn't matter) // dig recursively into v1 and v2 with corresponding rhs i-th item of a tuple if (auto lhs_tuple = lhs->try_as()) { const TypeDataTypedTuple* rhs_type_tuple = rhs_type->try_as(); if (!rhs_type_tuple) { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + " to a tuple"); } if (lhs_tuple->size() != rhs_type_tuple->size()) { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + ", sizes mismatch"); } V rhs_tuple_maybe = corresponding_maybe_rhs ? corresponding_maybe_rhs->try_as() : nullptr; for (int i = 0; i < lhs_tuple->size(); ++i) { process_assignment_lhs(lhs_tuple->get_item(i), rhs_type_tuple->items[i], rhs_tuple_maybe ? rhs_tuple_maybe->get_item(i) : nullptr); } return; } // check `untypedTuple.0 = rhs_tensor` and other non-1 width elements if (auto lhs_dot = lhs->try_as()) { if (lhs_dot->is_target_indexed_access() && lhs_dot->get_obj()->inferred_type == TypeDataTuple::create()) { if (rhs_type->get_width_on_stack() != 1) { fire_error_cannot_put_non1_stack_width_arg_to_tuple(cur_f, err_loc->loc, rhs_type); } } } // here is `v = rhs` (just assignment, not `var v = rhs`) / `a.0 = rhs` / `getObj(z=f()).0 = rhs` etc. // types were already inferred, so just check their compatibility // for strange lhs like `f() = rhs` type checking will pass, but will fail lvalue check later if (!lhs->inferred_type->can_rhs_be_assigned(rhs_type)) { if (lhs->try_as()) { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + " to variable of type " + to_string(lhs)); } else { fire(cur_f, err_loc->loc, "can not assign " + to_string(rhs_type) + " to " + to_string(lhs)); } } } void visit(V v) override { parent::visit(v->get_return_value()); if (cur_f->does_return_self()) { if (!is_expr_valid_as_return_self(v->get_return_value())) { fire(cur_f, v->loc, "invalid return from `self` function"); } return; } TypePtr expr_type = v->get_return_value()->inferred_type; if (!cur_f->inferred_return_type->can_rhs_be_assigned(expr_type)) { fire(cur_f, v->get_return_value()->loc, "can not convert type " + to_string(expr_type) + " to return type " + to_string(cur_f->inferred_return_type)); } } static bool is_expr_valid_as_return_self(AnyExprV return_expr) { // `return self` if (return_expr->type == ast_reference && return_expr->as()->get_name() == "self") { return true; } // `return self.someMethod()` if (auto v_call = return_expr->try_as(); v_call && v_call->is_dot_call()) { return v_call->fun_maybe && v_call->fun_maybe->does_return_self() && is_expr_valid_as_return_self(v_call->get_dot_obj()); } // `return cond ? ... : ...` if (auto v_ternary = return_expr->try_as()) { return is_expr_valid_as_return_self(v_ternary->get_when_true()) && is_expr_valid_as_return_self(v_ternary->get_when_false()); } return false; } void visit(V v) override { parent::visit(v); AnyExprV cond = v->get_cond(); if (!expect_integer(cond) && !expect_boolean(cond)) { fire(cur_f, cond->loc, "can not use " + to_string(cond) + " as a boolean condition"); } if (cond->is_always_true || cond->is_always_false) { warning_condition_always_true_or_false(cur_f, v->loc, cond, "ternary operator"); } } void visit(V v) override { parent::visit(v); AnyExprV cond = v->get_cond(); if (!expect_integer(cond) && !expect_boolean(cond)) { fire(cur_f, cond->loc, "can not use " + to_string(cond) + " as a boolean condition"); } if (cond->is_always_true || cond->is_always_false) { warning_condition_always_true_or_false(cur_f, v->loc, cond, "`if`"); } } void visit(V v) override { parent::visit(v); AnyExprV cond = v->get_cond(); if (!expect_integer(cond)) { fire(cur_f, cond->loc, "condition of `repeat` must be an integer, got " + to_string(cond)); } } void visit(V v) override { parent::visit(v); AnyExprV cond = v->get_cond(); if (!expect_integer(cond) && !expect_boolean(cond)) { fire(cur_f, cond->loc, "can not use " + to_string(cond) + " as a boolean condition"); } if (cond->is_always_true || cond->is_always_false) { warning_condition_always_true_or_false(cur_f, v->loc, cond, "`while`"); } } void visit(V v) override { parent::visit(v); AnyExprV cond = v->get_cond(); if (!expect_integer(cond) && !expect_boolean(cond)) { fire(cur_f, cond->loc, "can not use " + to_string(cond) + " as a boolean condition"); } if (cond->is_always_true || cond->is_always_false) { warning_condition_always_true_or_false(cur_f, v->loc, cond, "`do while`"); } } void visit(V v) override { parent::visit(v); if (!expect_integer(v->get_thrown_code())) { fire(cur_f, v->get_thrown_code()->loc, "excNo of `throw` must be an integer, got " + to_string(v->get_thrown_code())); } if (v->has_thrown_arg() && v->get_thrown_arg()->inferred_type->get_width_on_stack() != 1) { fire(cur_f, v->get_thrown_arg()->loc, "can not throw " + to_string(v->get_thrown_arg()) + ", exception arg must occupy exactly 1 stack slot"); } } void visit(V v) override { parent::visit(v); AnyExprV cond = v->get_cond(); if (!expect_integer(cond) && !expect_boolean(cond)) { fire(cur_f, cond->loc, "can not use " + to_string(cond) + " as a boolean condition"); } if (!expect_integer(v->get_thrown_code())) { fire(cur_f, v->get_thrown_code()->loc, "thrown excNo of `assert` must be an integer, got " + to_string(v->get_thrown_code())); } if (cond->is_always_true || cond->is_always_false) { warning_condition_always_true_or_false(cur_f, v->loc, cond, "`assert`"); } } void visit(V v) override { parent::visit(v); if (v->first_unreachable) { // it's essential to print "unreachable code" warning AFTER type checking // (printing it while inferring might be a false positive if types are incorrect, due to smart casts for example) // a more correct approach would be to access cfg here somehow, but since cfg is now available only while inferring, // a special v->first_unreachable was set specifically for this warning (again, which is correct if types match) v->first_unreachable->loc.show_warning("unreachable code"); } } public: bool should_visit_function(FunctionPtr fun_ref) override { return fun_ref->is_code_function() && !fun_ref->is_generic_function(); } void start_visiting_function(FunctionPtr fun_ref, V v_function) override { cur_f = fun_ref; parent::visit(v_function->get_body()); cur_f = nullptr; if (fun_ref->is_implicit_return() && fun_ref->declared_return_type) { if (!fun_ref->declared_return_type->can_rhs_be_assigned(TypeDataVoid::create()) || fun_ref->does_return_self()) { fire(fun_ref, v_function->get_body()->as()->loc_end, "missing return"); } } } }; void pipeline_check_inferred_types() { visit_ast_of_all_functions(); } } // namespace tolk