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 17 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
187 changes: 161 additions & 26 deletions gnovm/pkg/gnolang/values_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@
"strings"
)

// 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 25 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L24-L25

Added lines #L24 - L25 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 +61,21 @@
}

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

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L64

Added line #L64 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 70 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L69-L70

Added lines #L69 - L70 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 78 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L78

Added line #L78 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 +89,30 @@
}

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

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L92

Added line #L92 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 102 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L101-L102

Added lines #L101 - L102 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 +123,42 @@
}

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 126 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L126

Added line #L126 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 132 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L129-L132

Added lines #L129 - L132 were not covered by tests

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

// This method was limited and not working correctly previously. Allowing for it to work as
// intended means that it needs to ensure the type value is not nil before attempting to
// convert it to a string.
deelawn marked this conversation as resolved.
Show resolved Hide resolved
if pv.TV == nil {
return "&<nil>"
}

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L134-L142

Added lines #L134 - L142 were not covered by tests

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

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#L144

Added line #L144 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L148

Added line #L148 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 154 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L153-L154

Added lines #L153 - L154 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 161 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L161

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

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

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L194

Added line #L194 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 204 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L203-L204

Added lines #L203 - L204 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 +276,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 286 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L285-L286

Added lines #L285 - L286 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 291 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L290-L291

Added lines #L290 - L291 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 298 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L297-L298

Added lines #L297 - L298 were not covered by tests
}

// otherwise, default behavior.
Expand Down Expand Up @@ -225,9 +341,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 344 in gnovm/pkg/gnolang/values_string.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/values_string.go#L344

Added line #L344 was not covered by tests
case *FuncType:
switch fv := tv.V.(type) {
case nil:
Expand All @@ -253,30 +367,45 @@
return tv.V.(*PackageValue).String()
case *ChanType:
panic("not yet implemented")
// return tv.V.(*ChanValue).String()
default:
if debug {
panic(fmt.Sprintf(
"unexpected type %s",
tv.T.String()))
} else {
panic("should not happen")
if tv.V == nil {
return nilStr + " " + tv.T.String()
}
}
}

func printNilOrValue(tv *TypedValue, valueType interface{}) string {
if tv.V == nil {
return nilStr + " " + tv.T.String()
switch bt.(type) {
case *ArrayType:
return tv.V.(*ArrayValue).ProtectedString(seen)
case *SliceType:
return tv.V.(*SliceValue).ProtectedString(seen)
case *StructType:
return tv.V.(*StructValue).ProtectedString(seen)
case *MapType:
return tv.V.(*MapValue).ProtectedString(seen)
case *NativeType:
return tv.V.(*NativeValue).String()
case *TypeType:
return tv.V.(TypeValue).String()
thehowl marked this conversation as resolved.
Show resolved Hide resolved
default:
if debug {
panic(fmt.Sprintf(
"unexpected type %s",
tv.T.String()))
} else {
panic("should not happen")

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L384 - L394 were not covered by tests
}
}
}
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 +442,18 @@
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 = fmt.Sprintf("%v", tv.V)
deelawn marked this conversation as resolved.
Show resolved Hide resolved
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