Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The VM needs to account for super-bounded types (supertypes of the instantiated-to-bounds type) in type check optimizations #52848

Open
sstrickl opened this issue Jul 5, 2023 · 6 comments
Assignees
Labels
area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. P2 A bug or feature request we're likely to work on triaged Issue has been triaged by sub team

Comments

@sstrickl
Copy link
Contributor

sstrickl commented Jul 5, 2023

When developing CL 312400, I originally put a check in the VM when in DEBUG mode that verified that each type argument for an InterfaceType read from kernel is a subtype of the corresponding type argument when the class type parameters were instantiated to bounds.

However, that check turned out to be too strong for the CFE as it stands currently, as the CFE can produce kernel where this assumption is broken (from a failing build):

output: ../../runtime/vm/compiler/frontend/kernel_translation_helper.cc: 3335: error:
Expected Type: ExhaustivenessCache<X0, dynamic, dynamic, dynamic, X1> to be a subtype of
  Type: ExhaustivenessCache<Object, Object, Object, Object, Object>,
but Type: dynamic is not a subtype of Type: Object

I then introduced a weaker check, that only checked that fully instantiated types read from kernel were subtypes of the type where the class is instantiated to bounds. This allowed the build to succeed, but that check failed on some tests (see this example from the log for one of the failing trybots):

/==============================================================================================\
| co19/Language/Generics/Superbounded_types/class_A05_t01 broke (Pass -> Crash, expected Pass) |
\==============================================================================================/

...

../../runtime/vm/compiler/frontend/kernel_translation_helper.cc: 3333: error:
Expected non-nullable version of Type: A to be a subtype of Type: A<num>

So currently there are cases where the CFE can generate raw (all type arguments are dynamic) or partially raw types for classes where the (partially) raw type is an invalid instantiation of the class. I'm guessing there's a similar issue with TypedefTypes as well, though I haven't verified that on the VM side.

Should the CFE provide a guarantee that all types generated by the CFE are valid subtypes of the instantiated to bounds type?

@sstrickl sstrickl added the area-front-end Use area-front-end for front end / CFE / kernel format related issues. label Jul 5, 2023
@sstrickl
Copy link
Contributor Author

sstrickl commented Jul 5, 2023

/cc @johnniwinther @chloestefantsova for the CFE

@eernstg
Copy link
Member

eernstg commented Jul 5, 2023

Should the CFE provide a guarantee that all types generated by the CFE are valid subtypes of the instantiated to bounds type?

Not quite, IIUC. We do not maintain a constraint that every type in a Dart program is an error unless every declared type parameter bound is satisfied (we use the phrase regular-bounded to characterize a type of the form C<T1 .. Tk> where all actual type arguments satisfy the declared bounds for C, with suitable substitutions). In other words, some types are allowed to violate the declared bounds, in specific ways.

This kind of type is known as a super-bounded type, cf. https://github.com/dart-lang/language/blob/ccadba7019d3f39885c9652011de02a8f6879ed2/specification/dartLangSpec.tex#L7211.

I would assume that the CFE will reflect this language-level rule: The CFE would then only generate regular-bounded "types" when an expression like C<T1 .. Tk>(...) is used to create a new object, but in other situations (like e is T, e as T, T v = e;, and so on), the type (T) can be super-bounded.

A super-bounded type is, roughly, a type where all bounds have been satisfied, except that some subterms can denote a top type (like dynamic or Object?). For example:

class C<X extends num> {}
class D<X extends D<X>> {}

void foo(C<Object?> c, D<D<dynamic>> d) {} // OK.
C<Object> get g => throw 0; // Error, `C<Object>` is not well-bounded.

Instantiation to bounds can yield a super-bounded type: D means D<D<dynamic>>. So it is not safe, in general, to assume that every type which is obtained by instantiation to bounds (full or partial) is regular-bounded. On the other hand, it is a bug if there is any code that attempts to create an object whose run-time type isn't regular-bounded.

@sstrickl
Copy link
Contributor Author

sstrickl commented Jul 5, 2023

Okay, thanks for the clarification, Erik! I think that means the change in 7cc005e is fine and valid, but there may be cases where the CFE may provide a super-bounded type that we should treat as if it were the instantiated-to-bounds type (e.g., optimization of checking the runtime type of instances), and so I should do a followup to make that change. I'll leave this issue open, but move it over to my side of the pond :)

@sstrickl sstrickl added the area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. label Jul 5, 2023
@sstrickl sstrickl changed the title The common front end produces "raw" types that do not respect the bounds on the type parameters The VM needs to account for super-bounded types (supertypes of the instantiated-to-bounds type) in type check optimizations Jul 5, 2023
@sstrickl sstrickl removed the area-front-end Use area-front-end for front end / CFE / kernel format related issues. label Jul 5, 2023
@sstrickl sstrickl self-assigned this Jul 5, 2023
@eernstg
Copy link
Member

eernstg commented Jul 5, 2023

Sounds good, thanks!

