Index: dart/sdk/lib/_internal/lib/js_helper.dart |
diff --git a/dart/sdk/lib/_internal/lib/js_helper.dart b/dart/sdk/lib/_internal/lib/js_helper.dart |
index 6268aafd7b42a9634791e2a774b54a6fcbcc3d29..29d70da550127fa0f1bd04b6583103f06d2d7bd2 100644 |
--- a/dart/sdk/lib/_internal/lib/js_helper.dart |
+++ b/dart/sdk/lib/_internal/lib/js_helper.dart |
@@ -180,12 +180,32 @@ class JSInvocationMirror implements Invocation { |
} else { |
interceptor = null; |
} |
+ bool isCatchAll = false; |
var method = JS('var', '#[#]', receiver, name); |
- if (JS('String', 'typeof #', method) == 'function') { |
+ if (JS('bool', 'typeof # != "function"', method) ) { |
+ String baseName = _symbol_dev.Symbol.getName(memberName); |
+ method = JS('', '#[# + "*"]', receiver, baseName); |
+ if (method == null) { |
+ interceptor = getInterceptor(object); |
+ method = JS('', '#[# + "*"]', interceptor, baseName); |
+ if (method != null) { |
+ isIntercepted = true; |
+ receiver = interceptor; |
+ } else { |
+ interceptor = null; |
+ } |
+ } |
+ isCatchAll = true; |
+ } |
+ if (JS('bool', 'typeof # == "function"', method)) { |
if (!hasReflectableProperty(method)) { |
throwInvalidReflectionError(_symbol_dev.Symbol.getName(memberName)); |
} |
- return new CachedInvocation(method, isIntercepted, interceptor); |
+ if (isCatchAll) { |
+ return new CachedCatchAllInvocation(method, isIntercepted, interceptor); |
+ } else { |
+ return new CachedInvocation(method, isIntercepted, interceptor); |
+ } |
} else { |
// In this case, receiver doesn't implement name. So we should |
// invoke noSuchMethod instead (which will often throw a |
@@ -224,7 +244,7 @@ class CachedInvocation { |
bool get isNoSuchMethod => false; |
- /// Applies [jsFunction] to object with [arguments]. |
+ /// Applies [jsFunction] to [victim] with [arguments]. |
/// Users of this class must take care to check the arguments first. |
invokeOn(Object victim, List arguments) { |
var receiver = victim; |
@@ -238,6 +258,47 @@ class CachedInvocation { |
} |
} |
+class CachedCatchAllInvocation extends CachedInvocation { |
+ CachedCatchAllInvocation(jsFunction, |
+ bool isIntercepted, |
+ Interceptor cachedInterceptor) |
+ : super(jsFunction, isIntercepted, cachedInterceptor); |
+ |
+ invokeOn(Object victim, List arguments) { |
+ ReflectionInfo info = new ReflectionInfo(jsFunction); |
+ var receiver = victim; |
+ int providedArgumentCount; |
+ if (!isIntercepted) { |
+ if (arguments is! JSArray) arguments = new List.from(arguments); |
+ providedArgumentCount = arguments.length; |
+ } else { |
+ arguments = [victim]..addAll(arguments); |
+ if (cachedInterceptor != null) receiver = cachedInterceptor; |
+ providedArgumentCount = arguments.length - 1; |
+ } |
+ int fullParameterCount = |
+ info.requiredParameterCount + info.optionalParameterCount; |
+ if (info.areOptionalParametersNamed && |
+ (providedArgumentCount > info.requiredParameterCount)) { |
+ throw new UnimplementedNoSuchMethodError( |
+ "Invocation of unstubbed method '${info.reflectionName}'" |
+ " with ${arguments.length} arguments."); |
+ } else if (providedArgumentCount < info.requiredParameterCount) { |
+ throw new UnimplementedNoSuchMethodError( |
+ "Invocation of unstubbed method '${info.reflectionName}'" |
+ " with $providedArgumentCount arguments (too few)."); |
+ } else if (providedArgumentCount > fullParameterCount) { |
+ throw new UnimplementedNoSuchMethodError( |
+ "Invocation of unstubbed method '${info.reflectionName}'" |
+ " with $providedArgumentCount arguments (too many)."); |
+ } |
+ for (int i = providedArgumentCount; i < fullParameterCount; i++) { |
+ arguments.add(getMetadata(info.defaultValue(i))); |
+ } |
+ return JS("var", "#.apply(#, #)", jsFunction, receiver, arguments); |
+ } |
+} |
+ |
class CachedNoSuchMethodInvocation { |
/// Non-null interceptor if this is an intercepted call through an |
/// [Interceptor]. |
@@ -253,6 +314,95 @@ class CachedNoSuchMethodInvocation { |
} |
} |
+class ReflectionInfo { |
+ static const int REQUIRED_PARAMETERS_INFO = 0; |
+ static const int OPTIONAL_PARAMETERS_INFO = 1; |
+ static const int FUNCTION_TYPE_INDEX = 2; |
+ static const int FIRST_DEFAULT_ARGUMENT = 3; |
+ |
+ /// A JavaScript function object. |
+ final jsFunction; |
+ |
+ /// Raw reflection information. |
+ final List data; |
+ |
+ /// Is this a getter or a setter. |
+ final bool isAccessor; |
+ |
+ /// Number of required parameters. |
+ final int requiredParameterCount; |
+ |
+ /// Number of optional parameters. |
+ final int optionalParameterCount; |
+ |
+ /// Are optional parameters named. |
+ final bool areOptionalParametersNamed; |
+ |
+ /// Either an index to the function type in [:init.metadata:] or a JavaScript |
+ /// function object which can compute such a type (presumably due to free |
+ /// type variables). |
+ final functionType; |
+ |
+ ReflectionInfo.internal(this.jsFunction, |
+ this.data, |
+ this.isAccessor, |
+ this.requiredParameterCount, |
+ this.optionalParameterCount, |
+ this.areOptionalParametersNamed, |
+ this.functionType); |
+ |
+ factory ReflectionInfo(jsFunction) { |
+ List data = JS('JSExtendableArray|Null', r'#.$reflectionInfo', jsFunction); |
+ if (data == null) return null; |
+ data = JSArray.markFixedList(data); |
+ |
+ int requiredParametersInfo = |
+ JS('int', '#[#]', data, REQUIRED_PARAMETERS_INFO); |
+ int requiredParameterCount = JS('int', '# >> 1', requiredParametersInfo); |
+ bool isAccessor = (requiredParametersInfo & 1) == 1; |
+ |
+ int optionalParametersInfo = JS('int', '#[#]', data, OPTIONAL_PARAMETERS_INFO); |
Johnni Winther
2013/12/05 11:49:56
Long line.
ahe
2013/12/06 15:57:54
Done.
|
+ int optionalParameterCount = JS('int', '# >> 1', optionalParametersInfo); |
+ bool areOptionalParametersNamed = (optionalParametersInfo & 1) == 1; |
+ |
+ var functionType = JS('', '#[#]', data, FUNCTION_TYPE_INDEX); |
+ return new ReflectionInfo.internal( |
+ jsFunction, data, isAccessor, requiredParameterCount, |
+ optionalParameterCount, areOptionalParametersNamed, functionType); |
+ } |
+ |
+ String parameterName(int parameter) { |
+ int metadataIndex = JS('int', '#[# + # + #]', data, parameter, |
+ optionalParameterCount, FIRST_DEFAULT_ARGUMENT); |
+ return JS('String', 'init.metadata[#]', metadataIndex); |
+ } |
+ |
+ int defaultValue(int parameter) { |
+ if (parameter < requiredParameterCount) return null; |
+ return JS('int', '#[# + # - #]', data, |
+ FIRST_DEFAULT_ARGUMENT, parameter, requiredParameterCount); |
+ } |
+ |
+ @NoInline |
+ computeFunctionRti(jsConstructor) { |
+ if (JS('bool', 'typeof # == "number"', functionType)) { |
+ return getMetadata(functionType); |
+ } else if (JS('bool', 'typeof # == "function"', functionType)) { |
+ var fakeInstance = JS('', 'new #()', jsConstructor); |
+ setRuntimeTypeInfo( |
+ fakeInstance, JS('JSExtendableArray', '#["<>"]', fakeInstance)); |
+ return JS('=Object|Null', r'#.apply({$receiver:#})', |
+ functionType, fakeInstance); |
+ } else { |
+ throw new RuntimeError('Unexpected function type'); |
+ } |
+ } |
+ |
+ String get reflectionName => JS('String', r'#.$reflectionName', jsFunction); |
+} |
+ |
+getMetadata(int index) => JS('', 'init.metadata[#]', index); |
+ |
class Primitives { |
/// Isolate-unique ID for caching [JsClosureMirror.function]. |
/// Note the initial value is used by the first isolate (or if there are no |
@@ -680,62 +830,104 @@ class Primitives { |
JS('void', '#[#] = #', object, key, value); |
} |
- static applyFunction(Function function, |
- List positionalArguments, |
- Map<String, dynamic> namedArguments) { |
+ static functionNoSuchMethod(function, |
+ List positionalArguments, |
+ Map<String, dynamic> namedArguments) { |
int argumentCount = 0; |
- StringBuffer buffer = new StringBuffer(); |
List arguments = []; |
+ List namedArgumentList = []; |
if (positionalArguments != null) { |
argumentCount += positionalArguments.length; |
arguments.addAll(positionalArguments); |
} |
- if (JS('bool', r'# in #', JS_GET_NAME('CALL_CATCH_ALL'), function)) { |
- // We expect the closure to have a "call$catchAll" (the value of |
- // JS_GET_NAME('CALL_CATCH_ALL')) function that returns all the expected |
- // named parameters as a (new) JavaScript object literal. The keys in |
- // the object literal correspond to the argument names, and the values |
- // are the default values. The compiler emits the properties sorted by |
- // keys, and this order is preserved in JavaScript, so we don't need to |
- // sort the keys. Since a new object is returned each time we call |
- // call$catchAll, we can simply overwrite default entries with the |
- // provided named arguments. If there are incorrectly named arguments in |
- // [namedArguments], noSuchMethod will be called as expected. |
- var allNamedArguments = |
- JS('var', r'#[#]()', function, JS_GET_NAME('CALL_CATCH_ALL')); |
- if (namedArguments != null && !namedArguments.isEmpty) { |
- namedArguments.forEach((String key, argument) { |
- JS('void', '#[#] = #', allNamedArguments, key, argument); |
- }); |
+ String names = ''; |
+ if (namedArguments != null && !namedArguments.isEmpty) { |
+ namedArguments.forEach((String name, argument) { |
+ names = '$names\$$name'; |
+ namedArgumentList.add(name); |
+ arguments.add(argument); |
+ argumentCount++; |
+ }); |
+ } |
+ |
+ String selectorName = 'call\$$argumentCount$names'; |
+ |
+ return function.noSuchMethod( |
+ createUnmangledInvocationMirror( |
+ #call, |
+ selectorName, |
+ JSInvocationMirror.METHOD, |
+ arguments, |
+ namedArgumentList)); |
+ } |
+ |
+ static applyFunction(Function function, |
+ List positionalArguments, |
+ Map<String, dynamic> namedArguments) { |
+ if (namedArguments != null && !namedArguments.isEmpty) { |
+ // TODO(ahe): The following code can be shared with |
+ // JsInstanceMirror.invoke. |
+ var interceptor = getInterceptor(function); |
+ var jsFunction = JS('', '#["call*"]', interceptor); |
+ |
+ if (jsFunction == null) { |
+ return functionNoSuchMethod( |
+ function, positionalArguments, namedArguments); |
+ } |
+ ReflectionInfo info = new ReflectionInfo(jsFunction); |
+ if (info == null || !info.areOptionalParametersNamed) { |
+ return functionNoSuchMethod( |
+ function, positionalArguments, namedArguments); |
+ } |
+ |
+ if (positionalArguments != null) { |
+ positionalArguments = new List.from(positionalArguments); |
+ } else { |
+ positionalArguments = []; |
+ } |
+ // Check the number of positional arguments is valid. |
+ if (info.requiredParameterCount != positionalArguments.length) { |
+ return functionNoSuchMethod( |
+ function, positionalArguments, namedArguments); |
} |
- List<String> listOfNamedArguments = |
- JS('List', 'Object.getOwnPropertyNames(#)', allNamedArguments); |
- argumentCount += listOfNamedArguments.length; |
- listOfNamedArguments.forEach((String name) { |
- buffer.write('\$$name'); |
- arguments.add(JS('', '#[#]', allNamedArguments, name)); |
+ var defaultArguments = new Map(); |
+ for (int i = 0; i < info.optionalParameterCount; i++) { |
+ var parameterName = info.parameterName(i + info.requiredParameterCount); |
+ var defaultValue = |
+ getMetadata(info.defaultValue(i + info.requiredParameterCount)); |
+ defaultArguments[parameterName] = defaultValue; |
+ } |
+ bool bad = false; |
+ namedArguments.forEach((String parameter, value) { |
+ if (defaultArguments.containsKey(parameter)) { |
+ defaultArguments[parameter] = value; |
+ } else { |
+ // Extraneous named argument. |
+ bad = true; |
+ } |
}); |
- } else { |
- if (namedArguments != null && !namedArguments.isEmpty) { |
- namedArguments.forEach((String name, argument) { |
- buffer.write('\$$name'); |
- arguments.add(argument); |
- argumentCount++; |
- }); |
+ if (bad) { |
+ return functionNoSuchMethod( |
+ function, positionalArguments, namedArguments); |
} |
+ positionalArguments.addAll(defaultArguments.values); |
+ return JS('', '#.apply(#, #)', jsFunction, function, positionalArguments); |
+ } |
+ |
+ int argumentCount = 0; |
+ List arguments = []; |
+ |
+ if (positionalArguments != null) { |
+ argumentCount += positionalArguments.length; |
+ arguments.addAll(positionalArguments); |
} |
- String selectorName = 'call\$$argumentCount$buffer'; |
+ String selectorName = 'call\$$argumentCount'; |
var jsFunction = JS('var', '#[#]', function, selectorName); |
if (jsFunction == null) { |
- return function.noSuchMethod(createUnmangledInvocationMirror( |
- const Symbol('call'), |
- selectorName, |
- JSInvocationMirror.METHOD, |
- arguments, |
- namedArguments == null ? [] : namedArguments.keys.toList())); |
+ return functionNoSuchMethod(function, positionalArguments, namedArguments); |
Johnni Winther
2013/12/05 11:49:56
Long line.
ahe
2013/12/06 15:57:54
Done.
|
} |
// We bound 'this' to [function] because of how we compile |
// closures: escaped local variables are stored and accessed through |
@@ -1531,13 +1723,149 @@ convertDartClosureToJS(closure, int arity) { |
/** |
* Super class for Dart closures. |
*/ |
-class Closure implements Function { |
+abstract class Closure implements Function { |
+ // TODO(ahe): These constants must be in sync with |
+ // reflection_data_parser.dart. |
+ static const FUNCTION_INDEX = 0; |
+ static const NAME_INDEX = 1; |
+ static const CALL_NAME_INDEX = 2; |
+ static const REQUIRED_PARAMETER_INDEX = 3; |
+ static const OPTIONAL_PARAMETER_INDEX = 4; |
+ static const DEFAULT_ARGUMENTS_INDEX = 5; |
+ |
+ Closure(); |
+ |
+ /** |
+ * Creates a closure for use by implicit getters associated with a method. |
+ * |
+ * In other words, creates a tear-off closure. |
+ * |
+ * Called from [closureFromTearOff] as well as from reflection when tearing |
+ * of a method via [:getField:]. |
+ * |
+ * This method assumes that [functions] was created by JavaScript function |
Johnni Winther
2013/12/05 11:49:56
`JavaScript` -> `the JavaScript`
ahe
2013/12/06 15:57:54
Done.
|
+ * `addStubs` in `reflection_data_parser.dart`. That is, a list of JavaScript |
+ * function objects with properties `$stubName` and `$callName`. |
+ * |
+ * Further assumes that [reflectionInfo] was is the end of the array created |
Johnni Winther
2013/12/05 11:49:56
`was is the end` -> `is at the end`
ahe
2013/12/06 15:57:54
Done.
|
+ * by [dart2js.js_emitter.ContainerBuilder.addMemberMethod] starting with |
+ * required parameter count. |
+ * |
+ * Caution: this function may be called when building constants. |
+ * TODO(ahe): Don't call this function when building constants. |
+ */ |
+ factory Closure.fromTearOff(receiver, |
+ List functions, |
+ List reflectionInfo, |
+ bool isStatic, |
+ jsArguments, |
+ String propertyName) { |
+ var function = JS('', '#[#]', functions, 0); |
+ if (isStatic && JS('bool', '"\$tearOff" in #', function)) { |
+ // The implicit closure of a static function is always the same. |
+ return JS('Closure', '#.\$tearOff', function); |
+ } |
+ |
+ // printString('Trying to tear off $propertyName using ' |
Johnni Winther
2013/12/05 11:49:56
Remove debug code.
ahe
2013/12/06 15:57:54
Done.
|
+ // '[${JS("String", "String(#)", reflectionInfo)}]'); |
+ |
+ String name = JS('String|Null', '#.\$stubName', function); |
+ String callName = JS('String|Null', '#.\$callName', function); |
+ |
+ JS('', r'#.$reflectionInfo = #', function, reflectionInfo); |
+ ReflectionInfo info = new ReflectionInfo(function); |
+ |
+ var functionType = info.functionType; |
+ |
+ // Create a closure and "monkey" patch it with call stubs. |
+ Closure closure; |
+ var trampoline = function; |
+ if (!isStatic) { |
+ if (JS('bool', '#.length == 1', jsArguments)) { |
+ // Intercepted call. |
+ var argument = JS('', '#[0]', jsArguments); |
+ trampoline = forwardInterceptedCallTo(argument, receiver, function); |
+ closure = new BoundClosure(receiver, function, argument, name); |
+ } else { |
+ trampoline = forwardTo(receiver, function); |
+ closure = new BoundClosure(receiver, function, null, name); |
+ } |
+ } else { |
+ closure = new TearOffClosure(); |
+ JS('', '#.\$tearOff = #', function, closure); |
+ JS('', r'#.$name = #', closure, propertyName); |
+ } |
+ |
+ var signatureFunction; |
+ if (JS('bool', 'typeof # == "number"', functionType)) { |
+ signatureFunction = |
+ JS('', '(function(s){return function(){return init.metadata[s]}})(#)', |
+ functionType); |
+ } else if (!isStatic |
+ && JS('bool', 'typeof # == "function"', functionType)) { |
+ signatureFunction = functionType; |
+ JS('', r'#.$receiver = #', closure, receiver); |
+ } else { |
+ throw 'Error in reflectionInfo.'; |
+ } |
+ |
+ JS('', '#.\$signature = #', closure, signatureFunction); |
+ |
+ JS('', '#[#] = #', closure, callName, trampoline); |
+ for (int i = 1; i < functions.length; i++) { |
+ var stub = functions[i]; |
+ var stubCallName = JS('String|Null', '#.\$callName', stub); |
+ // TODO(ahe): Support interceptors here. |
+ JS('', '#[#] = #', closure, stubCallName, |
+ isStatic ? stub : forwardTo(receiver, stub)); |
+ } |
+ |
+ JS('', '#["call*"] = #', closure, function); |
+ |
+ return closure; |
+ } |
+ |
+ static forwardTo(receiver, function) { |
+ return JS( |
+ '', |
+ 'function(r,f){return function(){return f.apply(r,arguments)}}(#,#)', |
+ receiver, function); |
+ } |
+ |
+ static forwardInterceptedCallTo(self, interceptor, function) { |
+ return JS( |
+ '', |
+ 'function(i,s,f){return function(){' |
+ 'return f.call.bind(f,i,s).apply(i,arguments)}}(#,#,#)', |
+ interceptor, self, function); |
+ } |
+ |
String toString() => "Closure"; |
} |
+/// Called from implicit method getter (aka tear-off). |
+Closure closureFromTearOff(receiver, |
+ functions, |
+ reflectionInfo, |
+ isStatic, |
+ jsArguments, |
+ name) { |
+ return new Closure.fromTearOff( |
+ receiver, |
+ JSArray.markFixedList(functions), |
+ JSArray.markFixedList(reflectionInfo), |
+ JS('bool', '!!#', isStatic), |
+ jsArguments, |
+ JS('String', '#', name)); |
+} |
+ |
+/// Represents an implicit closure of a function. |
+class TearOffClosure extends Closure { |
+} |
+ |
/// Represents a 'tear-off' closure, that is an instance method bound |
/// to a specific receiver (instance). |
-class BoundClosure extends Closure { |
+class BoundClosure extends TearOffClosure { |
/// The receiver or interceptor. |
// TODO(ahe): This could just be the interceptor, we always know if |
// we need the interceptor when generating the call method. |
@@ -1552,6 +1880,8 @@ class BoundClosure extends Closure { |
/// The name of the function. Only used by the mirror system. |
final String _name; |
+ BoundClosure(this._self, this._target, this._receiver, this._name); |
+ |
bool operator==(other) { |
if (identical(this, other)) return true; |
if (other is! BoundClosure) return false; |
@@ -2403,3 +2733,13 @@ class FunctionTypeInfoDecoderRing { |
return _cachedToString = "$s"; |
} |
} |
+ |
+// TODO(ahe): Remove this class and call noSuchMethod instead. |
+class UnimplementedNoSuchMethodError extends Error |
+ implements NoSuchMethodError { |
+ final String _message; |
+ |
+ UnimplementedNoSuchMethodError(this._message); |
+ |
+ String toString() => "Unsupported operation: $_message"; |
+} |