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

[Tolk] Smart casts and control flow graph

With the introduction of nullable types, we want the
compiler to be smart in cases like
> if (x == null) return;
> // x is int now
or
> if (x == null) x = 0;
> // x is int now

These are called smart casts: when the type of variable
at particular usage might differ from its declaration.

Implementing smart casts is very challenging. They are based
on building control-flow graph and handling every AST vertex
with care. Actually, I represent cfg not a as a "graph with
edges". Instead, it's a "structured DFS" for the AST:
1) at every point of inferring, we have "current flow facts"
2) when we see an `if (...)`, we create two derived contexts
3) after `if`, finalize them at the end and unify
4) if we detect unreachable code, we mark that context
In other words, we get the effect of a CFG but in a more direct
approach. That's enough for AST-level data-flow.

Smart casts work for local variables and tensor/tuple indices.
Compilation errors have been reworked and now are more friendly.
There are also compilation warnings for always true/false
conditions inside if, assert, etc.
This commit is contained in:
tolk-vm 2025-02-24 20:14:16 +03:00
parent f3e620f48c
commit 7bcb8b895f
No known key found for this signature in database
GPG key ID: 7905DD7FE0324B12
47 changed files with 3057 additions and 833 deletions

View file

@ -178,7 +178,7 @@ fun test114(f: int, s: int) {
@method_id(115) @method_id(115)
fun test115() { fun test115() {
var y = [[[[true]]]]; var y = [[[[true]]]];
return (y, y.0.0.0.0 = !y.0.0.0.0, y.0); return (y, ((((y).0).0).0).0 = !y.0.0.0.0, y.0);
} }
@method_id(116) @method_id(116)
@ -248,7 +248,7 @@ fun test122(x: (int, int)) {
@method_id(123) @method_id(123)
fun test123() { fun test123() {
var t = [[10, 20]] as [[int,int]]?; var t = [[10, 20]] as [[int,int]]?;
t!.0.0 = t!.0.1 = 100; ((t!).0).0 = ((t!).0).1 = 100;
return t; return t;
} }

View file

@ -6,6 +6,6 @@ fun cantApplyPlusOnNullable() {
/** /**
@compilation_should_fail @compilation_should_fail
@stderr while instantiating generic function `calcSum<int?>` @stderr in function `calcSum<int?>`
@stderr can not apply operator `+` to `int?` and `int?` @stderr can not apply operator `+` to `int?` and `int?`
*/ */

View file

@ -0,0 +1,17 @@
fun eq<X>(v: X) {}
fun cantDeduceWhenNotInferred() {
// at type inferring (before type checking) they are unknown
var (x, y) = 2;
eq(x as int); // ok (since execution doesn't reach type checking)
eq<int>(x); // ok (since execution doesn't reach type checking)
eq(x);
}
/**
@compilation_should_fail
@stderr in function `cantDeduceWhenNotInferred`
@stderr can not deduce X for generic function `eq<X>`
@stderr eq(x);
*/

View file

@ -11,8 +11,7 @@ fun foo<X>(value: X) : X {
/** /**
@compilation_should_fail @compilation_should_fail
@stderr while instantiating generic function `foo<slice>` @stderr in function `bar<slice>`
@stderr while instantiating generic function `bar<slice>`
@stderr can not convert type `int` to return type `slice` @stderr can not convert type `int` to return type `slice`
@stderr return 1 @stderr return 1
*/ */

View file

@ -3,6 +3,7 @@ fun failBitwiseNotOnBool() {
if (~eq) { if (~eq) {
return 0; return 0;
} }
return -1;
} }
/** /**

View file

@ -0,0 +1,12 @@
fun getNullableInt(): int? { return 5; }
fun testCantApplyNotNullForAlwaysNull() {
var x: int? = getNullableInt();
if (x != null) { return 0; }
return x! + 1;
}
/**
@compilation_should_fail
@stderr operator `!` used for always null expression
*/

View file

@ -0,0 +1,15 @@
fun getNullableInt(): int? { return 5; }
fun testFlowContextAppliedInBinaryOperator() {
var x: int? = getNullableInt();
var y: int? = getNullableInt();
if ((y = null) < y) {
return -100;
}
return 0;
}
/**
@compilation_should_fail
@stderr can not apply operator `<` to `null` and `null`
*/

View file

@ -0,0 +1,14 @@
fun getNullableInt(): int? { return 5; }
fun testNeverTypeOccurs() {
var x: int? = getNullableInt();
if (x == null && x != null) {
return x + 0;
}
return 0;
}
/**
@compilation_should_fail
@stderr can not apply operator `+` to `never` and `int`
*/

View file

@ -0,0 +1,9 @@
fun testLogicalAndNotConditionDoesntAffect(x: int?) {
var gt1 = x != null && x > 1;
return x + 0;
}
/**
@compilation_should_fail
@stderr can not apply operator `+` to `int?` and `int`
*/

View file

@ -0,0 +1,15 @@
fun getTensor(): (int?, int?) { return (5, null); }
fun testSmartCastsForFieldsDropAfterAssign() {
var t = getTensor();
if (t.0 != null && t.1 != null) {
t = getTensor();
return t.0 + t.1;
}
return -1;
}
/**
@compilation_should_fail
@stderr can not apply operator `+` to `int?` and `int?`
*/

View file

@ -0,0 +1,16 @@
fun getNullableInt(): int? { return 5; }
fun getTensor(x: int?): (int?, int) { return (x, 0); }
fun testSmartCastsDropAfterAssign() {
var x: int? = 0;
var y: int? = 0;
(getTensor(x = getNullableInt()).0, getTensor(y = getNullableInt()).0) = (x + y, x - y);
return x+y;
}
/**
@compilation_should_fail
@stderr can not apply operator `+` to `int?` and `int?`
@stderr x + y, x - y
*/

View file

@ -0,0 +1,14 @@
fun takeNullableTensor(mutate ij: (int, int)?) { }
fun testSmartCastsDropAfterMutate() {
var x: (int, int)? = (1, 2);
return x.0; // ok
takeNullableTensor(mutate x);
return x.1; // error
}
/**
@compilation_should_fail
@stderr type `(int, int)?` is not indexable
@stderr return x.1
*/

View file

@ -0,0 +1,12 @@
fun getNullableInt(): int? { return 5; }
fun testAssertThrowIsConditional() {
var (x, y) = (getNullableInt(), getNullableInt());
assert(x != null) throw(y = 10);
return x + y;
}
/**
@compilation_should_fail
@stderr can not apply operator `+` to `int` and `int?`
*/

View file

@ -0,0 +1,18 @@
fun assignNull2<T1, T2>(mutate x: T1?, mutate y: T2?) {
if (false) {
x = null;
y = null;
}
}
fun testSmartCastsDropAfterNullableGeneric() {
var (x: int?, y: int?) = (1, 2);
x * y; // ok
assignNull2(x, y); // treated like assignments to nullable
x << y; // error
}
/**
@compilation_should_fail
@stderr can not apply operator `<<` to `int?` and `int?`
*/

View file

@ -0,0 +1,15 @@
fun getNullableInt(): int? { return 5; }
fun testReassignInRedef() {
var t1: int? = getNullableInt();
if (t1 != null) {
var (t1 redef, t2) = (getNullableInt(), 5);
return t1 + t2;
}
return -1;
}
/**
@compilation_should_fail
@stderr can not apply operator `+` to `int?` and `int`
*/

View file

@ -0,0 +1,14 @@
fun getNullableInt(): int? { return 5; }
fun testTryBodyDontSmartCast() {
var x = getNullableInt();
try {
x = 5;
} catch {}
return x * 10; // x is not int here; for now, we have no exception edges, assuming it can be anywhere inside try
}
/**
@compilation_should_fail
@stderr can not apply operator `*` to `int?` and `int`
*/

View file

@ -0,0 +1,15 @@
fun getNullableInt(): int? { return 5; }
fun testDoWhileCondition() {
var (x: int?, y: int?) = (10, 20);
do {
x = getNullableInt();
y = getNullableInt();
} while(x == null);
return x * y; // x is 100% int, but y is not
}
/**
@compilation_should_fail
@stderr can not apply operator `*` to `int` and `int?`
*/

View file

@ -0,0 +1,9 @@
fun cantAssignIntToTensor() {
var (x, y) = 2;
x + y;
}
/**
@compilation_should_fail
@stderr can not assign `int` to a tensor
*/

View file

@ -0,0 +1,9 @@
fun cantAssignSizesMismatch() {
var [x, y] = [2, 3, 4];
x + y;
}
/**
@compilation_should_fail
@stderr can not assign `[int, int, int]`, sizes mismatch
*/

View file

@ -0,0 +1,28 @@
fun takeInt(a: int) {}
@method_id(101)
fun test1(x: int?) {
if (x == null && x != null) {
var y = x;
__expect_type(y, "never");
__expect_type(y!, "never");
// `never` type is assignable to anything, flow won't reach this point
var t: (int, int) = x;
t = y;
takeInt(x);
var cb: (int) -> int = x;
x as int?;
x as (int, int)?;
x as never;
return x;
}
return 123;
}
fun main() {
__expect_type(test1, "(int?) -> int");
}
/**
@testcase | 101 | null | 123
*/

View file

@ -26,7 +26,9 @@ fun test2(x: int?) {
if (null != x) { if (null != x) {
var y: int? = null; var y: int? = null;
if (y != null) { return 10; } if (y != null) { return 10; }
return y; if (10 < 20) { // always true at runtime (not at compile-time)
return y;
}
} }
try { try {
return x! + 10; // will throw, since not a number return x! + 10; // will throw, since not a number
@ -45,14 +47,6 @@ fun test3(x: int) {
return myIsNull(x > 10 ? null : x); return myIsNull(x > 10 ? null : x);
} }
fun getUntypedNull() {
var untyped: null = null;
if (true) {
return untyped;
}
return untyped;
}
@method_id(104) @method_id(104)
fun test4(): null { fun test4(): null {
var (_, (_, untyped: null)) = (3, (createEmptyTuple, null)); var (_, (_, untyped: null)) = (3, (createEmptyTuple, null));
@ -62,12 +56,6 @@ fun test4(): null {
return untyped; return untyped;
} }
@method_id(105)
fun test5() {
var n: slice? = getUntypedNull();
return !(null == n) ? n!.loadInt(32) : 100;
}
@method_id(107) @method_id(107)
fun test7() { fun test7() {
var b = beginCell().storeMaybeRef(null) as builder?; var b = beginCell().storeMaybeRef(null) as builder?;
@ -85,6 +73,7 @@ fun test8() {
} }
fun main() { fun main() {
// the compiler optimizes this at compile-time
var i: int? = null; var i: int? = null;
if (i == null) { if (i == null) {
return 1; return 1;
@ -99,7 +88,6 @@ fun main() {
@testcase | 103 | 5 | 5 @testcase | 103 | 5 | 5
@testcase | 103 | 15 | -1 @testcase | 103 | 15 | -1
@testcase | 104 | | (null) @testcase | 104 | | (null)
@testcase | 105 | | 100
@testcase | 107 | | -11 @testcase | 107 | | -11
@fif_codegen @fif_codegen
""" """
@ -127,12 +115,7 @@ fun main() {
""" """
main PROC:<{ main PROC:<{
// //
PUSHNULL // i 1 PUSHINT // '3=1
ISNULL // '2
IFJMP:<{ //
1 PUSHINT // '3=1
}> //
10 PUSHINT // '4=10
}> }>
""" """

View file

@ -73,7 +73,7 @@ fun test104() {
var t1_1: (int, int)? = (1, 2); var t1_1: (int, int)? = (1, 2);
var t1_2: (int, int)? = t1_1; var t1_2: (int, int)? = t1_1;
var t1_3: (int, int)? = t1_1!; var t1_3: (int, int)? = t1_1!;
var t2_1: (int, int)? = null; var t2_1: (int, int)? = getNullableTensor(null);
var t2_2 = t2_1; var t2_2 = t2_1;
return (t1_3, t2_2); return (t1_3, t2_2);
} }
@ -101,9 +101,12 @@ fun test108(x1: (int, int)) {
incrementTensorComponents(mutate x1); incrementTensorComponents(mutate x1);
x1.incrementTensorComponents(); x1.incrementTensorComponents();
var x2: (int, int)? = x1; var x2: (int, int)? = x1;
__expect_type(x2, "(int, int)");
x2.incrementNullableTensorComponents().incrementNullableTensorComponents(); x2.incrementNullableTensorComponents().incrementNullableTensorComponents();
incrementNullableTensorComponents(mutate x2); incrementNullableTensorComponents(mutate x2);
__expect_type(x2, "(int, int)?");
var x3: (int, int)? = null; var x3: (int, int)? = null;
__expect_type(x3, "null");
x3.incrementNullableTensorComponents().incrementNullableTensorComponents(); x3.incrementNullableTensorComponents().incrementNullableTensorComponents();
incrementNullableTensorComponents(mutate x3); incrementNullableTensorComponents(mutate x3);
return (x1, x2, x3); return (x1, x2, x3);
@ -148,7 +151,7 @@ fun test111() {
var x = (1, 2); var x = (1, 2);
assignFirstComponent(mutate x, 50); assignFirstComponent(mutate x, 50);
var x2: (int, int)? = null; var x2: (int, int)? = null;
var x3 = x2; var x3 = x2 as (int, int)?;
assignFirstComponentNullable(mutate x2, 30); assignFirstComponentNullable(mutate x2, 30);
assignFirstComponentNullable(mutate x3, 70); assignFirstComponentNullable(mutate x3, 70);
g110_1 = (1, 2); g110_1 = (1, 2);
@ -361,23 +364,36 @@ fun test132() {
return (result, 777, aln1, aln2, doubleNulls.1 == null, doubleNulls); return (result, 777, aln1, aln2, doubleNulls.1 == null, doubleNulls);
} }
@method_id(133)
fun test133() {
var x: (int, int)? = (10, 20);
return sumOfTensor(x) + x.0 + x.1; // smart casted
}
@method_id(134)
fun test134(): (int, int)? {
var x: (int, int)? = (10, 20);
incrementTensorComponents(mutate x); // smart casted
return x;
}
fun getNormalNullableTensorWidth1(vLess100: int?): ([int?], ())? { fun getNormalNullableTensorWidth1(vLess100: int?): ([int?], ())? {
if (vLess100 != null && vLess100! >= 100) { if (vLess100 != null && vLess100 >= 100) {
return null; return null;
} }
return ([vLess100], ()); // such a nullable tensor can store NULL in the same slot return ([vLess100], ()); // such a nullable tensor can store NULL in the same slot
} }
fun getTrickyNullableTensorWidth1(vLess100: int?): (int?, ())? { fun getTrickyNullableTensorWidth1(vLess100: int?): (int?, ())? {
if (vLess100 != null && vLess100! >= 100) { if (vLess100 != null && vLess100 >= 100) {
return null; return null;
} }
return (vLess100, ()); // such a nullable tensor requires an extra stack slot for null presence return (vLess100, ()); // such a nullable tensor requires an extra stack slot for null presence
} }
fun getEvenTrickierNullableWidth1(vLess100: int?): ((), (int?, ()), ())? { fun getEvenTrickierNullableWidth1(vLess100: int?): ((), (int?, ()), ())? {
if (vLess100 != null && vLess100! >= 100) { if (vLess100 != null && vLess100 >= 100) {
return null; return null;
} }
return ((), (vLess100, ()), ()); return ((), (vLess100, ()), ());
@ -406,35 +422,35 @@ fun main(){}
/** /**
@testcase | 101 | | 1 2 -1 @testcase | 101 | | 1 2 -1
@testcase | 102 | | 1 2 -1 (null) (null) 0 @testcase | 102 | | 1 2 -1 (null) (null) 0
@testcase | 103 | 1 2 | 3 3 0 1 2 -1 @testcase | 103 | 1 2 | 3 3 0 1 2
@testcase | 104 | | 1 2 -1 (null) (null) 0 @testcase | 104 | | 1 2 (null) (null) 0
@testcase | 105 | | (null) (null) (null) 0 1 2 3 -1 @testcase | 105 | | (null) (null) (null) 0 1 2 3 -1
@testcase | 106 | | 1 2 -1 @testcase | 106 | | 1 2
@testcase | 107 | | 0 0 -1 0 0 -1 @testcase | 107 | | 0 0 -1 0 0 -1
@testcase | 108 | 5 6 | 7 8 10 11 -1 (null) (null) 0 @testcase | 108 | 5 6 | 7 8 10 11 -1 (null) (null) 0
@testcase | 109 | | 0 0 -1 0 -1 0 0 -1 -1 @testcase | 109 | | 0 0 -1 0 -1 0 0 -1 -1
@testcase | 110 | | 3 4 (null) (null) 0 6 7 -1 @testcase | 110 | | 3 4 (null) (null) 0 6 7 -1
@testcase | 111 | | 50 30 70 90 100 @testcase | 111 | | 50 30 70 90 100
@testcase | 112 | | 12 22 -1 @testcase | 112 | | 12 22
@testcase | 113 | | -1 @testcase | 113 | | -1
@testcase | 114 | | (null) (null) (null) 0 (null) (null) (null) 0 @testcase | 114 | | (null) (null) (null) 0 (null) (null) (null) 0
@testcase | 115 | | 2 3 7 (null) (null) 0 5 0 -1 0 @testcase | 115 | | 2 3 7 (null) (null) 0 5 0 -1 0
@testcase | 116 | -1 | (null) (null) 0 (null) (null) 0 @testcase | 116 | -1 | (null) (null) 0 (null) (null) 0
@testcase | 116 | 0 | 1 2 -1 1 2 -1 @testcase | 116 | 0 | 1 2 -1 1 2 -1
@testcase | 117 | | (null) (null) 0 1 3 @testcase | 117 | | (null) 1 3
@testcase | 118 | 5 | 5 10 -1 @testcase | 118 | 5 | 5 10 -1
@testcase | 118 | null | (null) (null) 0 @testcase | 118 | null | (null) (null) 0
@testcase | 119 | | (null) (null) 0 (null) (null) 0 1 2 -1 100 @testcase | 119 | | (null) (null) 1 2 -1 100
@testcase | 120 | -1 | (null) (null) 0 @testcase | 120 | -1 | (null) (null) 0
@testcase | 120 | 0 | 1 2 -1 @testcase | 120 | 0 | 1 2 -1
@testcase | 121 | | [ 1 [ 3 4 ] ] @testcase | 121 | | [ 1 [ 3 4 ] ]
@testcase | 122 | 0 | [ 1 [ 3 4 ] 4 (null) ] @testcase | 122 | 0 | [ 1 [ 3 4 ] 4 (null) ]
@testcase | 122 | -1 | [ 1 (null) 4 (null) ] @testcase | 122 | -1 | [ 1 (null) 4 (null) ]
@testcase | 123 | | 1 3 4 -1 @testcase | 123 | | 1 3 4 -1
@testcase | 124 | 0 | 1 3 4 -1 4 (null) (null) 0 -1 @testcase | 124 | 0 | 1 3 4 -1 4 (null) (null) 0
@testcase | 124 | -1 | 1 (null) (null) 0 4 (null) (null) 0 -1 @testcase | 124 | -1 | 1 (null) (null) 0 4 (null) (null) 0
@testcase | 125 | | 3 @testcase | 125 | | 3
@testcase | 126 | | 1 (null) (null) 0 2 @testcase | 126 | | 1 (null) 2
@testcase | 127 | 1 | 1 (null) (null) 0 2 @testcase | 127 | 1 | 1 (null) (null) 0 2
@testcase | 127 | 2 | 1 2 3 -1 4 @testcase | 127 | 2 | 1 2 3 -1 4
@testcase | 127 | 3 | 1 (null) (null) 0 5 @testcase | 127 | 3 | 1 (null) (null) 0 5
@ -447,6 +463,8 @@ fun main(){}
@testcase | 130 | -1 | 1 (null) (null) 0 @testcase | 130 | -1 | 1 (null) (null) 0
@testcase | 131 | | -1 777 0 777 777 777 0 0 -1 -1 777 -1 -1 -1 777 @testcase | 131 | | -1 777 0 777 777 777 0 0 -1 -1 777 -1 -1 -1 777
@testcase | 132 | | -1 0 -1 0 777 (null) (null) -1 0 0 @testcase | 132 | | -1 0 -1 0 777 (null) (null) -1 0 0
@testcase | 133 | | 60
@testcase | 134 | | 11 21 -1
@testcase | 135 | | [ 10 ] [ (null) ] (null) 777 10 -1 (null) -1 (null) 0 777 10 -1 (null) -1 (null) 0 777 0 0 -1 0 0 -1 0 0 -1 777 0 -1 0 0 -1 0 @testcase | 135 | | [ 10 ] [ (null) ] (null) 777 10 -1 (null) -1 (null) 0 777 10 -1 (null) -1 (null) 0 777 0 0 -1 0 0 -1 0 0 -1 777 0 -1 0 0 -1 0
@fif_codegen @fif_codegen

View file

@ -80,7 +80,7 @@ fun test107() {
@method_id(108) @method_id(108)
fun test108() { fun test108() {
var (a, b: cell?, c) = (1, beginCell().endCell(), 3); var (a, b: cell?, c) = (1, beginCell().endCell(), 3);
b = null; if (10>3) { b = null; }
return a + (b == null ? 0 : b!.beginParse().loadInt(32)) + c; return a + (b == null ? 0 : b!.beginParse().loadInt(32)) + c;
} }

View file

@ -0,0 +1,678 @@
// the goal of this file is not only to @testcase results —
// but to check that this file compiles
fun getNullableInt(): int? { return 5; }
fun getNullableSlice(): slice? { return null; }
fun takeNullableInt(a: int?) {}
fun takeNullableSlice(a: slice?) {}
fun increment(mutate self: int) { self += 1; }
fun assignToInt(mutate self: int, value: int) { self = value; }
fun assignToNullableInt(mutate self: int?, value: int) { self = value; }
fun sameTensor(t: (int, int)) { return t; }
fun sameTensor2(t: (int?, (slice, slice, slice, builder)?)) { return t; }
fun eq<T>(v: T) { return v; }
fun getTwo<X>(): X { return 2 as X; }
fun test1(): int {
var x = getNullableInt();
var y = getNullableInt();
if (x != null && y != null) {
__expect_type(x, "int");
__expect_type(y, "int");
return x + y;
}
return -1;
}
fun test2() {
var (x, y) = (getNullableInt(), getNullableInt());
if (x == null || y == null) {
return null;
}
__expect_type(x, "int");
__expect_type(y, "int");
return x + y;
}
fun test3(): int {
var ([x, y]) = [getNullableInt(), getNullableInt()];
if (x != null) {
if (((y)) != null) {
__expect_type(x, "int");
__expect_type(y, "int");
return x + y;
}
return x;
}
if (random() > -1) {
if (y == null) { return -1; }
else { return y; }
}
return 0;
}
fun test4() {
var x = getNullableInt();
if (x != null && x > 0) {
var x = getNullableInt();
if ((x) != null && x + 10 < 0) {
var x = getNullableInt();
return 10 > 3 && 10 < 10 && x != null && x + 8 > 10;
}
}
if (x != null && x < 1) {
return false;
}
if (x == null && x == null) {
__expect_type(x, "null");
return true;
}
return x < x + 3;
}
fun test5() {
var (a, (b, c)) = (getNullableInt(), (getNullableInt(), getNullableInt()));
if (a == null) { return -1; }
if (!(b != null)) { return -2; }
if (random() ? c == null && c == null : c == null) { return -3; }
return a + b + c;
}
fun test6() {
var a: int? = 5;
__expect_type(a, "int");
__expect_type(a != null ? a : null, "int");
__expect_type(a == null ? "" : a, "int");
takeNullableInt(a);
__expect_type(a, "int");
if (random()) {
a = null;
} else {
if (random()) { a = null; }
else { a = null; }
}
__expect_type(a, "null");
takeNullableSlice(a); // ok, `slice?` is `slice | null`, here a definitely null
var b: int? = true ? null : "sl";
__expect_type(b, "null");
takeNullableInt(b);
takeNullableSlice(b); // same reason
var c: int? = 10;
__expect_type(c, "int");
takeNullableSlice(c = null);
}
fun test7() {
var (a, b, c, d) = (getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt());
if (a == null && true) { return -1; }
if (true && true && 1 && !0 && b == null) { return -2; }
if (true ? c == null && (((c))) == null && true : false) { return -3; }
if (!true ? random() > 0 : a != null && (d == null && b != null)) { return -4; }
return a + b + c + d;
}
fun test8(x: int?, y: int?) {
var allGt1 = x != null && x > 1 && y != null && y > 1;
var xGtY = x != null && y != null && x > y;
var xLtEq0 = x == null || x < 0;
(x = 0) < random() || x > 10;
return x + 0;
}
fun test9() {
var x = getNullableInt();
var y = getNullableInt();
if (x == null || y == null) {
return -1;
}
__expect_type(x, "int");
__expect_type(y, "int");
return x + y;
}
fun test10(): int {
var (x, y) = (getNullableInt(), getNullableInt());
if (x == null) {
if (y == null) { return -1; }
__expect_type(x, "null");
__expect_type(y, "int");
return y;
}
if (y == null) {
return x;
}
__expect_type(x, "int");
__expect_type(y, "int");
return x + y;
}
fun test11() {
var [x, y] = [getNullableInt(), getNullableInt()];
if (random()) { return x == null || y == null ? -1 : x + y; }
if (true && (x == null || y == null) && !!true) { return 0; }
return x + y;
}
fun test12() {
var (x, y) = (getNullableInt(), getNullableInt());
if (random() ? x == null || y == null : x == null || y == null) { return -1; }
__expect_type(x, "int");
__expect_type(y, "int");
return x + y;
}
fun test13() {
var x: int? = getNullableInt();
var y: int? = 10;
var z = getNullableInt();
var w = getNullableInt();
beginCell().storeInt(x!, 32).storeInt(x = getNullableInt()!, 32).storeInt(x, 32)
.storeInt(y, 32).storeInt(z = 10, 32).storeInt(x + y + z, 32)
.storeInt(w == null ? -1 : w, 32).storeInt(!(null == w) ? w : -1, 32);
}
fun test14() {
var (x, y) = (getNullableInt(), getNullableInt());
if (x == null) {
x = 0;
}
if (y == null) {
if (random()) { return 0; }
else { y = 0; }
}
return x + y;
}
fun test20() {
var t = (getNullableInt(), getNullableInt());
if (t.0 != null && t.1 != null) {
__expect_type(t.0, "int");
__expect_type(t.1, "int");
return t.0 + t.1;
}
t.0 = 10;
if (t.1 == null) {
t.1 = 20;
}
__expect_type(t.0, "int");
__expect_type(t.1, "int");
return t.0 + t.1;
}
fun test21() {
var t = (getNullableInt(), (getNullableInt(), getNullableInt()));
if (t.0 != null && t.1.0 != null) {
if (t.1.1 != null) { return t.0 + t.1.0 + t.1.1; }
return t.0 + t.1.0;
}
if (t.0 != null) {
return t.0 + 0;
}
__expect_type(t.0, "null");
__expect_type(t.1.0, "int?");
return t.1.0 == null ? -1 : t.1.0 + 0;
}
fun test22() {
var t = (getNullableInt(), (getNullableInt(), getNullableInt()));
if (t.0 == null || t.1.0 == null || t.1.1 == null) {
return -1;
}
return t.0 + t.1.0 + t.1.1;
}
@method_id(123)
fun test23() {
var (x: int?, y: int?, z: int?) = (getNullableInt(), getNullableInt(), getNullableInt());
((x = 1, 0).0, (y = 2, 1).0) = (3, z = 4);
return x + y + z;
}
@method_id(124)
fun test24(x: int?) {
if (x == null) {
__expect_type(x, "null");
assignToNullableInt(mutate x, 10);
__expect_type(x, "int?");
x.assignToNullableInt(x! + 5);
} else {
__expect_type(x, "int");
increment(mutate x);
x.increment();
__expect_type(x, "int");
}
__expect_type(x, "int?");
return x;
}
fun test25() {
var x = (getNullableInt(), getNullableInt(), getNullableInt());
x.0 = x.2 = random();
return (x.0) + ((x.2));
}
fun test26() {
var x = [getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt(),
getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt()];
if (~(x.0 = random())) { return; }
if ((x.1 = random()) < (x.2 = random())) { return; }
else if (!(x.2 <=> (x.3 = random()))) { return; }
x.5 = (x.4 = random()) ? (x.6 = random()) : (x.6 = random());
if ((x.7 = random()) as int) { return; }
if (((((x.8 = random()) != null)))) { return; }
if ([x.1, (x.9 = random())!].1) { return; }
val result = x.0+x.1+x.2+x.3+x.4+x.5+x.6+x.7+x.8+x.9;
}
fun test27() {
var (x, _) = ([getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt(),
getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt()], []);
+(x.0 = random());
x.0 += [((x.1 = random()) < (x.2 = random() + x.1)) as int].0;
!(x.2 <=> (x.3 = random() + x.2));
x.5 = (x.4 = random()) ? (x.6 = random()) : (x.6 = random());
(x.7 = random()) as int;
(((((x.8 = random()) != null))));
[x.1, (x.9 = random())!].1;
return x.0+x.1+x.2+x.3+x.4+x.5+x.6+x.7+x.8+x.9;
}
fun test28() {
var x = (getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt());
__expect_type((x.0 = random(), x.0 += (x.1 = random()) as int, !(x.1 <=> (x.2 = random() + x.0)) == null, (x.3 = random()) ? x.3 : (!x.3) as int),
"(int, int, bool, int)");
}
fun test29() {
var x = (getNullableInt(), getNullableInt(), getNullableInt(), getNullableInt());
__expect_type([x.0 = random(), ((x.0 += (x.1 = random()) as int)), !(x.1 <=> (x.2 = random() + x.0)) == null, (x.3 = random()) ? x.3 : (!x.3) as int],
"[int, int, bool, int]");
}
@method_id(130)
fun test30(initial5: bool) {
var t: (int?, (int?, (int?, int?))) = initial5
? (getNullableInt(), (getNullableInt(), (getNullableInt(), getNullableInt())))
: (null, (null, (null, null)));
if (t.0 == null || t.1.0 == null || t.1.1.0 == null || t.1.1.1 == null) {
if (t.1.0 == null || t.1.1.0 == null) {
if (t.1.1.0 == null) {
t.1.1.0 = 4;
}
__expect_type(t.1.1.0, "int");
__expect_type(t.1.1.1, "int?");
__expect_type(t.1.0, "int?");
t.1.1.1 = 3;
t.1.0 = 2;
__expect_type(t.1.1.1, "int");
__expect_type(t.1.0, "int");
}
if (((((t.1.1.1)))) != null) {}
else { t.1.1.1 = 3; }
t.0 = 1;
}
return t.0 + t.1.0 + t.1.1.0 + t.1.1.1;
}
fun test31() {
var t = (getNullableInt(), getNullableInt());
t.0 == null ? (t.0, t.1) = (1, 2) : (t.1, t.0) = (4, 3);
return t.0 + t.1;
}
@method_id(132)
fun test32() {
var t: (int?, (int?, int?)?, (int?, int?)) = (getNullableInt(), (getNullableInt(), getNullableInt()), (getNullableInt(), getNullableInt()));
if (t.0 == null) { return -1; }
t.1 != null && t.1.0 == null ? t.1 = (1, 2) : t.1 = (3, 4);
if (t.2.1 != null) { t.2.0 = 1; t.2.1 = 2; }
else { [t.2.0, t.2.1] = [3, 4]; }
return t.0 + t.1.0! + t.1.1! + t.2.0 + t.2.1;
}
@method_id(133)
fun test33(): int {
var x = getNullableInt();
repeat (eq(x = 5)) {
__expect_type(x, "int");
increment(mutate x);
}
return x;
}
fun test34() {
var (x, y) = (getNullableInt(), getNullableInt());
if (random()) { throw (x = 1, y = 2); }
else { throw (x = 3, y = (1, getNullableInt()!).1); }
return x + y;
}
fun test35() {
var (x, y, z, t) = (getNullableInt(), getNullableInt(), getNullableInt(), (getNullableInt(), getNullableInt()));
assert (x != null, 404);
assert (t.0 != null && true && !(t.1 == null) && !(z = 4)) throw (y = 404);
__expect_type(y, "int?");
return x + t.0 + t.1 + z;
}
fun test36() {
var x = getNullableInt();
assert (x == null, x + 0); // check that x is int there
__expect_type(x, "null");
}
fun test37() {
var (x, code) = (getNullableInt()!, getNullableInt());
try {
} catch(code) {
x = 20;
return x + code; // code is scoped
}
return code == null ? x : x + code;
}
fun assignNull2<T1, T2>(mutate x: T1?, mutate y: T2?) {
x = null;
y = null;
}
fun test38() {
var (x: int?, y: int?) = (1, 2);
__expect_type(x, "int");
__expect_type(y, "int");
assignNull2<int, int>(mutate x, mutate y);
__expect_type(x, "int?");
__expect_type(y, "int?");
if (x != null) {
if (y == null) { return -1; }
return x + y;
}
var t: (int?, slice?) = (null, null);
if (!false) { t.0 = 1; }
if (true) { t.1 = beginCell().endCell().beginParse(); }
__expect_type(t.0, "int");
__expect_type(t.1, "slice");
t.0 + t.1.loadInt(32);
assignNull2(mutate t.0, mutate t.1);
__expect_type(t.0, "int?");
__expect_type(t.1, "slice?");
t.0 != null && t.1 != null ? t.0 + loadInt(mutate t.1, 32) : -1;
return t.0 != null && t.1 != null ? t.0 + loadInt(mutate t.1, 32) : -1;
}
@method_id(139)
fun test39() {
var x: (int?, int?)? = (4, null);
x.1 = 10;
x.1 += 1;
x!.1 += 1;
return (x!.0! + x.1);
}
@method_id(140)
fun test40(second: int?) {
var x: (int?, int?)? = (4, second);
if (x.1 != null) {
val result = x.1 + x!.1 + x!!.1 + x.1! + x!!.1!!;
}
if (x!.1 != null) {
val result = x.1 + x!.1 + x!!.1 + x.1! + x!!.1!!;
}
if (!(x!!.1 != null)) {
return -1;
}
return x.1 + x!.1 + x!!.1 + x.1! + x!!.1!!;
}
@method_id(141)
fun test41() {
var t: (int, int)? = null;
return sameTensor(t = (1, 2));
}
@method_id(142)
fun test42() {
var t: (int?, (int?, (int, int)?)?) = (getNullableInt(), (1, (2, 3)));
t.1 = (3,null);
__expect_type(t.1, "(int?, (int, int)?)");
__expect_type(t, "(int?, (int?, (int, int)?)?)");
return (t, t.1);
}
@method_id(143)
fun test43() {
var t1: ((int, int), int?) = ((1, 2), 3);
var t2: ((int?, int?), (int?,int?)?) = ((null, null), (null, 5));
t2.0 = t1.0 = (10, 11);
t2.1 = t1.1 = null;
return (t1, t2);
}
@method_id(144)
fun test44() {
var t1: ((int, int), int?) = ((1, 2), 3);
var t2: ((int?, int?), (int?,int?)?) = ((null, null), (null, 5));
t1.0 = t2.0 = (10, 11);
t1.1 = t2.1 = null;
__expect_type(t1, "((int, int), int?)");
__expect_type(t2, "((int?, int?), (int?, int?)?)");
return (t1, t2);
}
@method_id(145)
fun test45() {
var t: (int?, (int?, (int, int)?)?) = (getNullableInt(), (1, (2, 3)));
var t2 = sameTensor2(t.1 = (3,null));
return (t, t2, t.1);
}
fun autoInfer46() {
var t1: int? = 3;
var t2: (int, int)? = (4, 5);
__expect_type(t1, "int");
__expect_type(t2, "(int, int)");
return (t1, t2); // proven to be not null, inferred (int, (int,int))
}
@method_id(146)
fun test46() {
var r46_1: (int, (int,int)) = autoInfer46();
var r46_2: (int, (int,int)?) = autoInfer46();
return (r46_1, r46_2);
}
@method_id(147)
fun test47() {
var t1: int? = 3;
var t2: (int, int)? = (4, 5);
t1 = t2 = null;
__expect_type(t1, "null");
__expect_type(t2, "null");
var result = (t1, t2); // proven to be always null, inferred (null, null), 2 slots on a stack
return (result, 100, result.1, 100, t2 as (int, int)?);
}
fun test48() {
var t1: int? = getNullableInt();
if (t1 != null) {
var (t1 redef, t2) = (10, 5);
return t1 + t2;
var t2 redef = getNullableInt()!;
return t1 + t2;
}
return -1;
}
fun test49(x: int?) {
while (x == null) {
x = getNullableInt();
}
__expect_type(x, "int");
return x + 1;
}
fun test50() {
var (x: int?, y: int?) = (1, 2);
do {
x = getNullableInt();
y = getNullableInt();
} while (x == null || y == null);
return x + y;
}
fun test51() {
while (true) { return; }
// test that no error "control reaches end of function"
}
fun test52() {
do { } while (true);
}
fun test53() {
var x1: int? = getNullableInt();
var x2: int? = 5;
var x3: int? = 5;
var x10: int? = null;
var x11: int? = 5;
var x12: int? = 5;
while (x1 != null) {
__expect_type(x1, "int"); // because condition
__expect_type(x2, "int?"); // because re-assigned
__expect_type(x3, "int?"); // because re-assigned
__expect_type(x10, "null");
__expect_type(x11, "int");
x1 = getNullableInt();
__expect_type(x1, "int?");
assignToNullableInt(mutate x2, 5);
x3.assignToNullableInt(5);
x11 = 10;
assignToInt(mutate x12, 5);
}
__expect_type(x1, "null");
__expect_type(x2, "int?");
__expect_type(x3, "int?");
}
fun test54() {
var x1: int? = null;
var x2: int? = 5;
var x3: int? = 5;
var x10: int? = null;
var x11: int? = 5;
var x12: int? = 5;
do {
__expect_type(x1, "int?"); // because re-assigned
__expect_type(x2, "int?"); // because re-assigned
__expect_type(x3, "int?"); // because re-assigned
__expect_type(x10, "null");
__expect_type(x11, "int");
x1 = getNullableInt();
__expect_type(x1, "int?");
assignToNullableInt(mutate x2, 5);
if (random()) { x3.assignToNullableInt(5); }
x11 = 10;
assignToInt(mutate x12, 5);
} while (x1 != null);
__expect_type(x1, "null");
__expect_type(x2, "int?");
__expect_type(x3, "int?");
}
fun eq55<T>(v: T) { return v; }
fun test55() {
var x: int? = 4;
while (true) {
// currently, generic functions are instantiated at the type inferring step
// in case of loops, type inferring is re-enterable
// first iteration: x is int, eq<int> instantiated
// second (final) iteration: x is int?, eq<int?> instantiated
// (checked via codegen)
eq55(x);
__expect_type(x, "int?"); // types are checked (unlike generics instantiated) after inferring
x = random() ? 1 : null;
}
__expect_type(x, "int?");
}
fun test56() {
var i: int? = null;
var (j: int?, k: int?) = (null, null);
__expect_type(i, "null");
__expect_type(k, "null");
i = getTwo();
[j, ((k))] = [getTwo(), ((getTwo()))];
__expect_type(i, "int?");
__expect_type(j, "int?");
__expect_type(k, "int?");
}
fun test57(mutate x: int?): int {
if (x == null) { x = 5; }
else {
if (x < 10) { x = 10; }
else { x = 20; }
}
if (x != null) {
return 123;
}
__expect_type(x, "int");
// no "return" needed, because end of function is unreachable
}
@method_id(158)
fun test58() {
var (x1, x2: int?) = (getNullableInt(), null);
return (test57(mutate x1), x1, test57(mutate x2), x2);
}
fun test59() {
var (x1: int?, x2, x3) = (getNullableInt()!, getNullableInt(), 5);
if ((x2 = x3) != null) {
__expect_type(x2, "int");
}
__expect_type(x2, "int");
if ((x2 = getNullableInt()) != null) {
__expect_type(x2, "int");
}
__expect_type(x2, "int?");
if (((x1) = x2) == null) {
return;
}
__expect_type(x1, "int");
}
fun main(x: int?): int {
return x == null ? -1 : x;
}
/**
@testcase | 0 | 1 | 1
@testcase | 123 | | 7
@testcase | 124 | 4 | 6
@testcase | 124 | null | 15
@testcase | 130 | -1 | 20
@testcase | 130 | 0 | 10
@testcase | 132 | | 15
@testcase | 133 | | 10
@testcase | 139 | | 16
@testcase | 140 | 5 | 25
@testcase | 141 | | 1 2
@testcase | 142 | | 5 3 (null) (null) 0 -1 3 (null) (null) 0
@testcase | 143 | | 10 11 (null) 10 11 (null) (null) 0
@testcase | 144 | | 10 11 (null) 10 11 (null) (null) 0
@testcase | 145 | | 5 3 (null) (null) 0 -1 3 (null) (null) (null) (null) 0 3 (null) (null) 0
@testcase | 146 | | 3 4 5 3 4 5 -1
@testcase | 147 | | (null) (null) 100 (null) 100 (null) (null) 0
@testcase | 158 | | 123 10 123 5
@stderr warning: expression of type `int` is always not null, this condition is always true
@stderr warning: unreachable code
@stderr var t2 redef = getNullableInt()!;
@fif_codegen eq55<int?> PROC:<{
@fif_codegen eq55<int> PROC:<{
*/

View file

@ -0,0 +1,22 @@
fun main(x: int?) {
if (x != null && x == null) {
return 1 + 2;
}
if (x == null) {
return -1;
}
if (x != null) {
return -2;
}
return 3 + 4;
}
/**
@testcase | 0 | 5 | -2
@testcase | 0 | null | -1
@stderr warning: variable `x` of type `int` is always not null
@stderr if (x != null)
@stderr warning: unreachable code
@stderr return 3 + 4
*/

View file

@ -0,0 +1,28 @@
fun getNullableInt(): int? { return null; }
fun main() {
var c: int? = 6;
__expect_type(c, "int");
if (c == null) {}
var d: int? = c;
if (((d)) != null && tupleSize(createEmptyTuple())) {}
var e: int? = getNullableInt();
if (e != null) {
return true;
}
__expect_type(e, "null");
null == e;
return null != null;
}
/**
@testcase | 0 | | 0
@stderr warning: variable `c` of type `int` is always not null, this condition is always false
@stderr warning: variable `d` of type `int` is always not null, this condition is always true
@stderr warning: variable `e` is always null, this condition is always true
@stderr warning: expression is always null, this condition is always false
*/

View file

@ -0,0 +1,26 @@
fun main() {
var (a, b, c, d, e) = (1, beginCell(), beginCell().endCell().beginParse(), [1], true as bool?);
var alwaysInt = a != null ? 1 : null;
__expect_type(alwaysInt, "int");
if (!(c == null)) {
if (10 < 3) { assert(b == null, 100); }
}
while (d == null || false) {}
return e! != null;
}
/**
@testcase | 0 | | -1
@stderr warning: variable `a` of type `int` is always not null, this condition is always true
@stderr warning: condition of ternary operator is always true
@stderr warning: variable `c` of type `slice` is always not null, this condition is always false
@stderr warning: condition of `if` is always true
@stderr warning: variable `b` of type `builder` is always not null, this condition is always false
@stderr warning: condition of `assert` is always false
@stderr warning: condition of `while` is always false
@stderr warning: expression of type `bool` is always not null, this condition is always true
*/

View file

@ -12,8 +12,8 @@ set(TOLK_SOURCE
pipe-register-symbols.cpp pipe-register-symbols.cpp
pipe-resolve-identifiers.cpp pipe-resolve-identifiers.cpp
pipe-calc-rvalue-lvalue.cpp pipe-calc-rvalue-lvalue.cpp
pipe-detect-unreachable.cpp
pipe-infer-types-and-calls.cpp pipe-infer-types-and-calls.cpp
pipe-check-inferred-types.cpp
pipe-refine-lvalue-for-mutate.cpp pipe-refine-lvalue-for-mutate.cpp
pipe-check-rvalue-lvalue.cpp pipe-check-rvalue-lvalue.cpp
pipe-check-pure-impure.cpp pipe-check-pure-impure.cpp
@ -23,6 +23,7 @@ set(TOLK_SOURCE
pipe-find-unused-symbols.cpp pipe-find-unused-symbols.cpp
pipe-generate-fif-output.cpp pipe-generate-fif-output.cpp
type-system.cpp type-system.cpp
smart-casts-cfg.cpp
generics-helpers.cpp generics-helpers.cpp
abscode.cpp abscode.cpp
analyzer.cpp analyzer.cpp

View file

@ -414,7 +414,7 @@ std::vector<var_idx_t> CodeBlob::create_var(TypePtr var_type, SrcLocation loc, s
std::string null_flag_name = name.empty() ? name : name + ".NNFlag"; std::string null_flag_name = name.empty() ? name : name + ".NNFlag";
ir_idx = create_var(t_nullable->inner, loc, std::move(name)); ir_idx = create_var(t_nullable->inner, loc, std::move(name));
ir_idx.emplace_back(create_var(TypeDataBool::create(), loc, std::move(null_flag_name))[0]); ir_idx.emplace_back(create_var(TypeDataBool::create(), loc, std::move(null_flag_name))[0]);
} else if (var_type != TypeDataVoid::create()) { } else if (var_type != TypeDataVoid::create() && var_type != TypeDataNever::create()) {
#ifdef TOLK_DEBUG #ifdef TOLK_DEBUG
tolk_assert(stack_w == 1); tolk_assert(stack_w == 1);
#endif #endif

View file

@ -117,6 +117,11 @@ void ASTNodeExpressionBase::assign_lvalue_true() {
this->is_lvalue = true; this->is_lvalue = true;
} }
void ASTNodeExpressionBase::assign_always_true_or_false(int flow_true_false_state) {
this->is_always_true = flow_true_false_state == 1; // see smart-casts-cfg.h
this->is_always_false = flow_true_false_state == 2;
}
void Vertex<ast_reference>::assign_sym(const Symbol* sym) { void Vertex<ast_reference>::assign_sym(const Symbol* sym) {
this->sym = sym; this->sym = sym;
} }
@ -173,6 +178,10 @@ void Vertex<ast_is_null_check>::assign_is_negated(bool is_negated) {
this->is_negated = is_negated; this->is_negated = is_negated;
} }
void Vertex<ast_sequence>::assign_first_unreachable(AnyV first_unreachable) {
this->first_unreachable = first_unreachable;
}
void Vertex<ast_dot_access>::assign_target(const DotTarget& target) { void Vertex<ast_dot_access>::assign_target(const DotTarget& target) {
this->target = target; this->target = target;
} }

View file

@ -186,11 +186,14 @@ struct ASTNodeExpressionBase : ASTNodeBase {
TypePtr inferred_type = nullptr; TypePtr inferred_type = nullptr;
bool is_rvalue: 1 = false; bool is_rvalue: 1 = false;
bool is_lvalue: 1 = false; bool is_lvalue: 1 = false;
bool is_always_true: 1 = false; // inside `if`, `while`, ternary condition, `== null`, etc.
bool is_always_false: 1 = false; // (when expression is guaranteed to be always true or always false)
ASTNodeExpressionBase* mutate() const { return const_cast<ASTNodeExpressionBase*>(this); } ASTNodeExpressionBase* mutate() const { return const_cast<ASTNodeExpressionBase*>(this); }
void assign_inferred_type(TypePtr type); void assign_inferred_type(TypePtr type);
void assign_rvalue_true(); void assign_rvalue_true();
void assign_lvalue_true(); void assign_lvalue_true();
void assign_always_true_or_false(int flow_true_false_state);
ASTNodeExpressionBase(ASTNodeType type, SrcLocation loc) : ASTNodeBase(type, loc) {} ASTNodeExpressionBase(ASTNodeType type, SrcLocation loc) : ASTNodeBase(type, loc) {}
}; };
@ -734,10 +737,14 @@ template<>
// example: do while body is a sequence // example: do while body is a sequence
struct Vertex<ast_sequence> final : ASTStatementVararg { struct Vertex<ast_sequence> final : ASTStatementVararg {
SrcLocation loc_end; SrcLocation loc_end;
AnyV first_unreachable = nullptr;
const std::vector<AnyV>& get_items() const { return children; } const std::vector<AnyV>& get_items() const { return children; }
AnyV get_item(int i) const { return children.at(i); } AnyV get_item(int i) const { return children.at(i); }
Vertex* mutate() const { return const_cast<Vertex*>(this); }
void assign_first_unreachable(AnyV first_unreachable);
Vertex(SrcLocation loc, SrcLocation loc_end, std::vector<AnyV> items) Vertex(SrcLocation loc, SrcLocation loc_end, std::vector<AnyV> items)
: ASTStatementVararg(ast_sequence, loc, std::move(items)) : ASTStatementVararg(ast_sequence, loc, std::move(items))
, loc_end(loc_end) {} , loc_end(loc_end) {}

View file

@ -119,14 +119,14 @@ TypePtr GenericSubstitutionsDeduceForCall::replace_by_manually_specified(TypePtr
return replace_genericT_with_deduced(param_type, fun_ref->genericTs, substitutionTs); return replace_genericT_with_deduced(param_type, fun_ref->genericTs, substitutionTs);
} }
TypePtr GenericSubstitutionsDeduceForCall::auto_deduce_from_argument(SrcLocation loc, TypePtr param_type, TypePtr arg_type) { TypePtr GenericSubstitutionsDeduceForCall::auto_deduce_from_argument(FunctionPtr cur_f, SrcLocation loc, TypePtr param_type, TypePtr arg_type) {
try { try {
if (!manually_specified) { if (!manually_specified) {
consider_next_condition(param_type, arg_type); consider_next_condition(param_type, arg_type);
} }
return replace_genericT_with_deduced(param_type, fun_ref->genericTs, substitutionTs); return replace_genericT_with_deduced(param_type, fun_ref->genericTs, substitutionTs);
} catch (const GenericDeduceError& ex) { } catch (const GenericDeduceError& ex) {
throw ParseError(loc, ex.message + " for generic function `" + fun_ref->as_human_readable() + "`; instantiate it manually with `" + fun_ref->name + "<...>()`"); throw ParseError(cur_f, loc, ex.message + " for generic function `" + fun_ref->as_human_readable() + "`; instantiate it manually with `" + fun_ref->name + "<...>()`");
} }
} }
@ -201,7 +201,6 @@ static void run_pipeline_for_instantiated_function(FunctionPtr inst_fun_ref) {
// these pipes are exactly the same as in tolk.cpp — all preceding (and including) type inferring // these pipes are exactly the same as in tolk.cpp — all preceding (and including) type inferring
pipeline_resolve_identifiers_and_assign_symbols(inst_fun_ref); pipeline_resolve_identifiers_and_assign_symbols(inst_fun_ref);
pipeline_calculate_rvalue_lvalue(inst_fun_ref); pipeline_calculate_rvalue_lvalue(inst_fun_ref);
pipeline_detect_unreachable_statements(inst_fun_ref);
pipeline_infer_types_and_calls_and_fields(inst_fun_ref); pipeline_infer_types_and_calls_and_fields(inst_fun_ref);
} }

View file

@ -78,7 +78,7 @@ public:
void provide_manually_specified(std::vector<TypePtr>&& substitutionTs); void provide_manually_specified(std::vector<TypePtr>&& substitutionTs);
TypePtr replace_by_manually_specified(TypePtr param_type) const; TypePtr replace_by_manually_specified(TypePtr param_type) const;
TypePtr auto_deduce_from_argument(SrcLocation loc, TypePtr param_type, TypePtr arg_type); TypePtr auto_deduce_from_argument(FunctionPtr cur_f, SrcLocation loc, TypePtr param_type, TypePtr arg_type);
int get_first_not_deduced_idx() const; int get_first_not_deduced_idx() const;
std::vector<TypePtr>&& flush() { std::vector<TypePtr>&& flush() {

View file

@ -442,6 +442,21 @@ static std::vector<var_idx_t> transition_expr_to_runtime_type_impl(std::vector<v
const TypeDataNullable* t_nullable = target_type->try_as<TypeDataNullable>(); const TypeDataNullable* t_nullable = target_type->try_as<TypeDataNullable>();
const TypeDataNullable* o_nullable = original_type->try_as<TypeDataNullable>(); const TypeDataNullable* o_nullable = original_type->try_as<TypeDataNullable>();
// handle `never`
// it may occur due to smart cast and in unreachable branches
// we can't do anything reasonable here, but (hopefully) execution will never reach this point, and stack won't be polluted
if (original_type == TypeDataNever::create()) {
std::vector<var_idx_t> dummy_rvect;
dummy_rvect.reserve(target_w);
for (int i = 0; i < target_w; ++i) {
dummy_rvect.push_back(code.create_tmp_var(TypeDataUnknown::create(), loc, "(never)")[0]);
}
return dummy_rvect;
}
if (target_type == TypeDataNever::create()) {
return {};
}
// pass `null` to `T?` // pass `null` to `T?`
// for primitives like `int?`, no changes in rvect, null occupies the same TVM slot // for primitives like `int?`, no changes in rvect, null occupies the same TVM slot
// for tensors like `(int,int)?`, `null` is represented as N nulls + 1 null flag, insert N nulls // for tensors like `(int,int)?`, `null` is represented as N nulls + 1 null flag, insert N nulls
@ -493,6 +508,8 @@ static std::vector<var_idx_t> transition_expr_to_runtime_type_impl(std::vector<v
return rvect; return rvect;
} }
// pass `T?` to `null` // pass `T?` to `null`
// it may occur due to smart cast, when a `T?` variable is guaranteed to be always null
// (for instance, always-null `(int,int)?` will be represented as 1 TVM NULL value, not 3)
if (target_type == TypeDataNullLiteral::create() && original_type->can_rhs_be_assigned(target_type)) { if (target_type == TypeDataNullLiteral::create() && original_type->can_rhs_be_assigned(target_type)) {
tolk_assert(o_nullable || original_type == TypeDataUnknown::create()); tolk_assert(o_nullable || original_type == TypeDataUnknown::create());
if (o_nullable && !o_nullable->is_primitive_nullable()) { if (o_nullable && !o_nullable->is_primitive_nullable()) {
@ -502,10 +519,12 @@ static std::vector<var_idx_t> transition_expr_to_runtime_type_impl(std::vector<v
} }
return rvect; return rvect;
} }
// pass `T?` to `T` // pass `T?` to `T` (or, more generally, `T1?` to `T2`)
// it may occur due to operator `!` or smart cast // it may occur due to operator `!` or smart cast
// for primitives like `int?`, no changes in rvect // for primitives like `int?`, no changes in rvect
// for passing `(int, int)?` to `(int, int)`, drop the null flag from the tail // for passing `(int, int)?` to `(int, int)`, drop the null flag from the tail
// for complex scenarios like passing `(int, (int,int)?)?` to `(int, null)`, recurse the call
// (it may occur on `someF(t = (3,null))` when `(3,null)` at first targeted to lhs, but actually its result is rhs)
if (!t_nullable && o_nullable) { if (!t_nullable && o_nullable) {
if (!o_nullable->is_primitive_nullable()) { if (!o_nullable->is_primitive_nullable()) {
rvect.pop_back(); rvect.pop_back();
@ -572,6 +591,17 @@ static std::vector<var_idx_t> transition_to_target_type(std::vector<var_idx_t>&&
return rvect; return rvect;
} }
// the second overload of the same function, invoke impl only when original and target differ
#ifndef TOLK_DEBUG
GNU_ATTRIBUTE_ALWAYS_INLINE
#endif
static std::vector<var_idx_t> transition_to_target_type(std::vector<var_idx_t>&& rvect, CodeBlob& code, TypePtr original_type, TypePtr target_type, SrcLocation loc) {
if (target_type != original_type) {
rvect = transition_expr_to_runtime_type_impl(std::move(rvect), code, original_type, target_type, loc);
}
return rvect;
}
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 (GlobalVarPtr glob_ref = sym->try_as<GlobalVarPtr>()) { if (GlobalVarPtr glob_ref = sym->try_as<GlobalVarPtr>()) {
@ -617,20 +647,33 @@ std::vector<var_idx_t> pre_compile_symbol(SrcLocation loc, const Symbol* sym, Co
static std::vector<var_idx_t> process_reference(V<ast_reference> v, CodeBlob& code, TypePtr target_type, LValContext* lval_ctx) { static std::vector<var_idx_t> process_reference(V<ast_reference> v, CodeBlob& code, TypePtr target_type, LValContext* lval_ctx) {
std::vector<var_idx_t> rvect = pre_compile_symbol(v->loc, v->sym, code, lval_ctx); std::vector<var_idx_t> rvect = pre_compile_symbol(v->loc, v->sym, code, lval_ctx);
// a local variable might be smart cast at this point, for example we're in `if (v != null)`
// it means that we must drop the null flag (if it's a tensor), or maybe perform other stack transformations
// (from original var_ref->ir_idx to fit smart cast)
if (LocalVarPtr var_ref = v->sym->try_as<LocalVarPtr>()) {
// note, inside `if (v != null)` when `v` is used for writing, v->inferred_type is an original (declared_type)
// (smart casts apply only for rvalue, not for lvalue, we don't check it here, it's a property of inferring)
rvect = transition_to_target_type(std::move(rvect), code, var_ref->declared_type, v->inferred_type, v->loc);
}
return transition_to_target_type(std::move(rvect), code, target_type, v); return transition_to_target_type(std::move(rvect), code, target_type, v);
} }
static std::vector<var_idx_t> process_assignment(V<ast_assign> v, CodeBlob& code, TypePtr target_type) { static std::vector<var_idx_t> process_assignment(V<ast_assign> v, CodeBlob& code, TypePtr target_type) {
if (auto lhs_decl = v->get_lhs()->try_as<ast_local_vars_declaration>()) { AnyExprV lhs = v->get_lhs();
std::vector<var_idx_t> rvect = pre_compile_let(code, lhs_decl->get_expr(), v->get_rhs(), v->loc); AnyExprV rhs = v->get_rhs();
if (auto lhs_decl = lhs->try_as<ast_local_vars_declaration>()) {
std::vector<var_idx_t> rvect = pre_compile_let(code, lhs_decl->get_expr(), rhs, v->loc);
return transition_to_target_type(std::move(rvect), code, target_type, v); return transition_to_target_type(std::move(rvect), code, target_type, v);
} else { } else {
std::vector<var_idx_t> rvect = pre_compile_let(code, v->get_lhs(), v->get_rhs(), v->loc); std::vector<var_idx_t> rvect = pre_compile_let(code, lhs, rhs, v->loc);
// now rvect contains rhs IR vars constructed to fit lhs (for correct assignment, lhs type was target_type for rhs) // now rvect contains rhs IR vars constructed to fit lhs (for correct assignment, lhs type was target_type for rhs)
// but the type of `lhs = rhs` is RHS (see type inferring), so rvect now should fit rhs->inferred_type (= v->inferred_type) // but the type of `lhs = rhs` is RHS (see type inferring), so rvect now should fit rhs->inferred_type (= v->inferred_type)
// example: `t1 = t2 = null`, we're at `t2 = null`, earlier declared t1: `int?`, t2: `(int,int)?` // example: `t1 = t2 = null`, we're at `t2 = null`, earlier declared t1: `int?`, t2: `(int,int)?`
// currently "null" matches t2 (3 null slots), but type of this assignment is "plain null" (1 slot) assigned later to t1 // currently "null" matches t2 (3 null slots), but type of this assignment is "plain null" (1 slot) assigned later to t1
rvect = transition_expr_to_runtime_type_impl(std::move(rvect), code, v->get_lhs()->inferred_type, v->inferred_type, v->loc); rvect = transition_to_target_type(std::move(rvect), code, lhs->inferred_type, v->inferred_type, v->loc);
return transition_to_target_type(std::move(rvect), code, target_type, v); return transition_to_target_type(std::move(rvect), code, target_type, v);
} }
} }
@ -692,13 +735,21 @@ static std::vector<var_idx_t> process_ternary_operator(V<ast_ternary_operator> v
std::vector<var_idx_t> cond = pre_compile_expr(v->get_cond(), code, nullptr); std::vector<var_idx_t> cond = pre_compile_expr(v->get_cond(), code, nullptr);
tolk_assert(cond.size() == 1); tolk_assert(cond.size() == 1);
std::vector<var_idx_t> rvect = code.create_tmp_var(v->inferred_type, v->loc, "(cond)"); std::vector<var_idx_t> rvect = code.create_tmp_var(v->inferred_type, v->loc, "(cond)");
Op& if_op = code.emplace_back(v->loc, Op::_If, cond);
code.push_set_cur(if_op.block0); if (v->get_cond()->is_always_true) {
code.emplace_back(v->get_when_true()->loc, Op::_Let, rvect, pre_compile_expr(v->get_when_true(), code, v->inferred_type)); code.emplace_back(v->get_when_true()->loc, Op::_Let, rvect, pre_compile_expr(v->get_when_true(), code, v->inferred_type));
code.close_pop_cur(v->get_when_true()->loc); } else if (v->get_cond()->is_always_false) {
code.push_set_cur(if_op.block1); code.emplace_back(v->get_when_false()->loc, Op::_Let, rvect, pre_compile_expr(v->get_when_false(), code, v->inferred_type));
code.emplace_back(v->get_when_false()->loc, Op::_Let, rvect, pre_compile_expr(v->get_when_false(), code, v->inferred_type)); } else {
code.close_pop_cur(v->get_when_false()->loc); Op& if_op = code.emplace_back(v->loc, Op::_If, cond);
code.push_set_cur(if_op.block0);
code.emplace_back(v->get_when_true()->loc, Op::_Let, rvect, pre_compile_expr(v->get_when_true(), code, v->inferred_type));
code.close_pop_cur(v->get_when_true()->loc);
code.push_set_cur(if_op.block1);
code.emplace_back(v->get_when_false()->loc, Op::_Let, rvect, pre_compile_expr(v->get_when_false(), code, v->inferred_type));
code.close_pop_cur(v->get_when_false()->loc);
}
return transition_to_target_type(std::move(rvect), code, target_type, v); return transition_to_target_type(std::move(rvect), code, target_type, v);
} }
@ -768,6 +819,10 @@ static std::vector<var_idx_t> process_dot_access(V<ast_dot_access> v, CodeBlob&
stack_offset += t_tensor->items[i]->get_width_on_stack(); stack_offset += t_tensor->items[i]->get_width_on_stack();
} }
std::vector<var_idx_t> rvect{lhs_vars.begin() + stack_offset, lhs_vars.begin() + stack_offset + stack_width}; std::vector<var_idx_t> rvect{lhs_vars.begin() + stack_offset, lhs_vars.begin() + stack_offset + stack_width};
// a tensor index might be smart cast at this point, for example we're in `if (t.1 != null)`
// it means that we must drop the null flag (if `t.1` is a tensor), or maybe perform other stack transformations
// (from original rvect = (vars of t.1) to fit smart cast)
rvect = transition_to_target_type(std::move(rvect), code, t_tensor->items[index_at], v->inferred_type, v->loc);
return transition_to_target_type(std::move(rvect), code, target_type, v); return transition_to_target_type(std::move(rvect), code, target_type, v);
} }
// `tupleVar.0` // `tupleVar.0`
@ -1090,8 +1145,19 @@ static void process_repeat_statement(V<ast_repeat_statement> v, CodeBlob& code)
} }
static void process_if_statement(V<ast_if_statement> v, CodeBlob& code) { static void process_if_statement(V<ast_if_statement> v, CodeBlob& code) {
std::vector<var_idx_t> tmp_vars = pre_compile_expr(v->get_cond(), code, nullptr); std::vector<var_idx_t> cond = pre_compile_expr(v->get_cond(), code, nullptr);
Op& if_op = code.emplace_back(v->loc, Op::_If, std::move(tmp_vars)); tolk_assert(cond.size() == 1);
if (v->get_cond()->is_always_true) {
process_any_statement(v->get_if_body(), code); // v->is_ifnot does not matter here
return;
}
if (v->get_cond()->is_always_false) {
process_any_statement(v->get_else_body(), code);
return;
}
Op& if_op = code.emplace_back(v->loc, Op::_If, std::move(cond));
code.push_set_cur(if_op.block0); code.push_set_cur(if_op.block0);
process_any_statement(v->get_if_body(), code); process_any_statement(v->get_if_body(), code);
code.close_pop_cur(v->get_if_body()->loc_end); code.close_pop_cur(v->get_if_body()->loc_end);
@ -1192,6 +1258,10 @@ static void process_return_statement(V<ast_return_statement> v, CodeBlob& code)
code.emplace_back(v->loc, Op::_Return, std::move(return_vars)); code.emplace_back(v->loc, Op::_Return, std::move(return_vars));
} }
// append "return" (void) to the end of the function
// if it's not reachable, it will be dropped
// (IR cfg reachability may differ from FlowContext in case of "never" types, so there may be situations,
// when IR will consider this "return" reachable and leave it, but actually execution will never reach it)
static void append_implicit_return_statement(SrcLocation loc_end, CodeBlob& code) { static void append_implicit_return_statement(SrcLocation loc_end, CodeBlob& code) {
std::vector<var_idx_t> mutated_vars; std::vector<var_idx_t> mutated_vars;
if (code.fun_ref->has_mutate_params()) { if (code.fun_ref->has_mutate_params()) {
@ -1256,9 +1326,7 @@ static void convert_function_body_to_CodeBlob(FunctionPtr fun_ref, FunctionBodyC
for (AnyV item : v_body->get_items()) { for (AnyV item : v_body->get_items()) {
process_any_statement(item, *blob); process_any_statement(item, *blob);
} }
if (fun_ref->is_implicit_return()) { append_implicit_return_statement(v_body->loc_end, *blob);
append_implicit_return_statement(v_body->loc_end, *blob);
}
blob->close_blk(v_body->loc_end); blob->close_blk(v_body->loc_end);
code_body->set_code(blob); code_body->set_code(blob);

View file

@ -0,0 +1,586 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#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<ast_reference>()) {
if (v_ref->sym->try_as<LocalVarPtr>() || v_ref->sym->try_as<GlobalVarPtr>()) {
return "variable `" + static_cast<std::string>(v_ref->get_identifier()->name) + "`";
}
}
if (auto v_par = v->try_as<ast_parenthesized_expression>()) {
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<std::string>(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<std::string>(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<std::string>(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 + ": <type>`" + (is_assigned_null_literal ? " or use `null as <type>`" : ""));
}
// 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<ast_function_call> 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<ast_string_const>()->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<ast_set_assign> 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<ast_unary_operator> 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<ast_binary_operator> 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<ast_cast_as_operator> 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<ast_not_null_operator> 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<ast_is_null_check> 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<ast_typed_tuple> 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<ast_dot_access> v) override {
parent::visit(v);
TypePtr obj_type = v->get_obj()->inferred_type;
if (v->is_target_indexed_access()) {
if (obj_type->try_as<TypeDataTuple>() && 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<ast_function_call> 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<TypeDataFunCallable>();
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<ast_dot_access>()) {
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<ast_assign> 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<ast_local_vars_declaration>()) {
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<ast_local_var_lhs>()) {
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<LocalVarPtr>(), 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<ast_tensor>()) {
const TypeDataTensor* rhs_type_tensor = rhs_type->try_as<TypeDataTensor>();
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<ast_tensor> rhs_tensor_maybe = corresponding_maybe_rhs ? corresponding_maybe_rhs->try_as<ast_tensor>() : 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<ast_typed_tuple>()) {
const TypeDataTypedTuple* rhs_type_tuple = rhs_type->try_as<TypeDataTypedTuple>();
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<ast_typed_tuple> rhs_tuple_maybe = corresponding_maybe_rhs ? corresponding_maybe_rhs->try_as<ast_typed_tuple>() : 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<ast_dot_access>()) {
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<ast_reference>()) {
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<ast_return_statement> 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<ast_reference>()->get_name() == "self") {
return true;
}
// `return self.someMethod()`
if (auto v_call = return_expr->try_as<ast_function_call>(); 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<ast_ternary_operator>()) {
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<ast_ternary_operator> 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<ast_if_statement> 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<ast_repeat_statement> 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<ast_while_statement> 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<ast_do_while_statement> 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<ast_throw_statement> 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<ast_assert_statement> 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<ast_sequence> 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<ast_function_declaration> 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<ast_sequence>()->loc_end, "missing return");
}
}
}
};
void pipeline_check_inferred_types() {
visit_ast_of_all_functions<CheckInferredTypesVisitor>();
}
} // namespace tolk

View file

@ -1,138 +0,0 @@
/*
This file is part of TON Blockchain source code.
TON Blockchain is free software; you can redistribute it and/or
modify it under the terms of the GNU 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 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with TON Blockchain. If not, see <http://www.gnu.org/licenses/>.
*/
#include "tolk.h"
#include "ast.h"
#include "ast-visitor.h"
/*
* This pipe does two things:
* 1) detects unreachable code and prints warnings about it
* example: `fun main() { if(1){return;}else{return;} var x = 0; }` var is unreachable
* 2) if control flow reaches end of function, store a flag to insert an implicit return
* example: `fun main() { assert(...); }` has an implicit `return ()` statement before a brace
*
* Note, that it does not delete unreachable code, only prints warnings.
* Actual deleting is done much later (in "legacy" part), after AST is converted to Op.
*
* Note, that it's not CFG, it's just a shallow reachability detection.
* In the future, a true CFG should be introduced. For instance, in order to have nullable types,
* I'll need to implement smart casts. Then I'll think of a complicated granular control flow graph,
* considering data flow and exceptions (built before type inferring, of course),
* and detecting unreachable code will be a part of it.
*/
namespace tolk {
class UnreachableStatementsDetectVisitor final {
bool always_returns(AnyV v) {
switch (v->type) {
case ast_sequence: return always_returns(v->as<ast_sequence>());
case ast_return_statement: return always_returns(v->as<ast_return_statement>());
case ast_throw_statement: return always_returns(v->as<ast_throw_statement>());
case ast_function_call: return always_returns(v->as<ast_function_call>());
case ast_repeat_statement: return always_returns(v->as<ast_repeat_statement>());
case ast_while_statement: return always_returns(v->as<ast_while_statement>());
case ast_do_while_statement: return always_returns(v->as<ast_do_while_statement>());
case ast_try_catch_statement: return always_returns(v->as<ast_try_catch_statement>());
case ast_if_statement: return always_returns(v->as<ast_if_statement>());
default:
// unhandled statements (like assert) and statement expressions
return false;
}
}
bool always_returns(V<ast_sequence> v) {
bool always = false;
for (AnyV item : v->get_items()) {
if (always && item->type != ast_empty_statement) {
item->loc.show_warning("unreachable code");
break;
}
always |= always_returns(item);
}
return always;
}
static bool always_returns([[maybe_unused]] V<ast_return_statement> v) {
// quite obvious: `return expr` interrupts control flow
return true;
}
static bool always_returns([[maybe_unused]] V<ast_throw_statement> v) {
// todo `throw excNo` currently does not interrupt control flow
// (in other words, `throw 1; something` - something is reachable)
// the reason is that internally it's transformed to a call of built-in function __throw(),
// which is a regular function, like __throw_if() or loadInt()
// to fix this later on, it should be deeper, introducing Op::_Throw for example,
// to make intermediate representations and stack optimizer also be aware that after it there is unreachable
return false;
}
static bool always_returns([[maybe_unused]] V<ast_function_call> v) {
// neither annotations like @noreturn nor auto-detection of always-throwing functions also doesn't exist
// in order to do this in the future, it should be handled not only at AST/CFG level,
// but inside Op and low-level optimizer (at least if reachability detection is not moved out of there)
// see comments for `throw` above, similar to this case
return false;
}
bool always_returns(V<ast_repeat_statement> v) {
return always_returns(v->get_body());
}
bool always_returns(V<ast_while_statement> v) {
return always_returns(v->get_body());
}
bool always_returns(V<ast_do_while_statement> v) {
return always_returns(v->get_body());
}
bool always_returns(V<ast_try_catch_statement> v) {
return always_returns(v->get_try_body()) && always_returns(v->get_catch_body());
}
bool always_returns(V<ast_if_statement> v) {
return always_returns(v->get_if_body()) && always_returns(v->get_else_body());
}
public:
static bool should_visit_function(FunctionPtr fun_ref) {
return fun_ref->is_code_function() && !fun_ref->is_generic_function();
}
void start_visiting_function(FunctionPtr fun_ref, V<ast_function_declaration> v_function) {
bool control_flow_reaches_end = !always_returns(v_function->get_body()->as<ast_sequence>());
if (control_flow_reaches_end) {
fun_ref->mutate()->assign_is_implicit_return();
}
}
};
void pipeline_detect_unreachable_statements() {
visit_ast_of_all_functions<UnreachableStatementsDetectVisitor>();
}
void pipeline_detect_unreachable_statements(FunctionPtr fun_ref) {
UnreachableStatementsDetectVisitor visitor;
if (UnreachableStatementsDetectVisitor::should_visit_function(fun_ref)) {
visitor.start_visiting_function(fun_ref, fun_ref->ast_root->as<ast_function_declaration>());
}
}
} // namespace tolk

File diff suppressed because it is too large Load diff

View file

@ -59,20 +59,20 @@
namespace tolk { namespace tolk {
GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD
static void fire_error_undefined_symbol(V<ast_identifier> v) { static void fire_error_undefined_symbol(FunctionPtr cur_f, V<ast_identifier> v) {
if (v->name == "self") { if (v->name == "self") {
v->error("using `self` in a non-member function (it does not accept the first `self` parameter)"); throw ParseError(cur_f, v->loc, "using `self` in a non-member function (it does not accept the first `self` parameter)");
} else { } else {
v->error("undefined symbol `" + static_cast<std::string>(v->name) + "`"); throw ParseError(cur_f, v->loc, "undefined symbol `" + static_cast<std::string>(v->name) + "`");
} }
} }
GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD GNU_ATTRIBUTE_NORETURN GNU_ATTRIBUTE_COLD
static void fire_error_unknown_type_name(SrcLocation loc, const std::string &text) { static void fire_error_unknown_type_name(FunctionPtr cur_f, SrcLocation loc, const std::string &text) {
throw ParseError(loc, "unknown type name `" + text + "`"); throw ParseError(cur_f, loc, "unknown type name `" + text + "`");
} }
static void check_import_exists_when_using_sym(AnyV v_usage, const Symbol* used_sym) { static void check_import_exists_when_using_sym(FunctionPtr cur_f, AnyV v_usage, const Symbol* used_sym) {
SrcLocation sym_loc = used_sym->loc; SrcLocation sym_loc = used_sym->loc;
if (!v_usage->loc.is_symbol_from_same_or_builtin_file(sym_loc)) { if (!v_usage->loc.is_symbol_from_same_or_builtin_file(sym_loc)) {
const SrcFile* declared_in = sym_loc.get_src_file(); const SrcFile* declared_in = sym_loc.get_src_file();
@ -83,7 +83,7 @@ static void check_import_exists_when_using_sym(AnyV v_usage, const Symbol* used_
} }
} }
if (!has_import) { if (!has_import) {
v_usage->error("Using a non-imported symbol `" + used_sym->name + "`. Forgot to import \"" + declared_in->rel_filename + "\"?"); throw ParseError(cur_f, v_usage->loc, "Using a non-imported symbol `" + used_sym->name + "`. Forgot to import \"" + declared_in->rel_filename + "\"?");
} }
} }
} }
@ -137,38 +137,39 @@ struct NameAndScopeResolver {
struct TypeDataResolver { struct TypeDataResolver {
GNU_ATTRIBUTE_NOINLINE GNU_ATTRIBUTE_NOINLINE
static TypePtr resolve_identifiers_in_type_data(TypePtr type_data, const GenericsDeclaration* genericTs) { static TypePtr resolve_identifiers_in_type_data(FunctionPtr cur_f, TypePtr type_data, const GenericsDeclaration* genericTs) {
return type_data->replace_children_custom([genericTs](TypePtr child) { return type_data->replace_children_custom([cur_f, genericTs](TypePtr child) {
if (const TypeDataUnresolved* un = child->try_as<TypeDataUnresolved>()) { if (const TypeDataUnresolved* un = child->try_as<TypeDataUnresolved>()) {
if (genericTs && genericTs->has_nameT(un->text)) { if (genericTs && genericTs->has_nameT(un->text)) {
std::string nameT = un->text; std::string nameT = un->text;
return TypeDataGenericT::create(std::move(nameT)); return TypeDataGenericT::create(std::move(nameT));
} }
if (un->text == "auto") { if (un->text == "auto") {
throw ParseError(un->loc, "`auto` type does not exist; just omit a type for local variable (will be inferred from assignment); parameters should always be typed"); throw ParseError(cur_f, un->loc, "`auto` type does not exist; just omit a type for local variable (will be inferred from assignment); parameters should always be typed");
} }
if (un->text == "self") { if (un->text == "self") {
throw ParseError(un->loc, "`self` type can be used only as a return type of a function (enforcing it to be chainable)"); throw ParseError(cur_f, un->loc, "`self` type can be used only as a return type of a function (enforcing it to be chainable)");
} }
fire_error_unknown_type_name(un->loc, un->text); fire_error_unknown_type_name(cur_f, un->loc, un->text);
} }
return child; return child;
}); });
} }
}; };
static TypePtr finalize_type_data(TypePtr type_data, const GenericsDeclaration* genericTs) { static TypePtr finalize_type_data(FunctionPtr cur_f, TypePtr type_data, const GenericsDeclaration* genericTs) {
if (!type_data || !type_data->has_unresolved_inside()) { if (!type_data || !type_data->has_unresolved_inside()) {
return type_data; return type_data;
} }
return TypeDataResolver::resolve_identifiers_in_type_data(type_data, genericTs); return TypeDataResolver::resolve_identifiers_in_type_data(cur_f, type_data, genericTs);
} }
class AssignSymInsideFunctionVisitor final : public ASTVisitorFunctionBody { class AssignSymInsideFunctionVisitor final : public ASTVisitorFunctionBody {
// more correctly this field shouldn't be static, but currently there is no need to make it a part of state // more correctly this field shouldn't be static, but currently there is no need to make it a part of state
static NameAndScopeResolver current_scope; static NameAndScopeResolver current_scope;
static FunctionPtr current_function; static FunctionPtr cur_f;
static const GenericsDeclaration* current_genericTs;
static LocalVarPtr create_local_var_sym(std::string_view name, SrcLocation loc, TypePtr declared_type, bool immutable) { static LocalVarPtr create_local_var_sym(std::string_view name, SrcLocation loc, TypePtr declared_type, bool immutable) {
LocalVarData* v_sym = new LocalVarData(static_cast<std::string>(name), loc, declared_type, immutable * LocalVarData::flagImmutable, -1); LocalVarData* v_sym = new LocalVarData(static_cast<std::string>(name), loc, declared_type, immutable * LocalVarData::flagImmutable, -1);
@ -188,15 +189,15 @@ protected:
if (v->marked_as_redef) { if (v->marked_as_redef) {
const Symbol* sym = current_scope.lookup_symbol(v->get_name()); const Symbol* sym = current_scope.lookup_symbol(v->get_name());
if (sym == nullptr) { if (sym == nullptr) {
v->error("`redef` for unknown variable"); throw ParseError(cur_f, v->loc, "`redef` for unknown variable");
} }
LocalVarPtr var_ref = sym->try_as<LocalVarPtr>(); LocalVarPtr var_ref = sym->try_as<LocalVarPtr>();
if (!var_ref) { if (!var_ref) {
v->error("`redef` for unknown variable"); throw ParseError(cur_f, v->loc, "`redef` for unknown variable");
} }
v->mutate()->assign_var_ref(var_ref); v->mutate()->assign_var_ref(var_ref);
} else { } else {
TypePtr declared_type = finalize_type_data(v->declared_type, current_function->genericTs); TypePtr declared_type = finalize_type_data(cur_f, v->declared_type, current_genericTs);
LocalVarPtr var_ref = create_local_var_sym(v->get_name(), v->loc, declared_type, v->is_immutable); LocalVarPtr var_ref = create_local_var_sym(v->get_name(), v->loc, declared_type, v->is_immutable);
v->mutate()->assign_resolved_type(declared_type); v->mutate()->assign_resolved_type(declared_type);
v->mutate()->assign_var_ref(var_ref); v->mutate()->assign_var_ref(var_ref);
@ -211,20 +212,20 @@ protected:
void visit(V<ast_reference> v) override { void visit(V<ast_reference> v) override {
const Symbol* sym = current_scope.lookup_symbol(v->get_name()); const Symbol* sym = current_scope.lookup_symbol(v->get_name());
if (!sym) { if (!sym) {
fire_error_undefined_symbol(v->get_identifier()); fire_error_undefined_symbol(cur_f, v->get_identifier());
} }
v->mutate()->assign_sym(sym); v->mutate()->assign_sym(sym);
// for global functions, global vars and constants, `import` must exist // for global functions, global vars and constants, `import` must exist
if (!sym->try_as<LocalVarPtr>()) { if (!sym->try_as<LocalVarPtr>()) {
check_import_exists_when_using_sym(v, sym); check_import_exists_when_using_sym(cur_f, v, sym);
} }
// for `f<int, MyAlias>` / `f<T>`, resolve "MyAlias" and "T" // for `f<int, MyAlias>` / `f<T>`, resolve "MyAlias" and "T"
// (for function call `f<T>()`, this v (ast_reference `f<T>`) is callee) // (for function call `f<T>()`, this v (ast_reference `f<T>`) is callee)
if (auto v_instantiationTs = v->get_instantiationTs()) { if (auto v_instantiationTs = v->get_instantiationTs()) {
for (int i = 0; i < v_instantiationTs->size(); ++i) { for (int i = 0; i < v_instantiationTs->size(); ++i) {
TypePtr substituted_type = finalize_type_data(v_instantiationTs->get_item(i)->substituted_type, current_function->genericTs); TypePtr substituted_type = finalize_type_data(cur_f, v_instantiationTs->get_item(i)->substituted_type, current_genericTs);
v_instantiationTs->get_item(i)->mutate()->assign_resolved_type(substituted_type); v_instantiationTs->get_item(i)->mutate()->assign_resolved_type(substituted_type);
} }
} }
@ -235,7 +236,7 @@ protected:
// (for function call `t.tupleAt<MyAlias>()`, this v (ast_dot_access `t.tupleAt<MyAlias>`) is callee) // (for function call `t.tupleAt<MyAlias>()`, this v (ast_dot_access `t.tupleAt<MyAlias>`) is callee)
if (auto v_instantiationTs = v->get_instantiationTs()) { if (auto v_instantiationTs = v->get_instantiationTs()) {
for (int i = 0; i < v_instantiationTs->size(); ++i) { for (int i = 0; i < v_instantiationTs->size(); ++i) {
TypePtr substituted_type = finalize_type_data(v_instantiationTs->get_item(i)->substituted_type, current_function->genericTs); TypePtr substituted_type = finalize_type_data(cur_f, v_instantiationTs->get_item(i)->substituted_type, current_genericTs);
v_instantiationTs->get_item(i)->mutate()->assign_resolved_type(substituted_type); v_instantiationTs->get_item(i)->mutate()->assign_resolved_type(substituted_type);
} }
} }
@ -243,7 +244,7 @@ protected:
} }
void visit(V<ast_cast_as_operator> v) override { void visit(V<ast_cast_as_operator> v) override {
TypePtr cast_to_type = finalize_type_data(v->cast_to_type, current_function->genericTs); TypePtr cast_to_type = finalize_type_data(cur_f, v->cast_to_type, current_genericTs);
v->mutate()->assign_resolved_type(cast_to_type); v->mutate()->assign_resolved_type(cast_to_type);
parent::visit(v->get_expr()); parent::visit(v->get_expr());
} }
@ -284,16 +285,17 @@ public:
} }
void start_visiting_function(FunctionPtr fun_ref, V<ast_function_declaration> v) override { void start_visiting_function(FunctionPtr fun_ref, V<ast_function_declaration> v) override {
current_function = fun_ref; cur_f = fun_ref;
current_genericTs = fun_ref->genericTs;
for (int i = 0; i < v->get_num_params(); ++i) { for (int i = 0; i < v->get_num_params(); ++i) {
const LocalVarData& param_var = fun_ref->parameters[i]; const LocalVarData& param_var = fun_ref->parameters[i];
TypePtr declared_type = finalize_type_data(param_var.declared_type, fun_ref->genericTs); TypePtr declared_type = finalize_type_data(cur_f, param_var.declared_type, fun_ref->genericTs);
v->get_param(i)->mutate()->assign_param_ref(&param_var); v->get_param(i)->mutate()->assign_param_ref(&param_var);
v->get_param(i)->mutate()->assign_resolved_type(declared_type); v->get_param(i)->mutate()->assign_resolved_type(declared_type);
param_var.mutate()->assign_resolved_type(declared_type); param_var.mutate()->assign_resolved_type(declared_type);
} }
TypePtr return_type = finalize_type_data(fun_ref->declared_return_type, fun_ref->genericTs); TypePtr return_type = finalize_type_data(cur_f, fun_ref->declared_return_type, fun_ref->genericTs);
v->mutate()->assign_resolved_type(return_type); v->mutate()->assign_resolved_type(return_type);
fun_ref->mutate()->assign_resolved_type(return_type); fun_ref->mutate()->assign_resolved_type(return_type);
@ -308,12 +310,14 @@ public:
tolk_assert(current_scope.scopes.empty()); tolk_assert(current_scope.scopes.empty());
} }
current_function = nullptr; current_genericTs = nullptr;
cur_f = nullptr;
} }
}; };
NameAndScopeResolver AssignSymInsideFunctionVisitor::current_scope; NameAndScopeResolver AssignSymInsideFunctionVisitor::current_scope;
FunctionPtr AssignSymInsideFunctionVisitor::current_function = nullptr; FunctionPtr AssignSymInsideFunctionVisitor::cur_f = nullptr;
const GenericsDeclaration* AssignSymInsideFunctionVisitor::current_genericTs = nullptr;
void pipeline_resolve_identifiers_and_assign_symbols() { void pipeline_resolve_identifiers_and_assign_symbols() {
AssignSymInsideFunctionVisitor visitor; AssignSymInsideFunctionVisitor visitor;
@ -324,14 +328,16 @@ void pipeline_resolve_identifiers_and_assign_symbols() {
visitor.start_visiting_function(v_func->fun_ref, v_func); visitor.start_visiting_function(v_func->fun_ref, v_func);
} else if (auto v_global = v->try_as<ast_global_var_declaration>()) { } else if (auto v_global = v->try_as<ast_global_var_declaration>()) {
TypePtr declared_type = finalize_type_data(v_global->var_ref->declared_type, nullptr); TypePtr declared_type = finalize_type_data(nullptr, v_global->var_ref->declared_type, nullptr);
v_global->mutate()->assign_resolved_type(declared_type); v_global->mutate()->assign_resolved_type(declared_type);
v_global->var_ref->mutate()->assign_resolved_type(declared_type); v_global->var_ref->mutate()->assign_resolved_type(declared_type);
} else if (auto v_const = v->try_as<ast_constant_declaration>(); v_const && v_const->declared_type) { } else if (auto v_const = v->try_as<ast_constant_declaration>()) {
TypePtr declared_type = finalize_type_data(v_const->const_ref->declared_type, nullptr); if (v_const->declared_type) {
v_const->mutate()->assign_resolved_type(declared_type); TypePtr declared_type = finalize_type_data(nullptr, v_const->const_ref->declared_type, nullptr);
v_const->const_ref->mutate()->assign_resolved_type(declared_type); v_const->mutate()->assign_resolved_type(declared_type);
v_const->const_ref->mutate()->assign_resolved_type(declared_type);
}
} }
} }
} }

View file

@ -35,8 +35,8 @@ void pipeline_discover_and_parse_sources(const std::string& stdlib_filename, con
void pipeline_register_global_symbols(); void pipeline_register_global_symbols();
void pipeline_resolve_identifiers_and_assign_symbols(); void pipeline_resolve_identifiers_and_assign_symbols();
void pipeline_calculate_rvalue_lvalue(); void pipeline_calculate_rvalue_lvalue();
void pipeline_detect_unreachable_statements();
void pipeline_infer_types_and_calls_and_fields(); void pipeline_infer_types_and_calls_and_fields();
void pipeline_check_inferred_types();
void pipeline_refine_lvalue_for_mutate_arguments(); void pipeline_refine_lvalue_for_mutate_arguments();
void pipeline_check_rvalue_lvalue(); void pipeline_check_rvalue_lvalue();
void pipeline_check_pure_impure_operations(); void pipeline_check_pure_impure_operations();

472
tolk/smart-casts-cfg.cpp Normal file
View file

@ -0,0 +1,472 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "smart-casts-cfg.h"
#include "ast.h"
#include "tolk.h"
/*
* This file represents internals of AST-level control flow and data flow analysis.
* Data flow is mostly used for smart casts and is calculated AT THE TIME of type inferring.
* Not before, not after, but simultaneously with type inferring, because any local variable can be smart cast,
* which affects other expressions/variables types, generics instantiation, return auto-infer, etc.
* Though it's a part of type inferring, it's extracted as a separate file to keep inferring a bit clearer.
*
* Control flow is represented NOT as a "graph with edges". Instead, it's a "structured DFS" for the AST:
* 1) at every point of inferring, we have "current flow facts" (FlowContext)
* 2) when we see an `if (...)`, we create two derived contexts (by cloning current)
* 3) after `if`, finalize them at the end and unify
* 4) if we detect unreachable code, we mark that path's context as "unreachable"
* In other words, we get the effect of a CFG but in a more direct approach. That's enough for AST-level data-flow.
*
* FlowContext contains "data-flow facts that are definitely known": variables types (original or refined),
* sign state (definitely positive, definitely zero, etc.), boolean state (definitely true, definitely false).
* Each local variable is contained there, and possibly sub-fields of tensors/objects if definitely known:
* // current facts: x is int?, t is (int, int)
* if (x != null && t.0 > 0)
* // current facts: x is int, t is (int, int), t.0 is positive
* else
* // current facts: x is null, t is (int, int), t.0 is not positive
* When branches rejoin, facts are merged back (int+null = int? and so on, here they would be equal to before if).
* Another example:
* // current facts: x is int?
* if (x == null) {
* // current facts: x is null
* x = 1;
* // current facts: x is int
* } // else branch is empty, its facts are: x is int
* // current facts (after rejoin): x is int
*
* Every expression analysis result (performed along with type inferring) returns ExprFlow:
* 1) out_flow: facts after evaluating the whole expression, no matter how it evaluates (true or false)
* 2) true_flow: the environment if expression is definitely true
* 3) false_flow: the environment if expression is definitely false
*
* Note, that globals are NOT analyzed (smart casts work for locals only). The explanation is simple:
* don't encourage to use a global twice, it costs gas, better assign it to a local.
* See SinkExpression.
*
* An important highlight about internal structure of tensors / tuples / objects and `t.1` sink expressions.
* When a tensor/object is assigned, its fields are NOT tracked individually.
* For better understanding, I'll give some examples in TypeScript (having the same behavior):
* interface User { id: number | string, ... }
* var u: User = { id: 123, ... }
* u.id // it's number|string, not number
* u = { id: 'asdf', ... }
* u.id // it's number|string, not string
* if (typeof u.id === 'string') {
* // here `u.id` is string (smart cast)
* }
* u.id = 123;
* u.id // now it's number (smart cast) (until `u.id` or `u` are reassigned)
* // but `u` still has type `{ id: number | string, ... }`, not `{ id: number, ... }`; only `u.id` is refined
* The same example, but with nullable tensor in Tolk:
* var t: (int?, ...) = (123, ...)
* t.0 // it's int?, not int
* t = (null, ...)
* t.0 // it's int?, not null
* if (t.0 == null) {
* // here `t.0` is null (smart cast)
* }
* t.0 = 123;
* t.0 // now it's int (smart cast) (until `t.0` or `t` are reassigned)
* // but `t` still has type `(int?, ...)`, not `(int, ...)`; only `t.0` is refined
*
* In the future, not only smart casts, but other data-flow analysis can be implemented.
* 1) detect signs: `if (x > 0) { ... if (x < 0)` to warn always false
* 2) detect always true/false: `if (x) { return; } ... if (!x)` to warn always true
* These potential improvements are SignState and BoolState. Now they are NOT IMPLEMENTED, though declared.
* Their purpose is to show, that data flow is not only about smart casts, but eventually for other facts also.
* (though it's not obvious whether they should be analyzed at AST level or at IR level, like constants now)
*/
namespace tolk {
std::string SinkExpression::to_string() const {
std::string result = var_ref->name;
uint64_t cur_path = index_path;
while (cur_path != 0) {
result += ".";
result += std::to_string((cur_path & 0xFF) - 1);
cur_path >>= 8;
}
return result;
}
static std::string to_string(SignState s) {
static const char* txt[6 + 1] = {"sign=unknown", ">0", "<0", "=0", ">=0", "<=0", "sign=never"};
return txt[static_cast<int>(s)];
}
static std::string to_string(BoolState s) {
static const char* txt[4 + 1] = {"unknown", "always_true", "always_false", "bool=never"};
return txt[static_cast<int>(s)];
}
// from `expr!` get `expr`
static AnyExprV unwrap_not_null_operator(AnyExprV expr) {
while (auto v_not_null = expr->try_as<ast_not_null_operator>()) {
expr = v_not_null->get_expr();
}
return expr;
}
// "type lca" for a and b is T, so that both are assignable to T
// it's used
// 1) for auto-infer return type of the function if not specified
// example: `fun f(x: int?) { ... return 1; ... return x; }`; lca(`int`,`int?`) = `int?`
// 2) for auto-infer type of ternary and `match` expressions
// example: `cond ? beginCell() : null`; lca(`builder`,`null`) = `builder?`
// 3) when two data flows rejoin
// example: `if (tensorVar != null) ... else ...` rejoin `(int,int)` and `null` into `(int,int)?`
// when lca can't be calculated (example: `(int,int)` and `(int,int,int)`), nullptr is returned
static TypePtr calculate_type_lca(TypePtr a, TypePtr b) {
if (a == b) {
return a;
}
if (a == TypeDataNever::create()) {
return b;
}
if (b == TypeDataNever::create()) {
return a;
}
if (a->can_rhs_be_assigned(b)) {
return a;
}
if (b->can_rhs_be_assigned(a)) {
return b;
}
if (a == TypeDataUnknown::create() || b == TypeDataUnknown::create()) {
return TypeDataUnknown::create();
}
if (a == TypeDataNullLiteral::create()) {
return TypeDataNullable::create(b);
}
if (b == TypeDataNullLiteral::create()) {
return TypeDataNullable::create(a);
}
const auto* tensor1 = a->try_as<TypeDataTensor>();
const auto* tensor2 = b->try_as<TypeDataTensor>();
if (tensor1 && tensor2 && tensor1->size() == tensor2->size()) {
std::vector<TypePtr> types_lca;
types_lca.reserve(tensor1->size());
for (int i = 0; i < tensor1->size(); ++i) {
TypePtr next = calculate_type_lca(tensor1->items[i], tensor2->items[i]);
if (next == nullptr) {
return nullptr;
}
types_lca.push_back(next);
}
return TypeDataTensor::create(std::move(types_lca));
}
const auto* tuple1 = a->try_as<TypeDataTypedTuple>();
const auto* tuple2 = b->try_as<TypeDataTypedTuple>();
if (tuple1 && tuple2 && tuple1->size() == tuple2->size()) {
std::vector<TypePtr> types_lca;
types_lca.reserve(tuple1->size());
for (int i = 0; i < tuple1->size(); ++i) {
TypePtr next = calculate_type_lca(tuple1->items[i], tuple2->items[i]);
if (next == nullptr) {
return nullptr;
}
types_lca.push_back(next);
}
return TypeDataTypedTuple::create(std::move(types_lca));
}
return nullptr;
}
// merge (unify) of two sign states: what sign do we definitely have
// it's used on data flow rejoin
// example: `if (x > 0) ... else ...`; lca(Positive, NonPositive) = Unknown
SignState calculate_sign_lca(SignState a, SignState b) {
using s = SignState;
// a transformation lookup table, using the following rules:
// 1) if one is Unknown, the result is Unknown ("no definite constraints")
// 2) if one is Never (can't happen), the result is the other
// example: x is known > 0 already, given code `if (x > 0) {} else {}` merges Positive (always true) and Never
// 3) handle all other combinations carefully
static constexpr SignState transformations[7][7] = {
// b= Unknown | Positive | Negative | Zero | NonNegative | NonPositive | Never |
/* a=Unknown */ {s::Unknown, s::Unknown, s::Unknown, s::Unknown, s::Unknown, s::Unknown, s::Unknown },
/* a=Positive */ {s::Unknown, s::Positive, s::Unknown, s::NonNegative, s::NonNegative, s::Unknown, s::Positive },
/* a=Negative */ {s::Unknown, s::Unknown, s::Negative, s::NonPositive, s::Unknown, s::NonPositive, s::Negative },
/* a=Zero */ {s::Unknown, s::NonNegative, s::NonPositive, s::Zero, s::NonNegative, s::NonPositive, s::Zero },
/* a=NonNegative */ {s::Unknown, s::NonNegative, s::Unknown, s::NonNegative, s::NonNegative, s::Unknown, s::NonNegative},
/* a=NonPositive */ {s::Unknown, s::Unknown, s::NonPositive, s::NonPositive, s::Unknown, s::NonPositive, s::NonPositive},
/* a=Never */ {s::Unknown, s::Positive, s::Negative, s::Zero, s::NonNegative, s::NonPositive, s::Never }
};
return transformations[static_cast<int>(a)][static_cast<int>(b)];
}
// merge (unify) two bool state: what state do we definitely have
// it's used on data flow rejoin
// example: `if (x) ... else ...`; lca(AlwaysTrue, AlwaysFalse) = Unknown
BoolState calculate_bool_lca(BoolState a, BoolState b) {
using s = BoolState;
static constexpr BoolState transformations[4][4] = {
// b= Unknown | AlwaysTrue | AlwaysFalse | Never |
/* a=Unknown */ {s::Unknown, s::Unknown, s::Unknown, s::Unknown },
/* a=AlwaysTrue */ {s::Unknown, s::AlwaysTrue, s::Unknown, s::AlwaysTrue },
/* a=AlwaysFalse */ {s::Unknown, s::Unknown, s::AlwaysFalse, s::AlwaysFalse},
/* a=Never */ {s::Unknown, s::AlwaysTrue, s::AlwaysFalse, s::Never }
};
return transformations[static_cast<int>(a)][static_cast<int>(b)];
}
// see comments above TypeInferringUnifyStrategy
// this function calculates lca or currently stored result and next
bool TypeInferringUnifyStrategy::unify_with(TypePtr next) {
if (unified_result == nullptr) {
unified_result = next;
return true;
}
if (unified_result == next) {
return true;
}
TypePtr combined = calculate_type_lca(unified_result, next);
if (!combined) {
return false;
}
unified_result = combined;
return true;
}
bool TypeInferringUnifyStrategy::unify_with_implicit_return_void() {
if (unified_result == nullptr) {
unified_result = TypeDataVoid::create();
return true;
}
return unified_result == TypeDataVoid::create();
}
// invalidate knowledge about sub-fields of a variable or its field
// example: `tensorVar = 2`, invalidate facts about `tensorVar`, `tensorVar.0`, `tensorVar.1.2`, and all others
// example: `user.id = rhs`, invalidate facts about `user.id` (sign, etc.) and `user.id.*` if exist
void FlowContext::invalidate_all_subfields(LocalVarPtr var_ref, uint64_t parent_path, uint64_t parent_mask) {
for (auto it = known_facts.begin(); it != known_facts.end();) {
bool is_self_or_field = it->first.var_ref == var_ref && (it->first.index_path & parent_mask) == parent_path;
if (is_self_or_field) {
it = known_facts.erase(it);
} else {
++it;
}
}
}
// update current type of `local_var` / `tensorVar.0` / `obj.field`
// example: `local_var = rhs`
// example: `f(mutate obj.field)`
// example: `if (t.0 != null)`, in true_flow `t.0` assigned to "not-null of current", in false_flow to null
void FlowContext::register_known_type(SinkExpression s_expr, TypePtr assigned_type) {
// having index_path = (some bytes filled in the end),
// calc index_mask: replace every filled byte with 0xFF
// example: `t.0.1`, index_path = (1<<8) + 2, index_mask = 0xFFFF
uint64_t index_path = s_expr.index_path;
uint64_t index_mask = 0;
while (index_path > 0) {
index_mask = index_mask << 8 | 0xFF;
index_path >>= 8;
}
invalidate_all_subfields(s_expr.var_ref, s_expr.index_path, index_mask);
// if just `int` assigned, we have no considerations about its sign
// so, even if something existed by the key s_expr, drop all knowledge
known_facts[s_expr] = FactsAboutExpr(assigned_type, SignState::Unknown, BoolState::Unknown);
}
// mark control flow unreachable / interrupted
void FlowContext::mark_unreachable(UnreachableKind reason) {
unreachable = true;
// currently we don't save why control flow became unreachable (it's not obvious how, there may be consequent reasons),
// but it helps debugging and reading outer code
static_cast<void>(reason);
}
// "merge" two data-flow contexts occurs on control flow rejoins (if/else branches merging, for example)
// it's generating a new context that describes "knowledge that definitely outcomes from these two"
// example: in one branch x is `int`, in x is `null`, result is `int?` unless any of them is unreachable
FlowContext FlowContext::merge_flow(FlowContext&& c1, FlowContext&& c2) {
if (!c1.unreachable && c2.unreachable) {
return merge_flow(std::move(c2), std::move(c1));
}
std::map<SinkExpression, FactsAboutExpr> unified;
if (c1.unreachable && !c2.unreachable) {
// `if (...) return; else ...;` — copy facts about common variables only from else (c2)
for (const auto& [s_expr, i2] : c2.known_facts) {
auto it1 = c1.known_facts.find(s_expr);
bool need_add = it1 != c1.known_facts.end() || s_expr.index_path != 0;
if (need_add) {
unified.emplace(s_expr, i2);
}
}
} else {
// either both reachable, or both not — merge types and restrictions of common variables and fields
for (const auto& [s_expr, i1] : c1.known_facts) {
if (auto it2 = c2.known_facts.find(s_expr); it2 != c2.known_facts.end()) {
const FactsAboutExpr& i2 = it2->second;
unified.emplace(s_expr, i1 == i2 ? i1 : FactsAboutExpr(
calculate_type_lca(i1.expr_type, i2.expr_type),
calculate_sign_lca(i1.sign_state, i2.sign_state),
calculate_bool_lca(i1.bool_state, i2.bool_state)
));
}
}
}
return FlowContext(std::move(unified), c1.unreachable && c2.unreachable);
}
// return `T`, so that `T?` = type
// what for: `if (x != null)`, to smart cast x inside if
TypePtr calculate_type_subtract_null(TypePtr type) {
if (const auto* as_nullable = type->try_as<TypeDataNullable>()) {
return as_nullable->inner;
}
// union types will be handled here
return TypeDataNever::create();
}
// given any expression vertex, extract SinkExpression is possible
// example: `x.0` is { var_ref: x, index_path: 1 }
// example: `x.1` is { var_ref: x, index_path: 2 }
// example: `x!.1` is the same
// example: `x.1.2` is { var_ref: x, index_path: 2<<8 + 3 }
// example: `x!.1!.2` is the same
// not SinkExpressions: `globalVar` / `f()` / `obj.method().1`
SinkExpression extract_sink_expression_from_vertex(AnyExprV v) {
if (auto as_ref = v->try_as<ast_reference>()) {
if (LocalVarPtr var_ref = as_ref->sym->try_as<LocalVarPtr>()) {
return SinkExpression(var_ref);
}
}
if (auto as_dot = v->try_as<ast_dot_access>(); as_dot && as_dot->is_target_indexed_access()) {
V<ast_dot_access> cur_dot = as_dot;
uint64_t index_path = 0;
while (cur_dot->is_target_indexed_access()) {
int index_at = std::get<int>(cur_dot->target);
index_path = (index_path << 8) + index_at + 1;
if (auto parent_dot = unwrap_not_null_operator(cur_dot->get_obj())->try_as<ast_dot_access>()) {
cur_dot = parent_dot;
} else {
break;
}
}
if (auto as_ref = unwrap_not_null_operator(cur_dot->get_obj())->try_as<ast_reference>()) {
if (LocalVarPtr var_ref = as_ref->sym->try_as<LocalVarPtr>()) {
return SinkExpression(var_ref, index_path);
}
}
}
if (auto as_par = v->try_as<ast_parenthesized_expression>()) {
return extract_sink_expression_from_vertex(as_par->get_expr());
}
if (auto as_assign = v->try_as<ast_assign>()) {
return extract_sink_expression_from_vertex(as_assign->get_lhs());
}
return {};
}
// given `lhs = rhs`, calculate "original" type of `lhs`
// example: `var x: int? = ...; if (x != null) { x (here) = null; }`
// "(here)" x is `int` (smart cast), but originally declared as `int?`
// example: `if (x is (int,int)?) { x!.0 = rhs }`, here `x!.0` is `int`
TypePtr calc_declared_type_before_smart_cast(AnyExprV v) {
if (auto as_ref = v->try_as<ast_reference>()) {
if (LocalVarPtr var_ref = as_ref->sym->try_as<LocalVarPtr>()) {
return var_ref->declared_type;
}
}
if (auto as_dot = v->try_as<ast_dot_access>(); as_dot && as_dot->is_target_indexed_access()) {
TypePtr obj_type = as_dot->get_obj()->inferred_type; // v already inferred; hence, index_at is correct
int index_at = std::get<int>(as_dot->target);
if (const auto* t_tensor = obj_type->try_as<TypeDataTensor>()) {
return t_tensor->items[index_at];
}
if (const auto* t_tuple = obj_type->try_as<TypeDataTypedTuple>()) {
return t_tuple->items[index_at];
}
}
return v->inferred_type;
}
// given `lhs = rhs` (and `var x = rhs`), calculate probable smart cast for lhs
// it's NOT directly type of rhs! see comment at the top of the file about internal structure of tensors/tuples.
// obvious example: `var x: int? = 5`, it's `int` (most cases are like this)
// obvious example: `var x: (int,int)? = null`, it's `null` (`x == null` is always true, `x` can be passed to any `T?`)
// not obvious example: `var x: (int?, int?)? = (3,null)`, result is `(int?,int?)`, whereas type of rhs is `(int,null)`
TypePtr calc_smart_cast_type_on_assignment(TypePtr lhs_declared_type, TypePtr rhs_inferred_type) {
// assign `T` to `T?` (or at least "assignable-to-T" to "T?")
// smart cast to `T`
if (const auto* lhs_nullable = lhs_declared_type->try_as<TypeDataNullable>()) {
if (lhs_nullable->inner->can_rhs_be_assigned(rhs_inferred_type)) {
return lhs_nullable->inner;
}
}
// assign `null` to `T?`
// smart cast to `null`
if (lhs_declared_type->try_as<TypeDataNullable>() && rhs_inferred_type == TypeDataNullLiteral::create()) {
return TypeDataNullLiteral::create();
}
// no smart cast, type is the same as declared
// example: `var x: (int?,slice?) = (1, null)`, it's `(int?,slice?)`, not `(int,null)`
return lhs_declared_type;
}
std::ostream& operator<<(std::ostream& os, const FlowContext& flow) {
os << "(" << flow.known_facts.size() << " facts) " << (flow.unreachable ? "(unreachable) " : "");
for (const auto& [s_expr, facts] : flow.known_facts) {
os << ", " << s_expr.to_string() << ": " << facts;
}
return os;
}
std::ostream& operator<<(std::ostream& os, const FactsAboutExpr& facts) {
os << facts.expr_type;
if (facts.sign_state != SignState::Unknown) {
os << " " << to_string(facts.sign_state);
}
if (facts.bool_state != BoolState::Unknown) {
os << " " << to_string(facts.bool_state);
}
return os;
}
} // namespace tolk

207
tolk/smart-casts-cfg.h Normal file
View file

@ -0,0 +1,207 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "fwd-declarations.h"
#include "type-system.h"
#include <map>
#include <vector>
namespace tolk {
/*
* TypeInferringUnifyStrategy unifies types from various branches to a common result (lca).
* It's used to auto infer function return type based on return statements, like in TypeScript.
* Example: `fun f() { ... return 1; ... return null; }` inferred as `int?`.
*
* Besides function returns, it's also used for ternary `return cond ? 1 : null` and `match` expression.
* If types can't be unified (a function returns int and cell, for example), `unify()` returns false, handled outside.
* BTW, don't confuse this way of inferring with Hindley-Milner, they have nothing in common.
*/
class TypeInferringUnifyStrategy {
TypePtr unified_result = nullptr;
public:
bool unify_with(TypePtr next);
bool unify_with_implicit_return_void();
TypePtr get_result() const { return unified_result; }
};
/*
* SinkExpression is an expression that can be smart cast like `if (x != null)` (x is int inside)
* or analyzed by data flow is some other way like `if (x > 0) ... else ...` (x <= 0 inside else).
* In other words, it "absorbs" data flow facts.
* Examples: `localVar`, `localTensor.1`, `localTuple.1.2.3`, `localObj.field`
* These are NOT sink expressions: `globalVar`, `f()`, `f().1`
* Note, that globals are NOT sink: don't encourage to use a global twice, it costs gas, better assign it to a local.
*/
struct SinkExpression {
LocalVarPtr const var_ref; // smart casts and data flow applies only to locals
const uint64_t index_path; // 0 for just `v`; for `v.N` it's (N+1), for `v.N.M` it's (N+1) + (M+1)<<8, etc.
SinkExpression()
: var_ref(nullptr), index_path(0) {}
explicit SinkExpression(LocalVarPtr var_ref)
: var_ref(var_ref), index_path(0) {}
explicit SinkExpression(LocalVarPtr var_ref, uint64_t index_path)
: var_ref(var_ref), index_path(index_path) {}
SinkExpression(const SinkExpression&) = default;
SinkExpression& operator=(const SinkExpression&) = delete;
bool operator==(const SinkExpression& rhs) const { return var_ref == rhs.var_ref && index_path == rhs.index_path; }
bool operator<(const SinkExpression& rhs) const { return var_ref == rhs.var_ref ? index_path < rhs.index_path : var_ref < rhs.var_ref; }
explicit operator bool() const { return var_ref != nullptr; }
std::string to_string() const;
};
// UnreachableKind is a reason of why control flow is unreachable or interrupted
// example: `return;` interrupts control flow
// example: `if (true) ... else ...` inside "else" flow is unreachable because it can't happen
enum class UnreachableKind {
Unknown, // no definite info or not unreachable
CantHappen,
ReturnStatement,
CallNeverReturnFunction,
};
// SignState is "definitely positive", etc.
// example: inside `if (x > 0)`, x is Positive, in `else` it's NonPositive (if x is local, until reassigned)
enum class SignState {
Unknown, // no definite info
Positive,
Negative,
Zero,
NonNegative,
NonPositive,
Never // can't happen, like "never" type
};
// BoolState is "definitely true" or "definitely false"
// example: inside `if (x)`, x is AlwaysTrue, in `else` it's AlwaysFalse
enum class BoolState {
Unknown, // no definite info
AlwaysTrue,
AlwaysFalse,
Never // can't happen, like "never" type
};
// FactsAboutExpr represents "everything known about SinkExpression at a given execution point"
// example: after `var x = getNullableInt()`, x is `int?`, sign/bool is Unknown
// example: after `x = 2;`, x is `int`, sign is Positive, bool is AlwaysTrue
// example: inside `if (x != null && x > 0)`, x is `int`, sign is Positive (in else, no definite knowledge)
// remember, that indices/fields are also expressions, `t.1 = 2` or `u.id = 2` also store such facts
// WARNING! Detecting data-flow facts about sign state and bool state is NOT IMPLEMENTED
// (e.g. `if (x > 0)` / `if (!t.1)` is NOT analysed, therefore not updated, always Unknown now)
// it's a potential improvement for the future, for example `if (x > 0) { ... if (x < 0)` to warn always false
// their purpose for now is to show, that data flow is not only about smart casts, but eventually for other facts also
struct FactsAboutExpr {
TypePtr expr_type; // originally declared type or smart cast (Unknown if no info)
SignState sign_state; // definitely positive, etc. (Unknown if no info)
BoolState bool_state; // definitely true/false (Unknown if no info)
FactsAboutExpr()
: expr_type(nullptr), sign_state(SignState::Unknown), bool_state(BoolState::Unknown) {}
FactsAboutExpr(TypePtr smart_cast_type, SignState sign_state, BoolState bool_state)
: expr_type(smart_cast_type), sign_state(sign_state), bool_state(bool_state) {}
bool operator==(const FactsAboutExpr& rhs) const = default;
};
// FlowContext represents "everything known about control flow at a given execution point"
// while traversing AST, each statement node gets "in" FlowContext (prior knowledge)
// and returns "output" FlowContext (representing a state AFTER execution of a statement)
// on branching, like if/else, input context is cloned, two contexts for each branch calculated, and merged to a result
class FlowContext {
// std::map, not std::unordered_map, because LLDB visualises it better, for debugging
std::map<SinkExpression, FactsAboutExpr> known_facts; // all local vars plus (optionally) indices/fields of tensors/tuples/objects
bool unreachable = false; // if execution can't reach this point (after `return`, for example)
FlowContext(std::map<SinkExpression, FactsAboutExpr>&& known_facts, bool unreachable)
: known_facts(std::move(known_facts)), unreachable(unreachable) {}
void invalidate_all_subfields(LocalVarPtr var_ref, uint64_t parent_path, uint64_t parent_mask);
friend std::ostream& operator<<(std::ostream& os, const FlowContext& flow);
public:
FlowContext() = default;
FlowContext(FlowContext&&) noexcept = default;
FlowContext(const FlowContext&) = delete;
FlowContext& operator=(FlowContext&&) = default;
FlowContext& operator=(const FlowContext&) = delete;
FlowContext clone() const {
std::map<SinkExpression, FactsAboutExpr> copy = known_facts;
return FlowContext(std::move(copy), unreachable);
}
bool is_unreachable() const { return unreachable; }
TypePtr smart_cast_if_exists(SinkExpression s_expr) const {
auto it = known_facts.find(s_expr);
return it == known_facts.end() ? nullptr : it->second.expr_type;
}
void register_known_type(SinkExpression s_expr, TypePtr assigned_type);
void mark_unreachable(UnreachableKind reason);
static FlowContext merge_flow(FlowContext&& c1, FlowContext&& c2);
};
struct ExprFlow {
FlowContext out_flow;
// only calculated inside `if`, left of `&&`, etc. — there this expression is immediate condition, empty otherwise
FlowContext true_flow;
FlowContext false_flow;
ExprFlow(FlowContext&& out_flow, FlowContext&& true_flow, FlowContext&& false_flow)
: out_flow(std::move(out_flow))
, true_flow(std::move(true_flow))
, false_flow(std::move(false_flow)) {}
ExprFlow(FlowContext&& out_flow, const bool clone_flow_for_condition)
: out_flow(std::move(out_flow)) {
if (clone_flow_for_condition) {
true_flow = this->out_flow.clone();
false_flow = this->out_flow.clone();
}
}
ExprFlow(ExprFlow&&) noexcept = default;
ExprFlow(const ExprFlow&) = delete;
ExprFlow& operator=(ExprFlow&&) = delete;
ExprFlow& operator=(const ExprFlow&) = delete;
int get_always_true_false_state() const {
if (true_flow.is_unreachable() != false_flow.is_unreachable()) {
return false_flow.is_unreachable() ? 1 : 2; // 1 is "always true"
}
return 0;
}
};
std::ostream& operator<<(std::ostream& os, const FactsAboutExpr& facts);
std::ostream& operator<<(std::ostream& os, const FlowContext& flow);
TypePtr calculate_type_subtract_null(TypePtr type);
SinkExpression extract_sink_expression_from_vertex(AnyExprV v);
TypePtr calc_declared_type_before_smart_cast(AnyExprV v);
TypePtr calc_smart_cast_type_on_assignment(TypePtr lhs_declared_type, TypePtr rhs_inferred_type);
} // namespace tolk

View file

@ -18,6 +18,7 @@
#include "compiler-state.h" #include "compiler-state.h"
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <iomanip>
namespace tolk { namespace tolk {
@ -146,9 +147,10 @@ void SrcLocation::show_context(std::ostream& os) const {
return; return;
} }
SrcFile::SrcPosition pos = src_file->convert_offset(char_offset); SrcFile::SrcPosition pos = src_file->convert_offset(char_offset);
os << " " << pos.line_str << "\n"; os << std::right << std::setw(4) << pos.line_no << " | ";
os << pos.line_str << "\n";
os << " "; os << " " << " | ";
for (int i = 1; i < pos.char_no; ++i) { for (int i = 1; i < pos.char_no; ++i) {
os << ' '; os << ' ';
} }
@ -193,8 +195,11 @@ std::ostream& operator<<(std::ostream& os, const ParseError& error) {
} }
void ParseError::show(std::ostream& os) const { void ParseError::show(std::ostream& os) const {
os << where << ": error: " << message << std::endl; os << loc << ": error: " << message << std::endl;
where.show_context(os); if (current_function) {
os << " // in function `" << current_function->as_human_readable() << "`" << std::endl;
}
loc.show_context(os);
} }
} // namespace tolk } // namespace tolk

View file

@ -124,10 +124,14 @@ struct Fatal final : std::exception {
std::ostream& operator<<(std::ostream& os, const Fatal& fatal); std::ostream& operator<<(std::ostream& os, const Fatal& fatal);
struct ParseError : std::exception { struct ParseError : std::exception {
SrcLocation where; FunctionPtr current_function;
SrcLocation loc;
std::string message; std::string message;
ParseError(SrcLocation _where, std::string _msg) : where(_where), message(std::move(_msg)) {
} ParseError(SrcLocation loc, std::string message)
: current_function(nullptr), loc(loc), message(std::move(message)) {}
ParseError(FunctionPtr current_function, SrcLocation loc, std::string message)
: current_function(current_function), loc(loc), message(std::move(message)) {}
const char* what() const noexcept override { const char* what() const noexcept override {
return message.c_str(); return message.c_str();

View file

@ -102,9 +102,6 @@ void LocalVarData::assign_resolved_type(TypePtr declared_type) {
} }
void LocalVarData::assign_inferred_type(TypePtr inferred_type) { void LocalVarData::assign_inferred_type(TypePtr inferred_type) {
#ifdef TOLK_DEBUG
assert(this->declared_type == nullptr); // called when type declaration omitted, inferred from assigned value
#endif
this->declared_type = inferred_type; this->declared_type = inferred_type;
} }

View file

@ -58,8 +58,8 @@ int tolk_proceed(const std::string &entrypoint_filename) {
pipeline_register_global_symbols(); pipeline_register_global_symbols();
pipeline_resolve_identifiers_and_assign_symbols(); pipeline_resolve_identifiers_and_assign_symbols();
pipeline_calculate_rvalue_lvalue(); pipeline_calculate_rvalue_lvalue();
pipeline_detect_unreachable_statements();
pipeline_infer_types_and_calls_and_fields(); pipeline_infer_types_and_calls_and_fields();
pipeline_check_inferred_types();
pipeline_refine_lvalue_for_mutate_arguments(); pipeline_refine_lvalue_for_mutate_arguments();
pipeline_check_rvalue_lvalue(); pipeline_check_rvalue_lvalue();
pipeline_check_pure_impure_operations(); pipeline_check_pure_impure_operations();

View file

@ -84,6 +84,7 @@ TypePtr TypeDataTuple::singleton;
TypePtr TypeDataContinuation::singleton; TypePtr TypeDataContinuation::singleton;
TypePtr TypeDataNullLiteral::singleton; TypePtr TypeDataNullLiteral::singleton;
TypePtr TypeDataUnknown::singleton; TypePtr TypeDataUnknown::singleton;
TypePtr TypeDataNever::singleton;
TypePtr TypeDataVoid::singleton; TypePtr TypeDataVoid::singleton;
void type_system_init() { void type_system_init() {
@ -96,6 +97,7 @@ void type_system_init() {
TypeDataContinuation::singleton = new TypeDataContinuation; TypeDataContinuation::singleton = new TypeDataContinuation;
TypeDataNullLiteral::singleton = new TypeDataNullLiteral; TypeDataNullLiteral::singleton = new TypeDataNullLiteral;
TypeDataUnknown::singleton = new TypeDataUnknown; TypeDataUnknown::singleton = new TypeDataUnknown;
TypeDataNever::singleton = new TypeDataNever;
TypeDataVoid::singleton = new TypeDataVoid; TypeDataVoid::singleton = new TypeDataVoid;
} }
@ -325,53 +327,56 @@ bool TypeDataInt::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataBool::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataBool::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataCell::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataCell::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataSlice::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataSlice::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataBuilder::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataBuilder::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataTuple::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataTuple::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataContinuation::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataContinuation::can_rhs_be_assigned(TypePtr rhs) const {
if (rhs == this) { if (rhs == this) {
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataNullLiteral::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataNullLiteral::can_rhs_be_assigned(TypePtr rhs) const {
return rhs == this; if (rhs == this) {
return true;
}
return rhs == TypeDataNever::create();
} }
bool TypeDataNullable::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataNullable::can_rhs_be_assigned(TypePtr rhs) const {
@ -384,11 +389,17 @@ bool TypeDataNullable::can_rhs_be_assigned(TypePtr rhs) const {
if (const TypeDataNullable* rhs_nullable = rhs->try_as<TypeDataNullable>()) { 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_nullable->inner);
} }
return inner->can_rhs_be_assigned(rhs); if (inner->can_rhs_be_assigned(rhs)) {
return true;
}
return rhs == TypeDataNever::create();
} }
bool TypeDataFunCallable::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataFunCallable::can_rhs_be_assigned(TypePtr rhs) const {
return rhs == this; if (rhs == this) {
return true;
}
return rhs == TypeDataNever::create();
} }
bool TypeDataGenericT::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataGenericT::can_rhs_be_assigned(TypePtr rhs) const {
@ -405,7 +416,7 @@ bool TypeDataTensor::can_rhs_be_assigned(TypePtr rhs) const {
} }
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataTypedTuple::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataTypedTuple::can_rhs_be_assigned(TypePtr rhs) const {
@ -417,7 +428,7 @@ bool TypeDataTypedTuple::can_rhs_be_assigned(TypePtr rhs) const {
} }
return true; return true;
} }
return false; return rhs == TypeDataNever::create();
} }
bool TypeDataUnknown::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataUnknown::can_rhs_be_assigned(TypePtr rhs) const {
@ -429,8 +440,15 @@ bool TypeDataUnresolved::can_rhs_be_assigned(TypePtr rhs) const {
return false; return false;
} }
bool TypeDataNever::can_rhs_be_assigned(TypePtr rhs) const {
return true;
}
bool TypeDataVoid::can_rhs_be_assigned(TypePtr rhs) const { bool TypeDataVoid::can_rhs_be_assigned(TypePtr rhs) const {
return rhs == this; if (rhs == this) {
return true;
}
return rhs == TypeDataNever::create();
} }
@ -551,6 +569,10 @@ bool TypeDataUnresolved::can_be_casted_with_as_operator(TypePtr cast_to) const {
return false; return false;
} }
bool TypeDataNever::can_be_casted_with_as_operator(TypePtr cast_to) const {
return true;
}
bool TypeDataVoid::can_be_casted_with_as_operator(TypePtr cast_to) const { bool TypeDataVoid::can_be_casted_with_as_operator(TypePtr cast_to) const {
return cast_to == this; return cast_to == this;
} }
@ -584,6 +606,10 @@ bool TypeDataTensor::can_hold_tvm_null_instead() const {
return true; return true;
} }
bool TypeDataNever::can_hold_tvm_null_instead() const {
return false;
}
bool TypeDataVoid::can_hold_tvm_null_instead() const { bool TypeDataVoid::can_hold_tvm_null_instead() const {
return false; return false;
} }
@ -650,6 +676,7 @@ static TypePtr parse_simple_type(Lexer& lex) {
case 5: case 5:
if (str == "slice") return TypeDataSlice::create(); if (str == "slice") return TypeDataSlice::create();
if (str == "tuple") return TypeDataTuple::create(); if (str == "tuple") return TypeDataTuple::create();
if (str == "never") return TypeDataNever::create();
break; break;
case 7: case 7:
if (str == "builder") return TypeDataBuilder::create(); if (str == "builder") return TypeDataBuilder::create();

View file

@ -409,6 +409,27 @@ public:
bool can_be_casted_with_as_operator(TypePtr cast_to) const override; bool can_be_casted_with_as_operator(TypePtr cast_to) const override;
}; };
/*
* `never` is a special type meaning "no value can be hold".
* Is may appear due to smart casts, for example `if (x == null && x != null)` makes x "never".
* Functions returning "never" assume to never exit, calling them interrupts control flow.
* Such variables can not be cast to any other types, all their usage will trigger type mismatch errors.
*/
class TypeDataNever final : public TypeData {
TypeDataNever() : TypeData(19ULL, 0, 0) {}
static TypePtr singleton;
friend void type_system_init();
public:
static TypePtr create() { return singleton; }
std::string as_human_readable() const override { return "never"; }
bool can_rhs_be_assigned(TypePtr rhs) const override;
bool can_be_casted_with_as_operator(TypePtr cast_to) const override;
bool can_hold_tvm_null_instead() const override;
};
/* /*
* `void` is TypeDataVoid. * `void` is TypeDataVoid.
* From the type system point of view, `void` functions return nothing. * From the type system point of view, `void` functions return nothing.