Commit 5d8a61a4 authored by Keith Randall's avatar Keith Randall

cmd/compile: print recursive types correctly

Change the type printer to take a map of types that we're currently
printing. When we happen upon a type that we're already in the middle
of printing, print a reference to it instead.

A reference to another type is built using the offset of the first
byte of that type's string representation in the result. To facilitate
that computation (and it's probably more efficient, regardless), we
print the type to a buffer as we go, and build the string at the end.

It would be nice to use string.Builder instead of bytes.Buffer, but
string.Builder wasn't around in Go 1.4, and we'd like to bootstrap
from that version.

Fixes #29312

Change-Id: I49d788c1fa20f770df7b2bae3b9979d990d54803
Reviewed-on: https://go-review.googlesource.com/c/go/+/214239Reviewed-by: default avatarRobert Griesemer <gri@golang.org>
parent 52c44884
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package gc package gc
import ( import (
"bytes"
"cmd/compile/internal/types" "cmd/compile/internal/types"
"cmd/internal/src" "cmd/internal/src"
"fmt" "fmt"
...@@ -650,23 +651,55 @@ var basicnames = []string{ ...@@ -650,23 +651,55 @@ var basicnames = []string{
TBLANK: "blank", TBLANK: "blank",
} }
func typefmt(t *types.Type, flag FmtFlag, mode fmtMode, depth int) string { func tconv(t *types.Type, flag FmtFlag, mode fmtMode) string {
b := bytes.NewBuffer(make([]byte, 0, 64))
tconv2(b, t, flag, mode, nil)
return b.String()
}
// tconv2 writes a string representation of t to b.
// flag and mode control exactly what is printed.
// Any types x that are already in the visited map get printed as @%d where %d=visited[x].
// See #16897 before changing the implementation of tconv.
func tconv2(b *bytes.Buffer, t *types.Type, flag FmtFlag, mode fmtMode, visited map[*types.Type]int) {
if off, ok := visited[t]; ok {
// We've seen this type before, so we're trying to print it recursively.
// Print a reference to it instead.
fmt.Fprintf(b, "@%d", off)
return
}
if t == nil { if t == nil {
return "<T>" b.WriteString("<T>")
return
}
if t.Etype == types.TSSA {
b.WriteString(t.Extra.(string))
return
}
if t.Etype == types.TTUPLE {
b.WriteString(t.FieldType(0).String())
b.WriteByte(',')
b.WriteString(t.FieldType(1).String())
return
} }
flag, mode = flag.update(mode)
if mode == FTypeIdName {
flag |= FmtUnsigned
}
if t == types.Bytetype || t == types.Runetype { if t == types.Bytetype || t == types.Runetype {
// in %-T mode collapse rune and byte with their originals. // in %-T mode collapse rune and byte with their originals.
switch mode { switch mode {
case FTypeIdName, FTypeId: case FTypeIdName, FTypeId:
t = types.Types[t.Etype] t = types.Types[t.Etype]
default: default:
return sconv(t.Sym, FmtShort, mode) b.WriteString(sconv(t.Sym, FmtShort, mode))
return
} }
} }
if t == types.Errortype { if t == types.Errortype {
return "error" b.WriteString("error")
return
} }
// Unless the 'L' flag was specified, if the type has a name, just print that name. // Unless the 'L' flag was specified, if the type has a name, just print that name.
...@@ -675,161 +708,197 @@ func typefmt(t *types.Type, flag FmtFlag, mode fmtMode, depth int) string { ...@@ -675,161 +708,197 @@ func typefmt(t *types.Type, flag FmtFlag, mode fmtMode, depth int) string {
case FTypeId, FTypeIdName: case FTypeId, FTypeIdName:
if flag&FmtShort != 0 { if flag&FmtShort != 0 {
if t.Vargen != 0 { if t.Vargen != 0 {
return mode.Sprintf("%v·%d", sconv(t.Sym, FmtShort, mode), t.Vargen) fmt.Fprintf(b, "%s·%d", sconv(t.Sym, FmtShort, mode), t.Vargen)
return
} }
return sconv(t.Sym, FmtShort, mode) b.WriteString(sconv(t.Sym, FmtShort, mode))
return
} }
if mode == FTypeIdName { if mode == FTypeIdName {
return sconv(t.Sym, FmtUnsigned, mode) b.WriteString(sconv(t.Sym, FmtUnsigned, mode))
return
} }
if t.Sym.Pkg == localpkg && t.Vargen != 0 { if t.Sym.Pkg == localpkg && t.Vargen != 0 {
return mode.Sprintf("%v·%d", t.Sym, t.Vargen) b.WriteString(mode.Sprintf("%v·%d", t.Sym, t.Vargen))
return
} }
} }
return smodeString(t.Sym, mode) b.WriteString(smodeString(t.Sym, mode))
return
} }
if int(t.Etype) < len(basicnames) && basicnames[t.Etype] != "" { if int(t.Etype) < len(basicnames) && basicnames[t.Etype] != "" {
var name string
switch t { switch t {
case types.Idealbool: case types.Idealbool:
return "untyped bool" name = "untyped bool"
case types.Idealstring: case types.Idealstring:
return "untyped string" name = "untyped string"
case types.Idealint: case types.Idealint:
return "untyped int" name = "untyped int"
case types.Idealrune: case types.Idealrune:
return "untyped rune" name = "untyped rune"
case types.Idealfloat: case types.Idealfloat:
return "untyped float" name = "untyped float"
case types.Idealcomplex: case types.Idealcomplex:
return "untyped complex" name = "untyped complex"
default:
name = basicnames[t.Etype]
} }
return basicnames[t.Etype] b.WriteString(name)
return
} }
if mode == FDbg { // At this point, we might call tconv2 recursively. Add the current type to the visited list so we don't
return t.Etype.String() + "-" + typefmt(t, flag, FErr, depth) // try to print it recursively.
// We record the offset in the result buffer where the type's text starts. This offset serves as a reference
// point for any later references to the same type.
// Note that we remove the type from the visited map as soon as the recursive call is done.
// This prevents encoding types like map[*int]*int as map[*int]@4. (That encoding would work,
// but I'd like to use the @ notation only when strictly necessary.)
if visited == nil {
visited = map[*types.Type]int{}
} }
visited[t] = b.Len()
defer delete(visited, t)
if mode == FDbg {
b.WriteString(t.Etype.String())
b.WriteByte('-')
tconv2(b, t, flag, FErr, visited)
return
}
switch t.Etype { switch t.Etype {
case TPTR: case TPTR:
b.WriteByte('*')
switch mode { switch mode {
case FTypeId, FTypeIdName: case FTypeId, FTypeIdName:
if flag&FmtShort != 0 { if flag&FmtShort != 0 {
return "*" + tconv(t.Elem(), FmtShort, mode, depth) tconv2(b, t.Elem(), FmtShort, mode, visited)
return
} }
} }
return "*" + tmodeString(t.Elem(), mode, depth) tconv2(b, t.Elem(), 0, mode, visited)
case TARRAY: case TARRAY:
return "[" + strconv.FormatInt(t.NumElem(), 10) + "]" + tmodeString(t.Elem(), mode, depth) b.WriteByte('[')
b.WriteString(strconv.FormatInt(t.NumElem(), 10))
b.WriteByte(']')
tconv2(b, t.Elem(), 0, mode, visited)
case TSLICE: case TSLICE:
return "[]" + tmodeString(t.Elem(), mode, depth) b.WriteString("[]")
tconv2(b, t.Elem(), 0, mode, visited)
case TCHAN: case TCHAN:
switch t.ChanDir() { switch t.ChanDir() {
case types.Crecv: case types.Crecv:
return "<-chan " + tmodeString(t.Elem(), mode, depth) b.WriteString("<-chan ")
tconv2(b, t.Elem(), 0, mode, visited)
case types.Csend: case types.Csend:
return "chan<- " + tmodeString(t.Elem(), mode, depth) b.WriteString("chan<- ")
} tconv2(b, t.Elem(), 0, mode, visited)
default:
if t.Elem() != nil && t.Elem().IsChan() && t.Elem().Sym == nil && t.Elem().ChanDir() == types.Crecv { b.WriteString("chan ")
return "chan (" + tmodeString(t.Elem(), mode, depth) + ")" if t.Elem() != nil && t.Elem().IsChan() && t.Elem().Sym == nil && t.Elem().ChanDir() == types.Crecv {
b.WriteByte('(')
tconv2(b, t.Elem(), 0, mode, visited)
b.WriteByte(')')
} else {
tconv2(b, t.Elem(), 0, mode, visited)
}
} }
return "chan " + tmodeString(t.Elem(), mode, depth)
case TMAP: case TMAP:
return "map[" + tmodeString(t.Key(), mode, depth) + "]" + tmodeString(t.Elem(), mode, depth) b.WriteString("map[")
tconv2(b, t.Key(), 0, mode, visited)
b.WriteByte(']')
tconv2(b, t.Elem(), 0, mode, visited)
case TINTER: case TINTER:
if t.IsEmptyInterface() { if t.IsEmptyInterface() {
return "interface {}" b.WriteString("interface {}")
break
} }
buf := make([]byte, 0, 64) b.WriteString("interface {")
buf = append(buf, "interface {"...)
for i, f := range t.Fields().Slice() { for i, f := range t.Fields().Slice() {
if i != 0 { if i != 0 {
buf = append(buf, ';') b.WriteByte(';')
} }
buf = append(buf, ' ') b.WriteByte(' ')
switch { switch {
case f.Sym == nil: case f.Sym == nil:
// Check first that a symbol is defined for this type. // Check first that a symbol is defined for this type.
// Wrong interface definitions may have types lacking a symbol. // Wrong interface definitions may have types lacking a symbol.
break break
case types.IsExported(f.Sym.Name): case types.IsExported(f.Sym.Name):
buf = append(buf, sconv(f.Sym, FmtShort, mode)...) b.WriteString(sconv(f.Sym, FmtShort, mode))
default: default:
flag1 := FmtLeft flag1 := FmtLeft
if flag&FmtUnsigned != 0 { if flag&FmtUnsigned != 0 {
flag1 = FmtUnsigned flag1 = FmtUnsigned
} }
buf = append(buf, sconv(f.Sym, flag1, mode)...) b.WriteString(sconv(f.Sym, flag1, mode))
} }
buf = append(buf, tconv(f.Type, FmtShort, mode, depth)...) tconv2(b, f.Type, FmtShort, mode, visited)
} }
if t.NumFields() != 0 { if t.NumFields() != 0 {
buf = append(buf, ' ') b.WriteByte(' ')
} }
buf = append(buf, '}') b.WriteByte('}')
return string(buf)
case TFUNC: case TFUNC:
buf := make([]byte, 0, 64)
if flag&FmtShort != 0 { if flag&FmtShort != 0 {
// no leading func // no leading func
} else { } else {
if t.Recv() != nil { if t.Recv() != nil {
buf = append(buf, "method"...) b.WriteString("method")
buf = append(buf, tmodeString(t.Recvs(), mode, depth)...) tconv2(b, t.Recvs(), 0, mode, visited)
buf = append(buf, ' ') b.WriteByte(' ')
} }
buf = append(buf, "func"...) b.WriteString("func")
} }
buf = append(buf, tmodeString(t.Params(), mode, depth)...) tconv2(b, t.Params(), 0, mode, visited)
switch t.NumResults() { switch t.NumResults() {
case 0: case 0:
// nothing to do // nothing to do
case 1: case 1:
buf = append(buf, ' ') b.WriteByte(' ')
buf = append(buf, tmodeString(t.Results().Field(0).Type, mode, depth)...) // struct->field->field's type tconv2(b, t.Results().Field(0).Type, 0, mode, visited) // struct->field->field's type
default: default:
buf = append(buf, ' ') b.WriteByte(' ')
buf = append(buf, tmodeString(t.Results(), mode, depth)...) tconv2(b, t.Results(), 0, mode, visited)
} }
return string(buf)
case TSTRUCT: case TSTRUCT:
if m := t.StructType().Map; m != nil { if m := t.StructType().Map; m != nil {
mt := m.MapType() mt := m.MapType()
// Format the bucket struct for map[x]y as map.bucket[x]y. // Format the bucket struct for map[x]y as map.bucket[x]y.
// This avoids a recursive print that generates very long names. // This avoids a recursive print that generates very long names.
var subtype string
switch t { switch t {
case mt.Bucket: case mt.Bucket:
subtype = "bucket" b.WriteString("map.bucket[")
case mt.Hmap: case mt.Hmap:
subtype = "hdr" b.WriteString("map.hdr[")
case mt.Hiter: case mt.Hiter:
subtype = "iter" b.WriteString("map.iter[")
default: default:
Fatalf("unknown internal map type") Fatalf("unknown internal map type")
} }
return fmt.Sprintf("map.%s[%s]%s", subtype, tmodeString(m.Key(), mode, depth), tmodeString(m.Elem(), mode, depth)) tconv2(b, m.Key(), 0, mode, visited)
b.WriteByte(']')
tconv2(b, m.Elem(), 0, mode, visited)
break
} }
buf := make([]byte, 0, 64)
if funarg := t.StructType().Funarg; funarg != types.FunargNone { if funarg := t.StructType().Funarg; funarg != types.FunargNone {
buf = append(buf, '(') b.WriteByte('(')
var flag1 FmtFlag var flag1 FmtFlag
switch mode { switch mode {
case FTypeId, FTypeIdName, FErr: case FTypeId, FTypeIdName, FErr:
...@@ -838,42 +907,42 @@ func typefmt(t *types.Type, flag FmtFlag, mode fmtMode, depth int) string { ...@@ -838,42 +907,42 @@ func typefmt(t *types.Type, flag FmtFlag, mode fmtMode, depth int) string {
} }
for i, f := range t.Fields().Slice() { for i, f := range t.Fields().Slice() {
if i != 0 { if i != 0 {
buf = append(buf, ", "...) b.WriteString(", ")
} }
buf = append(buf, fldconv(f, flag1, mode, depth, funarg)...) fldconv(b, f, flag1, mode, visited, funarg)
} }
buf = append(buf, ')') b.WriteByte(')')
} else { } else {
buf = append(buf, "struct {"...) b.WriteString("struct {")
for i, f := range t.Fields().Slice() { for i, f := range t.Fields().Slice() {
if i != 0 { if i != 0 {
buf = append(buf, ';') b.WriteByte(';')
} }
buf = append(buf, ' ') b.WriteByte(' ')
buf = append(buf, fldconv(f, FmtLong, mode, depth, funarg)...) fldconv(b, f, FmtLong, mode, visited, funarg)
} }
if t.NumFields() != 0 { if t.NumFields() != 0 {
buf = append(buf, ' ') b.WriteByte(' ')
} }
buf = append(buf, '}') b.WriteByte('}')
} }
return string(buf)
case TFORW: case TFORW:
b.WriteString("undefined")
if t.Sym != nil { if t.Sym != nil {
return "undefined " + smodeString(t.Sym, mode) b.WriteByte(' ')
b.WriteString(smodeString(t.Sym, mode))
} }
return "undefined"
case TUNSAFEPTR: case TUNSAFEPTR:
return "unsafe.Pointer" b.WriteString("unsafe.Pointer")
case Txxx: case Txxx:
return "Txxx" b.WriteString("Txxx")
default:
// Don't know how to handle - fall back to detailed prints.
b.WriteString(mode.Sprintf("%v <%v>", t.Etype, t.Sym))
} }
// Don't know how to handle - fall back to detailed prints.
return mode.Sprintf("%v <%v>", t.Etype, t.Sym)
} }
// Statements which may be rendered with a simplestmt as init. // Statements which may be rendered with a simplestmt as init.
...@@ -1657,15 +1726,11 @@ func sconv(s *types.Sym, flag FmtFlag, mode fmtMode) string { ...@@ -1657,15 +1726,11 @@ func sconv(s *types.Sym, flag FmtFlag, mode fmtMode) string {
return symfmt(s, flag, mode) return symfmt(s, flag, mode)
} }
func tmodeString(t *types.Type, mode fmtMode, depth int) string { func fldconv(b *bytes.Buffer, f *types.Field, flag FmtFlag, mode fmtMode, visited map[*types.Type]int, funarg types.Funarg) {
return tconv(t, 0, mode, depth)
}
func fldconv(f *types.Field, flag FmtFlag, mode fmtMode, depth int, funarg types.Funarg) string {
if f == nil { if f == nil {
return "<T>" b.WriteString("<T>")
return
} }
flag, mode = flag.update(mode) flag, mode = flag.update(mode)
if mode == FTypeIdName { if mode == FTypeIdName {
flag |= FmtUnsigned flag |= FmtUnsigned
...@@ -1694,27 +1759,26 @@ func fldconv(f *types.Field, flag FmtFlag, mode fmtMode, depth int, funarg types ...@@ -1694,27 +1759,26 @@ func fldconv(f *types.Field, flag FmtFlag, mode fmtMode, depth int, funarg types
} }
} }
var typ string if name != "" {
b.WriteString(name)
b.WriteString(" ")
}
if f.IsDDD() { if f.IsDDD() {
var et *types.Type var et *types.Type
if f.Type != nil { if f.Type != nil {
et = f.Type.Elem() et = f.Type.Elem()
} }
typ = "..." + tmodeString(et, mode, depth) b.WriteString("...")
tconv2(b, et, 0, mode, visited)
} else { } else {
typ = tmodeString(f.Type, mode, depth) tconv2(b, f.Type, 0, mode, visited)
}
str := typ
if name != "" {
str = name + " " + typ
} }
if flag&FmtShort == 0 && funarg == types.FunargNone && f.Note != "" { if flag&FmtShort == 0 && funarg == types.FunargNone && f.Note != "" {
str += " " + strconv.Quote(f.Note) b.WriteString(" ")
b.WriteString(strconv.Quote(f.Note))
} }
return str
} }
// "%L" print definition, not name // "%L" print definition, not name
...@@ -1722,58 +1786,12 @@ func fldconv(f *types.Field, flag FmtFlag, mode fmtMode, depth int, funarg types ...@@ -1722,58 +1786,12 @@ func fldconv(f *types.Field, flag FmtFlag, mode fmtMode, depth int, funarg types
func typeFormat(t *types.Type, s fmt.State, verb rune, mode fmtMode) { func typeFormat(t *types.Type, s fmt.State, verb rune, mode fmtMode) {
switch verb { switch verb {
case 'v', 'S', 'L': case 'v', 'S', 'L':
// This is an external entry point, so we pass depth 0 to tconv. fmt.Fprint(s, tconv(t, fmtFlag(s, verb), mode))
// See comments in Type.String.
fmt.Fprint(s, tconv(t, fmtFlag(s, verb), mode, 0))
default: default:
fmt.Fprintf(s, "%%!%c(*Type=%p)", verb, t) fmt.Fprintf(s, "%%!%c(*Type=%p)", verb, t)
} }
} }
var deepTypes map[*types.Type]string
// See #16897 before changing the implementation of tconv.
func tconv(t *types.Type, flag FmtFlag, mode fmtMode, depth int) string {
if t == nil {
return "<T>"
}
if t.Etype == types.TSSA {
return t.Extra.(string)
}
if t.Etype == types.TTUPLE {
return t.FieldType(0).String() + "," + t.FieldType(1).String()
}
// Avoid endless recursion by setting an upper limit. This also
// limits the depths of valid composite types, but they are likely
// artificially created.
// TODO(gri) should have proper cycle detection here, eventually (issue #29312)
// For now, ensure that each of these really deep types are at least uniquely
// named, so that such types don't collide in the linker and thus allow security holes.
if depth > 250 {
if str := deepTypes[t]; str != "" {
return str
}
if deepTypes == nil {
deepTypes = map[*types.Type]string{}
}
id := len(deepTypes)
str := fmt.Sprintf("<...uniquetype_%d_in_%s>", id, curpkg().Path)
deepTypes[t] = str
return str
}
flag, mode = flag.update(mode)
if mode == FTypeIdName {
flag |= FmtUnsigned
}
str := typefmt(t, flag, mode, depth+1)
return str
}
func (n *Node) String() string { return fmt.Sprint(n) } func (n *Node) String() string { return fmt.Sprint(n) }
func (n *Node) modeString(mode fmtMode) string { return mode.Sprint(n) } func (n *Node) modeString(mode fmtMode) string { return mode.Sprint(n) }
......
...@@ -512,8 +512,8 @@ func Main(archInit func(*Arch)) { ...@@ -512,8 +512,8 @@ func Main(archInit func(*Arch)) {
types.Sconv = func(s *types.Sym, flag, mode int) string { types.Sconv = func(s *types.Sym, flag, mode int) string {
return sconv(s, FmtFlag(flag), fmtMode(mode)) return sconv(s, FmtFlag(flag), fmtMode(mode))
} }
types.Tconv = func(t *types.Type, flag, mode, depth int) string { types.Tconv = func(t *types.Type, flag, mode int) string {
return tconv(t, FmtFlag(flag), fmtMode(mode), depth) return tconv(t, FmtFlag(flag), fmtMode(mode))
} }
types.FormatSym = func(sym *types.Sym, s fmt.State, verb rune, mode int) { types.FormatSym = func(sym *types.Sym, s fmt.State, verb rune, mode int) {
symFormat(sym, s, verb, fmtMode(mode)) symFormat(sym, s, verb, fmtMode(mode))
......
...@@ -153,7 +153,7 @@ func init() { ...@@ -153,7 +153,7 @@ func init() {
// TODO(josharian): move universe initialization to the types package, // TODO(josharian): move universe initialization to the types package,
// so this test setup can share it. // so this test setup can share it.
types.Tconv = func(t *types.Type, flag, mode, depth int) string { types.Tconv = func(t *types.Type, flag, mode int) string {
return t.Etype.String() return t.Etype.String()
} }
types.Sconv = func(s *types.Sym, flag, mode int) string { types.Sconv = func(s *types.Sym, flag, mode int) string {
......
...@@ -19,7 +19,7 @@ var ( ...@@ -19,7 +19,7 @@ var (
Dowidth func(*Type) Dowidth func(*Type)
Fatalf func(string, ...interface{}) Fatalf func(string, ...interface{})
Sconv func(*Sym, int, int) string // orig: func sconv(s *Sym, flag FmtFlag, mode fmtMode) string Sconv func(*Sym, int, int) string // orig: func sconv(s *Sym, flag FmtFlag, mode fmtMode) string
Tconv func(*Type, int, int, int) string // orig: func tconv(t *Type, flag FmtFlag, mode fmtMode, depth int) string Tconv func(*Type, int, int) string // orig: func tconv(t *Type, flag FmtFlag, mode fmtMode) string
FormatSym func(*Sym, fmt.State, rune, int) // orig: func symFormat(sym *Sym, s fmt.State, verb rune, mode fmtMode) FormatSym func(*Sym, fmt.State, rune, int) // orig: func symFormat(sym *Sym, s fmt.State, verb rune, mode fmtMode)
FormatType func(*Type, fmt.State, rune, int) // orig: func typeFormat(t *Type, s fmt.State, verb rune, mode fmtMode) FormatType func(*Type, fmt.State, rune, int) // orig: func typeFormat(t *Type, s fmt.State, verb rune, mode fmtMode)
TypeLinkSym func(*Type) *obj.LSym TypeLinkSym func(*Type) *obj.LSym
...@@ -39,25 +39,23 @@ func (sym *Sym) Format(s fmt.State, verb rune) { ...@@ -39,25 +39,23 @@ func (sym *Sym) Format(s fmt.State, verb rune) {
} }
func (t *Type) String() string { func (t *Type) String() string {
// This is an external entry point, so we pass depth 0 to tconv.
// The implementation of tconv (including typefmt and fldconv) // The implementation of tconv (including typefmt and fldconv)
// must take care not to use a type in a formatting string // must handle recursive types correctly.
// to avoid resetting the recursion counter. return Tconv(t, 0, FErr)
return Tconv(t, 0, FErr, 0)
} }
// ShortString generates a short description of t. // ShortString generates a short description of t.
// It is used in autogenerated method names, reflection, // It is used in autogenerated method names, reflection,
// and itab names. // and itab names.
func (t *Type) ShortString() string { func (t *Type) ShortString() string {
return Tconv(t, FmtLeft, FErr, 0) return Tconv(t, FmtLeft, FErr)
} }
// LongString generates a complete description of t. // LongString generates a complete description of t.
// It is useful for reflection, // It is useful for reflection,
// or when a unique fingerprint or hash of a type is required. // or when a unique fingerprint or hash of a type is required.
func (t *Type) LongString() string { func (t *Type) LongString() string {
return Tconv(t, FmtLeft|FmtUnsigned, FErr, 0) return Tconv(t, FmtLeft|FmtUnsigned, FErr)
} }
func (t *Type) Format(s fmt.State, verb rune) { func (t *Type) Format(s fmt.State, verb rune) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment