Skip to content

Commit ce9b44e

Browse files
nshahanCommit Queue
authored and
Commit Queue
committed
[ddc] Add JSInterop support in the new type system
Encodes the necessary type rules and adds type identifier tags to support the legacy JavaScript interop behavior. Issue: #48585 Change-Id: I933db1c9899711898fb829821f3e051043f33be2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/305845 Reviewed-by: Srujan Gaddam <[email protected]> Commit-Queue: Nicholas Shahan <[email protected]> Reviewed-by: Mayank Patke <[email protected]>
1 parent f1e3bfc commit ce9b44e

File tree

9 files changed

+202
-33
lines changed

9 files changed

+202
-33
lines changed

pkg/dev_compiler/lib/src/kernel/compiler.dart

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,54 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
524524
}
525525
var universeClass =
526526
rtiLibrary.classes.firstWhere((cls) => cls.name == '_Universe');
527-
var typeRules = _typeRecipeGenerator.visitedInterfaceTypeRules;
528-
var template = "#._Universe.#(#, JSON.parse('${jsonEncode(typeRules)}'))";
529-
var addRulesStatement = js.call(template, [
530-
emitLibraryName(rtiLibrary),
531-
_emitMemberName('addRules', memberClass: universeClass),
532-
runtimeCall('typeUniverse')
533-
]).toStatement();
534-
moduleItems.add(addRulesStatement);
527+
var typeRules = _typeRecipeGenerator.liveInterfaceTypeRules;
528+
if (typeRules.isNotEmpty) {
529+
var template = '#._Universe.#(#, JSON.parse(#))';
530+
var addRulesStatement = js.call(template, [
531+
emitLibraryName(rtiLibrary),
532+
_emitMemberName('addRules', memberClass: universeClass),
533+
runtimeCall('typeUniverse'),
534+
js.string(jsonEncode(typeRules), "'")
535+
]).toStatement();
536+
moduleItems.add(addRulesStatement);
537+
}
538+
// Update type rules for `LegacyJavaScriptObject` to add all interop
539+
// types in this module as a supertype.
540+
var updateRules = _typeRecipeGenerator.updateLegacyJavaScriptObjectRules;
541+
if (updateRules.isNotEmpty) {
542+
// All JavaScript interop classes should be mutual subtypes with
543+
// `LegacyJavaScriptObject`. To achieve this the rules are manually
544+
// added here. There is special redirecting rule logic in the dart:_rti
545+
// library for interop types because otherwise they would duplicate
546+
// a lot of supertype information.
547+
var updateRulesStatement =
548+
js.statement('#._Universe.#(#, JSON.parse(#))', [
549+
emitLibraryName(rtiLibrary),
550+
_emitMemberName('addOrUpdateRules', memberClass: universeClass),
551+
runtimeCall('typeUniverse'),
552+
js.string(jsonEncode(updateRules), "'")
553+
]);
554+
moduleItems.add(updateRulesStatement);
555+
}
556+
var jsInteropTypeRecipes =
557+
_typeRecipeGenerator.visitedJsInteropTypeRecipes;
558+
if (jsInteropTypeRecipes.isNotEmpty) {
559+
// Update the `LegacyJavaScriptObject` class with the type tags for all
560+
// interop types in this module. This is the quick path for simple type
561+
// tests that matches the rules encoded above.
562+
var legacyJavaScriptObjectClass = _coreTypes.index
563+
.getClass('dart:_interceptors', 'LegacyJavaScriptObject');
564+
var legacyJavaScriptObjectClassRef = _emitClassRef(
565+
legacyJavaScriptObjectClass.getThisType(
566+
_coreTypes, Nullability.nonNullable));
567+
var interopRecipesArray = js_ast.stringArray([
568+
_typeRecipeGenerator.interfaceTypeRecipe(legacyJavaScriptObjectClass),
569+
...jsInteropTypeRecipes
570+
]);
571+
var jsInteropRules = runtimeStatement('addRtiResources(#, #)',
572+
[legacyJavaScriptObjectClassRef, interopRecipesArray]);
573+
moduleItems.add(jsInteropRules);
574+
}
535575
}
536576

537577
// Visit directives (for exports)
@@ -6043,6 +6083,10 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
60436083
}
60446084

