Make 'instanceof' behave like TS if the lhs is nullable

This commit is contained in:
dcodeIO 2018-06-09 02:01:45 +02:00
parent 7478c8a0d3
commit 47f2e0950a
8 changed files with 220 additions and 54 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2377,18 +2377,17 @@ export class Compiler extends DiagnosticEmitter {
} }
// any to void // any to void
if (toType.kind == TypeKind.VOID) { if (toType.kind == TypeKind.VOID) return module.createDrop(expr);
return module.createDrop(expr);
if (!fromType.isAssignableTo(toType)) {
if (conversionKind == ConversionKind.IMPLICIT) {
this.error(
DiagnosticCode.Conversion_from_type_0_to_1_requires_an_explicit_cast,
reportNode.range, fromType.toString(), toType.toString()
); // recoverable
}
} }
if (conversionKind == ConversionKind.IMPLICIT && !fromType.isAssignableTo(toType)) {
this.error(
DiagnosticCode.Conversion_from_type_0_to_1_requires_an_explicit_cast,
reportNode.range, fromType.toString(), toType.toString()
); // recoverable
}
// TODO: make this a proper switch?
if (fromType.is(TypeFlags.FLOAT)) { if (fromType.is(TypeFlags.FLOAT)) {
// float to float // float to float
@ -4502,7 +4501,7 @@ export class Compiler extends DiagnosticEmitter {
if (!target) return this.module.createUnreachable(); if (!target) return this.module.createUnreachable();
// to compile just the value, we need to know the target's type // to compile just the value, we need to know the target's type
var elementType: Type; var targetType: Type;
switch (target.kind) { switch (target.kind) {
case ElementKind.GLOBAL: { case ElementKind.GLOBAL: {
if (!this.compileGlobal(<Global>target)) { // reports; not yet compiled if a static field compiled as a global if (!this.compileGlobal(<Global>target)) { // reports; not yet compiled if a static field compiled as a global
@ -4513,7 +4512,7 @@ export class Compiler extends DiagnosticEmitter {
} }
case ElementKind.LOCAL: case ElementKind.LOCAL:
case ElementKind.FIELD: { case ElementKind.FIELD: {
elementType = (<VariableLikeElement>target).type; targetType = (<VariableLikeElement>target).type;
break; break;
} }
case ElementKind.PROPERTY: { case ElementKind.PROPERTY: {
@ -4522,7 +4521,7 @@ export class Compiler extends DiagnosticEmitter {
let instance = prototype.resolve(); // reports let instance = prototype.resolve(); // reports
if (!instance) return this.module.createUnreachable(); if (!instance) return this.module.createUnreachable();
assert(instance.signature.parameterTypes.length == 1); // parser must guarantee this assert(instance.signature.parameterTypes.length == 1); // parser must guarantee this
elementType = instance.signature.parameterTypes[0]; targetType = instance.signature.parameterTypes[0];
break; break;
} }
this.error( this.error(
@ -4551,7 +4550,7 @@ export class Compiler extends DiagnosticEmitter {
return this.module.createUnreachable(); return this.module.createUnreachable();
} }
assert(indexedSet.signature.parameterTypes.length == 2); // parser must guarantee this assert(indexedSet.signature.parameterTypes.length == 2); // parser must guarantee this
elementType = indexedSet.signature.parameterTypes[1]; // 2nd parameter is the element targetType = indexedSet.signature.parameterTypes[1]; // 2nd parameter is the element
break; break;
} }
// fall-through // fall-through
@ -4566,7 +4565,7 @@ export class Compiler extends DiagnosticEmitter {
} }
// compile the value and do the assignment // compile the value and do the assignment
var valueExpr = this.compileExpression(valueExpression, elementType, ConversionKind.IMPLICIT, WrapMode.NONE); var valueExpr = this.compileExpression(valueExpression, targetType, ConversionKind.IMPLICIT, WrapMode.NONE);
return this.compileAssignmentWithValue( return this.compileAssignmentWithValue(
expression, expression,
valueExpr, valueExpr,
@ -5882,12 +5881,26 @@ export class Compiler extends DiagnosticEmitter {
expression: InstanceOfExpression, expression: InstanceOfExpression,
contextualType: Type contextualType: Type
): ExpressionRef { ): ExpressionRef {
this.compileExpressionRetainType(expression.expression, this.options.usizeType, WrapMode.NONE); var module = this.module;
// NOTE that this differs from TypeScript in that the rhs is a type, not an expression. at the
// time of implementation, this seemed more useful because dynamic rhs expressions are not
// possible in AS anyway.
var expr = this.compileExpressionRetainType(expression.expression, this.options.usizeType, WrapMode.NONE);
var type = this.currentType; var type = this.currentType;
var isType = this.program.resolveType(expression.isType); var isType = this.program.resolveType(expression.isType);
this.currentType = Type.bool; this.currentType = Type.bool;
if (!isType) return this.module.createUnreachable(); if (!isType) return module.createUnreachable();
return this.module.createI32(type.isAssignableTo(isType, false) ? 1 : 0); return type.is(TypeFlags.NULLABLE) && !isType.is(TypeFlags.NULLABLE)
? type.nonNullableType.isAssignableTo(isType)
? module.createBinary( // not precomputeable
type.is(TypeFlags.LONG)
? BinaryOp.NeI64
: BinaryOp.NeI32,
expr,
type.toNativeZero(module)
)
: module.createI32(0)
: module.createI32(type.isAssignableTo(isType, true) ? 1 : 0);
} }
compileLiteralExpression( compileLiteralExpression(

View File

@ -1847,7 +1847,9 @@ export class Program extends DiagnosticEmitter {
if (node.kind == NodeKind.SIGNATURE) { if (node.kind == NodeKind.SIGNATURE) {
let signature = this.resolveSignature(<SignatureNode>node, contextualTypeArguments, reportNotFound); let signature = this.resolveSignature(<SignatureNode>node, contextualTypeArguments, reportNotFound);
if (!signature) return null; if (!signature) return null;
return Type.u32.asFunction(signature); return node.isNullable
? signature.type.asNullable()
: signature.type;
} }
var typeNode = <TypeNode>node; var typeNode = <TypeNode>node;
var simpleName = typeNode.name.text; var simpleName = typeNode.name.text;
@ -1867,7 +1869,10 @@ export class Program extends DiagnosticEmitter {
contextualTypeArguments, contextualTypeArguments,
null null
); // reports ); // reports
return instance ? instance.type : null; if (!instance) return null;
return node.isNullable
? instance.type.asNullable()
: instance.type;
} }
} }
} }

View File

@ -170,6 +170,7 @@ export class Type {
if (!this.cachedNullableType) { if (!this.cachedNullableType) {
assert(!this.is(TypeFlags.NULLABLE)); assert(!this.is(TypeFlags.NULLABLE));
this.cachedNullableType = new Type(this.kind, this.flags | TypeFlags.NULLABLE, this.size); this.cachedNullableType = new Type(this.kind, this.flags | TypeFlags.NULLABLE, this.size);
this.cachedNullableType.nonNullableType = this;
this.cachedNullableType.classReference = this.classReference; // either a class reference this.cachedNullableType.classReference = this.classReference; // either a class reference
this.cachedNullableType.signatureReference = this.signatureReference; // or a function reference this.cachedNullableType.signatureReference = this.signatureReference; // or a function reference
} }
@ -177,27 +178,29 @@ export class Type {
} }
/** Tests if a value of this type is assignable to a target of the specified type. */ /** Tests if a value of this type is assignable to a target of the specified type. */
isAssignableTo(target: Type, signednessIsImportant: bool = false): bool { isAssignableTo(target: Type, signednessIsRelevant: bool = false): bool {
var currentClass: Class | null; var currentClass: Class | null;
var targetClass: Class | null; var targetClass: Class | null;
var currentFunction: Signature | null; var currentFunction: Signature | null;
var targetFunction: Signature | null; var targetFunction: Signature | null;
if (this.is(TypeFlags.REFERENCE)) { if (this.is(TypeFlags.REFERENCE)) {
if (target.is(TypeFlags.REFERENCE)) { if (target.is(TypeFlags.REFERENCE)) {
if (currentClass = this.classReference) { if (!this.is(TypeFlags.NULLABLE) || target.is(TypeFlags.NULLABLE)) {
if (targetClass = target.classReference) { if (currentClass = this.classReference) {
return currentClass.isAssignableTo(targetClass); if (targetClass = target.classReference) {
} return currentClass.isAssignableTo(targetClass);
} else if (currentFunction = this.signatureReference) { }
if (targetFunction = target.signatureReference) { } else if (currentFunction = this.signatureReference) {
return currentFunction.isAssignableTo(targetFunction); if (targetFunction = target.signatureReference) {
return currentFunction.isAssignableTo(targetFunction);
}
} }
} }
} }
} else if (!target.is(TypeFlags.REFERENCE)) { } else if (!target.is(TypeFlags.REFERENCE)) {
if (this.is(TypeFlags.INTEGER)) { if (this.is(TypeFlags.INTEGER)) {
if (target.is(TypeFlags.INTEGER)) { if (target.is(TypeFlags.INTEGER)) {
if (!signednessIsImportant || this.is(TypeFlags.SIGNED) == target.is(TypeFlags.SIGNED)) { if (!signednessIsRelevant || this.is(TypeFlags.SIGNED) == target.is(TypeFlags.SIGNED)) {
return this.size <= target.size; return this.size <= target.size;
} }
} else if (target.kind == TypeKind.F32) { } else if (target.kind == TypeKind.F32) {
@ -223,6 +226,21 @@ export class Type {
/** Converts this type to its TypeScript representation. */ /** Converts this type to its TypeScript representation. */
toString(kindOnly: bool = false): string { toString(kindOnly: bool = false): string {
if (!kindOnly && this.is(TypeFlags.REFERENCE)) {
let classReference = this.classReference;
if (classReference) {
return this.is(TypeFlags.NULLABLE)
? classReference.toString() + " | null"
: classReference.toString();
}
let signatureReference = this.signatureReference;
if (signatureReference) {
return this.is(TypeFlags.NULLABLE)
? "(" + signatureReference.toString(true) + ") | null"
: signatureReference.toString(true);
}
assert(false);
}
switch (this.kind) { switch (this.kind) {
case TypeKind.I8: return "i8"; case TypeKind.I8: return "i8";
case TypeKind.I16: return "i16"; case TypeKind.I16: return "i16";
@ -231,23 +249,14 @@ export class Type {
case TypeKind.ISIZE: return "isize"; case TypeKind.ISIZE: return "isize";
case TypeKind.U8: return "u8"; case TypeKind.U8: return "u8";
case TypeKind.U16: return "u16"; case TypeKind.U16: return "u16";
case TypeKind.U32: { case TypeKind.U32: return "u32";
let functionType = this.signatureReference;
return kindOnly || !functionType ? "u32" : functionType.toString(true);
}
case TypeKind.U64: return "u64"; case TypeKind.U64: return "u64";
case TypeKind.USIZE: { case TypeKind.USIZE: return "usize";
let classType = this.classReference;
return kindOnly || !classType ? "usize" : classType.toString();
}
case TypeKind.BOOL: return "bool"; case TypeKind.BOOL: return "bool";
case TypeKind.F32: return "f32"; case TypeKind.F32: return "f32";
case TypeKind.F64: return "f64"; case TypeKind.F64: return "f64";
default: assert(false);
case TypeKind.VOID: return "void"; case TypeKind.VOID: return "void";
default: {
assert(false);
return "";
}
} }
} }

View File

@ -4,6 +4,7 @@
(type $Fi (func (param f64) (result i32))) (type $Fi (func (param f64) (result i32)))
(type $v (func)) (type $v (func))
(import "env" "abort" (func $~lib/env/abort (param i32 i32 i32 i32))) (import "env" "abort" (func $~lib/env/abort (param i32 i32 i32 i32)))
(global $instanceof/an (mut i32) (i32.const 0))
(memory $0 1) (memory $0 1)
(data (i32.const 8) "\0d\00\00\00i\00n\00s\00t\00a\00n\00c\00e\00o\00f\00.\00t\00s") (data (i32.const 8) "\0d\00\00\00i\00n\00s\00t\00a\00n\00c\00e\00o\00f\00.\00t\00s")
(export "memory" (memory $0)) (export "memory" (memory $0))
@ -14,7 +15,10 @@
(func $instanceof/isI32<f64> (; 2 ;) (type $Fi) (param $0 f64) (result i32) (func $instanceof/isI32<f64> (; 2 ;) (type $Fi) (param $0 f64) (result i32)
(i32.const 0) (i32.const 0)
) )
(func $start (; 3 ;) (type $v) (func $instanceof/isI32<u32> (; 3 ;) (type $ii) (param $0 i32) (result i32)
(i32.const 0)
)
(func $start (; 4 ;) (type $v)
(if (if
(i32.eqz (i32.eqz
(call $instanceof/isI32<i32> (call $instanceof/isI32<i32>
@ -45,5 +49,48 @@
(unreachable) (unreachable)
) )
) )
(if
(call $instanceof/isI32<u32>
(i32.const 0)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 40)
(i32.const 0)
)
(unreachable)
)
)
(if
(get_global $instanceof/an)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 43)
(i32.const 0)
)
(unreachable)
)
)
(set_global $instanceof/an
(i32.const 1)
)
(if
(i32.eqz
(get_global $instanceof/an)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 46)
(i32.const 0)
)
(unreachable)
)
)
) )
) )

View File

@ -37,13 +37,14 @@ function isI32<T>(v: T): bool {
assert( isI32(0)); assert( isI32(0));
assert(!isI32(0.0)); assert(!isI32(0.0));
assert(!isI32(<u32>0)); // signedness is relevant
// TODO: what about nullables? var an: A | null = null;
// var an: A | null; assert(!(an instanceof A)); // TS: null is not an instance of A
// var bn: B | null; assert( an instanceof A | null); // AS: null is an instance of A | null
// an = changetype<A | null>(1);
// assert(an instanceof A); assert( an instanceof A); // TS: non-null is an instance of A
// assert(bn instanceof A); assert( an instanceof A | null); // AS: non-null is an instance of A | null
//
// assert(!(an instanceof B)); // TODO: keep track of nullability during flows, so this becomes precomputable:
// assert(bn instanceof B); // assert(an !== null && an instanceof A);

View File

@ -8,6 +8,7 @@
(global $instanceof/b (mut i32) (i32.const 0)) (global $instanceof/b (mut i32) (i32.const 0))
(global $instanceof/i (mut i32) (i32.const 0)) (global $instanceof/i (mut i32) (i32.const 0))
(global $instanceof/f (mut f32) (f32.const 0)) (global $instanceof/f (mut f32) (f32.const 0))
(global $instanceof/an (mut i32) (i32.const 0))
(global $HEAP_BASE i32 (i32.const 40)) (global $HEAP_BASE i32 (i32.const 40))
(memory $0 1) (memory $0 1)
(data (i32.const 8) "\0d\00\00\00i\00n\00s\00t\00a\00n\00c\00e\00o\00f\00.\00t\00s\00") (data (i32.const 8) "\0d\00\00\00i\00n\00s\00t\00a\00n\00c\00e\00o\00f\00.\00t\00s\00")
@ -23,7 +24,12 @@
(i32.const 0) (i32.const 0)
) )
) )
(func $start (; 3 ;) (type $v) (func $instanceof/isI32<u32> (; 3 ;) (type $ii) (param $0 i32) (result i32)
(return
(i32.const 0)
)
)
(func $start (; 4 ;) (type $v)
(if (if
(i32.eqz (i32.eqz
(i32.const 1) (i32.const 1)
@ -304,5 +310,90 @@
(unreachable) (unreachable)
) )
) )
(if
(i32.eqz
(i32.eqz
(call $instanceof/isI32<u32>
(i32.const 0)
)
)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 40)
(i32.const 0)
)
(unreachable)
)
)
(if
(i32.eqz
(i32.eqz
(i32.ne
(get_global $instanceof/an)
(i32.const 0)
)
)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 43)
(i32.const 0)
)
(unreachable)
)
)
(if
(i32.eqz
(i32.const 1)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 44)
(i32.const 0)
)
(unreachable)
)
)
(set_global $instanceof/an
(i32.const 1)
)
(if
(i32.eqz
(i32.ne
(get_global $instanceof/an)
(i32.const 0)
)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 46)
(i32.const 0)
)
(unreachable)
)
)
(if
(i32.eqz
(i32.const 1)
)
(block
(call $~lib/env/abort
(i32.const 0)
(i32.const 8)
(i32.const 47)
(i32.const 0)
)
(unreachable)
)
)
) )
) )