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

[Tolk] Rewrite the type system from Hindley-Milner to static typing

FunC's (and Tolk's before this PR) type system is based on Hindley-Milner.
This is a common approach for functional languages, where
types are inferred from usage through unification.
As a result, type declarations are not necessary:
() f(a,b) { return a+b; } // a and b now int, since `+` (int, int)

While this approach works for now, problems arise with the introduction
of new types like bool, where `!x` must handle both int and bool.
It will also become incompatible with int32 and other strict integers.
This will clash with structure methods, struggle with proper generics,
and become entirely impractical for union types.

This PR completely rewrites the type system targeting the future.
1) type of any expression is inferred and never changed
2) this is available because dependent expressions already inferred
3) forall completely removed, generic functions introduced
   (they work like template functions actually, instantiated while inferring)
4) instantiation `<...>` syntax, example: `t.tupleAt<int>(0)`
5) `as` keyword, for example `t.tupleAt(0) as int`
6) methods binding is done along with type inferring, not before
   ("before", as worked previously, was always a wrong approach)
This commit is contained in:
tolk-vm 2024-12-30 22:31:27 +07:00
parent 3540424aa1
commit 799e2d1265
No known key found for this signature in database
GPG key ID: 7905DD7FE0324B12
101 changed files with 5402 additions and 2713 deletions

View file

@ -36,9 +36,18 @@ static void fire_error_cannot_be_used_as_lvalue(AnyV v, const std::string& detai
v->error(details + " can not be used as lvalue");
}
// handle when a function used as rvalue, like `var cb = f`
static void handle_function_used_as_noncall(AnyExprV v, const FunctionData* fun_ref) {
fun_ref->mutate()->assign_is_used_as_noncall();
GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD
static void fire_error_modifying_immutable_variable(AnyExprV v, const LocalVarData* var_ref) {
if (var_ref->idx == 0 && var_ref->name == "self") {
v->error("modifying `self`, which is immutable by default; probably, you want to declare `mutate self`");
} else {
v->error("modifying immutable variable `" + var_ref->name + "`");
}
}
// validate a function used as rvalue, like `var cb = f`
// it's not a generic function (ensured earlier at type inferring) and has some more restrictions
static void validate_function_used_as_noncall(AnyExprV v, const FunctionData* fun_ref) {
if (!fun_ref->arg_order.empty() || !fun_ref->ret_order.empty()) {
v->error("saving `" + fun_ref->name + "` into a variable will most likely lead to invalid usage, since it changes the order of variables on the stack");
}
@ -48,16 +57,30 @@ static void handle_function_used_as_noncall(AnyExprV v, const FunctionData* fun_
}
class CheckRValueLvalueVisitor final : public ASTVisitorFunctionBody {
void visit(V<ast_assign> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "assignment");
}
parent::visit(v);
}
void visit(V<ast_set_assign> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "assignment");
}
parent::visit(v);
}
void visit(V<ast_binary_operator> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "operator `" + static_cast<std::string>(v->operator_name));
fire_error_cannot_be_used_as_lvalue(v, "operator " + static_cast<std::string>(v->operator_name));
}
parent::visit(v);
}
void visit(V<ast_unary_operator> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "operator `" + static_cast<std::string>(v->operator_name));
fire_error_cannot_be_used_as_lvalue(v, "operator " + static_cast<std::string>(v->operator_name));
}
parent::visit(v);
}
@ -69,6 +92,11 @@ class CheckRValueLvalueVisitor final : public ASTVisitorFunctionBody {
parent::visit(v);
}
void visit(V<ast_cast_as_operator> v) override {
// if `x as int` is lvalue, then `x` is also lvalue, so check that `x` is ok
parent::visit(v->get_expr());
}
void visit(V<ast_int_const> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "literal");
@ -93,46 +121,45 @@ class CheckRValueLvalueVisitor final : public ASTVisitorFunctionBody {
}
}
void visit(V<ast_dot_access> v) override {
// a reference to a method used as rvalue, like `var v = t.tupleAt`
if (const FunctionData* fun_ref = v->target; v->is_rvalue) {
validate_function_used_as_noncall(v, fun_ref);
}
}
void visit(V<ast_function_call> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "function call");
}
if (!v->fun_maybe) {
parent::visit(v->get_called_f());
parent::visit(v->get_callee());
}
// for `f()` don't visit ast_reference `f`, to detect `f` usage as non-call, like `var cb = f`
// same for `obj.method()`, don't visit ast_reference method, visit only obj
if (v->is_dot_call()) {
parent::visit(v->get_dot_obj());
}
// for `f(...)` don't visit identifier `f`, to detect `f` usage as non-call, like `var cb = f`
for (int i = 0; i < v->get_num_args(); ++i) {
parent::visit(v->get_arg(i));
}
}
void visit(V<ast_dot_method_call> v) override {
if (v->is_lvalue) {
fire_error_cannot_be_used_as_lvalue(v, "method call");
}
parent::visit(v->get_obj());
for (int i = 0; i < v->get_num_args(); ++i) {
parent::visit(v->get_arg(i));
}
}
void visit(V<ast_local_var> v) override {
void visit(V<ast_local_var_lhs> v) override {
if (v->marked_as_redef) {
tolk_assert(v->var_maybe); // always filled, but for `var g_var redef` might point not to a local
if (const LocalVarData* var_ref = v->var_maybe->try_as<LocalVarData>(); var_ref && var_ref->is_immutable()) {
tolk_assert(v->var_ref);
if (v->var_ref->is_immutable()) {
v->error("`redef` for immutable variable");
}
}
}
void visit(V<ast_identifier> v) override {
void visit(V<ast_reference> v) override {
if (v->is_lvalue) {
tolk_assert(v->sym);
if (const auto* var_ref = v->sym->try_as<LocalVarData>(); var_ref && var_ref->is_immutable()) {
v->error("modifying immutable variable `" + var_ref->name + "`");
fire_error_modifying_immutable_variable(v, var_ref);
} else if (v->sym->try_as<GlobalConstData>()) {
v->error("modifying immutable constant");
} else if (v->sym->try_as<FunctionData>()) {
@ -142,13 +169,7 @@ class CheckRValueLvalueVisitor final : public ASTVisitorFunctionBody {
// a reference to a function used as rvalue, like `var v = someFunction`
if (const FunctionData* fun_ref = v->sym->try_as<FunctionData>(); fun_ref && v->is_rvalue) {
handle_function_used_as_noncall(v, fun_ref);
}
}
void visit(V<ast_self_keyword> v) override {
if (v->is_lvalue && v->param_ref->is_immutable()) {
v->error("modifying `self`, which is immutable by default; probably, you want to declare `mutate self`");
validate_function_used_as_noncall(v, fun_ref);
}
}
@ -163,10 +184,15 @@ class CheckRValueLvalueVisitor final : public ASTVisitorFunctionBody {
// skip catch(_,excNo), there are always vars due to grammar, lvalue/rvalue aren't set to them
parent::visit(v->get_catch_body());
}
public:
bool should_visit_function(const FunctionData* fun_ref) override {
return fun_ref->is_code_function() && !fun_ref->is_generic_function();
}
};
void pipeline_check_rvalue_lvalue(const AllSrcFiles& all_src_files) {
visit_ast_of_all_functions<CheckRValueLvalueVisitor>(all_src_files);
void pipeline_check_rvalue_lvalue() {
visit_ast_of_all_functions<CheckRValueLvalueVisitor>();
}
} // namespace tolk