Skip to content

Commit d843772

Browse files
authored
Implement non-null assertions (#443)
1 parent 2fe228f commit d843772

File tree

11 files changed

+413
-32
lines changed

11 files changed

+413
-32
lines changed

src/ast.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ export function nodeIsConstantValue(kind: NodeKind): bool {
116116
export function nodeIsCallable(kind: NodeKind): bool {
117117
switch (kind) {
118118
case NodeKind.IDENTIFIER:
119+
case NodeKind.ASSERTION: // if kind=NONNULL
119120
case NodeKind.CALL:
120121
case NodeKind.ELEMENTACCESS:
121-
case NodeKind.PROPERTYACCESS:
122-
case NodeKind.PARENTHESIZED: return true;
122+
case NodeKind.PARENTHESIZED:
123+
case NodeKind.PROPERTYACCESS: return true;
123124
}
124125
return false;
125126
}
@@ -286,14 +287,14 @@ export abstract class Node {
286287
static createAssertionExpression(
287288
assertionKind: AssertionKind,
288289
expression: Expression,
289-
toType: CommonTypeNode,
290+
toType: CommonTypeNode | null,
290291
range: Range
291292
): AssertionExpression {
292293
var expr = new AssertionExpression();
293294
expr.range = range;
294295
expr.assertionKind = assertionKind;
295296
expr.expression = expression; expression.parent = expr;
296-
expr.toType = toType; toType.parent = expr;
297+
expr.toType = toType; if (toType) toType.parent = expr;
297298
return expr;
298299
}
299300

@@ -1282,7 +1283,8 @@ export class ArrayLiteralExpression extends LiteralExpression {
12821283
/** Indicates the kind of an assertion. */
12831284
export enum AssertionKind {
12841285
PREFIX,
1285-
AS
1286+
AS,
1287+
NONNULL
12861288
}
12871289

12881290
/** Represents an assertion expression. */
@@ -1294,7 +1296,7 @@ export class AssertionExpression extends Expression {
12941296
/** Expression being asserted. */
12951297
expression: Expression;
12961298
/** Target type. */
1297-
toType: CommonTypeNode;
1299+
toType: CommonTypeNode | null;
12981300
}
12991301

13001302
/** Represents a binary expression. */

src/compiler.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import {
9191
Source,
9292
Range,
9393
DecoratorKind,
94+
AssertionKind,
9495

9596
Statement,
9697
BlockStatement,
@@ -2704,12 +2705,25 @@ export class Compiler extends DiagnosticEmitter {
27042705
}
27052706

27062707
compileAssertionExpression(expression: AssertionExpression, contextualType: Type): ExpressionRef {
2707-
var toType = this.resolver.resolveType( // reports
2708-
expression.toType,
2709-
this.currentFunction.flow.contextualTypeArguments
2710-
);
2711-
if (!toType) return this.module.createUnreachable();
2712-
return this.compileExpression(expression.expression, toType, ConversionKind.EXPLICIT, WrapMode.NONE);
2708+
switch (expression.assertionKind) {
2709+
case AssertionKind.PREFIX:
2710+
case AssertionKind.AS: {
2711+
let toType = this.resolver.resolveType( // reports
2712+
assert(expression.toType),
2713+
this.currentFunction.flow.contextualTypeArguments
2714+
);
2715+
if (!toType) return this.module.createUnreachable();
2716+
return this.compileExpression(expression.expression, toType, ConversionKind.EXPLICIT, WrapMode.NONE);
2717+
}
2718+
case AssertionKind.NONNULL: {
2719+
assert(!expression.toType);
2720+
let expr = this.compileExpressionRetainType(expression.expression, contextualType, WrapMode.NONE);
2721+
this.currentType = this.currentType.nonNullableType;
2722+
return expr;
2723+
}
2724+
default: assert(false);
2725+
}
2726+
return this.module.createUnreachable();
27132727
}
27142728

27152729
private f32ModInstance: Function | null = null;

src/extra/ast.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -477,15 +477,26 @@ export class ASTBuilder {
477477

478478
visitAssertionExpression(node: AssertionExpression): void {
479479
var sb = this.sb;
480-
if (node.assertionKind == AssertionKind.PREFIX) {
481-
sb.push("<");
482-
this.visitTypeNode(node.toType);
483-
sb.push(">");
484-
this.visitNode(node.expression);
485-
} else {
486-
this.visitNode(node.expression);
487-
sb.push(" as ");
488-
this.visitTypeNode(node.toType);
480+
switch (node.assertionKind) {
481+
case AssertionKind.PREFIX: {
482+
sb.push("<");
483+
this.visitTypeNode(assert(node.toType));
484+
sb.push(">");
485+
this.visitNode(node.expression);
486+
break;
487+
}
488+
case AssertionKind.AS: {
489+
this.visitNode(node.expression);
490+
sb.push(" as ");
491+
this.visitTypeNode(assert(node.toType));
492+
break;
493+
}
494+
case AssertionKind.NONNULL: {
495+
this.visitNode(node.expression);
496+
sb.push("!");
497+
break;
498+
}
499+
default: assert(false);
489500
}
490501
}
491502

src/parser.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3472,6 +3472,15 @@ export class Parser extends DiagnosticEmitter {
34723472
);
34733473
break;
34743474
}
3475+
case Token.EXCLAMATION: {
3476+
expr = Node.createAssertionExpression(
3477+
AssertionKind.NONNULL,
3478+
expr,
3479+
null,
3480+
tn.range(startPos, tn.pos)
3481+
);
3482+
break;
3483+
}
34753484
// InstanceOfExpression
34763485
case Token.INSTANCEOF: {
34773486
let isType = this.parseType(tn); // reports
@@ -3835,7 +3844,8 @@ function determinePrecedence(kind: Token): Precedence {
38353844
case Token.MINUS_MINUS: return Precedence.UNARY_POSTFIX;
38363845
case Token.DOT:
38373846
case Token.NEW:
3838-
case Token.OPENBRACKET: return Precedence.MEMBERACCESS;
3847+
case Token.OPENBRACKET:
3848+
case Token.EXCLAMATION: return Precedence.MEMBERACCESS;
38393849
}
38403850
return Precedence.NONE;
38413851
}

src/resolver.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import {
4848
Expression,
4949
IntegerLiteralExpression,
5050
UnaryPrefixExpression,
51-
UnaryPostfixExpression
51+
UnaryPostfixExpression,
52+
AssertionKind
5253
} from "./ast";
5354

5455
import {
@@ -685,17 +686,29 @@ export class Resolver extends DiagnosticEmitter {
685686
}
686687
switch (expression.kind) {
687688
case NodeKind.ASSERTION: {
689+
if ((<AssertionExpression>expression).assertionKind == AssertionKind.NONNULL) {
690+
return this.resolveExpression(
691+
(<AssertionExpression>expression).expression,
692+
contextualFunction,
693+
contextualType,
694+
reportMode
695+
);
696+
}
688697
let type = this.resolveType(
689-
(<AssertionExpression>expression).toType,
698+
assert((<AssertionExpression>expression).toType),
690699
contextualFunction.flow.contextualTypeArguments,
691700
reportMode
692701
);
693702
if (!type) return null;
694-
let classType = type.classReference;
695-
if (!classType) return null;
703+
let element: Element | null = type.classReference;
704+
if (!element) {
705+
let signature = type.signatureReference;
706+
if (!signature) return null;
707+
element = signature.asFunctionTarget(this.program);
708+
}
696709
this.currentThisExpression = null;
697710
this.currentElementExpression = null;
698-
return classType;
711+
return element;
699712
}
700713
case NodeKind.UNARYPREFIX: {
701714
// TODO: overloads
@@ -885,11 +898,7 @@ export class Resolver extends DiagnosticEmitter {
885898
} else {
886899
let signature = returnType.signatureReference;
887900
if (signature) {
888-
let functionTarget = signature.cachedFunctionTarget;
889-
if (!functionTarget) {
890-
functionTarget = new FunctionTarget(this.program, signature);
891-
signature.cachedFunctionTarget = functionTarget;
892-
}
901+
let functionTarget = signature.asFunctionTarget(this.program);
893902
// reuse resolvedThisExpression (might be property access)
894903
// reuse resolvedElementExpression (might be element access)
895904
return functionTarget;

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,13 @@ export class Signature {
526526
this.type = Type.u32.asFunction(this);
527527
}
528528

529+
asFunctionTarget(program: Program): FunctionTarget {
530+
var target = this.cachedFunctionTarget;
531+
if (!target) this.cachedFunctionTarget = target = new FunctionTarget(program, this);
532+
else assert(target.program == program);
533+
return target;
534+
}
535+
529536
/** Gets the known or, alternatively, generic parameter name at the specified index. */
530537
getParameterName(index: i32): string {
531538
var parameterNames = this.parameterNames;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
(module
2+
(type $ii (func (param i32) (result i32)))
3+
(type $i (func (result i32)))
4+
(type $v (func))
5+
(memory $0 0)
6+
(table $0 1 anyfunc)
7+
(elem (i32.const 0) $null)
8+
(global $~lib/allocator/arena/startOffset (mut i32) (i32.const 0))
9+
(global $~lib/allocator/arena/offset (mut i32) (i32.const 0))
10+
(global $~argc (mut i32) (i32.const 0))
11+
(export "memory" (memory $0))
12+
(export "table" (table $0))
13+
(export "testVar" (func $nonNullAssertion/testVar))
14+
(export "testObj" (func $nonNullAssertion/testObj))
15+
(export "testProp" (func $nonNullAssertion/testObj))
16+
(export "testArr" (func $nonNullAssertion/testArr))
17+
(export "testElem" (func $nonNullAssertion/testArr))
18+
(export "testAll" (func $nonNullAssertion/testAll))
19+
(export "testAll2" (func $nonNullAssertion/testAll))
20+
(export "testFn" (func $nonNullAssertion/testFn))
21+
(export "testFn2" (func $nonNullAssertion/testFn))
22+
(export "testRet" (func $nonNullAssertion/testFn))
23+
(export "testObjFn" (func $nonNullAssertion/testObjFn))
24+
(export "testObjRet" (func $nonNullAssertion/testObjFn))
25+
(start $start)
26+
(func $nonNullAssertion/testVar (; 0 ;) (type $ii) (param $0 i32) (result i32)
27+
get_local $0
28+
)
29+
(func $nonNullAssertion/testObj (; 1 ;) (type $ii) (param $0 i32) (result i32)
30+
get_local $0
31+
i32.load
32+
)
33+
(func $nonNullAssertion/testArr (; 2 ;) (type $ii) (param $0 i32) (result i32)
34+
i32.const 0
35+
get_local $0
36+
i32.load
37+
tee_local $0
38+
i32.load
39+
i32.const 2
40+
i32.shr_u
41+
i32.lt_u
42+
if (result i32)
43+
get_local $0
44+
i32.load offset=8
45+
else
46+
unreachable
47+
end
48+
)
49+
(func $nonNullAssertion/testAll (; 3 ;) (type $ii) (param $0 i32) (result i32)
50+
i32.const 0
51+
get_local $0
52+
i32.load
53+
tee_local $0
54+
i32.load
55+
i32.const 2
56+
i32.shr_u
57+
i32.lt_u
58+
if (result i32)
59+
get_local $0
60+
i32.load offset=8
61+
else
62+
unreachable
63+
end
64+
i32.load
65+
)
66+
(func $nonNullAssertion/testFn (; 4 ;) (type $ii) (param $0 i32) (result i32)
67+
i32.const 0
68+
set_global $~argc
69+
get_local $0
70+
call_indirect (type $i)
71+
)
72+
(func $nonNullAssertion/testObjFn (; 5 ;) (type $ii) (param $0 i32) (result i32)
73+
i32.const 0
74+
set_global $~argc
75+
get_local $0
76+
i32.load offset=4
77+
call_indirect (type $i)
78+
)
79+
(func $start (; 6 ;) (type $v)
80+
i32.const 8
81+
set_global $~lib/allocator/arena/startOffset
82+
get_global $~lib/allocator/arena/startOffset
83+
set_global $~lib/allocator/arena/offset
84+
)
85+
(func $null (; 7 ;) (type $v)
86+
nop
87+
)
88+
)

tests/compiler/nonNullAssertion.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import "allocator/arena";
2+
3+
export function testVar(n: Error | null): Error {
4+
return n!;
5+
}
6+
7+
class Foo {
8+
bar: Foo | null;
9+
baz: (() => Foo | null) | null;
10+
}
11+
12+
export function testObj(foo: Foo | null): Foo | null {
13+
return foo!.bar;
14+
}
15+
16+
export function testProp(foo: Foo): Foo {
17+
return foo.bar!;
18+
}
19+
20+
export function testArr(foo: Array<Foo> | null): Foo {
21+
return foo![0];
22+
}
23+
24+
export function testElem(foo: Array<Foo | null>): Foo {
25+
return foo[0]!;
26+
}
27+
28+
export function testAll(foo: Array<Foo | null> | null): Foo {
29+
return foo![0]!.bar!;
30+
}
31+
32+
export function testAll2(foo: Array<Foo | null> | null): Foo {
33+
return foo!![0]!!!.bar!!!!;
34+
}
35+
36+
export function testFn(fn: (() => Foo | null) | null): Foo | null {
37+
return fn!();
38+
}
39+
40+
export function testFn2(fn: (() => Foo | null) | null): Foo | null {
41+
var fn2 = fn!;
42+
return fn2();
43+
}
44+
45+
export function testRet(fn: (() => Foo | null) | null): Foo {
46+
return fn!()!;
47+
}
48+
49+
export function testObjFn(foo: Foo): Foo | null {
50+
return foo.baz!();
51+
}
52+
53+
export function testObjRet(foo: Foo): Foo {
54+
return foo.baz!()!;
55+
}

0 commit comments

Comments
 (0)