Skip to content

Commit 027f572

Browse files
nshahanCommit Queue
authored and
Commit Queue
committed
[ddc, dart2js] Add results cache to isSubtype
Optimize repetitive calls to isSubtype with a caches to store pairwise results. There are currently two caches for sound and unsound results but in the future that can be combined into a single cache once the library is aware of error reporting. That single cache could stores "pass", "fail", or "fails when sound mode but passes in unsound null safety". Issue: #48585 Change-Id: I49e5794703fd58f1b2bba50e426e25146800fbb8 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/323707 Reviewed-by: Mayank Patke <[email protected]> Commit-Queue: Nicholas Shahan <[email protected]>
1 parent 2ed87ff commit 027f572

File tree

7 files changed

+214
-30
lines changed

7 files changed

+214
-30
lines changed

pkg/compiler/lib/src/ssa/builder.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,9 @@ class KernelSsaGraphBuilder extends ir.Visitor<void> with ir.VisitorVoidMixin {
398398
return options.enableVariance;
399399
case 'LEGACY':
400400
return options.useLegacySubtyping;
401+
case 'EXTRA_NULL_SAFETY_CHECKS':
402+
// TODO(fishythefish): Handle this flag as needed.
403+
return false;
401404
case 'PRINT_LEGACY_STARS':
402405
return options.printLegacyStars;
403406
default:

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6323,6 +6323,14 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
63236323
// checks. This allows DDC to produce optional warnings or
63246324
// errors when tests pass but would fail in sound null safety.
63256325
: runtimeCall('legacyTypeChecks');
6326+
case 'EXTRA_NULL_SAFETY_CHECKS':
6327+
return _options.soundNullSafety
6328+
? js.boolean(false)
6329+
// When running the new runtime type system with weak null
6330+
// safety this flag gets toggled when performing `is` and `as`
6331+
// checks. This allows DDC to produce optional warnings or
6332+
// errors when tests pass but would fail in sound null safety.
6333+
: runtimeCall('extraNullSafetyChecks');
63266334
case 'MINIFIED':
63276335
return js.boolean(false);
63286336
case 'NEW_RUNTIME_TYPES':

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

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,56 @@ dindex(obj, index) => callMethod(obj, '_get', null, [index], null, '[]');
661661
dsetindex(obj, index, value) =>
662662
callMethod(obj, '_set', null, [index, value], null, '[]=');
663663

664+
// TODO(nshahan): Cleanup the following `is`, `as`, and isSubtype
665+
// implementations.
666+
//
667+
// The logic is currently too convoluted but is temporary while the
668+
// implementation gets added directly to the dart:rti library and moved out of
669+
// the DDC only runtime library.
670+
//
671+
// These methods are dead code when running with sound null safety.
672+
//
673+
// Ideally each runtime should be able to set the compile time flag
674+
// `JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')` and these helpers will no longer be
675+
// needed. The dart:rti library will know how to perform the checks and call a
676+
// method for the backend specific warning/error/logging.
677+
//
678+
// As currently written flow goes as follows:
679+
// 1) A type check (`is`, `as`, or isSubtype) is dispatched to the
680+
// corresponding helper.
681+
// 2) The legacy stars are stripped out of the test type. This is expected to
682+
// prove less impactful over time. Once all code has been migrated to ^2.12
683+
// getting a legacy type on the RHS of a type test is much harder. Possibly
684+
// only achievable through type checks involving type variables
685+
// instantiated in constants.
686+
// 3) Flags are set manually to tell the dart:rti library how to perform the
687+
// upcoming type check:
688+
// - `extraNullSafetyChecks`: This is currently used to signal that if the type
689+
// check reaches the full `isSubtype()` implementation, the legacy stars
690+
// should be erased from the rti that is extracted from the value being
691+
// tested.
692+
// - `legacyTypeChecks`: This is currently used to signal that the test
693+
// should be performed with sound semantics.
694+
// 4) Perform the sound type check using the legacy erased test type. This may
695+
// fall into a fast path optimization for simple checks like
696+
// `val is String?`. If the test involves a more complicated check it might
697+
// trigger the extraction of the rti from the value being tested and passed
698+
// to the full `isSubtype()` implementation. This uncertainty requires the
699+
// two flags described above.
700+
// - Note: `isSubtype()` uses two caches (sound and unsound) to speedup
701+
// repeated checks if the results have already been determined. This
702+
// could be reduced to a single cache when a reporting method is called
703+
// from the dart:rti library directly. The single cache could store one
704+
// of three values: "pass", "fail", and "disagree" (passes when unsound
705+
// but would fail if sound).
706+
// 5) Reset the flags back to the default state.
707+
// 6) If the check passes then it will also pass in weak mode so simply return
708+
// the result.
709+
// 7) Otherwise, perform the same type check in weak mode without erasing
710+
// any legacy types.
711+
// 8) If the result disagrees with the sound mode check issue a warning.
712+
// 9) Return the result.
713+
664714
/// General implementation of the Dart `is` operator.
665715
///
666716
/// Some basic cases are handled directly by the `.is` methods that are attached
@@ -670,22 +720,33 @@ dsetindex(obj, index, value) =>
670720
@JSExportName('is')
671721
bool instanceOf(obj, type) {
672722
if (JS_GET_FLAG('NEW_RUNTIME_TYPES')) {
673-
var testRti = JS<rti.Rti>('!', '#', type);
674723
// When running without sound null safety is type tests are dispatched here
675724
// to issue optional warnings/errors when the result is true but would be
676725
// false with sound null safety.
677-
var legacyErasedRecipe =
678-
rti.Rti.getLegacyErasedRecipe(JS<rti.Rti>('!', '#', testRti));
679-
var legacyErasedType = rti.findType(legacyErasedRecipe);
726+
var testRti = JS<rti.Rti>('!', '#', type);
727+
// TODO(nshahan): Move to isSubtype in dart:rti once all fast path checks
728+
// have been updated to be aware of
729+
// `JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')`.
730+
rti.Rti legacyErasedType;
731+
if (JS_GET_FLAG('PRINT_LEGACY_STARS')) {
732+
// When preserving the legacy stars in the runtime type, avoid caching
733+
// the version with erased types on the Rti.
734+
var legacyErasedRecipe = rti.Rti.getLegacyErasedRecipe(testRti);
735+
legacyErasedType = rti.findType(legacyErasedRecipe);
736+
} else {
737+
legacyErasedType = rti.getLegacyErasedRti(testRti);
738+
}
739+
extraNullSafetyChecks = true;
680740
legacyTypeChecks = false;
681741
var result = JS<bool>('bool', '#.#(#)', legacyErasedType,
682742
JS_GET_NAME(JsGetName.RTI_FIELD_IS), obj);
743+
extraNullSafetyChecks = false;
683744
legacyTypeChecks = true;
684745
if (result) return true;
685746
result =
686747
JS('bool', '#.#(#)', testRti, JS_GET_NAME(JsGetName.RTI_FIELD_IS), obj);
687748
if (result) {
688-
// Type test passes put would fail with sound null safety.
749+
// Type test returned true but would be false with sound null safety.
689750
var t1 = runtimeType(obj);
690751
var t2 = rti.createRuntimeType(testRti);
691752
_nullWarn('$t1 is not a subtype of $t2.');
@@ -713,18 +774,33 @@ cast(obj, type) {
713774
// optional warnings/errors when casts pass but would fail with sound null
714775
// safety.
715776
var testRti = JS<rti.Rti>('!', '#', type);
716-
var objRti = JS<rti.Rti>('!', '#', getReifiedType(obj));
717-
var legacyErasedRecipe = rti.Rti.getLegacyErasedRecipe(testRti);
718-
var legacyErasedType = rti.findType(legacyErasedRecipe);
777+
if (obj == null && !rti.isNullable(testRti)) {
778+
// Allow cast to pass but warn that it would fail in sound null safety.
779+
_nullWarnOnType(type);
780+
return obj;
781+
}
782+
// TODO(nshahan): Move to isSubtype in dart:rti once all fast path checks
783+
// have been updated to be aware of
784+
// `JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')`.
785+
rti.Rti legacyErasedType;
786+
if (JS_GET_FLAG('PRINT_LEGACY_STARS')) {
787+
// When preserving the legacy stars in the runtime type, avoid caching
788+
// the version with erased types on the Rti.
789+
var legacyErasedRecipe = rti.Rti.getLegacyErasedRecipe(testRti);
790+
legacyErasedType = rti.findType(legacyErasedRecipe);
791+
} else {
792+
legacyErasedType = rti.getLegacyErasedRti(testRti);
793+
}
794+
extraNullSafetyChecks = true;
719795
legacyTypeChecks = false;
720-
// Call `isSubtype()` to avoid throwing an error if it fails.
721-
var result = rti.isSubtype(
722-
JS_EMBEDDED_GLOBAL('', RTI_UNIVERSE), objRti, legacyErasedType);
796+
var result = JS<bool>('!', '#.#(#)', legacyErasedType,
797+
JS_GET_NAME(JsGetName.RTI_FIELD_IS), obj);
798+
extraNullSafetyChecks = false;
723799
legacyTypeChecks = true;
724800
if (result) return obj;
725801
// Perform the actual cast and allow the error to be thrown if it fails.
726802
JS('', '#.#(#)', testRti, JS_GET_NAME(JsGetName.RTI_FIELD_AS), obj);
727-
// Subtype check passes put would fail with sound null safety.
803+
// Cast succeeds but would fail with sound null safety.
728804
var t1 = runtimeType(obj);
729805
var t2 = rti.createRuntimeType(testRti);
730806
_nullWarn('$t1 is not a subtype of $t2.');
@@ -756,17 +832,29 @@ bool _isSubtypeWithWarning(@notNull t1, @notNull t2) {
756832
if (JS_GET_FLAG('NEW_RUNTIME_TYPES')) {
757833
var t1Rti = JS<rti.Rti>('!', '#', t1);
758834
var t2Rti = JS<rti.Rti>('!', '#', t2);
759-
var legacyErasedRecipe = rti.Rti.getLegacyErasedRecipe(t2Rti);
760-
var legacyErasedType = rti.findType(legacyErasedRecipe);
835+
// TODO(nshahan): Move to isSubtype in dart:rti once all fast path checks
836+
// have been updated to be aware of
837+
// `JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')`.
838+
rti.Rti legacyErasedType;
839+
if (JS_GET_FLAG('PRINT_LEGACY_STARS')) {
840+
// When preserving the legacy stars in the runtime type, avoid caching
841+
// the version with erased types on the Rti.
842+
var legacyErasedRecipe = rti.Rti.getLegacyErasedRecipe(t2Rti);
843+
legacyErasedType = rti.findType(legacyErasedRecipe);
844+
} else {
845+
legacyErasedType = rti.getLegacyErasedRti(t2Rti);
846+
}
847+
extraNullSafetyChecks = true;
761848
legacyTypeChecks = false;
762849
var validSubtype = rti.isSubtype(
763850
JS_EMBEDDED_GLOBAL('', RTI_UNIVERSE), t1Rti, legacyErasedType);
851+
extraNullSafetyChecks = false;
764852
legacyTypeChecks = true;
765853
if (validSubtype) return true;
766854
validSubtype =
767855
rti.isSubtype(JS_EMBEDDED_GLOBAL('', RTI_UNIVERSE), t1Rti, t2Rti);
768856
if (validSubtype) {
769-
// Subtype check passes put would fail with sound null safety.
857+
// Subtype check passes but would fail with sound null safety.
770858
_nullWarn('${rti.createRuntimeType(t1Rti)} '
771859
'is not a subtype of '
772860
'${rti.createRuntimeType(t2Rti)}.');

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,24 @@ import 'dart:_js_shared_embedded_names';
5656
import 'dart:_rti' as rti
5757
show
5858
bindingRtiFromList,
59-
createRuntimeType,
6059
constructorRtiCachePropertyName,
60+
createRuntimeType,
6161
findType,
6262
getFunctionParametersForDynamicChecks,
6363
getGenericFunctionBounds,
64+
getLegacyErasedRti,
65+
getRecordTypeElementTypes,
66+
getRecordTypeShapeKey,
6467
instanceType,
6568
instantiatedGenericFunctionType,
6669
interfaceTypeRecipePropertyName,
6770
isGenericFunctionType,
71+
isNullable,
72+
isRecordType,
6873
isSubtype,
6974
Rti,
70-
_Type,
71-
substitute,
7275
rtiToString,
73-
isFunctionType,
74-
isObjectType,
75-
isRecordInterfaceType,
76-
isRecordType,
77-
getRecordTypeElementTypes,
78-
getRecordTypeShapeKey,
79-
getLibraryUri;
76+
substitute;
8077

8178
export 'dart:_debugger' show getDynamicStats, clearDynamicStats, trackCall;
8279

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ _throwInvalidFlagError(String message) =>
2525
@notNull
2626
bool legacyTypeChecks = !compileTimeFlag("soundNullSafety");
2727

28+
/// Signals if the next type check should be considered to to be sound when
29+
/// running without sound null safety.
30+
///
31+
/// The provides a way for this library to communicate that intent to the
32+
/// dart:rti library.
33+
///
34+
/// This flag gets inlined by the compiler in the place of
35+
/// `JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')`.
36+
@notNull
37+
bool extraNullSafetyChecks = false;
38+
2839
@notNull
2940
bool _weakNullSafetyWarnings = false;
3041

sdk/lib/_internal/js_shared/lib/rti.dart

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,26 @@ class Rti {
150150
return future;
151151
}
152152

153-
Object? _precomputed2;
154-
Object? _precomputed3;
153+
// TODO(fishythefish): Replace with a single cache that stores one of three
154+
// possible values.
155+
Object? _isSubtypeCache;
156+
Object? _unsoundIsSubtypeCache;
157+
158+
static Object _getIsSubtypeCache(Rti rti) {
159+
if (JS_GET_FLAG('LEGACY')) {
160+
// Read/write unsound cache field.
161+
var probe = rti._unsoundIsSubtypeCache;
162+
if (probe != null) return probe;
163+
Object cache = JS('', 'new Map()');
164+
rti._unsoundIsSubtypeCache = cache;
165+
return cache;
166+
}
167+
var probe = rti._isSubtypeCache;
168+
if (probe != null) return probe;
169+
Object cache = JS('', 'new Map()');
170+
rti._isSubtypeCache = cache;
171+
return cache;
172+
}
155173

156174
/// If kind == kindFunction, stores an object used for checking function
157175
/// parameters in dynamic calls after the first use.
@@ -390,6 +408,17 @@ class Rti {
390408
}
391409
}
392410

411+
// TODO(nshahan): Make private and change the argument type to rti once this
412+
// method is no longer called from outside the library.
413+
Rti getLegacyErasedRti(Object? rti) {
414+
// When preserving the legacy stars in the runtime type no legacy erasure
415+
// happens so the cached version cannot be used.
416+
assert(!JS_GET_FLAG('PRINT_LEGACY_STARS'));
417+
var originalType = _Utils.asRti(rti);
418+
return Rti._getCachedRuntimeType(originalType)?._rti ??
419+
_createAndCacheRuntimeType(originalType)._rti;
420+
}
421+
393422
@pragma('dart2js:types:trust')
394423
@pragma('dart2js:index-bounds:trust')
395424
bool pairwiseIsTest(JSArray fieldRtis, JSArray values) {
@@ -3148,7 +3177,23 @@ class Variance {
31483177

31493178
// Future entry point from compiled code.
31503179
bool isSubtype(Object? universe, Rti s, Rti t) {
3151-
return _isSubtype(universe, s, null, t, null);
3180+
var sType = s;
3181+
// Erase any legacy types that may appear in the Object rti since those mask
3182+
// potential sound mode errors.
3183+
if (JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')) {
3184+
if (JS_GET_FLAG('PRINT_LEGACY_STARS')) {
3185+
var sLegacyErasedRecipe = Rti.getLegacyErasedRecipe(s);
3186+
sType = findType(sLegacyErasedRecipe);
3187+
} else {
3188+
sType = getLegacyErasedRti(s);
3189+
}
3190+
}
3191+
var sCache = Rti._getIsSubtypeCache(sType);
3192+
var probe = _Utils.mapGet(sCache, t);
3193+
if (probe != null) return _Utils.asBool(probe);
3194+
var result = _isSubtype(universe, sType, null, t, null);
3195+
_Utils.mapSet(sCache, t, result);
3196+
return result;
31523197
}
31533198

31543199
/// Based on

tests/dartdevc/weak_null_safety_errors_test.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,42 @@
88

99
import 'package:expect/expect.dart';
1010

11+
/// Code that runs without error when running with unsound null safety but
12+
/// should throw in sound mode or when running DDC with
13+
/// `--weak-null-safety-errors`.
14+
15+
void fn(StringBuffer arg) {}
16+
void testArg<T>(T t) => t is T;
17+
void testReturn<T>(T Function() f) => throw 'do not call';
18+
19+
const c = C<Duration>();
20+
21+
class C<T> {
22+
covariantCheck(List<T> t) {}
23+
const C();
24+
}
25+
1126
void main() {
1227
Expect.throwsTypeError(() => null as int);
1328
dynamic dynamicNull = null;
1429
Expect.throwsTypeError(() => fn(dynamicNull));
15-
}
1630

17-
void fn(StringBuffer arg) {}
31+
var l = [Duration(days: 1), null];
32+
Expect.throwsTypeError(() => l as List<Duration>);
33+
34+
Expect.throwsTypeError(() => (testReturn<Duration?>) as Duration Function());
35+
36+
// Constants get legacy types introduced in their type arguments.
37+
C<Duration?> c2 = c;
38+
Expect.throwsTypeError(() => c2.covariantCheck([Duration(days: 1), null]));
39+
40+
// Tearoff instantiations are "potentially constant" and are treated as a
41+
// constant by the CFE.
42+
// When compiling for unsound null safety the resulting type signature
43+
// attached to the tearoff is `void Function(Duration*)` which is a valid
44+
// subtype of `void Function(Duration?)`. In sound null safety the signature
45+
// is `void Function(Duration)` which should fail in the cast.
46+
Expect.throwsTypeError(() => (testArg<Duration>) as void Function(Duration?));
47+
Expect.throwsTypeError(
48+
() => (testReturn<Duration>) as void Function(Duration? Function()));
49+
}

0 commit comments

Comments
 (0)