60456085
if (args.length == 1) {
6086+
if (name == 'getInterceptor') {
6087+
var argExpression = args.single.accept(this);
6088+
return runtimeCall('getInterceptorForRti(#)', [argExpression]);
6089+
}
60466090
if (name == 'JS_GET_NAME') {
60476091
var staticGet = args.single as StaticGet;
60486092
var enumField = staticGet.target as Field;

pkg/dev_compiler/lib/src/kernel/type_recipe_generator.dart

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:path/path.dart' as p;
1212

1313
import '../compiler/js_names.dart';
1414
import 'future_or_normalizer.dart';
15+
import 'js_interop.dart';
1516
import 'kernel_helpers.dart';
1617
import 'type_environment.dart';
1718

@@ -64,6 +65,9 @@ class TypeRecipeGenerator {
6465
/// Returns a mapping of type hierarchies for all [InterfaceType]s that have
6566
/// appeared in type recipes.
6667
///
68+
/// While the value of the returned map is typed as `Object` it is always
69+
/// either a `String` or a `Map<String, List<String>>`.
70+
///
6771
/// This mapping is intended to satisfy the requirements for the
6872
/// `_Universe.addRules()` static method in the runtime rti library.
6973
///
@@ -78,11 +82,35 @@ class TypeRecipeGenerator {
7882
/// "type n": {...}}'
7983
/// ```
8084
///
85+
/// Additionally, the runtime rti library supports a special forwarding rule
86+
/// for non-static JavaScript interop types. With this format every interop
87+
/// type simply forwards to `LegacyJavaScriptObject` where they all share
88+
/// a single type rule
89+
///
90+
/// ```
91+
/// '{"interop type 0": "LegacyJavaScriptObject",
92+
/// ...
93+
/// "interop type n": "LegacyJavaScriptObject"}'
94+
/// ```
95+
///
8196
/// It is expected that this method is called at the end of a compilation
8297
/// after all types used in the module have already been visited. Otherwise
8398
/// the results will be incomplete.
84-
Map<String, Map<String, List<String>>> get visitedInterfaceTypeRules {
85-
var rules = <String, Map<String, List<String>>>{};
99+
Map<String, Object> get liveInterfaceTypeRules {
100+
// All non-static JavaScript interop classes should be mutual subtypes with
101+
// 'LegacyJavaScriptObject`. To achieve this the rules are manually
102+
// added here. There is special redirecting rule logic in the dart:_rti
103+
// library for interop types because otherwise they would duplicate
104+
// a lot of supertype information.
105+
var legacyJavaScriptObjectRecipe = interfaceTypeRecipe(_coreTypes.index
106+
.getClass('dart:_interceptors', 'LegacyJavaScriptObject'));
107+
var rules = <String, Object>{
108+
for (var recipe in visitedJsInteropTypeRecipes)
109+
recipe: legacyJavaScriptObjectRecipe
110+
};
111+
112+
// All Dart types hierarchy rules are generated by exploring their class
113+
// hierarchy.
86114
for (var type in _recipeVisitor.visitedInterfaceTypes) {
87115
var recipe = interfaceTypeRecipe(type.classNode);
88116
var cls = type.classNode;
@@ -121,8 +149,30 @@ class TypeRecipeGenerator {
121149
return rules;
122150
}
123151

152+
/// Returns a mapping of type hierarchies for all [InterfaceType]s that have
153+
/// appeared in type recipes.
154+
Map<String, Object> get updateLegacyJavaScriptObjectRules {
155+
var recipes = visitedJsInteropTypeRecipes;
156+
return {
157+
if (recipes.isNotEmpty)
158+
interfaceTypeRecipe(_coreTypes.index
159+
.getClass('dart:_interceptors', 'LegacyJavaScriptObject')):
160+
// Update type rules for `LegacyJavaScriptObject` to add all interop
161+
// types in this module as a supertype.
162+
{
163+
for (var interopType in _recipeVisitor._visitedJsInteropTypes)
164+
interfaceTypeRecipe(interopType.classNode): _recipeVisitor
165+
._listOfJSAnyType(interopType.typeArguments.length)
166+
}
167+
};
168+
}
169+
124170
String interfaceTypeRecipe(Class node) =>
125171
_recipeVisitor.interfaceTypeRecipe(node);
172+
173+
Iterable<String> get visitedJsInteropTypeRecipes =>
174+
_recipeVisitor.visitedJsInteropTypes
175+
.map((type) => interfaceTypeRecipe(type.classNode));
126176
}
127177

128178
/// A visitor to generate type recipe strings from a [DartType].
@@ -155,6 +205,7 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
155205

156206
/// All of the [InterfaceType]s visited.
157207
final _visitedInterfaceTypes = <InterfaceType>{};
208+
final _visitedJsInteropTypes = <InterfaceType>{};
158209
final CoreTypes _coreTypes;
159210

160211
_TypeRecipeVisitor(this._typeEnvironment, this._coreTypes);
@@ -176,6 +227,10 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
176227
Iterable<InterfaceType> get visitedInterfaceTypes =>
177228
Set.unmodifiable(_visitedInterfaceTypes);
178229

230+
/// The JavaScript interop types that have been visited.
231+
Iterable<InterfaceType> get visitedJsInteropTypes =>
232+
Set.unmodifiable(_visitedJsInteropTypes);
233+
179234
@override
180235
String defaultDartType(DartType node) =>
181236
throw UnimplementedError('Unknown DartType: $node');
@@ -188,15 +243,17 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
188243

189244
@override
190245
String visitInterfaceType(InterfaceType node) {
246+
var cls = node.classNode;
191247
addLiveType(node);
192248
// Generate the interface type recipe.
193-
var recipeBuffer = StringBuffer(interfaceTypeRecipe(node.classNode));
249+
var recipeBuffer = StringBuffer(interfaceTypeRecipe(cls));
194250
// Generate the recipes for all type arguments.
195251
if (node.typeArguments.isNotEmpty) {
252+
var argumentRecipes = hasJSInteropAnnotation(cls)
253+
? _listOfJSAnyType(node.typeArguments.length)
254+
: node.typeArguments.map((typeArgument) => typeArgument.accept(this));
196255
recipeBuffer.write(Recipe.startTypeArgumentsString);
197-
recipeBuffer.writeAll(
198-
node.typeArguments.map((typeArgument) => typeArgument.accept(this)),
199-
Recipe.separatorString);
256+
recipeBuffer.writeAll(argumentRecipes, Recipe.separatorString);
200257
recipeBuffer.write(Recipe.endTypeArgumentsString);
201258
}
202259
// Add nullability.
@@ -370,6 +427,17 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
370427
}
371428
}
372429

430+
/// Returns a list containing [n] copies of the "any" type recipe.
431+
///
432+
/// Used exclusively for the type arguments of the older package:js style
433+
/// JavaScript interop types when they are defined with generic type
434+
/// parameters. These are not used to support the newer static JavaScript
435+
/// interop.
436+
List<String> _listOfJSAnyType(int n) => n == 0
437+
? const []
438+
: List.filled(
439+
n, '${Recipe.pushAnyExtensionString}${Recipe.extensionOpString}');
440+
373441
/// Manually record [type] as being "live" without generating a recipe.
374442
///
375443
/// "Live" types here refer to the interface types that could potentially flow
@@ -393,7 +461,9 @@ class _TypeRecipeVisitor extends DartTypeVisitor<String> {
393461
var cls = type.classNode;
394462
var typeWithoutNullability =
395463
cls.getThisType(_coreTypes, Nullability.nonNullable);
396-
_visitedInterfaceTypes.add(typeWithoutNullability);
464+
hasJSInteropAnnotation(cls)
465+
? _visitedJsInteropTypes.add(typeWithoutNullability)
466+
: _visitedInterfaceTypes.add(typeWithoutNullability);
397467
}
398468
}
399469
}

sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,17 @@ bind(obj, name, method) {
5858
var objType = getType(obj);
5959
var methodType = getMethodType(objType, name);
6060
if (JS_GET_FLAG('NEW_RUNTIME_TYPES')) {
61-
if (rti.isGenericFunctionType(methodType)) {
62-
// Attach the default type argument values to the new function in case
63-
// they are needed for a dynamic call.
64-
var defaultTypeArgs = getMethodDefaultTypeArgs(objType, name);
65-
JS('', '#._defaultTypeArgs = #', f, defaultTypeArgs);
61+
// Native JavaScript methods do not have Dart signatures attached that need
62+
// to be copied.
63+
if (methodType != null) {
64+
if (rti.isGenericFunctionType(methodType)) {
65+
// Attach the default type argument values to the new function in case
66+
// they are needed for a dynamic call.
67+
var defaultTypeArgs = getMethodDefaultTypeArgs(objType, name);
68+
JS('', '#._defaultTypeArgs = #', f, defaultTypeArgs);
69+
}
70+
JS('', '#[#] = #', f, JS_GET_NAME(JsGetName.SIGNATURE_NAME), methodType);
6671
}
67-
JS('', '#[#] = #', f, JS_GET_NAME(JsGetName.SIGNATURE_NAME), methodType);
6872
} else {
6973
JS('', '#[#] = #', f, _runtimeType, methodType);
7074
}

sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ RecordType getRecordType(RecordImpl obj) {
120120
}
121121

122122
/// Returns the interceptor for [obj] as needed by the dart:rti library.
123+
///
124+
/// Calls to this method are generated by the compiler.
123125
@notNull
124126
Object getInterceptorForRti(obj) {
125127
var classRef;
@@ -134,13 +136,19 @@ Object getInterceptorForRti(obj) {
134136
case 'function':
135137
var signature =
136138
JS('', '#[#]', obj, JS_GET_NAME(JsGetName.SIGNATURE_NAME));
137-
if (signature != null) classRef = JS_CLASS_REF(Function);
139+
classRef = signature != null
140+
? JS_CLASS_REF(Function)
141+
// Dart functions should always be tagged with a signature, assume
142+
// this must be a JavaScript function.
143+
: JS_CLASS_REF(JavaScriptFunction);
138144
break;
139145
default:
140146
// The interceptors for native JavaScript types like bool, string, etc.
141147
// (excluding number and function, see above) are stored as a symbolized
142148
// property and can be accessed from the native value itself.
143149
classRef = JS('', '#[#]', obj, _extensionType);
150+
// If there is no extension type then this object must not be from Dart.
151+
if (classRef == null) classRef = JS_CLASS_REF(LegacyJavaScriptObject);
144152
}
145153
}
146154
if (classRef == null) throw 'Unknown interceptor for object: ($obj)';
@@ -170,8 +178,7 @@ getReifiedType(obj) {
170178
var signature =
171179
JS('', '#[#]', obj, JS_GET_NAME(JsGetName.SIGNATURE_NAME));
172180
if (signature != null) return signature;
173-
// TODO(nshahan) Handle JS interop functions.
174-
throw "Unknown function type";
181+
return typeRep<JavaScriptFunction>();
175182
case "undefined":
176183
return typeRep<Null>();
177184
case "number":

sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/runtime.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'dart:_foreign_helper'
2121
spread;
2222
import 'dart:_interceptors'
2323
show
24+
JavaScriptFunction,
2425
JSArray,
2526
JSInt,
2627
jsNull,

sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,20 @@ class DynamicType extends DartType {
184184
}
185185

186186
@notNull
187-
bool _isJsObject(obj) =>
188-
JS('!', '# === #', getReifiedType(obj), typeRep<LegacyJavaScriptObject>());
187+
bool _isJsObject(obj) => JS_GET_FLAG("NEW_RUNTIME_TYPES")
188+
? obj is LegacyJavaScriptObject
189+
: JS(
190+
'!', '# === #', getReifiedType(obj), typeRep<LegacyJavaScriptObject>());
189191

190-
/// Asserts that [f] is a native JS functions and returns it if so.
192+
/// Asserts that [f] is a native JS function and returns it if so.
191193
///
192194
/// This function should be used to ensure that a function is a native JS
193195
/// function before it is passed to native JS code.
196+
///
197+
/// NOTE: The generic type argument bound is not enforced due to the
198+
/// `@NoReifyGeneric` annotation. In practice values of other types are passed
199+
/// as [f]. All non-function values are allowed to pass through as well and
200+
/// are returned without error.
194201
@NoReifyGeneric()
195202
F assertInterop<F extends Function?>(F f) {
196203
assert(
@@ -202,7 +209,13 @@ F assertInterop<F extends Function?>(F f) {
202209

203210
bool isDartFunction(obj) =>
204211
JS<bool>('!', '# instanceof Function', obj) &&
205-
JS<bool>('!', '#[#] != null', obj, _runtimeType);
212+
JS<bool>(
213+
'!',
214+
'#[#] != null',
215+
obj,
216+
JS_GET_FLAG("NEW_RUNTIME_TYPES")
217+
? JS_GET_NAME(JsGetName.SIGNATURE_NAME)
218+
: _runtimeType);
206219

207220
Expando<Function> _assertInteropExpando = Expando<Function>();
208221

sdk/lib/_internal/js_dev_runtime/private/foreign_helper.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,11 @@ external JS_EMBEDDED_GLOBAL(String typeDescription, String name);
297297
external JS_BUILTIN(String typeDescription, JsBuiltin builtin,
298298
[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11]);
299299

300-
/// Returns the interceptor for [object].
300+
/// Returns the interceptor for [object] as needed by the dart:rti library.
301301
///
302-
// TODO(nshahan) Replace calls at compile time?
303-
Object getInterceptor(obj) => dart.getInterceptorForRti(obj);
302+
/// Calls to this method are replaced with `getInterceptorForRti()`.
303+
@notNull
304+
external Object getInterceptor(obj);
304305

305306
/// Returns the Rti object for the type for JavaScript arrays via JS-interop.
306307
///

sdk/lib/_internal/js_dev_runtime/private/interceptors.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,5 +312,9 @@ setDispatchProperty(object, value) {}
312312
// all web tests.
313313
findInterceptorForType(Type? type) {}
314314

315-
// TODO(nshahan) Find a correct representation for JS functions.
316-
typedef JavaScriptFunction = dart.FunctionType;
315+
/// Interceptor class for JavaScript function objects and Dart functions that
316+
/// have been converted to JavaScript functions.
317+
///
318+
/// A reference to this class is only used by `getInterceptor()` to return to
319+
/// the dart:rti library because stores information used for type checks.
320+
class JavaScriptFunction extends LegacyJavaScriptObject implements Function {}

0 commit comments

Comments
 (0)