[dart2js] Better const Maps and Sets

Constant Sets now have their own classes rather than being constructed as a wrapper over a const Map. The new scheme is similar to Maps - using a ConstantStringSet backed by a JavaScript Object 'dictionary' when the elements are all suitable String values, otherwise using a GeneralConstantSet backed by a list of elements that is converted to a lookup map on demand.

Constant Sets and Maps with suitable String keys are backed by a JavaScript Object literal 'dictionary'. The use of the Object literal has been changed so that the property values are no longer the values of the Dart Map, but the position of the key in the order of the entries. The values are provided as a List (Array), so there is an additional indexing operation to access the value on lookup. This does not seem to affect performance.

The advantage of the two-level lookup using the 'dictionary' is that Maps with the same keys (in the same order) can share a 'dictionary'. In order to achieve the advantage, the JavaScript Object is modeled as a first class ConstantValue - JavaScriptObjectConstantValue.

These changes achieve a code size benefit of -0.90% (~130K) on the main unit of a certain large app, and -0.35% for flute/benchmarks/lib/complex.dart

Change-Id: Icad2e6136218486a439e3c5ed0296462e3c3c4e6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/310020
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
This commit is contained in:
Stephen Adams 2023-06-20 23:44:08 +00:00 committed by Commit Queue
parent 739c9885a2
commit 5cbba84b91
18 changed files with 753 additions and 138 deletions

View file

@ -384,10 +384,12 @@ abstract class CommonElements {
return _env.createInterfaceType(classElement, sourceType.typeArguments);
}
InterfaceType getConstantSetTypeFor(InterfaceType sourceType) {
InterfaceType getConstantSetTypeFor(InterfaceType sourceType,
{bool onlyStringKeys = false}) {
// TODO(51534): Use CONST_CANONICAL_TYPE(T_i) for arguments.
return _env.createInterfaceType(
constSetLiteralClass, sourceType.typeArguments);
ClassEntity classElement =
onlyStringKeys ? constantStringSetClass : generalConstantSetClass;
return _env.createInterfaceType(classElement, sourceType.typeArguments);
}
/// Returns the field that holds the internal name in the implementation class
@ -641,9 +643,7 @@ abstract class CommonElements {
late final ClassEntity constMapLiteralClass = _findHelperClass('ConstantMap');
// TODO(fishythefish): Implement a `ConstantSet` class and update the backend
// impacts + constant emitter accordingly.
late final ClassEntity constSetLiteralClass = unmodifiableSetClass;
late final ClassEntity constSetLiteralClass = _findHelperClass('ConstantSet');
/// Base class for all records.
late final ClassEntity recordBaseClass = _findHelperClass('_Record');
@ -694,6 +694,12 @@ abstract class CommonElements {
late final ClassEntity generalConstantMapClass = _findHelperClass(
constant_system.JavaScriptMapConstant.DART_GENERAL_CLASS);
late final ClassEntity constantStringSetClass =
_findHelperClass(constant_system.JavaScriptSetConstant.DART_STRING_CLASS);
late final ClassEntity generalConstantSetClass = _findHelperClass(
constant_system.JavaScriptSetConstant.DART_GENERAL_CLASS);
late final ClassEntity annotationCreatesClass = _findHelperClass('Creates');
late final ClassEntity annotationReturnsClass = _findHelperClass('Returns');

View file

@ -143,14 +143,10 @@ bool isSubtype(DartTypes types, DartType s, DartType t) {
SetConstantValue createSet(CommonElements commonElements,
InterfaceType sourceType, List<ConstantValue> values) {
InterfaceType type = commonElements.getConstantSetTypeFor(sourceType);
DartType elementType = type.typeArguments.first;
InterfaceType mapType =
commonElements.mapType(elementType, commonElements.nullType);
List<NullConstantValue> nulls =
List<NullConstantValue>.filled(values.length, const NullConstantValue());
MapConstantValue entries = createMap(commonElements, mapType, values, nulls);
return JavaScriptSetConstant(type, entries);
JavaScriptObjectConstantValue? indexObject = _makeStringIndex(values);
InterfaceType type = commonElements.getConstantSetTypeFor(sourceType,
onlyStringKeys: indexObject != null);
return JavaScriptSetConstant(type, values, indexObject);
}
MapConstantValue createMap(
@ -158,16 +154,74 @@ MapConstantValue createMap(
InterfaceType sourceType,
List<ConstantValue> keys,
List<ConstantValue> values) {
bool onlyStringKeys = keys.every((key) =>
key is StringConstantValue &&
key.stringValue != JavaScriptMapConstant.PROTO_PROPERTY);
final JavaScriptObjectConstantValue? indexObject = _makeStringIndex(keys);
final onlyStringKeys = indexObject != null;
InterfaceType keysType =
commonElements.listType(sourceType.typeArguments.first);
InterfaceType valuesType =
commonElements.listType(sourceType.typeArguments.last);
ListConstantValue keysList = createList(commonElements, keysType, keys);
ListConstantValue valuesList = createList(commonElements, valuesType, values);
InterfaceType type = commonElements.getConstantMapTypeFor(sourceType,
onlyStringKeys: onlyStringKeys);
return JavaScriptMapConstant(type, keysList, values, onlyStringKeys);
return JavaScriptMapConstant(
type, keysList, valuesList, onlyStringKeys, indexObject);
}
JavaScriptObjectConstantValue? _makeStringIndex(List<ConstantValue> keys) {
for (final key in keys) {
if (key is! StringConstantValue) return null;
if (key.stringValue == JavaScriptMapConstant.PROTO_PROPERTY) return null;
}
// If we generate a JavaScript Object initializer with the keys in order, are
// the properties of the Object in the same order? If so, we can generate the
// keys of the map using `Object.keys`, otherwise we need to provide the key
// ordering explicitly, or sort by position at runtime, or have a Map/Set
// subclass for the case where sorting is necessary. For now we use the
// general constant Map/Set for the occasional case where the order is wrong.
if (!_valuesInObjectPropertyOrder(keys)) return null;
return JavaScriptObjectConstantValue(
keys, List.generate(keys.length, createIntFromInt));
}
final _numberRegExp = RegExp(r'^0$|^[1-9][0-9]*$');
/// If the values are the keys of a JavaScript Object initializer, is the result
/// of `Object.keys` in the same order as [keys]? This method may conservatively
/// return `false`.
///
/// Object keys are split into 'indexes' and 'names', with all the 'indexes'
/// before the 'names'. 'indexes' are strings that have the value of
/// `i.toString()` for some `i` from 0 up to some limit. The indexes are ordered
/// by their integer value. 'names' are in insertion order. The literal
/// `{"a":1,"10":2,"2":3,"b":4}` has `Object.keys` of `["2","10","a","b"]`
/// because `10` and `2` come before `a` and `b`.
bool _valuesInObjectPropertyOrder(List<ConstantValue> keys) {
int lastNumber = -1;
bool seenNonNumber = false;
for (final key in keys) {
if (key is! StringConstantValue) return false;
final string = key.stringValue;
if (_numberRegExp.hasMatch(string)) {
// This index would move before the non-number.
if (seenNonNumber) return false;
// Sufficiently large digit strings are not considered to be indexes. It
// is not clear where the cutoff is or whether it is consistent between
// JavaScript implementations.
if (string.length > 8) return false;
final value = int.parse(string);
// Adjacent indexes must be in increasing numerical order.
if (value <= lastNumber) return false;
lastNumber = value;
} else {
seenNonNumber = true;
}
}
return true;
}
ConstructedConstantValue createSymbol(
@ -859,13 +913,24 @@ class UnfoldedUnaryOperation implements UnaryOperation {
}
class JavaScriptSetConstant extends SetConstantValue {
final MapConstantValue entries;
static const String DART_STRING_CLASS = "ConstantStringSet";
static const String DART_GENERAL_CLASS = "GeneralConstantSet";
JavaScriptSetConstant(InterfaceType type, this.entries)
: super(type, entries.keys);
/// Index for all-string Sets.
final JavaScriptObjectConstantValue? indexObject;
JavaScriptSetConstant(super.type, super.elements, this.indexObject);
@override
List<ConstantValue> getDependencies() => [entries];
List<ConstantValue> getDependencies() {
if (indexObject == null) {
// For a general constant Set the values are emitted as a literal array.
return [...values];
} else {
// For a ConstantStringSet, the index contains the elements.
return [indexObject!];
}
}
}
class JavaScriptMapConstant extends MapConstantValue {
@ -877,30 +942,43 @@ class JavaScriptMapConstant extends MapConstantValue {
static const String DART_CLASS = "ConstantMap";
static const String DART_STRING_CLASS = "ConstantStringMap";
static const String DART_GENERAL_CLASS = "GeneralConstantMap";
static const String LENGTH_NAME = "_length";
static const String JS_OBJECT_NAME = "_jsObject";
static const String KEYS_NAME = "_keys";
static const String JS_DATA_NAME = "_jsData";
final ListConstantValue keyList;
final bool onlyStringKeys;
static const String JS_INDEX_NAME = '_jsIndex';
static const String VALUES_NAME = '_values';
JavaScriptMapConstant(InterfaceType type, ListConstantValue keyList,
List<ConstantValue> values, this.onlyStringKeys)
: this.keyList = keyList,
super(type, keyList.entries, values);
final ListConstantValue keyList;
final ListConstantValue valueList;
final bool onlyStringKeys;
final JavaScriptObjectConstantValue? indexObject;
JavaScriptMapConstant(InterfaceType type, this.keyList, this.valueList,
this.onlyStringKeys, this.indexObject)
: super(type, keyList.entries, valueList.entries);
@override
List<ConstantValue> getDependencies() {
List<ConstantValue> result = <ConstantValue>[];
if (onlyStringKeys) {
result.add(keyList);
// TODO(25230): If we use `valueList` instead of `...values`, that creates
// a constant list that has a name in the constant pool and the list has
// Dart type attached. The Map constant has a reference to the list. If we
// knew that the `valueList` was the only reference to the list, we could
// generate the array in-place and omit the type. See [here][1] for more
// on the idea of building constants with unnamed subexpressions.
//
// [1]: https://github.com/dart-lang/sdk/issues/25230
//
// For now the values are generated in a fresh Array, so add the values.
return [indexObject!, ...values];
} else {
// Add the keys individually to avoid generating an unused list constant
// for the keys.
result.addAll(keys);
// The general representation uses a list of key/value pairs, so add the
// keys and values individually to avoid generating an unused list
// constant for the keys and values.
return [...keys, ...values];
}
result.addAll(values);
return result;
}
}

View file

@ -27,6 +27,7 @@ enum ConstantValueKind {
RECORD,
TYPE,
INTERCEPTOR,
JAVASCRIPT_OBJECT,
JS_NAME,
DUMMY_INTERCEPTOR,
LATE_SENTINEL,
@ -54,6 +55,8 @@ abstract class ConstantValueVisitor<R, A> {
R visitType(covariant TypeConstantValue constant, covariant A arg);
R visitInterceptor(
covariant InterceptorConstantValue constant, covariant A arg);
R visitJavaScriptObject(
covariant JavaScriptObjectConstantValue constant, covariant A arg);
R visitDummyInterceptor(
covariant DummyInterceptorConstantValue constant, covariant A arg);
R visitLateSentinel(
@ -675,12 +678,7 @@ abstract class MapConstantValue extends ObjectConstantValue {
}
@override
List<ConstantValue> getDependencies() {
List<ConstantValue> result = [];
result.addAll(keys);
result.addAll(values);
return result;
}
List<ConstantValue> getDependencies() => [...keys, ...values];
int get length => keys.length;
@ -1103,6 +1101,79 @@ class InstantiationConstantValue extends ConstantValue {
}
}
/// A JavaScript Object Literal used as a constant.
class JavaScriptObjectConstantValue extends ConstantValue {
final List<ConstantValue> keys;
final List<ConstantValue> values;
@override
late final int hashCode = Hashing.listHash(values, Hashing.listHash(keys, 9));
JavaScriptObjectConstantValue(this.keys, this.values) {
assert(keys.length == values.length);
}
@override
bool operator ==(var other) {
return identical(this, other) ||
other is JavaScriptObjectConstantValue && _equals(this, other);
}
static bool _equals(
JavaScriptObjectConstantValue a, JavaScriptObjectConstantValue b) {
if (a.hashCode != b.hashCode) return false;
if (a.length != b.length) return false;
if (!_listsEqual(a.keys, b.keys)) return false;
if (!_listsEqual(a.values, b.values)) return false;
return true;
}
@override
List<ConstantValue> getDependencies() => [...keys, ...values];
int get length => keys.length;
@override
accept(ConstantValueVisitor visitor, arg) =>
visitor.visitJavaScriptObject(this, arg);
@override
DartType getType(CommonElements types) {
return types.dynamicType; // TODO: Lookup JavaScriptObject.
}
@override
ConstantValueKind get kind => ConstantValueKind.JAVASCRIPT_OBJECT;
@override
String toDartText(DartTypes? dartTypes) {
StringBuffer sb = StringBuffer();
sb.write('{');
for (int i = 0; i < length; i++) {
if (i > 0) sb.write(',');
sb.write(keys[i].toDartText(dartTypes));
sb.write(':');
sb.write(values[i].toDartText(dartTypes));
}
sb.write('}');
return sb.toString();
}
@override
String toStructuredText(DartTypes? dartTypes) {
StringBuffer sb = StringBuffer();
sb.write('JavaScriptObject(');
sb.write('{');
for (int i = 0; i < length; i++) {
if (i > 0) sb.write(', ');
sb.write(keys[i].toStructuredText(dartTypes));
sb.write(': ');
sb.write(values[i].toStructuredText(dartTypes));
}
sb.write('})');
return sb.toString();
}
}
/// A reference to a constant in another output unit.
///
/// Used for referring to deferred constants that appear as initializers of

View file

@ -1511,7 +1511,9 @@ class SimpleDartTypeSubstitutionVisitor
final List<DartType> parameters;
SimpleDartTypeSubstitutionVisitor(
this.dartTypes, this.arguments, this.parameters);
this.dartTypes, this.arguments, this.parameters)
: assert(arguments.length == parameters.length,
'Type substitution mismatch\n $arguments\n $parameters');
DartType substitute(DartType input) => visit(input, null);

View file

@ -141,4 +141,11 @@ class ConstantValueTypeMasks
TypeMask visitType(TypeConstantValue constant, JClosedWorld closedWorld) {
return _abstractValueDomain.typeType;
}
@override
TypeMask visitJavaScriptObject(
JavaScriptObjectConstantValue constant, JClosedWorld closedWorld) {
// TODO(sra): Change to plain JavaScript object.
return _abstractValueDomain.dynamicType;
}
}

View file

@ -223,9 +223,9 @@ class BackendImpacts {
late final BackendImpact constantSetLiteral = BackendImpact(
instantiatedClasses: [
_commonElements.constSetLiteralClass,
_commonElements.constantStringSetClass,
_commonElements.generalConstantSetClass,
],
otherImpacts: [constantMapLiteral],
);
late final BackendImpact constSymbol = BackendImpact(

View file

@ -210,6 +210,12 @@ class ModularConstantEmitter
@override
jsAst.Expression? visitRecord(RecordConstantValue constant, [_]) => null;
@override
jsAst.Expression? visitJavaScriptObject(
JavaScriptObjectConstantValue constant,
[_]) =>
null;
}
/// Generates the JavaScript expressions for constants.
@ -265,21 +271,36 @@ class ConstantEmitter extends ModularConstantEmitter {
InterfaceType sourceType = constant.type;
ClassEntity classElement = sourceType.element;
String className = classElement.name;
if (!identical(classElement, _commonElements.constSetLiteralClass)) {
failedAt(
classElement, "Compiler encountered unexpected set class $className");
if (constant.indexObject != null) {
if (!identical(classElement, _commonElements.constantStringSetClass)) {
failedAt(classElement,
"Compiler encountered unexpected set class $className");
}
List<jsAst.Expression> arguments = [
_constantReferenceGenerator(constant.indexObject!),
js.number(constant.length),
if (_rtiNeed.classNeedsTypeArguments(classElement))
_reifiedTypeNewRti(sourceType),
];
jsAst.Expression constructor = _emitter.constructorAccess(classElement);
return jsAst.New(constructor, arguments);
} else {
if (!identical(classElement, _commonElements.generalConstantSetClass)) {
failedAt(classElement,
"Compiler encountered unexpected set class $className");
}
List<jsAst.Expression> arguments = [
jsAst.ArrayInitializer([
for (final value in constant.values)
_constantReferenceGenerator(value)
]),
if (_rtiNeed.classNeedsTypeArguments(classElement))
_reifiedTypeNewRti(sourceType),
];
jsAst.Expression constructor = _emitter.constructorAccess(classElement);
return jsAst.New(constructor, arguments);
}
List<jsAst.Expression> arguments = [
_constantReferenceGenerator(constant.entries),
];
if (_rtiNeed.classNeedsTypeArguments(classElement)) {
arguments.add(_reifiedTypeNewRti(sourceType));
}
jsAst.Expression constructor = _emitter.constructorAccess(classElement);
return jsAst.New(constructor, arguments);
}
@override
@ -316,6 +337,12 @@ class ConstantEmitter extends ModularConstantEmitter {
return jsAst.ArrayInitializer(data);
}
jsAst.Expression jsValuesArray() {
return jsAst.ArrayInitializer([
for (final value in constant.values) _constantReferenceGenerator(value)
]);
}
ClassEntity classElement = constant.type.element;
String className = classElement.name;
@ -327,18 +354,20 @@ class ConstantEmitter extends ModularConstantEmitter {
_elementEnvironment.forEachInstanceField(classElement,
(ClassEntity enclosing, FieldEntity field) {
if (_fieldAnalysis.getFieldData(field as JField).isElided) return;
if (field.name == constant_system.JavaScriptMapConstant.LENGTH_NAME) {
final name = field.name;
if (name == constant_system.JavaScriptMapConstant.LENGTH_NAME) {
arguments
.add(jsAst.LiteralNumber('${constant.keyList.entries.length}'));
} else if (field.name ==
constant_system.JavaScriptMapConstant.JS_OBJECT_NAME) {
} else if (name == constant_system.JavaScriptMapConstant.JS_OBJECT_NAME) {
arguments.add(jsMap());
} else if (field.name ==
constant_system.JavaScriptMapConstant.KEYS_NAME) {
} else if (name == constant_system.JavaScriptMapConstant.KEYS_NAME) {
arguments.add(_constantReferenceGenerator(constant.keyList));
} else if (field.name ==
constant_system.JavaScriptMapConstant.JS_DATA_NAME) {
} else if (name == constant_system.JavaScriptMapConstant.JS_DATA_NAME) {
arguments.add(jsGeneralMap());
} else if (name == constant_system.JavaScriptMapConstant.VALUES_NAME) {
arguments.add(jsValuesArray());
} else if (name == constant_system.JavaScriptMapConstant.JS_INDEX_NAME) {
arguments.add(_constantReferenceGenerator(constant.indexObject!));
} else {
failedAt(field,
"Compiler has unexpected field ${field.name} for ${className}.");
@ -346,7 +375,7 @@ class ConstantEmitter extends ModularConstantEmitter {
emittedArgumentCount++;
});
if ((className == constant_system.JavaScriptMapConstant.DART_STRING_CLASS &&
emittedArgumentCount != 3) ||
emittedArgumentCount != 2) ||
(className ==
constant_system.JavaScriptMapConstant.DART_GENERAL_CLASS &&
emittedArgumentCount != 1)) {
@ -367,6 +396,18 @@ class ConstantEmitter extends ModularConstantEmitter {
return _emitter.staticFunctionAccess(helper) as jsAst.PropertyAccess;
}
@override
jsAst.Expression visitJavaScriptObject(JavaScriptObjectConstantValue constant,
[_]) {
final List<jsAst.Property> properties = [];
for (int i = 0; i < constant.keys.length; i++) {
properties.add(jsAst.Property(
_constantReferenceGenerator(constant.keys[i]),
_constantReferenceGenerator(constant.values[i])));
}
return jsAst.ObjectInitializer(properties);
}
@override
jsAst.Expression visitType(TypeConstantValue constant, [_]) {
DartType type = constant.representedType;

View file

@ -87,8 +87,8 @@ class CodegenImpactTransformer {
for (ConstantUse constantUse in impact.constantUses) {
switch (constantUse.value.kind) {
case ConstantValueKind.SET:
case ConstantValueKind.MAP:
case ConstantValueKind.SET:
case ConstantValueKind.CONSTRUCTED:
case ConstantValueKind.LIST:
transformed.registerStaticUse(StaticUse.staticInvoke(

View file

@ -1317,6 +1317,24 @@ class ConstantNamingVisitor implements ConstantValueVisitor {
}
}
@override
void visitJavaScriptObject(JavaScriptObjectConstantValue constant, [_]) {
addRoot('Object');
int length = constant.length;
if (constant.length == 0) {
add('empty');
} else if (length * 2 > MAX_FRAGMENTS) {
failed = true;
} else {
for (int i = 0; i < length; i++) {
_visit(constant.keys[i]);
if (failed) break;
_visit(constant.values[i]);
if (failed) break;
}
}
}
@override
void visitConstructed(ConstructedConstantValue constant, [_]) {
addRoot(constant.type.element.name);
@ -1443,6 +1461,7 @@ class ConstantCanonicalHasher implements ConstantValueVisitor<int, Null> {
static const int _seedList = 10;
static const int _seedSet = 11;
static const int _seedMap = 12;
static const int _seedJavaScriptObject = 13;
ConstantCanonicalHasher(this._namer, this._closedWorld);
@ -1546,6 +1565,14 @@ class ConstantCanonicalHasher implements ConstantValueVisitor<int, Null> {
return _hashString(_seedInterceptor, typeName);
}
@override
int visitJavaScriptObject(JavaScriptObjectConstantValue constant, [_]) {
int hash = _seedJavaScriptObject;
hash = _hashList(hash, constant.keys);
hash = _hashList(hash, constant.values);
return hash;
}
@override
int visitDummyInterceptor(DummyInterceptorConstantValue constant, [_]) {
throw failedAt(

View file

@ -200,6 +200,14 @@ class _ConstantOrdering
if (r != 0) return r;
return compareLists(compareDartTypes, a.typeArguments, b.typeArguments);
}
@override
int visitJavaScriptObject(
JavaScriptObjectConstantValue a, JavaScriptObjectConstantValue b) {
int r = compareLists(compareValues, a.keys, b.keys);
if (r != 0) return r;
return compareLists(compareValues, a.values, b.values);
}
}
/// Visitor for distinguishing types by kind.

View file

@ -783,27 +783,34 @@ class _ConstantConverter implements ConstantValueVisitor<ConstantValue, Null> {
ConstantValue visitSet(
covariant constant_system.JavaScriptSetConstant constant, _) {
DartType type = typeConverter.visit(constant.type, toBackendEntity);
MapConstantValue? entries = constant.entries.accept(this, null);
if (identical(entries, constant.entries) && type == constant.type) {
List<ConstantValue> values = _handleValues(constant.values);
JavaScriptObjectConstantValue? indexObject =
constant.indexObject?.accept(this, null);
if (identical(values, constant.values) &&
identical(indexObject, constant.indexObject) &&
type == constant.type) {
return constant;
}
return constant_system.JavaScriptSetConstant(
type as InterfaceType, entries!);
type as InterfaceType, values, indexObject);
}
@override
ConstantValue visitMap(
covariant constant_system.JavaScriptMapConstant constant, _) {
DartType type = typeConverter.visit(constant.type, toBackendEntity);
ListConstantValue keys = constant.keyList.accept(this, null);
List<ConstantValue> values = _handleValues(constant.values);
if (identical(keys, constant.keys) &&
identical(values, constant.values) &&
ListConstantValue keyList = constant.keyList.accept(this, null);
ListConstantValue valueList = constant.valueList.accept(this, null);
JavaScriptObjectConstantValue? indexObject =
constant.indexObject?.accept(this, null);
if (identical(keyList, constant.keyList) &&
identical(valueList, constant.valueList) &&
identical(indexObject, constant.indexObject) &&
type == constant.type) {
return constant;
}
return constant_system.JavaScriptMapConstant(
type as InterfaceType, keys, values, constant.onlyStringKeys);
return constant_system.JavaScriptMapConstant(type as InterfaceType, keyList,
valueList, constant.onlyStringKeys, indexObject);
}
@override
@ -829,6 +836,17 @@ class _ConstantConverter implements ConstantValueVisitor<ConstantValue, Null> {
return RecordConstantValue(constant.shape, values);
}
@override
ConstantValue visitJavaScriptObject(
JavaScriptObjectConstantValue constant, _) {
List<ConstantValue> keys = _handleValues(constant.keys);
List<ConstantValue> values = _handleValues(constant.values);
if (identical(keys, constant.keys) && identical(values, constant.values)) {
return constant;
}
return JavaScriptObjectConstantValue(keys, values);
}
@override
ConstantValue visitType(TypeConstantValue constant, _) {
DartType type = typeConverter.visit(constant.type, toBackendEntity);

View file

@ -1011,14 +1011,16 @@ class DataSinkWriter {
case ConstantValueKind.SET:
final constant = value as constant_system.JavaScriptSetConstant;
writeDartType(constant.type);
writeConstant(constant.entries);
writeConstants(constant.values);
writeConstantOrNull(constant.indexObject);
break;
case ConstantValueKind.MAP:
final constant = value as constant_system.JavaScriptMapConstant;
writeDartType(constant.type);
writeConstant(constant.keyList);
writeConstants(constant.values);
writeConstant(constant.valueList);
writeBool(constant.onlyStringKeys);
if (constant.onlyStringKeys) writeConstant(constant.indexObject!);
break;
case ConstantValueKind.CONSTRUCTED:
final constant = value as ConstructedConstantValue;
@ -1047,6 +1049,11 @@ class DataSinkWriter {
final constant = value as InterceptorConstantValue;
writeClass(constant.cls);
break;
case ConstantValueKind.JAVASCRIPT_OBJECT:
final constant = value as JavaScriptObjectConstantValue;
writeConstants(constant.keys);
writeConstants(constant.values);
break;
case ConstantValueKind.DEFERRED_GLOBAL:
final constant = value as DeferredGlobalConstantValue;
writeConstant(constant.referenced);

View file

@ -1320,15 +1320,20 @@ class DataSourceReader {
return ListConstantValue(type, entries);
case ConstantValueKind.SET:
final type = readDartType() as InterfaceType;
final entries = readConstant() as MapConstantValue;
return constant_system.JavaScriptSetConstant(type, entries);
final values = readConstants();
final indexObject =
readConstantOrNull() as JavaScriptObjectConstantValue?;
return constant_system.JavaScriptSetConstant(type, values, indexObject);
case ConstantValueKind.MAP:
final type = readDartType() as InterfaceType;
final keyList = readConstant() as ListConstantValue;
List<ConstantValue> values = readConstants();
final valueList = readConstant() as ListConstantValue;
bool onlyStringKeys = readBool();
final indexObject = onlyStringKeys
? readConstant() as JavaScriptObjectConstantValue
: null;
return constant_system.JavaScriptMapConstant(
type, keyList, values, onlyStringKeys);
type, keyList, valueList, onlyStringKeys, indexObject);
case ConstantValueKind.CONSTRUCTED:
final type = readDartType() as InterfaceType;
Map<FieldEntity, ConstantValue> fields =
@ -1352,6 +1357,10 @@ class DataSourceReader {
case ConstantValueKind.INTERCEPTOR:
ClassEntity cls = readClass();
return InterceptorConstantValue(cls);
case ConstantValueKind.JAVASCRIPT_OBJECT:
final keys = readConstants();
final values = readConstants();
return JavaScriptObjectConstantValue(keys, values);
case ConstantValueKind.DEFERRED_GLOBAL:
ConstantValue constant = readConstant();
OutputUnit unit = readOutputUnitReference();

View file

@ -294,4 +294,19 @@ class ConstantToTextVisitor
typeToText.visitList(constant.typeArguments, sb);
sb.write('>)');
}
@override
void visitJavaScriptObject(
JavaScriptObjectConstantValue constant, StringBuffer sb) {
sb.write('JavaScriptObject({');
for (int index = 0; index < constant.keys.length; index++) {
if (index > 0) {
sb.write(',');
}
visit(constant.keys[index], sb);
sb.write(':');
visit(constant.values[index], sb);
}
sb.write('})');
}
}

View file

@ -172,11 +172,9 @@ mapLiteral() => const {true: false};
stringMapLiteral() => const {'foo': false};
/*member: setLiteral:type=[
inst:ConstantMap<dynamic,dynamic>,
inst:ConstantStringMap<dynamic,dynamic>,
inst:GeneralConstantMap<dynamic,dynamic>,
inst:JSBool,
inst:_UnmodifiableSet<dynamic>]*/
inst:ConstantStringSet<dynamic>,
inst:GeneralConstantSet<dynamic>,
inst:JSBool]*/
setLiteral() => const {true, false};
/*member: instanceConstant:
@ -282,11 +280,9 @@ mapLiteralRef() => mapLiteralField;
stringMapLiteralRef() => stringMapLiteralField;
/*member: setLiteralRef:type=[
inst:ConstantMap<dynamic,dynamic>,
inst:ConstantStringMap<dynamic,dynamic>,
inst:GeneralConstantMap<dynamic,dynamic>,
inst:JSBool,
inst:_UnmodifiableSet<dynamic>]*/
inst:ConstantStringSet<dynamic>,
inst:GeneralConstantSet<dynamic>,
inst:JSBool]*/
setLiteralRef() => setLiteralField;
/*member: instanceConstantRef:
@ -385,11 +381,9 @@ stringMapLiteralDeferred() => defer.stringMapLiteralField;
// TODO(johnniwinther): Should we record that this is deferred?
/*member: setLiteralDeferred:type=[
inst:ConstantMap<dynamic,dynamic>,
inst:ConstantStringMap<dynamic,dynamic>,
inst:GeneralConstantMap<dynamic,dynamic>,
inst:JSBool,
inst:_UnmodifiableSet<dynamic>]*/
inst:ConstantStringSet<dynamic>,
inst:GeneralConstantSet<dynamic>,
inst:JSBool]*/
setLiteralDeferred() => defer.setLiteralField;
/*member: instanceConstantDeferred:

View file

@ -1411,9 +1411,12 @@ class Printer implements NodeVisitor<void> {
} else if (name is LiteralNumber) {
out(name.value);
} else {
// TODO(sra): Handle StringConcatenation.
// TODO(sra): Handle general expressions, .e.g. `{[x]: 1}`.
throw StateError('Unexpected Property name: $name');
// Handle general expressions, .e.g. `{[x]: 1}`.
// String concatenation could be better.
out('[');
visitNestedExpression(node.name, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(']');
}
endNode(node.name);
}

View file

@ -22,11 +22,16 @@ abstract class ConstantMap<K, V> implements Map<K, V> {
}
if (allStrings) {
var object = JS('=Object', '{}');
int index = 0;
for (final k in keys) {
V v = other[k];
JS('void', '#[#] = #', object, k, v);
JS('void', '#[#] = #', object, k, index++);
}
return ConstantStringMap<K, V>._(keys.length, object, keys);
final values = List<V>.from(other.values);
final map =
ConstantStringMap<K, V>._(object, JS<JSArray>('', '#', values));
map._setKeys(keys);
return map;
}
// TODO(lrn): Make a proper unmodifiable map implementation.
return ConstantMapView<K, V>(Map.from(other));
@ -45,7 +50,7 @@ abstract class ConstantMap<K, V> implements Map<K, V> {
throw UnsupportedError('Cannot modify unmodifiable Map');
}
void operator []=(K key, V val) {
void operator []=(K key, V value) {
_throwUnmodifiable();
}
@ -99,65 +104,126 @@ abstract class ConstantMap<K, V> implements Map<K, V> {
}
class ConstantStringMap<K, V> extends ConstantMap<K, V> {
// This constructor is not used for actual compile-time constants.
// The instantiation of constant maps is shortcut by the compiler.
const ConstantStringMap._(this._length, this._jsObject, this._keys)
: super._();
const ConstantStringMap._(this._jsIndex, this._values) : super._();
// TODO(18131): Ensure type inference knows the precise types of the fields.
final int _length;
// A constant map is backed by a JavaScript object.
final _jsObject;
final List<K> _keys;
// A ConstantStringMap is backed by a JavaScript Object mapping a String to an
// index into the `_values` Array. This is valid only for keys where the order
// of keys is preserved by JavaScript, either by one-at-a-time insertion or a
// JavaScript Object initializer.
final Object _jsIndex;
final JSArray _values;
int get length => JS('JSUInt31', '#', _length);
List<K> get _keysArray => JS('JSUnmodifiableArray', '#', _keys);
int get length => _values.length;
JSArray get _keys {
var keys = JS('', r'#.$keys', this);
if (keys == null) {
keys = _keysFromIndex(_jsIndex);
_setKeys(keys);
}
return JS('JSUnmodifiableArray', '#', keys);
}
ConstantStringMap<K, V> _setKeys(Object? keys) {
JS('', r'#.$keys = #', this, keys);
return this; // Allow chaining in JavaScript of constant pool code.
}
bool containsValue(Object? needle) {
return values.any((V value) => value == needle);
return _values.contains(needle);
}
bool containsKey(Object? key) {
if (key is! String) return false;
if ('__proto__' == key) return false;
return jsHasOwnProperty(_jsObject, key);
return jsHasOwnProperty(_jsIndex, key);
}
V? operator [](Object? key) {
if (!containsKey(key)) return null;
return JS('', '#', _fetch(key));
int index = JS('', '#[#]', _jsIndex, key);
return JS('', '#[#]', _values, index);
}
// [_fetch] is the indexer for keys for which `containsKey(key)` is true.
_fetch(key) => jsPropertyAccess(_jsObject, key);
void forEach(void f(K key, V value)) {
// Use a JS 'cast' to get efficient loop. Type inference doesn't get this
// since constant map representation is chosen after type inference and the
// instantiation is shortcut by the compiler.
var keys = _keysArray;
final keys = _keys;
final values = _values;
for (int i = 0; i < keys.length; i++) {
var key = keys[i];
f(key, _fetch(key));
K key = JS('', '#[#]', keys, i);
V value = JS('', '#[#]', values, i);
f(key, value);
}
}
Iterable<K> get keys {
return _ConstantMapKeyIterable<K>(this);
}
Iterable<K> get keys => _KeysOrValues<K>(_keys);
Iterable<V> get values {
return MappedIterable<K, V>(_keysArray, (key) => _fetch(key));
}
Iterable<V> get values => _KeysOrValues<V>(_values);
}
class _ConstantMapKeyIterable<K> extends Iterable<K> {
ConstantStringMap<K, dynamic> _map;
_ConstantMapKeyIterable(this._map);
/// Converts a JavaScript index object to an untyped Array of the String keys.
///
/// The [index] is a JavaScript object used for lookup of String keys. The own
/// property names are the keys. The index object maps these names to a position
/// in the sequence of entries of the Map or elements of the Set. This is a
/// compact representation since the positions (values of the JavaScript
/// properties) are small integers.
//
/// For Sets we don't need the property values to be positions, but using the
/// same representation allows sharing of indexes between Map and Set constants
/// and allows the enhancement below.
JSArray _keysFromIndex(Object? index) {
return JS('', 'Object.keys(#)', index);
Iterator<K> get iterator => _map._keysArray.iterator;
// Currently the compiler ensures that the JavaScript object literal has its
// properties ordered so that the first one has value `0`, the second has
// value `1`, etc. If the Dart collection's ordering cannot be expressed as a
// JavaScript object (a problem only for String keys 'integers', e.g. `"1"`
// following `"2"`), a different, more general representation is chosen.
//
// We could instead ensure that the keys are sorted by position by 'sorting'
// the keys by the property value. This would be a single pass to assign keys
// to their correct positions in a new Array.
}
int get length => _map._keysArray.length;
/// An Iterable that wraps a JavaScript Array to provide a type and other
/// operations.
class _KeysOrValues<E> extends Iterable<E> {
final JSArray _elements;
_KeysOrValues(this._elements);
int get length => _elements.length;
bool get isEmpty => 0 == length;
bool get isNotEmpty => 0 != length;
_KeysOrValuesOrElementsIterator<E> get iterator =>
_KeysOrValuesOrElementsIterator<E>(this._elements);
}
/// A typed Iterator over an untyped but unmodified Array.
class _KeysOrValuesOrElementsIterator<E> implements Iterator<E> {
final JSArray _elements;
final int _length;
int _index = 0;
E? _current;
_KeysOrValuesOrElementsIterator(this._elements) : _length = _elements.length;
E get current => _current as E;
bool moveNext() {
if (_index >= _length) {
_current = null;
return false;
}
_current = JS<E>('', '#[#]', _elements, _index);
_index++;
return true;
}
// This Iterator is rather like ArrayIterator. Could ArrayIterator be modified
// so that we can use it instead? That might open the possibility of using the
// special optimizations for for-in on Arrays. One problem is that here we are
// wrapping Arrays that never change but there is no signal that they are
// fixed length.
}
class GeneralConstantMap<K, V> extends ConstantMap<K, V> {
@ -209,3 +275,131 @@ class GeneralConstantMap<K, V> extends ConstantMap<K, V> {
int get length => _getMap().length;
}
abstract class ConstantSet<E> extends SetBase<E> {
const ConstantSet();
static Never _throwUnmodifiable() {
throw UnsupportedError('Cannot modify constant Set');
}
void clear() {
_throwUnmodifiable();
}
bool add(E value) {
_throwUnmodifiable();
}
void addAll(Iterable<E> elements) {
_throwUnmodifiable();
}
bool remove(Object? value) {
_throwUnmodifiable();
}
void removeAll(Iterable<Object?> elements) {
_throwUnmodifiable();
}
void removeWhere(bool test(E element)) {
_throwUnmodifiable();
}
void retainAll(Iterable<Object?> elements) {
_throwUnmodifiable();
}
void retainWhere(bool test(E element)) {
_throwUnmodifiable();
}
}
class ConstantStringSet<E> extends ConstantSet<E> {
// A ConstantStringSet is backed by a JavaScript Object whose properties are
// the elements of the Set. This is valid only for sets where the order of
// elements is preserved by JavaScript, either by one-at-a-time insertion or a
// JavaScript Object initializer.
final Object? _jsIndex;
final int _length;
const ConstantStringSet(this._jsIndex, this._length);
int get length => JS('JSUInt31', '#', _length);
bool get isEmpty => _length == 0;
bool get isNotEmpty => !isEmpty;
JSArray get _keys {
var keys = JS('', r'#.$keys', this);
if (keys == null) {
keys = _keysFromIndex(_jsIndex);
_setKeys(keys);
}
return JS('JSUnmodifiableArray', '#', keys);
}
ConstantStringSet<E> _setKeys(Object? keys) {
JS('', r'#.$keys = #', this, keys);
return this; // Allow chaining in JavaScript of constant pool code.
}
Iterator<E> get iterator => _KeysOrValuesOrElementsIterator<E>(_keys);
bool contains(Object? key) {
if (key is! String) return false;
if ('__proto__' == key) return false;
return jsHasOwnProperty(_jsIndex, key);
}
E? lookup(Object? element) {
// There is no way to tell the Set element from [element] for strings, so we
// don't bother to return the stored element. If the set contains the
// element, it must be of type `E`, so use `JS` for a free cast.
return contains(element) ? JS('', '#', element) : null;
}
// TODO(sra): Use the `_keys` Array.
Set<E> toSet() => Set.of(this);
// Consider implementations of operations that can directly use the untyped
// elements list.
//
// List<E> toList({bool growable = true));
// String join([String separator = '']);
}
class GeneralConstantSet<E> extends ConstantSet<E> {
final JSArray _elements;
const GeneralConstantSet(this._elements);
int get length => _elements.length;
bool get isEmpty => length == 0;
bool get isNotEmpty => !isEmpty;
Iterator<E> get iterator => _KeysOrValuesOrElementsIterator(_elements);
// We cannot create the backing map on creation since hashCode interceptors
// have not been defined when constants are created. It is also not desirable
// to add this execution cost to initial program load.
Map<E, E> _getMap() {
LinkedHashMap<E, E>? backingMap = JS('LinkedHashMap|Null', r'#.$map', this);
if (backingMap == null) {
backingMap = JsConstantLinkedHashMap<E, E>();
for (final element in _elements) {
E key = JS('', '#', element);
backingMap[key] = key;
}
JS('', r'#.$map = #', this, backingMap);
}
return backingMap;
}
bool contains(Object? key) {
return _getMap().containsKey(key);
}
E? lookup(Object? element) => _getMap()[element];
Set<E> toSet() => Set.of(this);
}

View file

@ -0,0 +1,135 @@
// 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.
import "package:expect/expect.dart";
// More tests that map literals are ordered.
@pragma('dart2js:never-inline')
@pragma('vm:never-inline')
void check<K, V>(String expectedKeys, String expectedValues, Map<K, V> map) {
Expect.equals(expectedKeys, map.keys.join(','));
Expect.equals(expectedValues, map.values.join(','));
Expect.equals(expectedKeys, List.of(map.keys).join(','));
Expect.equals(expectedValues, List.of(map.values).join(','));
}
main() {
check('', '', const {});
check('1,2', '10,20', const {1: 10, 2: 20});
check('2,1', '20,10', const {2: 20, 1: 10});
// Integer keys.
check('100,20,3', '1,2,3', const {100: 1, 20: 2, 3: 3});
check('100,3,20', '1,3,2', const {100: 1, 3: 3, 20: 2});
check('20,100,3', '2,1,3', const {20: 2, 100: 1, 3: 3});
check('20,3,100', '2,3,1', const {20: 2, 3: 3, 100: 1});
check('3,100,20', '3,1,2', const {3: 3, 100: 1, 20: 2});
check('3,20,100', '3,2,1', const {3: 3, 20: 2, 100: 1});
// Strings with integer-like values.
check('100,20,3', '1,2,3', const {'100': 1, '20': 2, '3': 3});
check('100,3,20', '1,3,2', const {'100': 1, '3': 3, '20': 2});
check('20,100,3', '2,1,3', const {'20': 2, '100': 1, '3': 3});
check('20,3,100', '2,3,1', const {'20': 2, '3': 3, '100': 1});
check('3,100,20', '3,1,2', const {'3': 3, '100': 1, '20': 2});
check('3,20,100', '3,2,1', const {'3': 3, '20': 2, '100': 1});
// ASCII order
check('100,200,30,40,5,6', '1,2,3,4,5,6',
const {'100': 1, '200': 2, '30': 3, '40': 4, '5': 5, '6': 6});
// Reverse ASCII order
check('6,5,40,30,200,100', '6,5,4,3,2,1',
const {'6': 6, '5': 5, '40': 4, '30': 3, '200': 2, '100': 1});
// Numeric order
check('5,6,30,40,100,200', '5,6,3,4,1,2',
const {'5': 5, '6': 6, '30': 3, '40': 4, '100': 1, '200': 2});
// Prefix in numeric order
check('5,6,100,200,30,40', '5,6,1,2,3,4',
const {'5': 5, '6': 6, '100': 1, '200': 2, '30': 3, '40': 4});
// String keys, mixed integer-like and other.
check('100,20,3,a', '1,2,3,z', const {'100': 1, '20': 2, '3': 3, 'a': 'z'});
check('100,3,20,a', '1,3,2,z', const {'100': 1, '3': 3, '20': 2, 'a': 'z'});
check('20,100,3,a', '2,1,3,z', const {'20': 2, '100': 1, '3': 3, 'a': 'z'});
check('20,3,100,a', '2,3,1,z', const {'20': 2, '3': 3, '100': 1, 'a': 'z'});
check('3,100,20,a', '3,1,2,z', const {'3': 3, '100': 1, '20': 2, 'a': 'z'});
check('3,20,100,a', '3,2,1,z', const {'3': 3, '20': 2, '100': 1, 'a': 'z'});
check('a,100,20,3', 'z,1,2,3', const {'a': 'z', '100': 1, '20': 2, '3': 3});
check('a,100,3,20', 'z,1,3,2', const {'a': 'z', '100': 1, '3': 3, '20': 2});
check('a,20,100,3', 'z,2,1,3', const {'a': 'z', '20': 2, '100': 1, '3': 3});
check('a,20,3,100', 'z,2,3,1', const {'a': 'z', '20': 2, '3': 3, '100': 1});
check('a,3,100,20', 'z,3,1,2', const {'a': 'z', '3': 3, '100': 1, '20': 2});
check('a,3,20,100', 'z,3,2,1', const {'a': 'z', '3': 3, '20': 2, '100': 1});
// Records.
check(
'(1, 1),(2, 2),(3, 3)', '1,2,3', const {(1, 1): 1, (2, 2): 2, (3, 3): 3});
check(
'(1, 1),(3, 3),(2, 2)', '1,3,2', const {(1, 1): 1, (3, 3): 3, (2, 2): 2});
check(
'(2, 2),(1, 1),(3, 3)', '2,1,3', const {(2, 2): 2, (1, 1): 1, (3, 3): 3});
check(
'(2, 2),(3, 3),(1, 1)', '2,3,1', const {(2, 2): 2, (3, 3): 3, (1, 1): 1});
check(
'(3, 3),(1, 1),(2, 2)', '3,1,2', const {(3, 3): 3, (1, 1): 1, (2, 2): 2});
check(
'(3, 3),(2, 2),(1, 1)', '3,2,1', const {(3, 3): 3, (2, 2): 2, (1, 1): 1});
// Same as above, without `const`.
check('', '', {});
check('1,2', '10,20', {1: 10, 2: 20});
check('2,1', '20,10', {2: 20, 1: 10});
// Integer keys.
check('100,20,3', '1,2,3', {100: 1, 20: 2, 3: 3});
check('100,3,20', '1,3,2', {100: 1, 3: 3, 20: 2});
check('20,100,3', '2,1,3', {20: 2, 100: 1, 3: 3});
check('20,3,100', '2,3,1', {20: 2, 3: 3, 100: 1});
check('3,100,20', '3,1,2', {3: 3, 100: 1, 20: 2});
check('3,20,100', '3,2,1', {3: 3, 20: 2, 100: 1});
// Strings with integer-like values.
check('100,20,3', '1,2,3', {'100': 1, '20': 2, '3': 3});
check('100,3,20', '1,3,2', {'100': 1, '3': 3, '20': 2});
check('20,100,3', '2,1,3', {'20': 2, '100': 1, '3': 3});
check('20,3,100', '2,3,1', {'20': 2, '3': 3, '100': 1});
check('3,100,20', '3,1,2', {'3': 3, '100': 1, '20': 2});
check('3,20,100', '3,2,1', {'3': 3, '20': 2, '100': 1});
check('100,200,30,40,5,6', '1,2,3,4,5,6',
{'100': 1, '200': 2, '30': 3, '40': 4, '5': 5, '6': 6});
check('6,5,40,30,200,100', '6,5,4,3,2,1',
{'6': 6, '5': 5, '40': 4, '30': 3, '200': 2, '100': 1});
check('5,6,30,40,100,200', '5,6,3,4,1,2',
{'5': 5, '6': 6, '30': 3, '40': 4, '100': 1, '200': 2});
check('5,6,100,200,30,40', '5,6,1,2,3,4',
{'5': 5, '6': 6, '100': 1, '200': 2, '30': 3, '40': 4});
// String keys, mixed integer-like and other.
check('100,20,3,a', '1,2,3,z', {'100': 1, '20': 2, '3': 3, 'a': 'z'});
check('100,3,20,a', '1,3,2,z', {'100': 1, '3': 3, '20': 2, 'a': 'z'});
check('20,100,3,a', '2,1,3,z', {'20': 2, '100': 1, '3': 3, 'a': 'z'});
check('20,3,100,a', '2,3,1,z', {'20': 2, '3': 3, '100': 1, 'a': 'z'});
check('3,100,20,a', '3,1,2,z', {'3': 3, '100': 1, '20': 2, 'a': 'z'});
check('3,20,100,a', '3,2,1,z', {'3': 3, '20': 2, '100': 1, 'a': 'z'});
check('a,100,20,3', 'z,1,2,3', {'a': 'z', '100': 1, '20': 2, '3': 3});
check('a,100,3,20', 'z,1,3,2', {'a': 'z', '100': 1, '3': 3, '20': 2});
check('a,20,100,3', 'z,2,1,3', {'a': 'z', '20': 2, '100': 1, '3': 3});
check('a,20,3,100', 'z,2,3,1', {'a': 'z', '20': 2, '3': 3, '100': 1});
check('a,3,100,20', 'z,3,1,2', {'a': 'z', '3': 3, '100': 1, '20': 2});
check('a,3,20,100', 'z,3,2,1', {'a': 'z', '3': 3, '20': 2, '100': 1});
// Records.
check('(1, 1),(2, 2),(3, 3)', '1,2,3', {(1, 1): 1, (2, 2): 2, (3, 3): 3});
check('(1, 1),(3, 3),(2, 2)', '1,3,2', {(1, 1): 1, (3, 3): 3, (2, 2): 2});
check('(2, 2),(1, 1),(3, 3)', '2,1,3', {(2, 2): 2, (1, 1): 1, (3, 3): 3});
check('(2, 2),(3, 3),(1, 1)', '2,3,1', {(2, 2): 2, (3, 3): 3, (1, 1): 1});
check('(3, 3),(1, 1),(2, 2)', '3,1,2', {(3, 3): 3, (1, 1): 1, (2, 2): 2});
check('(3, 3),(2, 2),(1, 1)', '3,2,1', {(3, 3): 3, (2, 2): 2, (1, 1): 1});
}