mirror of
https://github.com/ton-blockchain/ton
synced 2025-03-09 15:40:10 +00:00
[Tolk] Nullable types T?
and null safety
This commit introduces nullable types `T?` that are distinct from non-nullable `T`. Example: `int?` (int or null) and `int` are different now. Previously, `null` could be assigned to any primitive type. Now, it can be assigned only to `T?`. A non-null assertion operator `!` was also introduced, similar to `!` in TypeScript and `!!` in Kotlin. If `int?` still occupies 1 stack slot, `(int,int)?` and other nullable tensors occupy N+1 slots, the last for "null precedence". `v == null` actually compares that slot. Assigning `(int,int)` to `(int,int)?` implicitly creates a null presence slot. Assigning `null` to `(int,int)?` widens this null value to 3 slots. This is called "type transitioning". All stdlib functions prototypes have been updated to reflect whether they return/accept a nullable or a strict value. This commit also contains refactoring from `const FunctionData*` to `FunctionPtr` and similar.
This commit is contained in:
parent
1389ff6789
commit
f3e620f48c
62 changed files with 2031 additions and 702 deletions
|
@ -108,6 +108,19 @@ void type_system_init() {
|
|||
// and creates an object only if it isn't found in a global hashtable
|
||||
//
|
||||
|
||||
TypePtr TypeDataNullable::create(TypePtr inner) {
|
||||
TypeDataTypeIdCalculation hash(1774084920039440885ULL);
|
||||
hash.feed_child(inner);
|
||||
|
||||
if (TypePtr existing = hash.get_existing()) {
|
||||
return existing;
|
||||
}
|
||||
// most types (int?, slice?, etc.), when nullable, still occupy 1 stack slot (holding TVM NULL at runtime)
|
||||
// but for example for `(int, int)` we need an extra stack slot "null flag"
|
||||
int width_on_stack = inner->can_hold_tvm_null_instead() ? 1 : inner->get_width_on_stack() + 1;
|
||||
return hash.register_unique(new TypeDataNullable(hash.type_id(), hash.children_flags(), width_on_stack, inner));
|
||||
}
|
||||
|
||||
TypePtr TypeDataFunCallable::create(std::vector<TypePtr>&& params_types, TypePtr return_type) {
|
||||
TypeDataTypeIdCalculation hash(3184039965511020991ULL);
|
||||
for (TypePtr param : params_types) {
|
||||
|
@ -143,7 +156,11 @@ TypePtr TypeDataTensor::create(std::vector<TypePtr>&& items) {
|
|||
if (TypePtr existing = hash.get_existing()) {
|
||||
return existing;
|
||||
}
|
||||
return hash.register_unique(new TypeDataTensor(hash.type_id(), hash.children_flags(), std::move(items)));
|
||||
int width_on_stack = 0;
|
||||
for (TypePtr item : items) {
|
||||
width_on_stack += item->get_width_on_stack();
|
||||
}
|
||||
return hash.register_unique(new TypeDataTensor(hash.type_id(), hash.children_flags(), width_on_stack, std::move(items)));
|
||||
}
|
||||
|
||||
TypePtr TypeDataTypedTuple::create(std::vector<TypePtr>&& items) {
|
||||
|
@ -178,6 +195,12 @@ TypePtr TypeDataUnresolved::create(std::string&& text, SrcLocation loc) {
|
|||
// only non-trivial implementations are here; trivial are defined in .h file
|
||||
//
|
||||
|
||||
std::string TypeDataNullable::as_human_readable() const {
|
||||
std::string nested = inner->as_human_readable();
|
||||
bool embrace = inner->try_as<TypeDataFunCallable>();
|
||||
return embrace ? "(" + nested + ")?" : nested + "?";
|
||||
}
|
||||
|
||||
std::string TypeDataFunCallable::as_human_readable() const {
|
||||
std::string result = "(";
|
||||
for (TypePtr param : params_types) {
|
||||
|
@ -223,6 +246,11 @@ std::string TypeDataTypedTuple::as_human_readable() const {
|
|||
// only non-trivial implementations are here; by default (no children), `callback(this)` is executed
|
||||
//
|
||||
|
||||
void TypeDataNullable::traverse(const TraverserCallbackT& callback) const {
|
||||
callback(this);
|
||||
inner->traverse(callback);
|
||||
}
|
||||
|
||||
void TypeDataFunCallable::traverse(const TraverserCallbackT& callback) const {
|
||||
callback(this);
|
||||
for (TypePtr param : params_types) {
|
||||
|
@ -254,6 +282,10 @@ void TypeDataTypedTuple::traverse(const TraverserCallbackT& callback) const {
|
|||
// only non-trivial implementations are here; by default (no children), `return callback(this)` is executed
|
||||
//
|
||||
|
||||
TypePtr TypeDataNullable::replace_children_custom(const ReplacerCallbackT& callback) const {
|
||||
return callback(create(inner->replace_children_custom(callback)));
|
||||
}
|
||||
|
||||
TypePtr TypeDataFunCallable::replace_children_custom(const ReplacerCallbackT& callback) const {
|
||||
std::vector<TypePtr> mapped;
|
||||
mapped.reserve(params_types.size());
|
||||
|
@ -282,53 +314,17 @@ TypePtr TypeDataTypedTuple::replace_children_custom(const ReplacerCallbackT& cal
|
|||
}
|
||||
|
||||
|
||||
// --------------------------------------------
|
||||
// calc_width_on_stack()
|
||||
//
|
||||
// returns the number of stack slots occupied by a variable of this type
|
||||
// only non-trivial implementations are here; by default (most types) occupy 1 stack slot
|
||||
//
|
||||
|
||||
int TypeDataGenericT::calc_width_on_stack() const {
|
||||
// this function is invoked only in functions with generics already instantiated
|
||||
assert(false);
|
||||
return -999999;
|
||||
}
|
||||
|
||||
int TypeDataTensor::calc_width_on_stack() const {
|
||||
int sum = 0;
|
||||
for (TypePtr item : items) {
|
||||
sum += item->calc_width_on_stack();
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int TypeDataUnresolved::calc_width_on_stack() const {
|
||||
// since early pipeline stages, no unresolved types left
|
||||
assert(false);
|
||||
return -999999;
|
||||
}
|
||||
|
||||
int TypeDataVoid::calc_width_on_stack() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------
|
||||
// can_rhs_be_assigned()
|
||||
//
|
||||
// on `var lhs: <lhs_type> = rhs`, having inferred rhs_type, check that it can be assigned without any casts
|
||||
// the same goes for passing arguments, returning values, etc. — where the "receiver" (lhs) checks "applier" (rhs)
|
||||
// for now, `null` can be assigned to any TVM primitive, be later we'll have T? types and null safety
|
||||
//
|
||||
|
||||
bool TypeDataInt::can_rhs_be_assigned(TypePtr rhs) const {
|
||||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -336,9 +332,6 @@ bool TypeDataBool::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -346,9 +339,6 @@ bool TypeDataCell::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -356,9 +346,6 @@ bool TypeDataSlice::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -366,9 +353,6 @@ bool TypeDataBuilder::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -376,9 +360,6 @@ bool TypeDataTuple::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -386,9 +367,6 @@ bool TypeDataContinuation::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -396,6 +374,19 @@ bool TypeDataNullLiteral::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
return rhs == this;
|
||||
}
|
||||
|
||||
bool TypeDataNullable::can_rhs_be_assigned(TypePtr rhs) const {
|
||||
if (rhs == this) {
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
if (const TypeDataNullable* rhs_nullable = rhs->try_as<TypeDataNullable>()) {
|
||||
return inner->can_rhs_be_assigned(rhs_nullable->inner);
|
||||
}
|
||||
return inner->can_rhs_be_assigned(rhs);
|
||||
}
|
||||
|
||||
bool TypeDataFunCallable::can_rhs_be_assigned(TypePtr rhs) const {
|
||||
return rhs == this;
|
||||
}
|
||||
|
@ -414,7 +405,6 @@ bool TypeDataTensor::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
// note, that tensors can not accept null
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -427,9 +417,6 @@ bool TypeDataTypedTuple::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
if (rhs == TypeDataNullLiteral::create()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -455,41 +442,69 @@ bool TypeDataVoid::can_rhs_be_assigned(TypePtr rhs) const {
|
|||
//
|
||||
|
||||
bool TypeDataInt::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) { // `int` as `int?`
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this;
|
||||
}
|
||||
|
||||
bool TypeDataBool::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this || cast_to == TypeDataInt::create();
|
||||
}
|
||||
|
||||
bool TypeDataCell::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this;
|
||||
}
|
||||
|
||||
bool TypeDataSlice::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this;
|
||||
}
|
||||
|
||||
bool TypeDataBuilder::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this;
|
||||
}
|
||||
|
||||
bool TypeDataTuple::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this;
|
||||
}
|
||||
|
||||
bool TypeDataContinuation::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return cast_to == this;
|
||||
}
|
||||
|
||||
bool TypeDataNullLiteral::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
return cast_to == this
|
||||
|| cast_to == TypeDataInt::create() || cast_to == TypeDataBool::create() || cast_to == TypeDataCell::create() || cast_to == TypeDataSlice::create()
|
||||
|| cast_to == TypeDataBuilder::create() || cast_to == TypeDataContinuation::create() || cast_to == TypeDataTuple::create()
|
||||
|| cast_to->try_as<TypeDataTypedTuple>();
|
||||
return cast_to == this || cast_to->try_as<TypeDataNullable>();
|
||||
}
|
||||
|
||||
bool TypeDataNullable::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return inner->can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TypeDataFunCallable::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return this == cast_to;
|
||||
}
|
||||
|
||||
|
@ -506,6 +521,9 @@ bool TypeDataTensor::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -518,14 +536,15 @@ bool TypeDataTypedTuple::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
if (const auto* to_nullable = cast_to->try_as<TypeDataNullable>()) {
|
||||
return can_be_casted_with_as_operator(to_nullable->inner);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TypeDataUnknown::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
// 'unknown' can be cast to any type
|
||||
// (though it's not valid for exception arguments when casting them to non-1 stack width,
|
||||
// but to ensure it, we need a special type "unknown TVM primitive", which is overwhelming I think)
|
||||
return true;
|
||||
// 'unknown' can be cast to any TVM value
|
||||
return cast_to->get_width_on_stack() == 1;
|
||||
}
|
||||
|
||||
bool TypeDataUnresolved::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
||||
|
@ -537,12 +556,45 @@ bool TypeDataVoid::can_be_casted_with_as_operator(TypePtr cast_to) const {
|
|||
}
|
||||
|
||||
|
||||
// --------------------------------------------
|
||||
// can_hold_tvm_null_instead()
|
||||
//
|
||||
// assigning `null` to a primitive variable like `int?` / `cell?` can store TVM NULL inside the same slot
|
||||
// (that's why the default implementation is just "return true", and most of types occupy 1 slot)
|
||||
// but for complex variables, like `(int, int)?`, "null presence" is kept in a separate slot (UTag for union types)
|
||||
// though still, tricky situations like `(int, ())?` can still "embed" TVM NULL in parallel with original value
|
||||
//
|
||||
|
||||
bool TypeDataNullable::can_hold_tvm_null_instead() const {
|
||||
if (get_width_on_stack() != 1) { // `(int, int)?` / `()?` can not hold null instead
|
||||
return false; // only `int?` / `cell?` / `StructWith1IntField?` can
|
||||
} // and some tricky situations like `(int, ())?`, but not `(int?, ())?`
|
||||
return !inner->can_hold_tvm_null_instead();
|
||||
}
|
||||
|
||||
bool TypeDataTensor::can_hold_tvm_null_instead() const {
|
||||
if (get_width_on_stack() != 1) { // `(int, int)` / `()` can not hold null instead, since null is 1 slot
|
||||
return false; // only `((), int)` and similar can:
|
||||
} // one item is width 1 (and not nullable), others are 0
|
||||
for (TypePtr item : items) {
|
||||
if (item->get_width_on_stack() == 1 && !item->can_hold_tvm_null_instead()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TypeDataVoid::can_hold_tvm_null_instead() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------
|
||||
// parsing type from tokens
|
||||
//
|
||||
// here we implement parsing types (mostly after colon) to TypeData
|
||||
// example: `var v: int` is TypeDataInt
|
||||
// example: `var v: (builder, [cell])` is TypeDataTensor(TypeDataBuilder, TypeDataTypedTuple(TypeDataCell))
|
||||
// example: `var v: (builder?, [cell])` is TypeDataTensor(TypeDataNullable(TypeDataBuilder), TypeDataTypedTuple(TypeDataCell))
|
||||
// example: `fun f(): ()` is TypeDataTensor() (an empty one)
|
||||
//
|
||||
// note, that unrecognized type names (MyEnum, MyStruct, T) are parsed as TypeDataUnresolved,
|
||||
|
@ -633,7 +685,8 @@ static TypePtr parse_type_nullable(Lexer& lex) {
|
|||
TypePtr result = parse_simple_type(lex);
|
||||
|
||||
if (lex.tok() == tok_question) {
|
||||
lex.error("nullable types are not supported yet");
|
||||
lex.next();
|
||||
result = TypeDataNullable::create(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue