diff --git a/pkgs/_macro_host/lib/src/macro_cache.dart b/pkgs/_macro_host/lib/src/macro_cache.dart index 14281d0c..a81f7085 100644 --- a/pkgs/_macro_host/lib/src/macro_cache.dart +++ b/pkgs/_macro_host/lib/src/macro_cache.dart @@ -36,7 +36,7 @@ class MacroResultsCache { .skip(1) .fold(queryResults.first.response, (model, next) => model.mergeWith(next.response)) - .identityHash, + .fingerprint, response: response ); } @@ -61,7 +61,7 @@ class MacroResultsCache { .skip(1) .fold(queryResults.first.model, (model, next) => model.mergeWith(next.model)) - .identityHash; + .fingerprint; if (newResultsHash != cached.resultsHash) { _cache.remove(cacheKey); return null; diff --git a/pkgs/dart_model/lib/src/dart_model.dart b/pkgs/dart_model/lib/src/dart_model.dart index 29e03875..666ad6e2 100644 --- a/pkgs/dart_model/lib/src/dart_model.dart +++ b/pkgs/dart_model/lib/src/dart_model.dart @@ -2,8 +2,6 @@ // 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:collection/collection.dart'; - import 'dart_model.g.dart'; import 'json_buffer/json_buffer_builder.dart'; import 'lazy_merged_map.dart'; @@ -33,7 +31,12 @@ extension ModelExtension on Model { /// An identity hash for `this`, used for comparing query results. /// /// TODO: A faster/better implementation? - int get identityHash => const DeepCollectionEquality().hash(node); + int get fingerprint { + // TODO: Implementation for non-buffer maps? + var node = this.node as MapInBuffer; + return node.buffer.fingerprint(node.pointer, + type: Type.typedMapPointer, alreadyDereferenced: true); + } /// Looks up [name] in `this`. /// diff --git a/pkgs/dart_model/lib/src/json_buffer/closed_list.dart b/pkgs/dart_model/lib/src/json_buffer/closed_list.dart index 5ef820c7..6ae5e684 100644 --- a/pkgs/dart_model/lib/src/json_buffer/closed_list.dart +++ b/pkgs/dart_model/lib/src/json_buffer/closed_list.dart @@ -77,10 +77,19 @@ class _ClosedList with ListMixin { set length(int length) { throw UnsupportedError('This JsonBufferBuilder list is read-only.'); } + + int get fingerprint { + var iterator = _ClosedListHashIterator(_buffer, _pointer, length); + var hash = 0; + while (iterator.moveNext()) { + hash = Object.hash(hash, iterator.current); + } + return hash; + } } /// `Iterator` that reads a "closed list" in a [JsonBufferBuilder]. -class _ClosedListIterator implements Iterator { +class _ClosedListIterator implements Iterator { final JsonBufferBuilder _buffer; final _Pointer _last; _Pointer _pointer; @@ -91,7 +100,7 @@ class _ClosedListIterator implements Iterator { _pointer = pointer + _lengthSize - ClosedLists._valueSize; @override - Object? get current => _buffer._readAny(_pointer); + T get current => _buffer._readAny(_pointer) as T; @override bool moveNext() { @@ -101,3 +110,10 @@ class _ClosedListIterator implements Iterator { return _pointer != _last; } } + +class _ClosedListHashIterator extends _ClosedListIterator { + _ClosedListHashIterator(super.buffer, super.pointer, super.length); + + @override + int get current => _buffer.fingerprint(_pointer); +} diff --git a/pkgs/dart_model/lib/src/json_buffer/closed_map.dart b/pkgs/dart_model/lib/src/json_buffer/closed_map.dart index cc6f2f6b..1a4ab650 100644 --- a/pkgs/dart_model/lib/src/json_buffer/closed_map.dart +++ b/pkgs/dart_model/lib/src/json_buffer/closed_map.dart @@ -55,14 +55,15 @@ class _ClosedMap implements MapInBuffer { @override final JsonBufferBuilder buffer; - final _Pointer _pointer; + @override + final _Pointer pointer; @override final Map? parent; @override final int length; - _ClosedMap(this.buffer, this._pointer, this.parent) - : length = buffer._readLength(_pointer); + _ClosedMap(this.buffer, this.pointer, this.parent) + : length = buffer._readLength(pointer); @override Object? operator [](Object? key) { @@ -75,18 +76,18 @@ class _ClosedMap @override late final Iterable keys = _IteratorFunctionIterable( - () => _ClosedMapKeyIterator(buffer, this, _pointer, length), + () => _ClosedMapKeyIterator(buffer, this, pointer, length), length: length); @override late final Iterable values = _IteratorFunctionIterable( - () => _ClosedMapValueIterator(buffer, this, _pointer, length), + () => _ClosedMapValueIterator(buffer, this, pointer, length), length: length); @override late final Iterable> entries = _IteratorFunctionIterable( - () => _ClosedMapEntryIterator(buffer, this, _pointer, length), + () => _ClosedMapEntryIterator(buffer, this, pointer, length), length: length); @override @@ -109,18 +110,25 @@ class _ClosedMap @override bool operator ==(Object other) => - other is _ClosedMap && - other.buffer == buffer && - other._pointer == _pointer; + other is _ClosedMap && other.buffer == buffer && other.pointer == pointer; @override - int get hashCode => Object.hash(buffer, _pointer); + int get hashCode => Object.hash(buffer, pointer); + + int get fingerprint { + var iterator = _ClosedMapHashIterator(buffer, null, pointer, length); + var hash = 0; + while (iterator.moveNext()) { + hash = Object.hash(hash, iterator.current); + } + return hash; + } } /// `Iterator` that reads a "closed map" in a [JsonBufferBuilder]. abstract class _ClosedMapIterator implements Iterator { final JsonBufferBuilder _buffer; - final _ClosedMap _parent; + final _ClosedMap? _parent; final _Pointer _last; _Pointer _pointer; @@ -148,7 +156,7 @@ abstract class _ClosedMapIterator implements Iterator { class _ClosedMapKeyIterator extends _ClosedMapIterator { _ClosedMapKeyIterator( - super._buffer, super._porent, super.pointer, super.length); + super._buffer, super._parent, super.pointer, super.length); @override String get current => _currentKey; @@ -156,7 +164,7 @@ class _ClosedMapKeyIterator extends _ClosedMapIterator { class _ClosedMapValueIterator extends _ClosedMapIterator { _ClosedMapValueIterator( - super._buffer, super._porent, super.pointer, super.length); + super._buffer, super._parent, super.pointer, super.length); @override Object? get current => _currentValue; @@ -165,8 +173,19 @@ class _ClosedMapValueIterator extends _ClosedMapIterator { class _ClosedMapEntryIterator extends _ClosedMapIterator> { _ClosedMapEntryIterator( - super._buffer, super._porent, super.pointer, super.length); + super._buffer, super._parent, super.pointer, super.length); @override MapEntry get current => MapEntry(_currentKey, _currentValue); } + +class _ClosedMapHashIterator extends _ClosedMapIterator { + _ClosedMapHashIterator( + super._buffer, super._parent, super.pointer, super.length); + + @override + int get current => Object.hash( + _buffer._fingerprint(_pointer, Type.stringPointer), + _buffer.fingerprint(_pointer + ClosedMaps._keySize), + ); +} diff --git a/pkgs/dart_model/lib/src/json_buffer/growable_map.dart b/pkgs/dart_model/lib/src/json_buffer/growable_map.dart index 8a955ccc..d2406a3f 100644 --- a/pkgs/dart_model/lib/src/json_buffer/growable_map.dart +++ b/pkgs/dart_model/lib/src/json_buffer/growable_map.dart @@ -52,7 +52,7 @@ extension GrowableMaps on JsonBufferBuilder { /// [createGrowableMap]. Otherwise, [UnsupportedError] is thrown. _Pointer _pointerToGrowableMap(_GrowableMap map) { _checkGrowableMapOwnership(map); - return map._pointer; + return map.pointer; } /// Throws if [map is backed by a different buffer to `this`. @@ -75,14 +75,15 @@ class _GrowableMap implements MapInBuffer { @override final JsonBufferBuilder buffer; - final _Pointer _pointer; + @override + final _Pointer pointer; @override final Map? parent; int _length; _Pointer? _lastPointer; - _GrowableMap(this.buffer, this._pointer, this.parent) - : _length = buffer._readLength(_pointer + _pointerSize); + _GrowableMap(this.buffer, this.pointer, this.parent) + : _length = buffer._readLength(pointer + _pointerSize); @override int get length => _length; @@ -100,17 +101,17 @@ class _GrowableMap @override late final Iterable keys = _IteratorFunctionIterable( - () => _GrowableMapKeyIterator(buffer, this, _pointer), + () => _GrowableMapKeyIterator(buffer, this, pointer), length: length); @override late final Iterable values = _IteratorFunctionIterable( - () => _GrowableMapValueIterator(buffer, this, _pointer), + () => _GrowableMapValueIterator(buffer, this, pointer), length: length); @override late final Iterable> entries = _IteratorFunctionIterable( - () => _GrowableMapEntryIterator(buffer, this, _pointer), + () => _GrowableMapEntryIterator(buffer, this, pointer), length: length); /// Add [value] to the map with key [key]. @@ -125,27 +126,27 @@ class _GrowableMap // If `_lastPointer` is not set yet, walk the map to find the end of it. if (_lastPointer == null) { - final iterator = _GrowableMapEntryIterator(buffer, this, _pointer); - _lastPointer = _pointer; + final iterator = _GrowableMapEntryIterator(buffer, this, pointer); + _lastPointer = pointer; while (iterator.moveNext()) { _lastPointer = iterator._pointer; } } // Reserve and write the new node. - final pointer = buffer._reserve(GrowableMaps._entrySize); - final entryPointer = pointer + _pointerSize; + final newPointer = buffer._reserve(GrowableMaps._entrySize); + final entryPointer = newPointer + _pointerSize; buffer._writePointer(entryPointer, buffer._pointerToString(key)); buffer._writeAny(entryPointer + _pointerSize, value); // Point to the new node in the previous node. - buffer._writePointer(_lastPointer!, pointer); + buffer._writePointer(_lastPointer!, newPointer); // Update `_lastPointer` to the new node. - _lastPointer = pointer; + _lastPointer = newPointer; // Update length. ++_length; - buffer._writeLength(_pointer + _pointerSize, length, allowOverwrite: true); + buffer._writeLength(pointer + _pointerSize, length, allowOverwrite: true); buffer._explanations?.pop(); } @@ -163,16 +164,25 @@ class _GrowableMap bool operator ==(Object other) => other is _GrowableMap && other.buffer == buffer && - other._pointer == _pointer; + other.pointer == pointer; @override - int get hashCode => Object.hash(buffer, _pointer); + int get hashCode => Object.hash(buffer, pointer); + + int get fingerprint { + var iterator = _GrowableMapHashIterator(buffer, null, pointer); + var hash = 0; + while (iterator.moveNext()) { + hash = Object.hash(hash, iterator.current); + } + return hash; + } } /// `Iterator` that reads a "growable map" in a [JsonBufferBuilder]. abstract class _GrowableMapIterator implements Iterator { final JsonBufferBuilder _buffer; - final _GrowableMap _parent; + final _GrowableMap? _parent; _Pointer _pointer; _GrowableMapIterator(this._buffer, this._parent, this._pointer); @@ -214,3 +224,13 @@ class _GrowableMapEntryIterator @override MapEntry get current => MapEntry(_currentKey, _currentValue as V); } + +class _GrowableMapHashIterator extends _GrowableMapIterator { + _GrowableMapHashIterator(super._buffer, super._parent, super._pointer); + + @override + int get current => Object.hash( + _buffer._fingerprint(_pointer + _pointerSize, Type.stringPointer), + _buffer.fingerprint(_pointer + _pointerSize + GrowableMaps._keySize), + ); +} diff --git a/pkgs/dart_model/lib/src/json_buffer/iterables.dart b/pkgs/dart_model/lib/src/json_buffer/iterables.dart index d08aedee..82306145 100644 --- a/pkgs/dart_model/lib/src/json_buffer/iterables.dart +++ b/pkgs/dart_model/lib/src/json_buffer/iterables.dart @@ -48,4 +48,7 @@ abstract interface class MapInBuffer { /// The `Map` that contains this value, or `null` if this value has not been /// added to a `Map` or is itself the root `Map`. Map? get parent; + + /// A pointer to the start of this object in [buffer]. + int get pointer; } diff --git a/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart b/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart index 762d7481..b9bf1677 100644 --- a/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart +++ b/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart @@ -52,6 +52,67 @@ class JsonBufferBuilder { map = createGrowableMap(); } + /// Computes the identity hash of the object at [pointer] from its raw bytes. + /// + /// Any nested pointers use the hash of the values they point to. + /// + /// If [type] is provided, [pointer] should point directly at the object. + /// Otherwise a [Type] will be read first, followed by the value. + /// + /// If [alreadyDereferenced] is `true`, then for types which are pointers, + /// [pointer] already points at the top of the object, and should not be + /// followed before reading the object. + int fingerprint(int pointer, {Type? type, bool alreadyDereferenced = false}) { + if (type == null) { + type = _readType(pointer); + pointer += _typeSize; + } + return _fingerprint(pointer, type, + alreadyDereferenced: alreadyDereferenced); + } + + /// Computes the identity hash of the object at [pointer] with a known [type] + /// from its raw bytes. + /// + /// If [alreadyDereferenced] is `true`, then for types which are pointers, + /// [pointer] already points at the top of the object, and should not be + /// followed before reading the object. + int _fingerprint(_Pointer pointer, Type type, + {bool alreadyDereferenced = false}) { + // Dereference [pointer] if it is a pointer type, and hasn't already been + // dereferenced. + if (type.isPointer && !alreadyDereferenced) { + pointer = _readPointer(pointer); + } + + switch (type) { + case Type.nil: + return null.hashCode; + case Type.type: + return _buffer[pointer]; + case Type.pointer: + return fingerprint(pointer); + case Type.uint32: + return _readUint32(pointer); + case Type.boolean: + return _buffer[pointer]; + case Type.anyPointer: + return fingerprint(pointer); + case Type.stringPointer: + final length = _readLength(pointer); + pointer += _lengthSize; + return Object.hashAll(_buffer.sublist(pointer, pointer + length)); + case Type.closedListPointer: + return _ClosedList(this, pointer).fingerprint; + case Type.closedMapPointer: + return _ClosedMap(this, pointer, null).fingerprint; + case Type.growableMapPointer: + return _GrowableMap(this, pointer, null).fingerprint; + case Type.typedMapPointer: + return _TypedMap(this, pointer, null).fingerprint; + } + } + /// The JSON data. /// /// The buffer is _not_ copied, unpredictable behavior will result if it is diff --git a/pkgs/dart_model/lib/src/json_buffer/type.dart b/pkgs/dart_model/lib/src/json_buffer/type.dart index 75506394..be5b2013 100644 --- a/pkgs/dart_model/lib/src/json_buffer/type.dart +++ b/pkgs/dart_model/lib/src/json_buffer/type.dart @@ -23,17 +23,22 @@ typedef _Pointer = int; /// The type of a value in the buffer. enum Type { - nil, - type, - pointer, - uint32, - boolean, - anyPointer, - stringPointer, - closedListPointer, - closedMapPointer, - growableMapPointer, - typedMapPointer; + nil(false), + type(false), + pointer(true), + uint32(false), + boolean(false), + anyPointer(false), // This is actually a type followed by a pointer. + stringPointer(true), + closedListPointer(true), + closedMapPointer(true), + growableMapPointer(true), + typedMapPointer(true); + + /// Whether this object is always stored as a raw pointer. + final bool isPointer; + + const Type(this.isPointer); /// Returns the [Type] of [value], or throws if it is not a supported type. /// diff --git a/pkgs/dart_model/lib/src/json_buffer/typed_map.dart b/pkgs/dart_model/lib/src/json_buffer/typed_map.dart index edc3dce2..0a8b4f2b 100644 --- a/pkgs/dart_model/lib/src/json_buffer/typed_map.dart +++ b/pkgs/dart_model/lib/src/json_buffer/typed_map.dart @@ -306,7 +306,7 @@ extension TypedMaps on JsonBufferBuilder { /// The [map] must have been created in this buffer using [createTypedMap]. _Pointer _pointerToTypedMap(_TypedMap map) { _checkTypedMapOwnership(map); - return map._pointer; + return map.pointer; } /// Throws if [map is backed by a different buffer to `this`. @@ -329,17 +329,18 @@ class _TypedMap implements Map, MapInBuffer { @override final JsonBufferBuilder buffer; - final _Pointer _pointer; + @override + final _Pointer pointer; @override final Map? parent; // If a `TypedMap` is created then immediately added to another `Map` then - // these values are never needed, just the `_pointer`. Use `late` so they are + // these values are never needed, just the `pointer`. Use `late` so they are // only computed if needed. late final _Pointer _schemaPointer = // The high byte of the schema pointer indicates "filled", omit it. - buffer._readPointer(_pointer) & 0x7fffffff; + buffer._readPointer(pointer) & 0x7fffffff; /// The schema of this "typed map" giving its field names and types. late final TypedMapSchema _schema = @@ -347,9 +348,9 @@ class _TypedMap TypedMapSchema(buffer._readClosedMap(_schemaPointer, null).cast()); /// Whether all fields are present, meaning no explicit field set was written. - late final bool filled = (buffer._readPointer(_pointer) & 0x80000000) != 0; + late final bool filled = (buffer._readPointer(pointer) & 0x80000000) != 0; - _TypedMap(this.buffer, this._pointer, this.parent); + _TypedMap(this.buffer, this.pointer, this.parent); /// Whether the field at [index] is present. bool _hasField(int index) { @@ -360,7 +361,7 @@ class _TypedMap if (filled) return true; final byte = index ~/ 8; final bit = index % 8; - return buffer._readBit(_pointer + _pointerSize + byte, bit); + return buffer._readBit(pointer + _pointerSize + byte, bit); } @override @@ -428,12 +429,24 @@ class _TypedMap @override bool operator ==(Object other) => - other is _TypedMap && - other.buffer == buffer && - other._pointer == _pointer; + other is _TypedMap && other.buffer == buffer && other.pointer == pointer; @override - int get hashCode => Object.hash(buffer, _pointer); + int get hashCode => Object.hash(buffer, pointer); + + int get fingerprint { + // Note: we could include the schema but don't need to. If something can + // be one of multiple types, that type will be included in a `type` field + // in the map. + var hash = 0; + final iterator = _schema._isAllBooleans + ? _AllBoolsTypedMapHashIterator(this) + : _PartialTypedMapHashIterator(this); + while (iterator.moveNext()) { + hash = Object.hash(hash, iterator.current); + } + return hash; + } } /// `Iterator` that reads a "typed map" in a [JsonBufferBuilder]. @@ -452,7 +465,7 @@ abstract class _PartialTypedMapIterator implements Iterator { _PartialTypedMapIterator(this._map) : _buffer = _map.buffer, _schema = _map._schema, - _valuesPointer = _map._pointer + + _valuesPointer = _map.pointer + _pointerSize + (_map.filled ? 0 : _map._schema._fieldSetSize); @@ -506,6 +519,16 @@ class _PartialTypedMapEntryIterator MapEntry get current => MapEntry(_currentKey, _currentValue); } +class _PartialTypedMapHashIterator extends _PartialTypedMapIterator { + _PartialTypedMapHashIterator(super._map); + + @override + int get current => Object.hash( + _currentKey, + _buffer._fingerprint( + _valuesPointer + _offset, _schema._valueTypes[_index])); +} + /// `Iterator` that reads a "typed map" in a [JsonBufferBuilder] with all /// values of type [Type.boolean]. abstract class _AllBoolsTypedMapIterator implements Iterator { @@ -526,7 +549,7 @@ abstract class _AllBoolsTypedMapIterator implements Iterator { _AllBoolsTypedMapIterator(this._map) : _buffer = _map.buffer, _schema = _map._schema, - _valuesPointer = _map._pointer + + _valuesPointer = _map.pointer + _pointerSize + (_map.filled ? 0 : _map._schema._fieldSetSize); @@ -579,3 +602,10 @@ class _AllBoolsTypedMapEntryIterator @override MapEntry get current => MapEntry(_currentKey, _currentValue); } + +class _AllBoolsTypedMapHashIterator extends _AllBoolsTypedMapIterator { + _AllBoolsTypedMapHashIterator(super._map); + + @override + int get current => Object.hash(_currentKey, _currentValue); +} diff --git a/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart b/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart index a046b365..5cdcb359 100644 --- a/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart +++ b/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart @@ -6,7 +6,7 @@ import 'package:dart_model/src/json_buffer/json_buffer_builder.dart'; import 'package:test/test.dart'; void main() { - group(JsonBufferBuilder, () { + group('JsonBufferBuilder', () { late JsonBufferBuilder builder; setUp(() { @@ -87,5 +87,87 @@ void main() { JsonBufferBuilder.deserialize(JsonBufferBuilder().serialize()); expect(() => deserializedBuilder.map['a'] = 'b', throwsStateError); }); + + group('fingerprint', () { + /// Re-usable fingerprint tests, [a] and [b] should be different values, + /// possibly of different types. + void testFingerprint(Object? a, Object? b) { + assert(a != b); + expect(fingerprint({'a': a}), equals(fingerprint({'a': a}))); + expect( + fingerprint({ + 'a': {'b': b} + }), + equals(fingerprint({ + 'a': {'b': b} + }))); + expect(fingerprint({'a': a}), isNot(equals(fingerprint({'a': b})))); + expect(fingerprint({'a': a}), isNot(equals(fingerprint({'b': a})))); + expect(fingerprint({'a': a, 'b': b}), + isNot(equals(fingerprint({'a': b, 'b': a})))); + } + + test('boolean fields', () { + testFingerprint(true, false); + }); + + test('String fields', () { + testFingerprint('a', 'b'); + }); + + test('int fields', () { + testFingerprint(1, 2); + }); + + test('null fields', () { + testFingerprint(null, 0); + testFingerprint(null, true); + testFingerprint(null, false); + testFingerprint(null, {}); + }); + + test('closed list fields', () { + testFingerprint([], [1]); + testFingerprint([1, 2], [2, 1]); + }); + + test('closed map fields', () { + testFingerprint({}, {'a': 1}); + testFingerprint({'a': 'b'}, {'b': 'a'}); + }); + + test('growable map fields', () { + final builderA = JsonBufferBuilder(); + final builderB = JsonBufferBuilder(); + testFingerprint(builderA.createGrowableMap()..['a'] = 1, + builderB.createGrowableMap()..['a'] = 2); + }); + + test('typed maps with same schema', () { + final builderA = JsonBufferBuilder(); + final builderB = JsonBufferBuilder(); + final schema = TypedMapSchema({ + 'a': Type.stringPointer, + }); + testFingerprint(builderA.createTypedMap(schema, 'a'), + builderB.createTypedMap(schema, 'b')); + }); + }); }); } + +int fingerprint(Map map) { + final builder = + map is MapInBuffer ? (map as MapInBuffer).buffer : JsonBufferBuilder() + ..map.deepCopy(map); + return builder.fingerprint((builder.map as MapInBuffer).pointer, + type: Type.growableMapPointer, alreadyDereferenced: true); +} + +extension on Map { + void deepCopy(Map from) { + for (var entry in from.entries) { + this[entry.key as String] = entry.value; + } + } +}