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
if (toType.kind == TypeKind.VOID) {
return module.createDrop(expr);
if (toType.kind == TypeKind.VOID) 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)) {
// float to float
@ -4502,7 +4501,7 @@ export class Compiler extends DiagnosticEmitter {
if (!target) return this.module.createUnreachable();
// to compile just the value, we need to know the target's type
var elementType: Type;
var targetType: Type;
switch (target.kind) {
case ElementKind.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.FIELD: {
elementType = (<VariableLikeElement>target).type;
targetType = (<VariableLikeElement>target).type;
break;
}
case ElementKind.PROPERTY: {
@ -4522,7 +4521,7 @@ export class Compiler extends DiagnosticEmitter {
let instance = prototype.resolve(); // reports
if (!instance) return this.module.createUnreachable();
assert(instance.signature.parameterTypes.length == 1); // parser must guarantee this
elementType = instance.signature.parameterTypes[0];
targetType = instance.signature.parameterTypes[0];
break;
}
this.error(
@ -4551,7 +4550,7 @@ export class Compiler extends DiagnosticEmitter {
return this.module.createUnreachable();
}
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;
}
// fall-through
@ -4566,7 +4565,7 @@ export class Compiler extends DiagnosticEmitter {
}
// 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(
expression,
valueExpr,
@ -5882,12 +5881,26 @@ export class Compiler extends DiagnosticEmitter {
expression: InstanceOfExpression,
contextualType: Type
): 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 isType = this.program.resolveType(expression.isType);
this.currentType = Type.bool;
if (!isType) return this.module.createUnreachable();
return this.module.createI32(type.isAssignableTo(isType, false) ? 1 : 0);
if (!isType) return module.createUnreachable();
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(

View File

@ -1847,7 +1847,9 @@ export class Program extends DiagnosticEmitter {
if (node.kind == NodeKind.SIGNATURE) {
let signature = this.resolveSignature(<SignatureNode>node, contextualTypeArguments, reportNotFound);
if (!signature) return null;
return Type.u32.asFunction(signature);
return node.isNullable
? signature.type.asNullable()
: signature.type;
}
var typeNode = <TypeNode>node;
var simpleName = typeNode.name.text;
@ -1867,7 +1869,10 @@ export class Program extends DiagnosticEmitter {
contextualTypeArguments,
null
); // 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) {
assert(!this.is(TypeFlags.NULLABLE));
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.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. */
isAssignableTo(target: Type, signednessIsImportant: bool = false): bool {
isAssignableTo(target: Type, signednessIsRelevant: bool = false): bool {
var currentClass: Class | null;
var targetClass: Class | null;
var currentFunction: Signature | null;
var targetFunction: Signature | null;
if (this.is(TypeFlags.REFERENCE)) {
if (target.is(TypeFlags.REFERENCE)) {
if (currentClass = this.classReference) {
if (targetClass = target.classReference) {
return currentClass.isAssignableTo(targetClass);
}
} else if (currentFunction = this.signatureReference) {
if (targetFunction = target.signatureReference) {
return currentFunction.isAssignableTo(targetFunction);
if (!this.is(TypeFlags.NULLABLE) || target.is(TypeFlags.NULLABLE)) {
if (currentClass = this.classReference) {
if (targetClass = target.classReference) {
return currentClass.isAssignableTo(targetClass);
}
} else if (currentFunction = this.signatureReference) {
if (targetFunction = target.signatureReference) {
return currentFunction.isAssignableTo(targetFunction);
}
}
}
}
} else if (!target.is(TypeFlags.REFERENCE)) {
if (this.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;
}
} else if (target.kind == TypeKind.F32) {
@ -223,6 +226,21 @@ export class Type {
/** Converts this type to its TypeScript representation. */
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) {
case TypeKind.I8: return "i8";
case TypeKind.I16: return "i16";
@ -231,23 +249,14 @@ export class Type {
case TypeKind.ISIZE: return "isize";
case TypeKind.U8: return "u8";
case TypeKind.U16: return "u16";
case TypeKind.U32: {
let functionType = this.signatureReference;
return kindOnly || !functionType ? "u32" : functionType.toString(true);
}
case TypeKind.U32: return "u32";
case TypeKind.U64: return "u64";
case TypeKind.USIZE: {
let classType = this.classReference;
return kindOnly || !classType ? "usize" : classType.toString();
}
case TypeKind.USIZE: return "usize";
case TypeKind.BOOL: return "bool";
case TypeKind.F32: return "f32";
case TypeKind.F64: return "f64";
default: assert(false);
case TypeKind.VOID: return "void";
default: {
assert(false);
return "";
}
}
}

View File

@ -4,6 +4,7 @@
(type $Fi (func (param f64) (result i32)))
(type $v (func))
(import "env" "abort" (func $~lib/env/abort (param i32 i32 i32 i32)))
(global $instanceof/an (mut i32) (i32.const 0))
(memory $0 1)
(data (i32.const 8) "\0d\00\00\00i\00n\00s\00t\00a\00n\00c\00e\00o\00f\00.\00t\00s")
(export "memory" (memory $0))
@ -14,7 +15,10 @@
(func $instanceof/isI32<f64> (; 2 ;) (type $Fi) (param $0 f64) (result i32)
(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
(i32.eqz
(call $instanceof/isI32<i32>
@ -45,5 +49,48 @@
(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.0));
assert(!isI32(<u32>0)); // signedness is relevant
// TODO: what about nullables?
// var an: A | null;
// var bn: B | null;
//
// assert(an instanceof A);
// assert(bn instanceof A);
//
// assert(!(an instanceof B));
// assert(bn instanceof B);
var an: A | null = null;
assert(!(an instanceof A)); // TS: null is not an instance of A
assert( an instanceof A | null); // AS: null is an instance of A | null
an = changetype<A | null>(1);
assert( an instanceof A); // TS: non-null is an instance of A
assert( an instanceof A | null); // AS: non-null is an instance of A | null
// TODO: keep track of nullability during flows, so this becomes precomputable:
// assert(an !== null && an instanceof A);

View File

@ -8,6 +8,7 @@
(global $instanceof/b (mut i32) (i32.const 0))
(global $instanceof/i (mut i32) (i32.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))
(memory $0 1)
(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)
)
)
(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
(i32.eqz
(i32.const 1)
@ -304,5 +310,90 @@
(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)
)
)
)
)