Skip to content

Commit

Permalink
Provide WrapperValue and WrapperStorable interfaces
Browse files Browse the repository at this point in the history
Currently, Cadence interpreter.SomeValue is opaque to packages
like Atree, so interpreter.SomeValue is treated as atomic value.
This causes a container wrapped by interpreter.SomeValue to
be treated as atomic value since it is opaque.

Given this, the integration between Cadence and Atree does not
establish parent-child relationship of a wrapped container
since the inner values of interpreter.SomeValue are
not visible to Atree.

Resolving this issue requires modifying onflow/cadence to
unwrap containers wrapped by interpreter.SomeValue.

This commit simplifies and reduces the amount of changes
needed by onflow/cadence by providing two new interfaces here:
- WrapperValue
- WrapperStorable

When Cadence passes objects that implement these interfaces,
Atree will call the interface functions (that implement
unwrapping) when setting child-parent callbacks.

This approach simplifies changes needed by integration in
Cadence to allow Atree to support containers wrapped
by interpreter.SomeValue.
  • Loading branch information
fxamacker authored and turbolent committed Jan 27, 2025
1 parent ea00755 commit 54629e8
Show file tree
Hide file tree
Showing 8 changed files with 871 additions and 33 deletions.
21 changes: 18 additions & 3 deletions array.go
Original file line number Diff line number Diff line change
Expand Up @@ -2772,11 +2772,20 @@ func (a *Array) setParentUpdater(f parentUpdater) {
// setCallbackWithChild sets up callback function with child value (child)
// so parent array (a) can be notified when child value is modified.
func (a *Array) setCallbackWithChild(i uint64, child Value, maxInlineSize uint64) {
c, ok := child.(mutableValueNotifier)
// Unwrap child value if needed (e.g. interpreter.SomeValue)
unwrappedChild, wrapperSize := unwrapValue(child)

c, ok := unwrappedChild.(mutableValueNotifier)
if !ok {
return
}

if maxInlineSize < wrapperSize {
maxInlineSize = 0
} else {
maxInlineSize -= wrapperSize
}

vid := c.ValueID()

// mutableElementIndex is lazily initialized.
Expand Down Expand Up @@ -2809,6 +2818,8 @@ func (a *Array) setCallbackWithChild(i uint64, child Value, maxInlineSize uint64
return false, err
}

storable = unwrapStorable(storable)

// Verify retrieved element is either SlabIDStorable or Slab, with identical value ID.
switch storable := storable.(type) {
case SlabIDStorable:
Expand All @@ -2827,15 +2838,19 @@ func (a *Array) setCallbackWithChild(i uint64, child Value, maxInlineSize uint64
return false, nil
}

// NOTE: Must reset child using original child (not unwrapped child)

// Set child value with parent array using updated index.
// Set() calls c.Storable() which returns inlined or not-inlined child storable.
existingValueStorable, err := a.set(adjustedIndex, c)
// Set() calls child.Storable() which returns inlined or not-inlined child storable.
existingValueStorable, err := a.set(adjustedIndex, child)
if err != nil {
return false, err
}

// Verify overwritten storable has identical value ID.

existingValueStorable = unwrapStorable(existingValueStorable)

switch existingValueStorable := existingValueStorable.(type) {
case SlabIDStorable:
sid := SlabID(existingValueStorable)
Expand Down
268 changes: 267 additions & 1 deletion array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4991,7 +4991,7 @@ func TestArrayNestedStorables(t *testing.T) {
for i := uint64(0); i < arraySize; i++ {
s := strings.Repeat("a", int(i))
v := SomeValue{Value: NewStringValue(s)}
values[i] = v
values[i] = someValue{NewStringValue(s)}

err := array.Append(v)
require.NoError(t, err)
Expand Down Expand Up @@ -8930,3 +8930,269 @@ func testExistingInlinedArraySetType(
require.Equal(t, expectedCount, childArray2.Count())
require.Equal(t, newTypeInfo, childArray2.Type())
}

func TestArrayWrapperValue(t *testing.T) {

address := Address{1, 2, 3, 4, 5, 6, 7, 8}

// createArrayWithSomeValue creates an array in the format of [SomeValue(uint64)]
createArrayWithSomeValue := func(
storage SlabStorage,
address Address,
typeInfo TypeInfo,
arraySize int,
) (*Array, []Value) {
array, err := NewArray(storage, address, typeInfo)
require.NoError(t, err)

var values []Value

for i := 0; i < arraySize; i++ {
v := Uint64Value(i)

err := array.Append(SomeValue{v})
require.NoError(t, err)

values = append(values, someValue{v})
}

return array, values
}

// createNestedArrayWithSomeValue creates an array in the format of [SomeValue([SomeValue(uint64)])]
createNestedArrayWithSomeValue := func(
storage SlabStorage,
address Address,
typeInfo TypeInfo,
arraySize int,
childArraySize int,
) (*Array, []Value) {
array, err := NewArray(storage, address, typeInfo)
require.NoError(t, err)

var values []Value

for i := 0; i < arraySize; i++ {
childArray, expectedChildValues := createArrayWithSomeValue(storage, address, typeInfo, childArraySize)

err := array.Append(SomeValue{childArray})
require.NoError(t, err)

values = append(values, someValue{arrayValue(expectedChildValues)})
}

return array, values
}

t.Run("[SomeValue([SomeValue(value)])]", func(t *testing.T) {
const (
arraySize = 3
childArraySize = 2
)

typeInfo := testTypeInfo{42}

createStorageWithSomeValue := func(arraySize int) (
_ BaseStorage,
rootSlabID SlabID,
expectedValues []Value,
) {
storage := newTestPersistentStorage(t)

array, err := NewArray(storage, address, typeInfo)
require.NoError(t, err)

var values []Value
for i := 0; i < arraySize; i++ {

childArray, childValues := createArrayWithSomeValue(storage, address, typeInfo, childArraySize)

err := array.Append(SomeValue{childArray})
require.NoError(t, err)

values = append(values, someValue{arrayValue(childValues)})
}

testArray(t, storage, typeInfo, address, array, values, false)

err = storage.FastCommit(runtime.NumCPU())
require.NoError(t, err)

return storage.baseStorage, array.SlabID(), values
}

// Create a base storage with array in the format of
// [SomeValue([SomeValue(uint64)])]
baseStorage, rootSlabID, expectedValues := createStorageWithSomeValue(arraySize)
require.Equal(t, arraySize, len(expectedValues))

// Create a new storage with encoded array
storage := newTestPersistentStorageWithBaseStorage(t, baseStorage)

// Load existing array from storage
array, err := NewArrayWithRootID(storage, rootSlabID)
require.NoError(t, err)
require.Equal(t, uint64(len(expectedValues)), array.Count())

// Get and verify first element as SomeValue(array)

expectedValue := expectedValues[0]

// Get array element (SomeValue)
element, err := array.Get(uint64(0))
require.NoError(t, err)

elementAsSomeValue, isSomeValue := element.(SomeValue)
require.True(t, isSomeValue)

unwrappedChildArray, isArray := elementAsSomeValue.Value.(*Array)
require.True(t, isArray)

expectedValuesAsSomeValue, isSomeValue := expectedValue.(someValue)
require.True(t, isSomeValue)

expectedUnwrappedChildArray, isArrayValue := expectedValuesAsSomeValue.Value.(arrayValue)
require.True(t, isArrayValue)

require.Equal(t, uint64(len(expectedUnwrappedChildArray)), unwrappedChildArray.Count())

// Modify wrapped child array of SomeValue

newValue := NewStringValue("x")
err = unwrappedChildArray.Append(SomeValue{newValue})
require.NoError(t, err)

expectedUnwrappedChildArray = append(expectedUnwrappedChildArray, someValue{newValue})
expectedValues[0] = someValue{expectedUnwrappedChildArray}

err = storage.FastCommit(runtime.NumCPU())
require.NoError(t, err)

// Verify modified wrapped child array of SomeValue using new storage with committed data

storage2 := newTestPersistentStorageWithBaseStorage(t, storage.baseStorage)

// Load existing array from storage
array2, err := NewArrayWithRootID(storage2, rootSlabID)
require.NoError(t, err)
require.Equal(t, uint64(len(expectedValues)), array2.Count())

testArray(t, storage, typeInfo, address, array2, expectedValues, true)
})

t.Run("[SomeValue([SomeValue([SomeValue(uint64)])])]", func(t *testing.T) {
const (
arraySize = 4
childArraySize = 3
gchildArraySize = 2
)

typeInfo := testTypeInfo{42}

createStorageWithNestedSomeValue := func(arraySize int) (
_ BaseStorage,
rootSlabID SlabID,
expectedValues []Value,
) {
storage := newTestPersistentStorage(t)

array, err := NewArray(storage, address, typeInfo)
require.NoError(t, err)

var values []Value
for i := 0; i < arraySize; i++ {

childArray, childValues := createNestedArrayWithSomeValue(storage, address, typeInfo, childArraySize, gchildArraySize)

err := array.Append(SomeValue{childArray})
require.NoError(t, err)

values = append(values, someValue{arrayValue(childValues)})
}

testArray(t, storage, typeInfo, address, array, values, true)

err = storage.FastCommit(runtime.NumCPU())
require.NoError(t, err)

return storage.baseStorage, array.SlabID(), values
}

// Create a base storage with array in the format of
// [SomeValue([SomeValue([SomeValue(uint64)])])]
baseStorage, rootSlabID, expectedValues := createStorageWithNestedSomeValue(arraySize)
require.Equal(t, arraySize, len(expectedValues))

// Create a new storage with encoded array
storage := newTestPersistentStorageWithBaseStorage(t, baseStorage)

// Load existing array from storage
array, err := NewArrayWithRootID(storage, rootSlabID)
require.NoError(t, err)
require.Equal(t, uint64(len(expectedValues)), array.Count())

// Get and verify first element as SomeValue(array)

expectedValue := expectedValues[0]

// Get array element (SomeValue)
element, err := array.Get(uint64(0))
require.NoError(t, err)

elementAsSomeValue, isSomeValue := element.(SomeValue)
require.True(t, isSomeValue)

unwrappedChildArray, isArray := elementAsSomeValue.Value.(*Array)
require.True(t, isArray)

expectedValuesAsSomeValue, isSomeValue := expectedValue.(someValue)
require.True(t, isSomeValue)

expectedUnwrappedChildArray, isArrayValue := expectedValuesAsSomeValue.Value.(arrayValue)
require.True(t, isArrayValue)

require.Equal(t, uint64(len(expectedUnwrappedChildArray)), unwrappedChildArray.Count())

// Get and verify nested child element as SomeValue(array)

childArrayElement, err := unwrappedChildArray.Get(uint64(0))
require.NoError(t, err)

childArrayElementAsSomeValue, isSomeValue := childArrayElement.(SomeValue)
require.True(t, isSomeValue)

unwrappedGChildArray, isArray := childArrayElementAsSomeValue.Value.(*Array)
require.True(t, isArray)

expectedChildValuesAsSomeValue, isSomeValue := expectedUnwrappedChildArray[0].(someValue)
require.True(t, isSomeValue)

expectedUnwrappedGChildArray, isArrayValue := expectedChildValuesAsSomeValue.Value.(arrayValue)
require.True(t, isArrayValue)

require.Equal(t, uint64(len(expectedUnwrappedGChildArray)), unwrappedGChildArray.Count())

// Modify wrapped gchild array of SomeValue

newValue := NewStringValue("x")
err = unwrappedGChildArray.Append(SomeValue{newValue})
require.NoError(t, err)

expectedUnwrappedGChildArray = append(expectedUnwrappedGChildArray, someValue{newValue})
expectedValues[0].(someValue).Value.(arrayValue)[0] = someValue{expectedUnwrappedGChildArray}

err = storage.FastCommit(runtime.NumCPU())
require.NoError(t, err)

// Verify modified wrapped child array of SomeValue using new storage with committed data

storage2 := newTestPersistentStorageWithBaseStorage(t, storage.baseStorage)

// Load existing array from storage
array2, err := NewArrayWithRootID(storage2, rootSlabID)
require.NoError(t, err)
require.Equal(t, uint64(len(expectedValues)), array2.Count())

testArray(t, storage, typeInfo, address, array2, expectedValues, true)
})
}
21 changes: 18 additions & 3 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -4892,11 +4892,20 @@ func (m *OrderedMap) setCallbackWithChild(
child Value,
maxInlineSize uint64,
) {
c, ok := child.(mutableValueNotifier)
// Unwrap child value if needed (e.g. interpreter.SomeValue)
unwrappedChild, wrapperSize := unwrapValue(child)

c, ok := unwrappedChild.(mutableValueNotifier)
if !ok {
return
}

if maxInlineSize < wrapperSize {
maxInlineSize = 0
} else {
maxInlineSize -= wrapperSize
}

vid := c.ValueID()

c.setParentUpdater(func() (found bool, err error) {
Expand All @@ -4921,6 +4930,8 @@ func (m *OrderedMap) setCallbackWithChild(
return false, err
}

valueStorable = unwrapStorable(valueStorable)

// Verify retrieved element value is either SlabIDStorable or Slab, with identical value ID.
switch valueStorable := valueStorable.(type) {
case SlabIDStorable:
Expand All @@ -4939,15 +4950,19 @@ func (m *OrderedMap) setCallbackWithChild(
return false, nil
}

// NOTE: Must reset child using original child (not unwrapped child)

// Set child value with parent map using same key.
// Set() calls c.Storable() which returns inlined or not-inlined child storable.
existingValueStorable, err := m.set(comparator, hip, key, c)
// Set() calls child.Storable() which returns inlined or not-inlined child storable.
existingValueStorable, err := m.set(comparator, hip, key, child)
if err != nil {
return false, err
}

// Verify overwritten storable has identical value ID.

existingValueStorable = unwrapStorable(existingValueStorable)

switch existingValueStorable := existingValueStorable.(type) {
case SlabIDStorable:
sid := SlabID(existingValueStorable)
Expand Down
Loading

0 comments on commit 54629e8

Please sign in to comment.