Skip to content

Support non-null assertion expression x! #235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum NodeKind {
// expressions
IDENTIFIER,
ASSERTION,
NON_NULL_ASSERTION,
BINARY,
CALL,
CLASS,
Expand Down Expand Up @@ -117,7 +118,8 @@ export function nodeIsCallable(kind: NodeKind): bool {
case NodeKind.IDENTIFIER:
case NodeKind.CALL:
case NodeKind.ELEMENTACCESS:
case NodeKind.PROPERTYACCESS: return true;
case NodeKind.PROPERTYACCESS:
case NodeKind.NON_NULL_ASSERTION: return true;
}
return false;
}
Expand Down Expand Up @@ -424,6 +426,16 @@ export abstract class Node {
return expr;
}

static createNonNullAssertionExpression(
expression: Expression,
range: Range
): NonNullAssertionExpression {
var expr = new NonNullAssertionExpression();
expr.range = range;
expr.expression = expression;
return expr;
}

static createNullExpression(
range: Range
): NullExpression {
Expand Down Expand Up @@ -1374,6 +1386,11 @@ export class NewExpression extends CallExpression {
kind = NodeKind.NEW;
}

export class NonNullAssertionExpression extends Expression {
kind = NodeKind.NON_NULL_ASSERTION;
expression: Expression;
}

/** Represents a `null` expression. */
export class NullExpression extends IdentifierExpression {
kind = NodeKind.NULL;
Expand Down
12 changes: 12 additions & 0 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ import {
LiteralExpression,
LiteralKind,
NewExpression,
NonNullAssertionExpression,
ObjectLiteralExpression,
ParenthesizedExpression,
PropertyAccessExpression,
Expand Down Expand Up @@ -2422,6 +2423,10 @@ export class Compiler extends DiagnosticEmitter {
expr = this.compileNewExpression(<NewExpression>expression, contextualType);
break;
}
case NodeKind.NON_NULL_ASSERTION: {
expr = this.compileNonNullAssertionExpression(<NonNullAssertionExpression>expression, contextualType);
break;
}
case NodeKind.PARENTHESIZED: {
expr = this.compileParenthesizedExpression(<ParenthesizedExpression>expression, contextualType);
break;
Expand Down Expand Up @@ -2674,6 +2679,13 @@ export class Compiler extends DiagnosticEmitter {
return this.compileExpression(expression.expression, toType, ConversionKind.EXPLICIT, WrapMode.NONE);
}

compileNonNullAssertionExpression(expression: NonNullAssertionExpression, contextualType: Type): ExpressionRef {
const inner = expression.expression;
const res = this.compileExpressionRetainType(inner, contextualType.asNullableIfPossible(), WrapMode.NONE);
this.currentType = this.currentType.nonNullableType;
return res;
}

private f32ModInstance: Function | null = null;
private f64ModInstance: Function | null = null;
private f32PowInstance: Function | null = null;
Expand Down
121 changes: 82 additions & 39 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ import {

mangleInternalPath,
nodeIsCallable,
nodeIsGenericCallable
nodeIsGenericCallable,
NonNullAssertionExpression
} from "./ast";

/** Parser interface. */
Expand Down Expand Up @@ -3256,6 +3257,12 @@ export class Parser extends DiagnosticEmitter {
if (!expr) return null;
var startPos = expr.range.start;

// Handle this here as well as in the loop to support `f!()` expressions (since we handle calls just ahead)
if (//determinePrecedence(Token.EXCLAMATION) >= precedence &&
tn.skip(Token.EXCLAMATION)) {
expr = Node.createNonNullAssertionExpression(expr, tn.range(startPos, tn.pos));
}

// CallExpression?
if (nodeIsCallable(expr.kind)) {
let typeArguments: CommonTypeNode[] | null = null;
Expand Down Expand Up @@ -3339,6 +3346,10 @@ export class Parser extends DiagnosticEmitter {
);
break;
}
case Token.EXCLAMATION: {
expr = Node.createNonNullAssertionExpression(expr, tn.range(startPos, tn.pos));
break;
}
// TernaryExpression
case Token.QUESTION: {
let ifThen = this.parseExpression(tn);
Expand Down Expand Up @@ -3381,50 +3392,81 @@ export class Parser extends DiagnosticEmitter {
: nextPrecedence + 1
);
if (!next) return null;

// PropertyAccessExpression
if (token == Token.DOT) {
if (next.kind == NodeKind.IDENTIFIER) {
expr = Node.createPropertyAccessExpression(
expr,
<IdentifierExpression>next,
tn.range(startPos, tn.pos)
);
} else if (next.kind == NodeKind.CALL) { // join
let propertyCall = <CallExpression>next;
if (propertyCall.expression.kind == NodeKind.IDENTIFIER) {
propertyCall.expression = Node.createPropertyAccessExpression(
expr,
<IdentifierExpression>propertyCall.expression,
tn.range(startPos, tn.pos)
);
} else {
this.error(
DiagnosticCode.Identifier_expected,
propertyCall.expression.range
);
return null;
}
expr = propertyCall;
} else {
this.error(
DiagnosticCode.Identifier_expected,
next.range
);
return null;
}

// BinaryExpression
} else {
expr = Node.createBinaryExpression(token, expr, next, tn.range(startPos, tn.pos));
}
expr = this.attachExpression(tn, startPos, expr, token, next);
if (!expr) return null;
break;
}
}
}
return expr;
}

// We just parsed `expr`, then `token`, then `next`. Graft these three together to a single expression.
private attachExpression(
tn: Tokenizer,
startPos: number,
expr: Expression,
token: Token,
next: Expression
): Expression | null {
// PropertyAccessExpression
if (token == Token.DOT) {
if (next.kind === NodeKind.NON_NULL_ASSERTION) {
const inner = this.attachExpression(tn, startPos, expr, token, (<NonNullAssertionExpression> next).expression);
return inner && Node.createNonNullAssertionExpression(inner, tn.range(startPos, tn.pos));
} else if (next.kind == NodeKind.IDENTIFIER) {
return Node.createPropertyAccessExpression(
expr,
<IdentifierExpression>next,
tn.range(startPos, tn.pos)
);
} else if (next.kind == NodeKind.CALL) { // join
let propertyCall = <CallExpression>next;
let newExpr = this.attachExpressionToCallLHS(tn, startPos, expr, propertyCall.expression); //name
if (!newExpr) return null;
propertyCall.expression = newExpr;
return propertyCall;
} else {
this.error(
DiagnosticCode.Identifier_expected,
next.range
);
return null;
}

// BinaryExpression
} else {
return Node.createBinaryExpression(token, expr, next, tn.range(startPos, tn.pos));
}
}

// We parsed `expr`, then `.`, then a call expression `f()` whose expression is `f`.
// Graft `expr.x` into a single expression.
private attachExpressionToCallLHS(
tn: Tokenizer,
startPos: number,
expr: Expression,
calledExpression: Expression
): Expression | null {
if (calledExpression.kind === NodeKind.NON_NULL_ASSERTION) {
const innerCalled = (<NonNullAssertionExpression> calledExpression).expression;
const inner = this.attachExpressionToCallLHS(tn, startPos, innerCalled, expr);
return inner && Node.createNonNullAssertionExpression(inner, tn.range(startPos, tn.pos));
} else if (calledExpression.kind == NodeKind.IDENTIFIER) {
return Node.createPropertyAccessExpression(
expr,
<IdentifierExpression>calledExpression,
tn.range(startPos, tn.pos)
);
} else {
this.error(
DiagnosticCode.Identifier_expected,
calledExpression.range
);
return null;
}
}

/** Skips over a statement on errors in an attempt to reduce unnecessary diagnostic noise. */
skipStatement(tn: Tokenizer): void {
tn.peek(true);
Expand Down Expand Up @@ -3597,7 +3639,8 @@ function determinePrecedence(kind: Token): Precedence {
case Token.MINUS_MINUS: return Precedence.UNARY_POSTFIX;
case Token.DOT:
case Token.NEW:
case Token.OPENBRACKET: return Precedence.MEMBERACCESS;
case Token.OPENBRACKET:
case Token.EXCLAMATION: return Precedence.MEMBERACCESS;
}
return Precedence.NONE;
}
Expand Down
7 changes: 6 additions & 1 deletion src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import {
LiteralKind,
ParenthesizedExpression,
AssertionExpression,
Expression
Expression,
NonNullAssertionExpression
} from "./ast";

import {
Expand Down Expand Up @@ -559,6 +560,10 @@ export class Resolver extends DiagnosticEmitter {
}
return null;
}
case NodeKind.NON_NULL_ASSERTION: {
const inner = (<NonNullAssertionExpression> expression).expression;
return this.resolveExpression(inner, contextualFunction, reportMode);
}
case NodeKind.BINARY: { // TODO: string concatenation, mostly
throw new Error("not implemented");
}
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ export class Type {
return this.cachedNullableType;
}

// TODO: GH#233 support nullable primitives, then just use `asNullable()`
asNullableIfPossible(): Type {
return this.is(TypeFlags.REFERENCE) ? this.asNullable() : this;
}

/** Tests if a value of this type is assignable to a target of the specified type. */
isAssignableTo(target: Type, signednessIsRelevant: bool = false): bool {
var currentClass: Class | null;
Expand Down
22 changes: 22 additions & 0 deletions tests/compiler/nonNullAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function test(n: Error | null): Error {
return n!;
}

class Foo {
boo: i32;
arr: Array<i32 | null>;
f: (() => i32 | null) | null;
}
export function test2(foo: Foo | null): i32 {
let f = foo!.f; // Test as lone call expression in addition to member access call expression
let arr = foo!.arr;
return foo!.boo + foo!.arr![0]! + foo!.f!()! + f!()! + arr![0]!;
}

// Without this there is a compiler error, see GH#237
export function useTest2(): void {
let foo = new Foo();
foo.boo = 0;
foo.arr = [];
foo.f = (): i32 => 1;
}
Loading