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: account for recursion when stringing to avoid overflow #1315

Merged
merged 21 commits into from
Dec 21, 2023
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 153 additions & 21 deletions gnovm/pkg/gnolang/values_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,47 @@
"strings"
)

type protectedStringer interface {
ProtectedString(*seenValues) string
}

// This indicates the maximum ancticipated depth of the stack when printing a Value type.
const defaultSeenValuesSize = 32

type seenValues struct {
values []Value
}

func (sv *seenValues) Put(v Value) {
sv.values = append(sv.values, v)
}

func (sv *seenValues) Contains(v Value) bool {
deelawn marked this conversation as resolved.
Show resolved Hide resolved
for _, vv := range sv.values {
if vv == v {
return true
}

Check warning on line 29 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L28-L29

Added lines #L28 - L29 were not covered by tests
thehowl marked this conversation as resolved.
Show resolved Hide resolved
}

return false
}

// Pop should be called by using a defer after each Put.
// Consider why this is necessary:
// - we are printing an array of structs
// - each invocation of struct.ProtectedString adds the value to the seenValues
// - without calling Pop before exiting struct.ProtectedString, the next call to
// struct.ProtectedString in the array.ProtectedString loop will not result in the value
// being printed if the value has already been print
// - this is NOT recursion and SHOULD be printed
func (sv *seenValues) Pop() {
sv.values = sv.values[:len(sv.values)-1]
}

func newSeenValues() *seenValues {
return &seenValues{values: make([]Value, 0, defaultSeenValuesSize)}
}

func (v StringValue) String() string {
return strconv.Quote(string(v))
}
Expand All @@ -24,10 +65,21 @@
}

func (av *ArrayValue) String() string {
return av.ProtectedString(newSeenValues())

Check warning on line 68 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L68

Added line #L68 was not covered by tests
}

func (av *ArrayValue) ProtectedString(seen *seenValues) string {
if seen.Contains(av) {
return fmt.Sprintf("%p", av)
}

Check warning on line 74 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L73-L74

Added lines #L73 - L74 were not covered by tests

seen.Put(av)
defer seen.Pop()

ss := make([]string, len(av.List))
if av.Data == nil {
for i, e := range av.List {
ss[i] = e.String()
ss[i] = e.ProtectedString(seen)

Check warning on line 82 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L82

Added line #L82 was not covered by tests
}
// NOTE: we may want to unify the representation,
// but for now tests expect this to be different.
Expand All @@ -41,17 +93,30 @@
}

func (sv *SliceValue) String() string {
return sv.ProtectedString(newSeenValues())

Check warning on line 96 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L96

Added line #L96 was not covered by tests
}

func (sv *SliceValue) ProtectedString(seen *seenValues) string {
if sv.Base == nil {
return "nil-slice"
}

if seen.Contains(sv) {
return fmt.Sprintf("%p", sv)
}

Check warning on line 106 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L105-L106

Added lines #L105 - L106 were not covered by tests

if ref, ok := sv.Base.(RefValue); ok {
return fmt.Sprintf("slice[%v]", ref)
}

seen.Put(sv)
defer seen.Pop()

vbase := sv.Base.(*ArrayValue)
if vbase.Data == nil {
ss := make([]string, sv.Length)
for i, e := range vbase.List[sv.Offset : sv.Offset+sv.Length] {
ss[i] = e.String()
ss[i] = e.ProtectedString(seen)
}
return "slice[" + strings.Join(ss, ",") + "]"
}
Expand All @@ -62,16 +127,40 @@
}

func (pv PointerValue) String() string {
// NOTE: cannot do below, due to recursion problems.
// TODO: create a different String2(...) function.
// return fmt.Sprintf("&%s", v.TypedValue.String())
return fmt.Sprintf("&%p.(*%s)", pv.TV, pv.TV.T.String())
return pv.ProtectedString(newSeenValues())

Check warning on line 130 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L130

Added line #L130 was not covered by tests
}

func (pv PointerValue) ProtectedString(seen *seenValues) string {
if seen.Contains(pv) {
return fmt.Sprintf("%p", &pv)
}

Check warning on line 136 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L133-L136

Added lines #L133 - L136 were not covered by tests

seen.Put(pv)
defer seen.Pop()

// Handle nil TV's, avoiding a nil pointer deref below.
if pv.TV == nil {
return "&<nil>"
}

Check warning on line 144 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L138-L144

Added lines #L138 - L144 were not covered by tests

return fmt.Sprintf("&%s", pv.TV.ProtectedString(seen))

Check warning on line 146 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L146

Added line #L146 was not covered by tests
}

func (sv *StructValue) String() string {
return sv.ProtectedString(newSeenValues())

Check warning on line 150 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L150

Added line #L150 was not covered by tests
}

func (sv *StructValue) ProtectedString(seen *seenValues) string {
if seen.Contains(sv) {
return fmt.Sprintf("%p", sv)
}

Check warning on line 156 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L155-L156

Added lines #L155 - L156 were not covered by tests

seen.Put(sv)
defer seen.Pop()

ss := make([]string, len(sv.Fields))
for i, f := range sv.Fields {
ss[i] = f.String()
ss[i] = f.ProtectedString(seen)

Check warning on line 163 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L163

Added line #L163 was not covered by tests
}
return "struct{" + strings.Join(ss, ",") + "}"
}
Expand Down Expand Up @@ -104,9 +193,21 @@
}

