[ddc] Fix broken named record elements
Some named elements were colliding with existing properties on record object instances. Use a symbol in the runtime library for the shape and values properties. Ensure that the `.constructor` and `.prototype` getters are renamed to match the expectation when compiling the access. Add regression test for all 4 named elements. Fixes: https://github.com/dart-lang/sdk/issues/54375 Change-Id: I7f3577455bfcff7dece6350e8c7e3ee7ffdbbac6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/342703 Reviewed-by: Sigmund Cherem <sigmund@google.com> Commit-Queue: Nicholas Shahan <nshahan@google.com> Reviewed-by: Mark Zhou <markzipan@google.com>
This commit is contained in:
committed by
Commit Queue
parent
b574db4493
commit
4bebe882be
@@ -304,7 +304,7 @@ Object getObjectMetadata(@notNull Object object) {
|
||||
} else if (object is Function) {
|
||||
_set(result, 'runtimeKind', RuntimeObjectKind.function);
|
||||
} else if (object is RecordImpl) {
|
||||
var shape = object.shape;
|
||||
var shape = JS<Shape>('!', '#[#]', object, shapeProperty);
|
||||
var positionalCount = shape.positionals;
|
||||
var namedCount = shape.named?.length ?? 0;
|
||||
var length = positionalCount + namedCount;
|
||||
@@ -452,10 +452,10 @@ Object getTypeFields(@notNull Type type) {
|
||||
/// ```
|
||||
@notNull
|
||||
Object getRecordFields(@notNull RecordImpl record) {
|
||||
var shape = record.shape;
|
||||
var shape = JS<Shape>('!', '#[#]', record, shapeProperty);
|
||||
var positionalCount = shape.positionals;
|
||||
var named = shape.named?.toList();
|
||||
var values = record.values;
|
||||
var values = JS('!', '#[#]', record, valuesProperty);
|
||||
|
||||
return _createJsObject({
|
||||
'positionalCount': positionalCount,
|
||||
|
||||
@@ -20,16 +20,15 @@ final class Shape {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to store a [Shape] on an instance of a record object.
|
||||
final shapeProperty = JS('', 'Symbol("shape")');
|
||||
|
||||
/// Used to store a [JSArray] on an instance of a record object containing all
|
||||
/// the elements in the record in order.
|
||||
final valuesProperty = JS('', 'Symbol("values")');
|
||||
|
||||
/// Internal base class for all concrete records.
|
||||
final class RecordImpl implements Record {
|
||||
final Shape shape;
|
||||
|
||||
/// Stores the elements of this record.
|
||||
///
|
||||
/// Contains all positional elements followed by all named elements in the
|
||||
/// order corresponding to names as they appear in [shape].
|
||||
final List values;
|
||||
|
||||
/// Cache for faster access after the first call of [hashCode].
|
||||
int? _hashCode;
|
||||
|
||||
@@ -38,16 +37,17 @@ final class RecordImpl implements Record {
|
||||
/// NOTE: Does not contain the cached result of the "safe" [_toString] call.
|
||||
String? _printed;
|
||||
|
||||
RecordImpl(this.shape, this.values) {
|
||||
var valueCount = JS<int>('!', '#.length', values);
|
||||
RecordImpl(Shape shape, JSArray values) {
|
||||
// Coerce all undefined values to null because dynamic gets of record
|
||||
// elements rely on the getter returning undefined to signal that the getter
|
||||
// does not exist.
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (JS<bool>('!', '#[#] === void 0', values, i)) {
|
||||
JS('', '#[#] = null', values, i);
|
||||
}
|
||||
}
|
||||
JS('!', '#[#] = #', this, shapeProperty, shape);
|
||||
JS('!', '#[#] = #', this, valuesProperty, values);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,12 +55,16 @@ final class RecordImpl implements Record {
|
||||
if (!(other is RecordImpl)) return false;
|
||||
// Shapes are canonicalized and stored in a map so there will only ever be
|
||||
// one instance of the same shape.
|
||||
if (JS<bool>('!', '# !== #', shape, other.shape)) return false;
|
||||
if (JS<bool>(
|
||||
'!', '#[#] !== #[#]', this, shapeProperty, other, shapeProperty))
|
||||
return false;
|
||||
// If the shapes are identical then the two records have the same number of
|
||||
// positional elements and the same named elements.
|
||||
// This implies: `values.length == other.values.length`.
|
||||
var values = JS<JSArray>('!', '#[#]', this, valuesProperty);
|
||||
var otherValues = JS<JSArray>('!', '#[#]', other, valuesProperty);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
if (values[i] != other.values[i]) {
|
||||
if (values[i] != otherValues[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +75,8 @@ final class RecordImpl implements Record {
|
||||
int get hashCode {
|
||||
final cachedValue = _hashCode;
|
||||
if (cachedValue != null) return cachedValue;
|
||||
var shape = JS<Shape>('!', '#[#]', this, shapeProperty);
|
||||
var values = JS<JSArray>('!', '#[#]', this, valuesProperty);
|
||||
return _hashCode = Object.hashAll([shape, ...values]);
|
||||
}
|
||||
|
||||
@@ -85,6 +91,8 @@ final class RecordImpl implements Record {
|
||||
final cachedValue = _printed;
|
||||
if (!safe && cachedValue != null) return cachedValue;
|
||||
var buffer = StringBuffer();
|
||||
var shape = JS<Shape>('!', '#[#]', this, shapeProperty);
|
||||
var values = JS<JSArray>('!', '#[#]', this, valuesProperty);
|
||||
var posCount = shape.positionals;
|
||||
var count = values.length;
|
||||
|
||||
@@ -173,8 +181,8 @@ Object registerRecord(
|
||||
JS('!', '#.prototype = #.prototype', newRecord, recordClass);
|
||||
var recordPrototype = JS('', '#.prototype', recordClass);
|
||||
|
||||
_recordGet(@notNull int index) =>
|
||||
JS('!', 'function recordGet() {return this.values[#];}', index);
|
||||
_recordGet(@notNull int index) => JS(
|
||||
'!', 'function recordGet() {return this[#][#];}', valuesProperty, index);
|
||||
|
||||
// Add convenience getters for accessing the record's field values.
|
||||
var count = 0;
|
||||
@@ -185,7 +193,12 @@ Object registerRecord(
|
||||
count++;
|
||||
}
|
||||
if (named != null) {
|
||||
for (final name in named) {
|
||||
for (var name in named) {
|
||||
if (name == 'constructor' || name == 'prototype') {
|
||||
// This renaming is directly coupled to the renaming logic at compile
|
||||
// time in js_names.dart `.memberNameForDartMember()`.
|
||||
name = '_$name';
|
||||
}
|
||||
defineAccessor(recordPrototype, name,
|
||||
get: _recordGet(count), enumerable: true);
|
||||
count++;
|
||||
|
||||
@@ -96,7 +96,7 @@ getFunctionType(obj) {
|
||||
RecordType getRecordType(RecordImpl obj) {
|
||||
var type = JS<RecordType?>('', '#[#]', obj, _runtimeType);
|
||||
if (type == null) {
|
||||
var shape = obj.shape;
|
||||
var shape = JS<Shape>('!', '#[#]', obj, shapeProperty);
|
||||
var named = shape.named;
|
||||
var positionals = shape.positionals;
|
||||
var types = [];
|
||||
|
||||
@@ -2376,12 +2376,13 @@ class RecordType extends DartType {
|
||||
@JSExportName('is')
|
||||
bool is_T(obj) {
|
||||
if (!(obj is RecordImpl)) return false;
|
||||
if (shape != obj.shape) return false;
|
||||
if (types.length != obj.values.length) {
|
||||
if (shape != JS<Shape>('!', '#[#]', obj, shapeProperty)) return false;
|
||||
var values = JS<JSArray>('!', '#[#]', obj, valuesProperty);
|
||||
if (types.length != values.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
if (!JS<bool>('!', '#.is(#)', types[i], obj.values[i])) {
|
||||
if (!JS<bool>('!', '#.is(#)', types[i], values[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,9 +882,10 @@ Object? createRecordTypePredicate(String partialShapeTag, JSArray fieldRtis) {
|
||||
return (obj) {
|
||||
return JS<bool>(
|
||||
'!', '# instanceof #', obj, JS_CLASS_REF(dart.RecordImpl)) &&
|
||||
JS<dart.RecordImpl>('!', '#', obj).shape ==
|
||||
JS<dart.Shape>('!', '#[#]', obj, dart.shapeProperty) ==
|
||||
JS<dart.Shape?>('', '#.get(#)', dart.shapes, shapeKey) &&
|
||||
rti.pairwiseIsTest(fieldRtis, JS<JSArray>('!', '#.values', obj));
|
||||
rti.pairwiseIsTest(
|
||||
fieldRtis, JS<JSArray>('!', '#[#]', obj, dart.valuesProperty));
|
||||
};
|
||||
} else {
|
||||
dart.throwUnimplementedInCurrentRti();
|
||||
@@ -902,14 +903,16 @@ rti.Rti getRtiForRecord(Object? record) {
|
||||
if (JS_GET_FLAG('NEW_RUNTIME_TYPES')) {
|
||||
var recordObj = JS<dart.RecordImpl>('!', '#', record);
|
||||
var recipeBuffer = StringBuffer('+');
|
||||
var named = recordObj.shape.named;
|
||||
var shape = JS<dart.Shape>('!', '#[#]', recordObj, dart.shapeProperty);
|
||||
var values = JS<JSArray>('!', '#[#]', recordObj, dart.valuesProperty);
|
||||
var named = shape.named;
|
||||
if (named != null) recipeBuffer.writeAll(named, ',');
|
||||
recipeBuffer.write('(');
|
||||
var elementCount = recordObj.values.length;
|
||||
var elementCount = values.length;
|
||||
recipeBuffer.writeAll([for (var i = 1; i <= elementCount; i++) i], ',');
|
||||
recipeBuffer.write(')');
|
||||
|
||||
return rti.evaluateRtiForRecord(recipeBuffer.toString(), recordObj.values);
|
||||
return rti.evaluateRtiForRecord(recipeBuffer.toString(), values);
|
||||
} else {
|
||||
dart.throwUnimplementedInCurrentRti();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2023, 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.
|
||||
|
||||
/// Regression test for https://github.com/dart-lang/sdk/issues/54375.
|
||||
///
|
||||
/// Records should support named elements with the names 'shape', 'values',
|
||||
/// 'constructor', and 'prototype'.
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
|
||||
@pragma('dart2js:noInline')
|
||||
@pragma('dart2js:assumeDynamic')
|
||||
confuse(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
void main() {
|
||||
var r = (shape: 123);
|
||||
Expect.equals(123, r.shape);
|
||||
var d = confuse(r);
|
||||
Expect.equals(123, d.shape);
|
||||
|
||||
var r2 = (values: 'hello');
|
||||
Expect.equals('hello', r2.values);
|
||||
d = confuse(r2);
|
||||
Expect.equals('hello', d.values);
|
||||
|
||||
var r3 = (constructor: Duration.zero);
|
||||
Expect.equals(Duration.zero, r3.constructor);
|
||||
d = confuse(r3);
|
||||
Expect.equals(Duration.zero, d.constructor);
|
||||
|
||||
var r4 = (prototype: null);
|
||||
Expect.equals(null, r4.prototype);
|
||||
d = confuse(r4);
|
||||
Expect.equals(null, d.prototype);
|
||||
}
|
||||
Reference in New Issue
Block a user