[VM/CFE] Fix expression compilation with record types

Fixes https://github.com/dart-lang/sdk/issues/56859

TEST=pkg/vm_service/test/evaluate_with_record_{1,2}_test.dart + CFE test

Change-Id: I4fdd2bc8e6c04fc59aaad28a192fe0b1833f000d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/388841
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
Jens Johansen
2024-10-09 11:01:57 +00:00
committed by Commit Queue
parent 21a4425043
commit 54f1ee4067
7 changed files with 294 additions and 43 deletions
@@ -9,7 +9,9 @@ import 'package:kernel/ast.dart'
DynamicType,
InterfaceType,
Library,
NamedType,
Nullability,
RecordType,
TypeParameter;
import 'package:kernel/library_index.dart' show LibraryIndex;
@@ -91,8 +93,8 @@ List<ParsedType> parseDefinitionTypes(List<String> definitionTypes) {
int i = 0;
List<ParsedType> argumentReceivers = [];
while (i < definitionTypes.length) {
String uriOrNullString = definitionTypes[i];
if (uriOrNullString == "null") {
String uriOrSpecialString = definitionTypes[i];
if (uriOrSpecialString == "null") {
if (argumentReceivers.isEmpty) {
result.add(new ParsedType.nullType());
} else {
@@ -103,6 +105,30 @@ List<ParsedType> parseDefinitionTypes(List<String> definitionTypes) {
}
i++;
continue;
} else if (uriOrSpecialString == "record") {
// Record.
// We expect at least 4 elements: "record", nullability, num fields,
// num positional fields.
if (i + 4 > definitionTypes.length) throw "invalid input";
int nullability = int.parse(definitionTypes[i + 1]);
int numFields = int.parse(definitionTypes[i + 2]);
int numPositionalFields = int.parse(definitionTypes[i + 3]);
i += 4;
List<String?> fieldNames = new List<String?>.filled(numFields, null);
for (int j = numPositionalFields; j < numFields; j++) {
fieldNames[j] = definitionTypes[i++];
}
ParsedType type = new ParsedType.record(nullability, fieldNames);
if (argumentReceivers.isEmpty) {
result.add(type);
} else {
argumentReceivers.removeLast().arguments!.add(type);
}
for (int j = 0; j < numFields; j++) {
argumentReceivers.add(type);
}
} else {
// We expect at least 4 elements: Uri, class name, nullability,
// number of type arguments.
@@ -110,7 +136,8 @@ List<ParsedType> parseDefinitionTypes(List<String> definitionTypes) {
String className = definitionTypes[i + 1];
int nullability = int.parse(definitionTypes[i + 2]);
int typeArgumentsCount = int.parse(definitionTypes[i + 3]);
ParsedType type = new ParsedType(uriOrNullString, className, nullability);
ParsedType type =
new ParsedType.interface(uriOrSpecialString, className, nullability);
if (argumentReceivers.isEmpty) {
result.add(type);
} else {
@@ -128,26 +155,44 @@ List<ParsedType> parseDefinitionTypes(List<String> definitionTypes) {
return result;
}
enum ParsedTypeKind {
Null,
Interface,
Record,
}
// Coverage-ignore(suite): Not run.
class ParsedType {
final ParsedTypeKind type;
final String? uri;
final String? className;
final int? nullability;
final List<ParsedType>? arguments;
final List<String?>? recordFieldNames;
bool get isNullType => uri == null;
ParsedType.interface(this.uri, this.className, this.nullability)
: type = ParsedTypeKind.Interface,
arguments = [],
recordFieldNames = null;
ParsedType(this.uri, this.className, this.nullability) : arguments = [];
ParsedType.record(this.nullability, this.recordFieldNames)
: type = ParsedTypeKind.Record,
uri = null,
className = null,
arguments = [];
ParsedType.nullType()
: uri = null,
: type = ParsedTypeKind.Null,
uri = null,
className = null,
nullability = null,
arguments = null;
arguments = null,
recordFieldNames = null;
@override
bool operator ==(Object other) {
if (other is! ParsedType) return false;
if (type != other.type) return false;
if (uri != other.uri) return false;
if (className != other.className) return false;
if (nullability != other.nullability) return false;
@@ -157,42 +202,81 @@ class ParsedType {
if (arguments![i] != other.arguments![i]) return false;
}
}
if (recordFieldNames?.length != other.recordFieldNames?.length) {
return false;
}
if (recordFieldNames != null) {
for (int i = 0; i < recordFieldNames!.length; i++) {
if (recordFieldNames![i] != other.recordFieldNames![i]) return false;
}
}
return true;
}
@override
int get hashCode {
if (isNullType) return 0;
if (type == ParsedTypeKind.Null) return 0;
int hash = 0x3fffffff & uri.hashCode;
hash = 0x3fffffff & (hash * 31 + (hash ^ className.hashCode));
hash = 0x3fffffff & (hash * 31 + (hash ^ nullability.hashCode));
for (ParsedType argument in arguments!) {
hash = 0x3fffffff & (hash * 31 + (hash ^ argument.hashCode));
}
if (recordFieldNames != null) {
for (String? name in recordFieldNames!) {
hash = 0x3fffffff & (hash * 31 + (hash ^ name.hashCode));
}
}
return hash;
}
@override
String toString() {
if (isNullType) return "null-type";
return "$uri[$className] ($nullability) <$arguments>";
switch (type) {
case ParsedTypeKind.Null:
return "null-type";
case ParsedTypeKind.Interface:
return "Record[$recordFieldNames] ($nullability) ($arguments)";
case ParsedTypeKind.Record:
if (arguments?.isEmpty ?? true) {
return "$uri[$className] ($nullability)";
}
return "$uri[$className] ($nullability) <$arguments>";
}
}
DartType createDartType(LibraryIndex libraryIndex) {
if (isNullType) return new DynamicType();
Class? classNode = libraryIndex.tryGetClass(uri!, className!);
if (classNode == null) return new DynamicType();
switch (type) {
case ParsedTypeKind.Null:
return new DynamicType();
case ParsedTypeKind.Record:
List<DartType> positional = [];
List<NamedType> named = [];
for (int i = 0; i < arguments!.length; i++) {
String? name = recordFieldNames![i];
DartType type = arguments![i].createDartType(libraryIndex);
if (name == null) {
positional.add(type);
} else {
named.add(new NamedType(name, type));
}
}
return new RecordType(positional, named, _getDartNullability());
case ParsedTypeKind.Interface:
Class? classNode = libraryIndex.tryGetClass(uri!, className!);
if (classNode == null) return new DynamicType();
return new InterfaceType(
classNode,
_getDartNullability(),
arguments
?.map((e) => e.createDartType(libraryIndex))
.toList(growable: false));
return new InterfaceType(
classNode,
_getDartNullability(),
arguments
?.map((e) => e.createDartType(libraryIndex))
.toList(growable: false));
}
}
Nullability _getDartNullability() {
if (isNullType) throw "No nullability on the null type";
if (type == ParsedTypeKind.Null) throw "No nullability on the null type";
if (nullability == 0) return Nullability.nullable;
if (nullability == 1) return Nullability.nonNullable;
if (nullability == 2) return Nullability.legacy;
@@ -206,9 +290,14 @@ Set<String> collectParsedTypeUris(List<ParsedType> parsedTypes) {
List<ParsedType> workList = new List.from(parsedTypes);
while (workList.isNotEmpty) {
ParsedType type = workList.removeLast();
if (type.isNullType) continue;
result.add(type.uri!);
workList.addAll(type.arguments!);
if (type.arguments != null) workList.addAll(type.arguments!);
switch (type.type) {
case ParsedTypeKind.Null:
case ParsedTypeKind.Record:
continue;
case ParsedTypeKind.Interface:
result.add(type.uri!);
}
}
return result;
}
@@ -17,12 +17,12 @@ void main() {
"1",
"0",
]),
[new ParsedType("dart:core", "_OneByteString", 1)]);
[new ParsedType.interface("dart:core", "_OneByteString", 1)]);
// List<something it can't represent which thus becomes an explicit
// dynamic/null>, kNonNullable.
expect(parseDefinitionTypes(["dart:core", "List", "1", "1", "null"]), [
new ParsedType("dart:core", "List", 1)
new ParsedType.interface("dart:core", "List", 1)
..arguments!.add(new ParsedType.nullType())
]);
@@ -39,8 +39,8 @@ void main() {
"0",
]),
[
new ParsedType("dart:core", "_GrowableList", 1)
..arguments!.add(new ParsedType("dart:core", "int", 1))
new ParsedType.interface("dart:core", "_GrowableList", 1)
..arguments!.add(new ParsedType.interface("dart:core", "int", 1))
]);
// Map<int, int>, kNonNullable
@@ -60,9 +60,9 @@ void main() {
"0",
]),
[
new ParsedType("dart:core", "Map", 1)
..arguments!.add(new ParsedType("dart:core", "int", 1))
..arguments!.add(new ParsedType("dart:core", "int", 1))
new ParsedType.interface("dart:core", "Map", 1)
..arguments!.add(new ParsedType.interface("dart:core", "int", 1))
..arguments!.add(new ParsedType.interface("dart:core", "int", 1))
]);
// [0] = String
@@ -102,33 +102,52 @@ void main() {
]),
<ParsedType>[
// String
new ParsedType("dart:core", "_OneByteString", 1),
new ParsedType.interface("dart:core", "_OneByteString", 1),
// int
new ParsedType("dart:core", "_Smi", 1),
new ParsedType.interface("dart:core", "_Smi", 1),
// List<String>
new ParsedType("dart:core", "_GrowableList", 1)
new ParsedType.interface("dart:core", "_GrowableList", 1)
..arguments!.addAll([
new ParsedType("dart:core", "String", 1),
new ParsedType.interface("dart:core", "String", 1),
]),
// Bar
new ParsedType("file://wherever/t.dart", "Bar", 1),
new ParsedType.interface("file://wherever/t.dart", "Bar", 1),
// null value
new ParsedType.nullType(),
// HashMap<Map<int, List<int>>, List<String>>
new ParsedType("dart:collection", "_InternalLinkedHashMap", 1)
new ParsedType.interface("dart:collection", "_InternalLinkedHashMap", 1)
..arguments!.addAll([
new ParsedType("dart:core", "Map", 1)
new ParsedType.interface("dart:core", "Map", 1)
..arguments!.addAll([
new ParsedType("dart:core", "int", 1),
new ParsedType("dart:core", "List", 1)
new ParsedType.interface("dart:core", "int", 1),
new ParsedType.interface("dart:core", "List", 1)
..arguments!.addAll([
new ParsedType("dart:core", "int", 1),
new ParsedType.interface("dart:core", "int", 1),
]),
]),
new ParsedType("dart:core", "List", 1)
new ParsedType.interface("dart:core", "List", 1)
..arguments!.addAll([
new ParsedType("dart:core", "String", 1),
new ParsedType.interface("dart:core", "String", 1),
]),
]),
]);
// Set<(int, {int foo})>, kNonNullable
expect(
parseDefinitionTypes([
//
/**/ "dart:_compact_hash", "_Set", "1", "1",
/* */ "record", "1", "2", "1", "foo",
/* */ "dart:core", "int", "1", "0",
/* */ "dart:core", "int", "1", "0",
]),
[
new ParsedType.interface("dart:_compact_hash", "_Set", 1)
..arguments!.add(
new ParsedType.record(1, [null, "foo"])
..arguments!.add(new ParsedType.interface("dart:core", "int", 1))
..arguments!.add(new ParsedType.interface("dart:core", "int", 1)),
)
],
);
}
@@ -0,0 +1,22 @@
# Copyright (c) 2024, 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.
sources: |
void stopHere() {
List<(int, {double foo, String bar})> listOfRecord = [(42, foo: 42.42, bar: "fortytwo")];
print(helper(listOfRecord));
}
bool helper(List<(int, {double foo, String bar})> listOfRecord) {
final record = listOfRecord.first;
return record.$1 == 42 && record.foo >= 42.0 && record.bar.length >= 4;
}
definitions: ["listOfRecord"]
# List<(int, {String bar, double foo})> // Note the names being sorted!
definition_types: ["dart:core", "List", "1", "1", "record", "1", "3", "1", "bar", "foo", "dart:core", "int", "1", "0", "dart:core", "String", "1", "0", "dart:core", "double", "1", "0"]
type_definitions: []
type_bounds: []
type_defaults: []
method: "stopHere"
expression: |
helper(listOfRecord)
@@ -0,0 +1,4 @@
Errors: {
}
method /* from org-dartlang-debug:synthetic_debug_expression */ debugExpr(dart.core::List<(dart.core::int, {bar: dart.core::String, foo: dart.core::double})> listOfRecord) → dynamic
return #lib1::helper(listOfRecord);
@@ -0,0 +1,42 @@
// Copyright (c) 2024, 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.
import 'dart:developer';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
void testFunction() {
final listOfRecord = [(42, foo: 42.42, bar: 'fortytwo')];
debugger();
print(helper(listOfRecord));
}
bool helper(List<(int, {double foo, String bar})> listOfRecord) {
final record = listOfRecord.first;
return record.$1 == 42 && record.foo >= 42.0 && record.bar.length >= 4;
}
final tests = <IsolateTest>[
hasStoppedAtBreakpoint,
(VmService service, IsolateRef isolateRef) async {
final result = await service.evaluateInFrame(
isolateRef.id!,
0,
'helper(listOfRecord)',
) as InstanceRef;
expect(result.valueAsString, equals('true'));
expect(result.kind, equals(InstanceKind.kBool));
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'evaluate_with_record_1_test.dart',
testeeConcurrent: testFunction,
);
@@ -0,0 +1,48 @@
// Copyright (c) 2024, 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.
import 'dart:developer';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
void testFunction() {
final set = {for (var i = 0; i < 4; i++) (i, foo: 42.42, bar: 'x-')};
print(takesSetOfRecord(set));
}
String takesSetOfRecord(Set<(int, {double foo, String bar})> set) {
debugger();
return helper(set);
}
String helper(Set<(int, {double foo, String bar})> set) {
final int i = set.fold(0, (a, b) => a + b.$1);
final double foo = set.fold(0, (a, b) => a + b.foo);
final String bar = set.fold('', (a, b) => a + b.bar);
return (i, foo: foo, bar: bar).toString();
}
final tests = <IsolateTest>[
hasStoppedAtBreakpoint,
(VmService service, IsolateRef isolateRef) async {
final result = await service.evaluateInFrame(
isolateRef.id!,
0,
'helper(set)',
) as InstanceRef;
expect(result.valueAsString, equals('(6, bar: x-x-x-x-, foo: 169.68)'));
expect(result.kind, equals(InstanceKind.kString));
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'evaluate_with_record_2_test.dart',
testeeConcurrent: testFunction,
);
+28 -1
View File
@@ -2991,8 +2991,35 @@ static void CollectStringifiedType(Thread* thread,
return;
}
if (type.IsRecordType()) {
// _Record class is not useful for the CFE. We use null instead.
const auto& record = RecordType::Cast(type);
const intptr_t num_fields = record.NumFields();
const Array& field_names =
Array::Handle(zone, record.GetFieldNames(thread));
const intptr_t num_positional_fields = num_fields - field_names.Length();
// Records have their own encoding:
// "record" <nullability> <num fields> <num positional fields>
// <field names> <encoding of field>
instance ^= String::New("record");
output.Add(instance);
instance ^= Smi::New((intptr_t)type.nullability());
output.Add(instance);
instance ^= Smi::New(num_fields);
output.Add(instance);
instance ^= Smi::New(num_positional_fields);
output.Add(instance);
String& name = String::Handle(zone);
for (intptr_t i = 0, n = field_names.Length(); i < n; ++i) {
name ^= field_names.At(i);
output.Add(name);
}
AbstractType& field_type = AbstractType::Handle(zone);
for (intptr_t i = 0, n = num_fields; i < n; ++i) {
field_type = record.FieldTypeAt(i);
CollectStringifiedType(thread, zone, field_type, output);
}
return;
}
if (type.IsDynamicType()) {