Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: properly mark array elements when an realm slice is updated #1305

Merged
merged 16 commits into from
Jan 4, 2024

Conversation

deelawn
Copy link
Contributor

@deelawn deelawn commented Oct 26, 2023

Addresses #1167, #960, and #1170

Consider the following situation:

  • A slice of structs exists with a length of zero and a capacity of one
  • A new struct literal is appended to the slice
  • The code panics because the newly allocated struct literal was never marked as "new"
package append

import (
	"gno.land/p/demo/ufmt"
)

type T struct{ i int }

var a []T

func init() {
        a = make([]T, 0, 1)
}

func Append(i int) {
	a = append(a, T{i: i})
}

Invoking the Append function will cause a panic.

The solution is to traverse each of the array elements after slice append assignment to make sure any new or updated elements are marked as such.

This PR also includes a change to ensure that marking an object as dirty and managing references to the object are mutually exclusive. I think this is correct but am not sure.

The changes include txtar test cases that incorporate the issue described by @tbruyelle in #1170

@github-actions github-actions bot added the 📦 🤖 gnovm Issues or PRs gnovm related label Oct 26, 2023
@deelawn deelawn changed the title Fix: properly mark array elements when an realm slice is updated fix: properly mark array elements when an realm slice is updated Oct 26, 2023
@codecov
Copy link

codecov bot commented Oct 26, 2023

Codecov Report

Attention: 17 lines in your changes are missing coverage. Please review.

Comparison is base (b1b514c) 56.08% compared to head (5425504) 55.85%.

Files Patch % Lines
gnovm/pkg/gnolang/values.go 33.33% 10 Missing ⚠️
gnovm/pkg/gnolang/uverse.go 75.86% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1305      +/-   ##
==========================================
- Coverage   56.08%   55.85%   -0.24%     
==========================================
  Files         432      431       -1     
  Lines       65971    65758     -213     
==========================================
- Hits        37001    36728     -273     
- Misses      26078    26157      +79     
+ Partials     2892     2873      -19     
Flag Coverage Δ
go-1.21.x ∅ <ø> (∅)
misc ∅ <ø> (∅)
misc-_test.genstd ∅ <ø> (∅)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@github-actions github-actions bot added the 📦 ⛰️ gno.land Issues or PRs gno.land package related label Oct 27, 2023
@deelawn deelawn marked this pull request as ready for review October 27, 2023 14:44
@deelawn deelawn requested review from jaekwon, moul and a team as code owners October 27, 2023 14:44
@n0izn0iz
Copy link
Contributor

Do you think we can close #960 if this is merged?

@thehowl
Copy link
Member

thehowl commented Oct 27, 2023

Great job on this!

FWIW, I'm not sure this is related to #1167 (it's different from #1170). #1167 specifically is about the underlying values of the slice turning to be "zero values", which doesn't seem to be the case here (although the fix may address that too).

I can try to check out if the behaviour in GnoChess is fixed with this PR by reverting the commit and trying to do the replication steps for that behaviour (or I can give them to you if you want to try that, I'm going a bit by memory though, don't know if they were written down).

@deelawn
Copy link
Contributor Author

deelawn commented Oct 27, 2023

Do you think we can close #960 if this is merged?

I think so, yes

@deelawn
Copy link
Contributor Author

deelawn commented Oct 27, 2023

@thehowl I see your point. I didn't realize they are different issues. Though I'm not sure I completely understand the issue based on the description in #1167. Is it possible to replicate the same bad behavior using a txtar file? In any case, I'll follow up with you on this.

@thehowl
Copy link
Member

thehowl commented Oct 27, 2023

Is it possible to replicate the same bad behavior using a txtar file?

I've made a note, I wanted to do this already but I'll prioritize so we can find out if it's the same issue. Thanks!

@tbruyelle
Copy link
Contributor

tbruyelle@71b27ee

So I think we can state that the bugs we have in #960, #1167 and #1170,
are all related to slice storage when its capacity is different than its
length.

@deelawn found a great way to overcome that bug, but the bug is still
there, somewhere in the VM code I think. I spent the last couple of days
trying to find it, unfortunately without success.

That said, I found a workaround, that could be also applied: when a
slice is stored, ignore any capacity different than the slice's length.