(Also, I'm sure it would still be useful if @johnniwinther or @chloestefantsova could clarify how the CFE has chosen to handle super-bounded types, and if there's anything extra to think about).

@eernstg
Copy link
Member

eernstg commented Jul 5, 2023

I should mention that the language specification section about super-bounded types specifies pre-nnbd rules. The rules for the language with null safety are under review in dart-lang/language#2605.

copybara-service bot pushed a commit that referenced this issue Jul 5, 2023
Previously, the FFI transformer could produce is checks where the
type to check against was Pointer<dynamic>. However, given that
the Pointer class is defined as:

abstract class Pointer<X extends NativeType> ...

the instantiated to bounds version of its type is Pointer<NativeType>.
Pointer<dynamic> is not a subtype of Pointer<NativeType>, and thus is an
invalid instantiation, but the only place this type could occur was as
the right hand side of an is check.

Before 7cc005e, Class::RareType() returned the class instantiated with
the null (all-dynamic) type arguments vector. Among other things, this
"rare" type was compared to the right-hand side of is checks and, if it
matched, performed a simple (cid-only) check of the instance type
arguments in unoptimized code.

Afterwards, Class::RareType() returns the class instantiated with a type
arguments vector where each type parameter is instantiated to bounds, so
now the "rare" type check fails and it falls back to the full check of
the instance type arguments, which causes a ~25% regression in some
unoptimized benchmarks.

This CL fixes the generation of those is checks in the FFI transformer
to use the instantiated to bounds version of the Pointer type instead.

TEST=pkg/front_end/test

Issue: #52843
Issue: #52848
Cq-Include-Trybots: luci.dart.try:vm-ffi-qemu-linux-release-riscv64-try,vm-ffi-qemu-linux-release-arm-try,vm-aot-linux-debug-x64-try,vm-linux-debug-x64-try
Change-Id: Ic9ac6d75ba2743e233065444fad13ab098094349
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/312400
Reviewed-by: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Erik Ernst <eernst@google.com>
Auto-Submit: Tess Strickland <sstrickl@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
copybara-service bot pushed a commit that referenced this issue Jul 6, 2023
The CFE may return super-bounded types (types that are a strict
supertype of the rare type) for certain type positions like the right
hand sides of `is` and `as` checks, so we can't assume that all types
involving a given class is a subtype of its rare type. Note that this is
only for uses that do not involve instantiation, as all instances of a
class must have runtime types that are a subtype of its rare type.

Keeping this in mind, here are the changes being made in this CL.

-----

In the flow graph builder when producting unoptimized code, we
previously assumed that if the checked type is not equal to the rare
type, then the code can't use _simpleInstanceOf, which only performs
class ID checks.  However, if the checked type is a supertype of the
rare type, we can, because any instance is guaranteed to be a subtype of
the checked type.

Note that this didn't cause incorrect behavior, just more work than
needed in some cases.

-----

In the generation of optimized type testing stubs, we assumed that the
null type argument vector (TAV) is a possible instance TAV for any
instance. However, if the rare type for a class has a non-null TAV, then
no instance of that class can have a null TAV and we can skip the null
check. (We keep it in the DEBUG case, but just to immediately trigger
a breakpoint if seen.)

Again, no incorrect behavior caused here previously, just an unnecessary
check that we now remove in some cases.

------

Also when generating optimized type testing stubs, when checking for the
null TAV prior to instance type argument checks, the code assumed that
it was possible to have to check type arguments if the checked type was
a super type of the rare type for the instance's class.  However, that's
backwards: if the checked type is a super type, then the runtime type of
any instance of the class is guaranteed to be a subtype and we shouldn't
be checking type arguments at all.

Thus, add an ASSERT that checks this, and instead only generate the code
that goes to the runtime if the null TAV is seen, as the runtime type of
the instance is the rare type and the rare type is guaranteed to not be
a subtype of the checked type.

Again, the previous version of this wouldn't have caused incorrect
behavior either, because the VM should never generate type arguments
checking in the type testing stub if the checked type is a supertype of
the class's rare type. Thus, that branch in the code generation was
just dead code.

-----

TEST=vm/cc/TTS, ci (especially DEBUG bots)

Issue: #52848
Cq-Include-Trybots: luci.dart.try:vm-aot-linux-debug-x64-try,vm-linux-debug-x64-try
Change-Id: I86d159693d6218f88dd1f04dd34cba702744b4d2
Fixed: 52848
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/312500
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
@chloestefantsova
Copy link
Contributor

Sounds good, thanks!

(Also, I'm sure it would still be useful if @johnniwinther or @chloestefantsova could clarify how the CFE has chosen to handle super-bounded types, and if there's anything extra to think about).

We do that like you described: we retain the super-bounded types written by the programmer and we may generate a super-bounded type as the output of the instantiate-to-bound algorithm.

@a-siva a-siva added triaged Issue has been triaged by sub team P2 A bug or feature request we're likely to work on labels Nov 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. P2 A bug or feature request we're likely to work on triaged Issue has been triaged by sub team
Projects
None yet
Development

No branches or pull requests

4 participants