blob: 4d402db12c117e7ed6b633ca64e7a420543b7322 [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// This library defines the representation of runtime types.
part of dart._runtime;
final metadata = JS('', 'Symbol("metadata")');
/// The symbol used to store the cached `Type` object associated with a class.
final _typeObject = JS('', 'Symbol("typeObject")');
/// Types in dart are represented internally at runtime as follows.
///
/// - Normal nominal types, produced from classes, are represented
/// at runtime by the JS class of which they are an instance.
/// If the type is the result of instantiating a generic class,
/// then the "classes" module manages the association between the
/// instantiated class and the original class declaration
/// and the type arguments with which it was instantiated. This
/// assocation can be queried via the "classes" module".
///
/// - All other types are represented as instances of class TypeRep,
/// defined in this module.
/// - Dynamic, Void, and Bottom are singleton instances of sentinal
/// classes.
/// - Function types are instances of subclasses of AbstractFunctionType.
///
/// Function types are represented in one of two ways:
/// - As an instance of FunctionType. These are eagerly computed.
/// - As an instance of TypeDef. The TypeDef representation lazily
/// computes an instance of FunctionType, and delegates to that instance.
///
/// These above "runtime types" are what is used for implementing DDC's
/// internal type checks. These objects are distinct from the objects exposed
/// to user code by class literals and calling `Object.runtimeType`. In DDC,
/// the latter are represented by instances of WrappedType which contain a
/// real runtime type internally. This ensures that the returned object only
/// exposes the API that Type defines:
///
/// get String name;
/// String toString();
///
/// These "runtime types" have methods for performing type checks. The methods
/// have the following JavaScript names which are designed to not collide with
/// static methods, which are also placed 'on' the class constructor function.
///
/// T.is(o): Implements 'o is T'.
/// T.as(o): Implements 'o as T'.
/// T._check(o): Implements the type assertion of 'T x = o;'
///
/// By convention, we used named JavaScript functions for these methods with the
/// name 'is_X', 'as_X' and 'check_X' for various X to indicate the type or the
/// implementation strategy for the test (e.g 'is_String', 'is_G' for generic
/// types, etc.)
// TODO(jmesserly): we shouldn't implement Type here. It should be moved down
// to AbstractFunctionType.
class TypeRep implements Type {
TypeRep() {
_initialize;
}
String get name => this.toString();
}
class Dynamic extends TypeRep {
toString() => 'dynamic';
}
class LazyJSType implements Type {
final _jsTypeCallback;
final _dartName;
LazyJSType(this._jsTypeCallback, this._dartName);
get _rawJSType => JS('', '#()', _jsTypeCallback);
toString() => _jsTypeCallback != null ? typeName(_rawJSType) : _dartName;
}
void _warn(arg) {
JS('void', 'console.warn(#)', arg);
}
_isInstanceOfLazyJSType(o, LazyJSType t) {
if (t._jsTypeCallback != null) {
if (t._rawJSType == null) {
var expected = t._dartName;
var actual = typeName(getReifiedType(o));
_warn('Cannot find native JavaScript type ($expected) '
'to type check $actual');
return true;
}
return JS('bool', 'dart.is(#, #)', o, t._rawJSType);
}
if (o == null) return false;
// Anonymous case: match any JS type.
return _isJSObject(o);
}
_asInstanceOfLazyJSType(o, LazyJSType t) {
if (t._jsTypeCallback != null) {
if (t._rawJSType == null) {
var expected = t._dartName;
var actual = typeName(getReifiedType(o));
_warn('Cannot find native JavaScript type ($expected) '
'to type check $actual');
return o;
}
return JS('bool', 'dart.as(#, #)', o, t._rawJSType);
}
// Anonymous case: allow any JS type.
if (o == null) return null;
if (!_isJSObject(o)) _throwCastError(o, t, true);
return o;
}
bool _isJSObject(o) => JS('bool', '!dart.getReifiedType(o)[dart._runtimeType]');
@JSExportName('dynamic')
final _dynamic = new Dynamic();
final _initialize = _initialize2();
_initialize2() => JS(
'',
'''(() => {
// JavaScript API forwards to runtime library.
$TypeRep.prototype.is = function is_T(object) {
return dart.is(object, this);
};
$TypeRep.prototype.as = function as_T(object) {
return dart.as(object, this);
};
$TypeRep.prototype._check = function check_T(object) {
return dart.check(object, this);
};
// Fast path for type `dynamic`.
$Dynamic.prototype.is = function is_Dynamic(object) {
return true;
};
$Dynamic.prototype.as = function as_Dynamic(object) {
return object;
};
$Dynamic.prototype._check = function check_Dynamic(object) {
return object;
};
$LazyJSType.prototype.is = function is_T(object) {
return $_isInstanceOfLazyJSType(object, this);
};
$LazyJSType.prototype.as = function as_T(object) {
return $_asInstanceOfLazyJSType(object, this);
};
$LazyJSType.prototype._check = function check_T(object) {
return $_asInstanceOfLazyJSType(object, this);
};
})()''');
class Void extends TypeRep {
toString() => 'void';
}
@JSExportName('void')
final _void = new Void();
class Bottom extends TypeRep {
toString() => 'bottom';
}
final bottom = new Bottom();
class JSObject extends TypeRep {
toString() => 'NativeJavaScriptObject';
}
final jsobject = new JSObject();
class WrappedType extends Type {
final _wrappedType;
WrappedType(this._wrappedType);
toString() => typeName(_wrappedType);
}
final AbstractFunctionType = JS(
'',
'''
class AbstractFunctionType extends $TypeRep {
constructor() {
super();
this._stringValue = null;
}
toString() { return this.name; }
get name() {
if (this._stringValue) return this._stringValue;
let buffer = '(';
for (let i = 0; i < this.args.length; ++i) {
if (i > 0) {
buffer += ', ';
}
buffer += $typeName(this.args[i]);
}
if (this.optionals.length > 0) {
if (this.args.length > 0) buffer += ', ';
buffer += '[';
for (let i = 0; i < this.optionals.length; ++i) {
if (i > 0) {
buffer += ', ';
}
buffer += $typeName(this.optionals[i]);
}
buffer += ']';
} else if (Object.keys(this.named).length > 0) {
if (this.args.length > 0) buffer += ', ';
buffer += '{';
let names = $getOwnPropertyNames(this.named).sort();
for (let i = 0; i < names.length; ++i) {
if (i > 0) {
buffer += ', ';
}
buffer += names[i] + ': ' + $typeName(this.named[names[i]]);
}
buffer += '}';
}
buffer += ') -> ' + $typeName(this.returnType);
this._stringValue = buffer;
return buffer;
}
}
''');
/// Memo table for named argument groups. A named argument packet
/// {name1 : type1, ..., namen : typen} corresponds to the path
/// n, name1, type1, ...., namen, typen. The element of the map
/// reached via this path (if any) is the canonical representative
/// for this packet.
final _fnTypeNamedArgMap = JS('', 'new Map()');
/// Memo table for positional argument groups. A positional argument
/// packet [type1, ..., typen] (required or optional) corresponds to
/// the path n, type1, ...., typen. The element reached via
/// this path (if any) is the canonical representative for this
/// packet. Note that required and optional parameters packages
/// may have the same canonical representation.
final _fnTypeArrayArgMap = JS('', 'new Map()');
/// Memo table for function types. The index path consists of the
/// path length - 1, the returnType, the canonical positional argument
/// packet, and if present, the canonical optional or named argument
/// packet. A level of indirection could be avoided here if desired.
final _fnTypeTypeMap = JS('', 'new Map()');
/// Memo table for small function types with no optional or named
/// arguments and less than a fixed n (currently 3) number of
/// required arguments. Indexing into this table by the number
/// of required arguments yields a map which is indexed by the
/// argument types themselves. The element reached via this
/// index path (if present) is the canonical function type.
final _fnTypeSmallMap = JS('', '[new Map(), new Map(), new Map()]');
final FunctionType = JS(
'',
'''
class FunctionType extends $AbstractFunctionType {
static _memoizeArray(map, arr, create) {
let len = arr.length;
map = FunctionType._lookupNonTerminal(map, len);
for (var i = 0; i < len-1; ++i) {
map = FunctionType._lookupNonTerminal(map, arr[i]);
}
let result = map.get(arr[len-1]);
if (result !== void 0) return result;
map.set(arr[len-1], result = create());
return result;
}
// Map dynamic to bottom. If meta-data is present,
// we slice off the remaining meta-data and make
// it the second element of a packet for processing
// later on in the constructor.
static _normalizeParameter(a) {
if (a instanceof Array) {
let result = [];
result.push((a[0] == $dynamic) ? $bottom : a[0]);
result.push(a.slice(1));
return result;
}
return (a == $dynamic) ? $bottom : a;
}
static _canonicalizeArray(definite, array, map) {
let arr = (definite)
? array
: array.map(FunctionType._normalizeParameter);
return FunctionType._memoizeArray(map, arr, () => arr);
}
// TODO(leafp): This only canonicalizes of the names are
// emitted in a consistent order.
static _canonicalizeNamed(definite, named, map) {
let key = [];
let names = $getOwnPropertyNames(named);
let r = {};
for (var i = 0; i < names.length; ++i) {
let name = names[i];
let type = named[name];
if (!definite) r[name] = type = FunctionType._normalizeParameter(type);
key.push(name);
key.push(type);
}
if (!definite) named = r;
return FunctionType._memoizeArray(map, key, () => named);
}
static _lookupNonTerminal(map, key) {
let result = map.get(key);
if (result !== void 0) return result;
map.set(key, result = new Map());
return result;
}
// TODO(leafp): This handles some low hanging fruit, but
// really we should make all of this faster, and also
// handle more cases here.
static _createSmall(count, definite, returnType, required) {
let map = $_fnTypeSmallMap[count];
let args = (definite) ? required
: required.map(FunctionType._normalizeParameter);
for (var i = 0; i < count; ++i) {
map = FunctionType._lookupNonTerminal(map, args[i]);
}
let result = map.get(returnType);
if (result !== void 0) return result;
result = new FunctionType(returnType, args, [], {});
map.set(returnType, result);
return result;
}
/**
* Construct a function type. There are two arrow constructors,
* distinguished by the "definite" flag.
*
* The fuzzy arrow (definite is false) treats any arguments
* of type dynamic as having type bottom, and will always be
* called with a dynamic invoke.
*
* The definite arrow (definite is true) leaves arguments unchanged.
*
* We eagerly normalize the argument types to avoid having to deal with
* this logic in multiple places.
*
* This code does best effort canonicalization. It does not guarantee
* that all instances will share.
*
*/
static create(definite, returnType, args, extra) {
// Note that if extra is ever passed as an empty array
// or an empty map, we can end up with semantically
// identical function types that don't canonicalize
// to the same object since we won't fall into this
// fast path.
if (extra === void 0 && args.length < 3) {
return FunctionType._createSmall(
args.length, definite, returnType, args);
}
args = FunctionType._canonicalizeArray(
definite, args, $_fnTypeArrayArgMap);
let keys;
let create;
if (extra === void 0) {
keys = [returnType, args];
create = () => new FunctionType(returnType, args, [], {});
} else if (extra instanceof Array) {
let optionals =
FunctionType._canonicalizeArray(definite, extra, $_fnTypeArrayArgMap);
keys = [returnType, args, optionals];
create =
() => new FunctionType(returnType, args, optionals, {});
} else {
let named =
FunctionType._canonicalizeNamed(definite, extra, $_fnTypeNamedArgMap);
keys = [returnType, args, named];
create = () => new FunctionType(returnType, args, [], named);
}
return FunctionType._memoizeArray($_fnTypeTypeMap, keys, create);
}
constructor(returnType, args, optionals, named) {
super();
this.returnType = returnType;
this.args = args;
this.optionals = optionals;
this.named = named;
// TODO(vsm): This is just parameter metadata for now.
this.metadata = [];
function process(array, metadata) {
var result = [];
for (var i = 0; i < array.length; ++i) {
var arg = array[i];
if (arg instanceof Array) {
metadata.push(arg.slice(1));
result.push(arg[0]);
} else {
metadata.push([]);
result.push(arg);
}
}
return result;
}
this.args = process(this.args, this.metadata);
this.optionals = process(this.optionals, this.metadata);
// TODO(vsm): Add named arguments.
}
}
''');
final Typedef = JS(
'',
'''
class Typedef extends $AbstractFunctionType {
constructor(name, closure) {
super();
this._name = name;
this._closure = closure;
this._functionType = null;
}
get name() {
return this._name;
}
get functionType() {
if (!this._functionType) {
this._functionType = this._closure();
}
return this._functionType;
}
get returnType() {
return this.functionType.returnType;
}
get args() {
return this.functionType.args;
}
get optionals() {
return this.functionType.optionals;
}
get named() {
return this.functionType.named;
}
get metadata() {
return this.functionType.metadata;
}
}
''');
final _typeFormalCount = JS('', 'Symbol("_typeFormalCount")');
_functionType(definite, returnType, args, extra) => JS(
'',
'''(() => {
// TODO(jmesserly): this is a bit of a retrofit, to easily fit
// generic functions into all of the existing ways we generate function
// signatures. Given `(T) => [T, [T]]` we'll return a function that does
// `(T) => _functionType(definite, T, [T])` ... we could do this in the
// compiler instead, at a slight cost to code size.
if ($args === void 0 && $extra === void 0) {
const fnTypeParts = $returnType;
// A closure that computes the remaining arguments.
// Return a function that makes the type.
function makeGenericFnType(...types) {
let parts = fnTypeParts.apply(null, types);
return $FunctionType.create($definite, parts[0], parts[1], parts[2]);
}
makeGenericFnType[$_typeFormalCount] = fnTypeParts.length;
return makeGenericFnType;
}
return $FunctionType.create($definite, $returnType, $args, $extra);
})()''');
///
/// Create a "fuzzy" function type. If any arguments are dynamic
/// they will be replaced with bottom.
///
functionType(returnType, args, extra) =>
_functionType(false, returnType, args, extra);
///
/// Create a definite function type. No substitution of dynamic for
/// bottom occurs.
///
definiteFunctionType(returnType, args, extra) =>
_functionType(true, returnType, args, extra);
typedef(name, closure) => JS('', 'new #(#, #)', Typedef, name, closure);
typeName(type) => JS(
'',
'''(() => {
if ($type === void 0) return "undefined type";
if ($type === null) return "null type";
// Non-instance types
if ($type instanceof $TypeRep) {
if ($type instanceof $Typedef) {
return $type.name + "(" + $type.functionType.toString() + ")";
}
return $type.toString();
}
// Wrapped types
if ($type instanceof $WrappedType) {
return "Wrapped(" + $unwrapType($type) + ")";
}
// Instance types
let tag = $_getRuntimeType($type);
if (tag === $Type) {
let name = $type.name;
let args = $getGenericArgs($type);
if (!args) return name;
let result = name;
let allDynamic = true;
result += '<';
for (let i = 0; i < args.length; ++i) {
if (i > 0) result += ', ';
let argName = $typeName(args[i]);
if (argName != 'dynamic') allDynamic = false;
result += argName;
}
result += '>';
// Don't print the type arguments if they are all dynamic. Show "raw"
// types as just the bare type name.
if (allDynamic) return name;
return result;
}
if (tag) return "Not a type: " + tag.name;
return "JSObject<" + $type.name + ">";
})()''');
/// Get the underlying function type, potentially from the call method
/// for a class type.
getImplicitFunctionType(type) {
if (isFunctionType(type)) return type;
return getMethodTypeFromType(type, 'call');
}
bool isFunctionType(type) => JS('bool', '# instanceof # || # === #', type,
AbstractFunctionType, type, Function);
isLazyJSSubtype(LazyJSType t1, LazyJSType t2, covariant) {
if (t1 == t2) return true;
// All anonymous JS types are subtypes of each other.
if (t1._jsTypeCallback == null || t2._jsTypeCallback == null) return true;
return isClassSubType(t1._rawJSType, t2._rawJSType, covariant);
}
/// Returns true if [ft1] <: [ft2].
/// Returns false if [ft1] </: [ft2] in both spec and strong mode
/// Returns null if [ft1] </: [ft2] in strong mode, but spec mode
/// may differ
/// If [covariant] is true, then we are checking subtyping in a covariant
/// position, and hence the direction of the check for function types
/// corresponds to the direction of the check according to the Dart spec.
isFunctionSubtype(ft1, ft2, covariant) => JS(
'',
'''(() => {
if ($ft2 === $Function) {
return true;
}
if ($ft1 === $Function) {
return false;
}
let ret1 = $ft1.returnType;
let ret2 = $ft2.returnType;
let args1 = $ft1.args;
let args2 = $ft2.args;
if (args1.length > args2.length) {
// If we're in a covariant position, then Dart's arity rules
// agree with strong mode, otherwise we can't be sure.
return ($covariant) ? false : null;
}
for (let i = 0; i < args1.length; ++i) {
if (!$_isSubtype(args2[i], args1[i], !$covariant)) {
// Even if isSubtype returns false, assignability
// means that we can't be definitive
return null;
}
}
let optionals1 = $ft1.optionals;
let optionals2 = $ft2.optionals;
if (args1.length + optionals1.length < args2.length + optionals2.length) {
return ($covariant) ? false : null;
}
let j = 0;
for (let i = args1.length; i < args2.length; ++i, ++j) {
if (!$_isSubtype(args2[i], optionals1[j], !$covariant)) {
return null;
}
}
for (let i = 0; i < optionals2.length; ++i, ++j) {
if (!$_isSubtype(optionals2[i], optionals1[j], !$covariant)) {
return null;
}
}
let named1 = $ft1.named;
let named2 = $ft2.named;
let names = $getOwnPropertyNames(named2);
for (let i = 0; i < names.length; ++i) {
let name = names[i];
let n1 = named1[name];
let n2 = named2[name];
if (n1 === void 0) {
return ($covariant) ? false : null;
}
if (!$_isSubtype(n2, n1, !$covariant)) {
return null;
}
}
// Check return type last, so that arity mismatched functions can be
// definitively rejected.
// We allow any type to subtype a void return type, but not vice versa
if (ret2 === $_void) return true;
// Dart allows void functions to subtype dynamic functions, but not
// other functions.
if (ret1 === $_void) return (ret2 === $dynamic);
if (!$_isSubtype(ret1, ret2, $covariant)) return null;
return true;
})()''');
/// TODO(leafp): This duplicates code in operations.dart.
/// I haven't found a way to factor it out that makes the
/// code generator happy though.
_subtypeMemo(f) => JS(
'',
'''(() => {
let memo = new Map();
return (t1, t2) => {
let map = memo.get(t1);
let result;
if (map) {
result = map.get(t2);
if (result !== void 0) return result;
} else {
memo.set(t1, map = new Map());
}
result = $f(t1, t2);
map.set(t2, result);
return result;
};
})()''');
/// Returns true if [t1] <: [t2].
/// Returns false if [t1] </: [t2] in both spec and strong mode
/// Returns undefined if [t1] </: [t2] in strong mode, but spec
/// mode may differ
final isSubtype = JS(
'', '$_subtypeMemo((t1, t2) => (t1 === t2) || $_isSubtype(t1, t2, true))');
_isBottom(type) => JS('bool', '# == #', type, bottom);
_isTop(type) => JS('bool', '# == # || # == #', type, Object, type, dynamic);
_isSubtype(t1, t2, covariant) => JS(
'',
'''(() => {
if ($t1 === $t2) return true;
// Trivially true.
if ($_isTop($t2) || $_isBottom($t1)) {
return true;
}
// Trivially false.
if ($_isBottom($t2)) return null;
if ($_isTop($t1)) {
if ($t1 === $dynamic) return null;
return false;
}
// "Traditional" name-based subtype check. Avoid passing
// function types to the class subtype checks, since we don't
// currently distinguish between generic typedefs and classes.
if (!($t1 instanceof $AbstractFunctionType) &&
!($t2 instanceof $AbstractFunctionType)) {
let result = $isClassSubType($t1, $t2, $covariant);
if (result === true || result === null) return result;
}
// Function subtyping.
// Handle Objects with call methods. Those are functions
// even if they do not *nominally* subtype core.Function.
t1 = $getImplicitFunctionType(t1);
if (!t1) return false;
if ($isFunctionType($t1) && $isFunctionType($t2)) {
return $isFunctionSubtype($t1, $t2, $covariant);
}
if ($t1 instanceof $LazyJSType && $t2 instanceof $LazyJSType) {
return $isLazyJSSubtype($t1, $t2, $covariant);
}
return false;
})()''');
isClassSubType(t1, t2, covariant) => JS(
'',
'''(() => {
// We support Dart's covariant generics with the caveat that we do not
// substitute bottom for dynamic in subtyping rules.
// I.e., given T1, ..., Tn where at least one Ti != dynamic we disallow:
// - S !<: S<T1, ..., Tn>
// - S<dynamic, ..., dynamic> !<: S<T1, ..., Tn>
if ($t1 == $t2) return true;
if ($t1 == $Object) return false;
// If t1 is a JS Object, we may not hit core.Object.
if ($t1 == null) return $t2 == $Object || $t2 == $dynamic;
// Check if t1 and t2 have the same raw type. If so, check covariance on
// type parameters.
let raw1 = $getGenericClass($t1);
let raw2 = $getGenericClass($t2);
if (raw1 != null && raw1 == raw2) {
let typeArguments1 = $getGenericArgs($t1);
let typeArguments2 = $getGenericArgs($t2);
let length = typeArguments1.length;
if (typeArguments2.length == 0) {
// t2 is the raw form of t1
return true;
} else if (length == 0) {
// t1 is raw, but t2 is not
if (typeArguments2.every($_isTop)) return true;
return null;
}
$assert_(length == typeArguments2.length);
for (let i = 0; i < length; ++i) {
let result =
$_isSubtype(typeArguments1[i], typeArguments2[i], $covariant);
if (!result) {
return result;
}
}
return true;
}
let indefinite = false;
function definitive(t1, t2) {
let result = $isClassSubType(t1, t2, $covariant);
if (result == null) {
indefinite = true;
return false;
}
return result;
}
if (definitive($t1.__proto__, $t2)) return true;
// Check mixins.
let mixins = $getMixins($t1);
if (mixins) {
for (let m1 of mixins) {
// TODO(jmesserly): remove the != null check once we can load core libs.
if (m1 != null && definitive(m1, $t2)) return true;
}
}
// Check interfaces.
let getInterfaces = $getImplements($t1);
if (getInterfaces) {
for (let i1 of getInterfaces()) {
// TODO(jmesserly): remove the != null check once we can load core libs.
if (i1 != null && definitive(i1, $t2)) return true;
}
}
// We found no definite supertypes, and at least one indefinite supertype
// so the answer is indefinite.
if (indefinite) return null;
// We found no definite supertypes and no indefinite supertypes, so we
// can return false.
return false;
})()''');
// TODO(jmesserly): this isn't currently used, but it could be if we want
// `obj is NonGroundType<T,S>` to be rejected at runtime instead of compile
// time.
isGroundType(type) => JS(
'',
'''(() => {
// TODO(vsm): Cache this if we start using it at runtime.
if ($type instanceof $AbstractFunctionType) {
if (!$_isTop($type.returnType)) return false;
for (let i = 0; i < $type.args.length; ++i) {
if (!$_isBottom($type.args[i])) return false;
}
for (let i = 0; i < $type.optionals.length; ++i) {
if (!$_isBottom($type.optionals[i])) return false;
}
let names = $getOwnPropertyNames($type.named);
for (let i = 0; i < names.length; ++i) {
if (!$_isBottom($type.named[names[i]])) return false;
}
return true;
}
let typeArgs = $getGenericArgs($type);
if (!typeArgs) return true;
for (let t of typeArgs) {
if (t != $Object && t != $dynamic) return false;
}
return true;
})()''');