I think this is a good workaround because its one-line and because we
don't really care about storing slice with capacity higher than their
length (unless I'm missing something).

Tell me what you think.

@n0izn0iz
Copy link
Contributor

n0izn0iz commented Nov 1, 2023

that's the first workaround I attempted, but I had failing tests when I did
the capacity of a slice is not some behind-the-scene value, it can be used in "user-space" algorithms so it will alter the behavior, I'll try to write some examples here when I have more time

@deelawn
Copy link
Contributor Author

deelawn commented Nov 1, 2023

@tbruyelle It doesn't seem like something we'd want as a long term solution, but I'm not sure why that might be. Other than the failing tests observed by @n0izn0iz, the first thing that comes to my mind is the question of how this might affect the frequency of growing slices' underlying arrays. But I don't know where that happens so it's just a guess 🤷

In any case, I believe this is close to being merged; it should address all the issues you've listed. Or were you suggesting that there is a separate bug that should be fixed and this PR isn't addressing some underlying root cause? If that's the case then let's have a chat about the nature of the underlying bug and how we can identify where to fix it.

@tbruyelle
Copy link
Contributor

@deelawn I probably spent too much time on this, I thought I had a case where your fix doesn't work but I cannot reproduce. Sorry for that.

Anyway for the memo, I think I found a way to reproduce the bug with a single transaction, using a gno file in gnovm/tests/files/ folder and the Realm: instruction:

Here is the content of gnovm/tests/files/slice_cap.gno

// PKGPATH: gno.land/r/slice_cap
package slice_cap

var a []int

func init() {
	a = make([]int, 0, 1)
}

func main() {
	a = append(a, 42)
	println(a)
}

// Output:
// slice[(42 int)]

// Realm:
// switchrealm["gno.land/r/slice_cap"]
// u[95eec26514f13645e29d560a2f0866a8dfe44dee:4]={
//     "Data": null,
//     "List": [
//         {
//             "N": "KgAAAAAAAAA=",
//             "T": {
//                 "@type": "/gno.PrimitiveType",
//                 "value": "32"
//             }
//         }
//     ],
//     "ObjectInfo": {
//         "ID": "95eec26514f13645e29d560a2f0866a8dfe44dee:4",
//         "ModTime": "4",
//         "OwnerID": "95eec26514f13645e29d560a2f0866a8dfe44dee:2",
//         "RefCount": "1"
//     }
// }
// u[95eec26514f13645e29d560a2f0866a8dfe44dee:2]={
//     "Blank": {},
//     "ObjectInfo": {
//         "ID": "95eec26514f13645e29d560a2f0866a8dfe44dee:2",
//         "IsEscaped": true,
//         "ModTime": "4",
//         "RefCount": "2"
//     },
//     "Parent": null,
//     "Source": {
//         "@type": "/gno.RefNode",
//         "BlockNode": null,
//         "Location": {
//             "File": "",
//             "Line": "0",
//             "Nonce": "0",
//             "PkgPath": "gno.land/r/slice_cap"
//         }
//     },
//     "Values": [
//         {
//             "T": {
//                 "@type": "/gno.FuncType",
//                 "Params": [],
//                 "Results": []
//             },
//             "V": {
//                 "@type": "/gno.FuncValue",
//                 "Closure": {
//                     "@type": "/gno.RefValue",
//                     "Escaped": true,
//                     "ObjectID": "95eec26514f13645e29d560a2f0866a8dfe44dee:3"
//                 },
//                 "FileName": "main.gno",
//                 "IsMethod": false,
//                 "Name": "init.0",
//                 "PkgPath": "gno.land/r/slice_cap",
//                 "Source": {
//                     "@type": "/gno.RefNode",
//                     "BlockNode": null,
//                     "Location": {
//                         "File": "main.gno",
//                         "Line": "6",
//                         "Nonce": "0",
//                         "PkgPath": "gno.land/r/slice_cap"
//                     }
//                 },
//                 "Type": {
//                     "@type": "/gno.FuncType",
//                     "Params": [],
//                     "Results": []
//                 }
//             }
//         },
//         {
//             "T": {
//                 "@type": "/gno.FuncType",
//                 "Params": [],
//                 "Results": []
//             },
//             "V": {
//                 "@type": "/gno.FuncValue",
//                 "Closure": {
//                     "@type": "/gno.RefValue",
//                     "Escaped": true,
//                     "ObjectID": "95eec26514f13645e29d560a2f0866a8dfe44dee:3"
//                 },
//                 "FileName": "main.gno",
//                 "IsMethod": false,
//                 "Name": "main",
//                 "PkgPath": "gno.land/r/slice_cap",
//                 "Source": {
//                     "@type": "/gno.RefNode",
//                     "BlockNode": null,
//                     "Location": {
//                         "File": "main.gno",
//                         "Line": "10",
//                         "Nonce": "0",
//                         "PkgPath": "gno.land/r/slice_cap"
//                     }
//                 },
//                 "Type": {
//                     "@type": "/gno.FuncType",
//                     "Params": [],
//                     "Results": []
//                 }
//             }
//         },
//         {
//             "T": {
//                 "@type": "/gno.SliceType",
//                 "Elt": {
//                     "@type": "/gno.PrimitiveType",
//                     "value": "32"
//                 },
//                 "Vrd": false
//             },
//             "V": {
//                 "@type": "/gno.SliceValue",
//                 "Base": {
//                     "@type": "/gno.RefValue",
//                     "Hash": "4ba43144bcf3cb8df3f5b60efaa9e30e1232b468",
//                     "ObjectID": "95eec26514f13645e29d560a2f0866a8dfe44dee:4"
//                 },
//                 "Length": "1",
//                 "Maxcap": "1",
//                 "Offset": "0"
//             }
//         }
//     ]
// }

Using the Realm: instruction, we can attest what is stored or not. Here there's 2 objects saved:

  • u[95eec26514f13645e29d560a2f0866a8dfe44dee:4] is the slice
  • u[95eec26514f13645e29d560a2f0866a8dfe44dee:2] is the whole block/package

So this version works on this branch, while it fails on master because the first object u[95eec26514f13645e29d560a2f0866a8dfe44dee:4] is missing. This typically demonstrates that without your fix, the slice is not saved properly. Maybe that can help for better testing/refac iterations.

@deelawn
Copy link
Contributor Author

deelawn commented Nov 2, 2023

@thehowl added a test case to #1167 that this PR does not address. I'm going to unlink that issue from this PR and will address it in a subsequent PR. I've been discussing with @piux2 and am trying to figure out a way to rework the part of this PR that is concerned with marking appended slice elements as "new created" when the append does not result in the resizing of the underlying array.

@deelawn deelawn requested a review from piux2 November 8, 2023 16:52
@piux2
Copy link
Contributor

piux2 commented Nov 9, 2023

This looks good to me and the fix works well.

Two issues got fixed in this PR

  1. A updated realm slice is not marked dirty: a realm slice foo does not persist when foo = append( foo, i), in which left foo and right foo share the same underline array and i is a primitive type variable. When the capacity expand, left foo and right foo do not share the same underline array, and it does not trigger the issue.

  2. A struct variable is not marked new created: it panics and fails to persist when a struct type variable i appends to a slice which shares the same array with a realm slice.

@deelawn and I looked into the options to fix the second problem at the append() in uverse.go, and breaking down to two statements in preprocess stage. We have not found better solutions than the current one, which may have more efficient alternatives.

maybe @jaekwon can take a look as well.

@jaekwon
Copy link
Contributor

jaekwon commented Nov 11, 2023

looking at this now. please wait for my approval to merge.

gnovm/pkg/gnolang/realm.go Outdated Show resolved Hide resolved
gnovm/pkg/gnolang/realm.go Outdated Show resolved Hide resolved
@jaekwon
Copy link
Contributor

jaekwon commented Nov 11, 2023

Maybe append() in uverse isn't property setting the array as dirty by calling DidUpdate.

NOTE: The design consideration for DidUpdate is that it must be fast.
The idea is to just keep enough information (by appending to newCreated/newEscaped/newDeleted/updated etc) to be processed in batch later at the end of the transaction. So we shouldn't be iterating over arrays here, since it might not even be useful if say the array is dropped later.

Here is a stab at suggested changes to append() (while reverting to original DidUpdate):
(not complete)

diff --git a/gnovm/pkg/gnolang/uverse.go b/gnovm/pkg/gnolang/uverse.go
index 57f8f6d3..de22a29a 100644
--- a/gnovm/pkg/gnolang/uverse.go
+++ b/gnovm/pkg/gnolang/uverse.go
@@ -294,14 +294,23 @@ func UverseNode() *PackageNode {
 								// append(*SliceValue.List, *SliceValue) ---------
 								list := xvb.List
 								if argsb.Data == nil {
-									copy(
-										list[xvo+xvl:xvo+xvl+argsl],
-										argsb.List[argso:argso+argsl])
+									// XXX find other uses of native copy()
+									// and replace with .Copy(alloc) as necessary.
+									for i := 0; i < argsl; i++ {
+										oldelem := list[xvo+xvl+i]
+										newelem := argsb.List[argso+i].Copy(m.Alloc)
+										list[xvo+xvl+i] = newelem
+										m.Realm.DidUpdate(xvb,
+											oldelem.GetFirstObject(m.Store),
+											newelem.GetFirstObject(m.Store),
+										)
+									}
 								} else {
 									copyDataToList(
 										list[xvo+xvl:xvo+xvl+argsl],
 										argsb.Data[argso:argso+argsl],
 										xt.Elem())
+									m.Realm.DidUpdate(xvb, nil, nil)
 								}
 							} else {
 								// append(*SliceValue.Data, *SliceValue) ---------
@@ -315,6 +324,7 @@ func UverseNode() *PackageNode {
 										data[xvo+xvl:xvo+xvl+argsl],
 										argsb.Data[argso:argso+argsl])
 								}
+								m.Realm.DidUpdate(xvb, nil, nil)
 							}
 							m.PushValue(TypedValue{
 								T: xt,
@@ -363,9 +373,9 @@ func UverseNode() *PackageNode {
 						list := make([]TypedValue, xvl+argsl)
 						if 0 < xvl {
 							if xvb.Data == nil {
-								copy(
-									list[:xvl],
-									xvb.List[xvo:xvo+xvl])
+								for i := 0; i < xvl; i++ {
+									list[i] = xvb.List[xvo+i].Copy(m.Alloc)
+								}
 							} else {
 								panic("should not happen")
 								/*
@@ -379,9 +389,9 @@ func UverseNode() *PackageNode {
 						}
 						if 0 < argsl {
 							if argsb.Data == nil {
-								copy(
-									list[xvl:xvl+argsl],
-									argsb.List[argso:argso+argsl])
+								for i := 0; i < argsl; i++ {
+									list[xvl+i] = argsb.List[argso+i].Copy(m.Alloc)
+								}
 							} else {
 								copyDataToList(
 									list[xvl:xvl+argsl],

@jaekwon
Copy link
Contributor

jaekwon commented Nov 12, 2023

Yeah the above patch seems to fix the problem for test 1167.

image

DidUpdate and FinalizeRealmTransaction is among the trickiest logic btw, so please definitely include me in the review process! Thanks for the heads up.

@jaekwon
Copy link
Contributor

jaekwon commented Jan 3, 2024

looking

switch tv.V.(type) {
case RefValue:
cp.T = tv.T
refObject := tv.GetFirstObject(store)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we do store.GetObject(cv.ObjectID) directly here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to me it seems a little strange to uise GetFirstObject outside of a realm-persistence context.
Maybe s/GetFirstObject/getFirstObject/g.

@@ -1010,6 +1010,28 @@ func (tv TypedValue) Copy(alloc *Allocator) (cp TypedValue) {
return
}

// DeepCopy makes of copy of the underlying value in the case of reference values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a misnomer. It doesn't do a deep copy, a deep copy should deep copy all children recursively. It only does a slightly deeper copy if tv happens to be a RefValue. But this function is useful at least in the context of uverse, so I would call this "unrefCopy" and not even expose it.

Copy link
Contributor

@jaekwon jaekwon Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"// unrefCopy makes a copy of the underlying value, but first unreferences if RefValue."

@jaekwon
Copy link
Contributor

jaekwon commented Jan 3, 2024

What about this part?

// XXX find other uses of native copy()
// and replace with .Copy(alloc) as necessary.

Now we have to look at the codebase and see if there are in the very least other uverse functions that need the same fixing... they all seem to use Assign2() which calls DidUpdate already, which is good.

Any other places besides uverse? I don't think so.

Copy link
Contributor

@jaekwon jaekwon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-approving assuming all comments addressed or please re-assign to me

@jaekwon jaekwon merged commit b525e8b into master Jan 4, 2024
186 of 188 checks passed
@jaekwon jaekwon deleted the bug/slice-append branch January 4, 2024 23:19
gfanton pushed a commit to moul/gno that referenced this pull request Jan 18, 2024
…lang#1305)

Addresses gnolang#1167,  gnolang#960, and gnolang#1170 

Consider the following situation:
- A slice of structs exists with a length of zero and a capacity of one
- A new struct literal is appended to the slice
- The code panics because the newly allocated struct literal was never
marked as "new"

``` go
package append

import (
	"gno.land/p/demo/ufmt"
)

type T struct{ i int }

var a []T

func init() {
        a = make([]T, 0, 1)
}

func Append(i int) {
	a = append(a, T{i: i})
}

```

Invoking the `Append` function will cause a panic.

The solution is to traverse each of the array elements after slice
append assignment to make sure any new or updated elements are marked as
such.

This PR also includes a change to ensure that marking an object as dirty
and managing references to the object are mutually exclusive. I think
this is correct but am not sure.

The changes include txtar test cases that incorporate the issue
described by @tbruyelle in gnolang#1170

---------

Co-authored-by: jaekwon <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

6 participants