func (mv *MapValue) String() string {
return mv.ProtectedString(newSeenValues())

Check warning on line 196 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L196

Added line #L196 was not covered by tests
}

func (mv *MapValue) ProtectedString(seen *seenValues) string {
if mv.List == nil {
return "zero-map"
}

if seen.Contains(mv) {
return fmt.Sprintf("%p", mv)
}

Check warning on line 206 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L205-L206

Added lines #L205 - L206 were not covered by tests

seen.Put(mv)
defer seen.Pop()

ss := make([]string, 0, mv.GetLength())
next := mv.List.Head
for next != nil {
Expand Down Expand Up @@ -177,9 +278,26 @@
res := m.Eval(Call(Sel(&ConstExpr{TypedValue: *tv}, "Error")))
return res[0].GetString()
}

return tv.ProtectedSprint(newSeenValues(), true)
}

func (tv *TypedValue) ProtectedSprint(seen *seenValues, considerDeclaredType bool) string {
if seen.Contains(tv.V) {
return fmt.Sprintf("%p", tv)
}

Check warning on line 288 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L287-L288

Added lines #L287 - L288 were not covered by tests

// print declared type
if _, ok := tv.T.(*DeclaredType); ok {
return tv.String()
if _, ok := tv.T.(*DeclaredType); ok && considerDeclaredType {
return tv.ProtectedString(seen)
}

Check warning on line 293 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L292-L293

Added lines #L292 - L293 were not covered by tests

// This is a special case that became necessary after adding `ProtectedString()` methods to
// reliably prevent recursive print loops.
if tv.V != nil {
if v, ok := tv.V.(RefValue); ok {
return v.String()
}

Check warning on line 300 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L299-L300

Added lines #L299 - L300 were not covered by tests
}

// otherwise, default behavior.
Expand Down Expand Up @@ -225,9 +343,7 @@
if tv.V == nil {
return "invalid-pointer"
}
return tv.V.(PointerValue).String()
case *ArrayType, *SliceType, *StructType, *MapType, *TypeType, *NativeType:
return printNilOrValue(tv, tv.V)
return tv.V.(PointerValue).ProtectedString(seen)

Check warning on line 346 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L346

Added line #L346 was not covered by tests
case *FuncType:
switch fv := tv.V.(type) {
case nil:
Expand All @@ -253,8 +369,22 @@
return tv.V.(*PackageValue).String()
case *ChanType:
panic("not yet implemented")
// return tv.V.(*ChanValue).String()
case *TypeType:
return tv.V.(TypeValue).String()

Check warning on line 373 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L372-L373

Added lines #L372 - L373 were not covered by tests
default:
// The remaining types may have a nil value.
if tv.V == nil {
return nilStr + " " + tv.T.String()
}

// *ArrayType, *SliceType, *StructType, *MapType
if ps, ok := tv.V.(protectedStringer); ok {
return ps.ProtectedString(seen)
} else if s, ok := tv.V.(fmt.Stringer); ok {
// *NativeType
return s.String()
}

Check warning on line 386 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L384-L386

Added lines #L384 - L386 were not covered by tests

if debug {
panic(fmt.Sprintf(
"unexpected type %s",
Expand All @@ -265,18 +395,15 @@
}
}

func printNilOrValue(tv *TypedValue, valueType interface{}) string {
if tv.V == nil {
return nilStr + " " + tv.T.String()
}
return fmt.Sprintf("%v", valueType)
}

// ----------------------------------------
// TypedValue.String()

// For gno debugging/testing.
func (tv TypedValue) String() string {
return tv.ProtectedString(newSeenValues())
}

func (tv TypedValue) ProtectedString(seen *seenValues) string {
if tv.IsUndefined() {
return "(undefined)"
}
Expand Down Expand Up @@ -313,12 +440,17 @@
vs = fmt.Sprintf("%v", tv.GetFloat32())
case Float64Type:
vs = fmt.Sprintf("%v", tv.GetFloat64())
// Complex types that require recusion protection.
default:
vs = nilStr
}
} else {
vs = fmt.Sprintf("%v", tv.V)
vs = tv.ProtectedSprint(seen, false)
if base := baseOf(tv.T); base == StringType || base == UntypedStringType {
vs = strconv.Quote(vs)
}
}

ts := tv.T.String()
return fmt.Sprintf("(%s %s)", vs, ts) // TODO improve
}
Loading