Commit 1d5dc4fd authored by Russ Cox's avatar Russ Cox

cmd/gc: emit explicit type information for local variables

The type information is (and for years has been) included
as an extra field in the address chunk of an instruction.
Unfortunately, suppose there is a string at a+24(FP) and
we have an instruction reading its length. It will say:

        MOVQ x+32(FP), AX

and the type of *that* argument is int (not slice), because
it is the length being read. This confuses the picture seen
by debuggers and now, worse, by the garbage collector.

Instead of attaching the type information to all uses,
emit an explicit list of TYPE instructions with the information.
The TYPE instructions are no-ops whose only role is to
provide an address to attach type information to.

For example, this function:

        func f(x, y, z int) (a, b string) {
                return
        }

now compiles into:

        --- prog list "f" ---
        0000 (/Users/rsc/x.go:3) TEXT    f+0(SB),$0-56
        0001 (/Users/rsc/x.go:3) LOCALS  ,
        0002 (/Users/rsc/x.go:3) TYPE    x+0(FP){int},$8
        0003 (/Users/rsc/x.go:3) TYPE    y+8(FP){int},$8
        0004 (/Users/rsc/x.go:3) TYPE    z+16(FP){int},$8
        0005 (/Users/rsc/x.go:3) TYPE    a+24(FP){string},$16
        0006 (/Users/rsc/x.go:3) TYPE    b+40(FP){string},$16
        0007 (/Users/rsc/x.go:3) MOVQ    $0,b+40(FP)
        0008 (/Users/rsc/x.go:3) MOVQ    $0,b+48(FP)
        0009 (/Users/rsc/x.go:3) MOVQ    $0,a+24(FP)
        0010 (/Users/rsc/x.go:3) MOVQ    $0,a+32(FP)
        0011 (/Users/rsc/x.go:4) RET     ,

The { } show the formerly hidden type information.
The { } syntax is used when printing from within the gc compiler.
It is not accepted by the assemblers.

The same type information is now included on global variables:

0055 (/Users/rsc/x.go:15) GLOBL   slice+0(SB){[]string},$24(AL*0)

This more accurate type information fixes a bug in the
garbage collector's precise heap collection.

The linker only cares about globals right now, but having the
local information should make things a little nicer for Carl
in the future.

Fixes #4907.

R=ken2
CC=golang-dev
https://golang.org/cl/7395056
parent a411b104
...@@ -28,6 +28,9 @@ void ...@@ -28,6 +28,9 @@ void
markautoused(Prog* p) markautoused(Prog* p)
{ {
for (; p; p = p->link) { for (; p; p = p->link) {
if (p->as == ATYPE)
continue;
if (p->from.name == D_AUTO && p->from.node) if (p->from.name == D_AUTO && p->from.node)
p->from.node->used = 1; p->from.node->used = 1;
...@@ -40,12 +43,21 @@ markautoused(Prog* p) ...@@ -40,12 +43,21 @@ markautoused(Prog* p)
void void
fixautoused(Prog* p) fixautoused(Prog* p)
{ {
for (; p; p = p->link) { Prog **lp;
for (lp=&p; (p=*lp) != P; ) {
if (p->as == ATYPE && p->from.node && p->from.name == D_AUTO && !p->from.node->used) {
*lp = p->link;
continue;
}
if (p->from.name == D_AUTO && p->from.node) if (p->from.name == D_AUTO && p->from.node)
p->from.offset += p->from.node->stkdelta; p->from.offset += p->from.node->stkdelta;
if (p->to.name == D_AUTO && p->to.node) if (p->to.name == D_AUTO && p->to.node)
p->to.offset += p->to.node->stkdelta; p->to.offset += p->to.node->stkdelta;
lp = &p->link;
} }
} }
......
...@@ -192,15 +192,16 @@ gjmp(Prog *to) ...@@ -192,15 +192,16 @@ gjmp(Prog *to)
} }
void void
ggloblnod(Node *nam, int32 width) ggloblnod(Node *nam)
{ {
Prog *p; Prog *p;
p = gins(AGLOBL, nam, N); p = gins(AGLOBL, nam, N);
p->lineno = nam->lineno; p->lineno = nam->lineno;
p->from.gotype = ngotype(nam);
p->to.sym = S; p->to.sym = S;
p->to.type = D_CONST; p->to.type = D_CONST;
p->to.offset = width; p->to.offset = nam->type->width;
if(nam->readonly) if(nam->readonly)
p->reg = RODATA; p->reg = RODATA;
if(nam->type != T && !haspointers(nam->type)) if(nam->type != T && !haspointers(nam->type))
......
...@@ -196,7 +196,10 @@ Dconv(Fmt *fp) ...@@ -196,7 +196,10 @@ Dconv(Fmt *fp)
// goto conv; // goto conv;
} }
conv: conv:
return fmtstrcpy(fp, str); fmtstrcpy(fp, str);
if(a->gotype)
fmtprint(fp, "{%s}", a->gotype->name);
return 0;
} }
int int
......
...@@ -76,6 +76,8 @@ peep(void) ...@@ -76,6 +76,8 @@ peep(void)
case AGLOBL: case AGLOBL:
case ANAME: case ANAME:
case ASIGNAME: case ASIGNAME:
case ALOCALS:
case ATYPE:
p = p->link; p = p->link;
} }
} }
......
...@@ -248,6 +248,8 @@ regopt(Prog *firstp) ...@@ -248,6 +248,8 @@ regopt(Prog *firstp)
case AGLOBL: case AGLOBL:
case ANAME: case ANAME:
case ASIGNAME: case ASIGNAME:
case ALOCALS:
case ATYPE:
continue; continue;
} }
r = rega(); r = rega();
......
...@@ -198,6 +198,7 @@ enum as ...@@ -198,6 +198,7 @@ enum as
AUSEFIELD, AUSEFIELD,
ALOCALS, ALOCALS,
ATYPE,
ALAST, ALAST,
}; };
......
...@@ -578,6 +578,10 @@ loop: ...@@ -578,6 +578,10 @@ loop:
pc++; pc++;
break; break;
case ATYPE:
pc++;
goto loop;
case ATEXT: case ATEXT:
if(cursym != nil && cursym->text) { if(cursym != nil && cursym->text) {
histtoauto(); histtoauto();
......
...@@ -833,6 +833,7 @@ buildop(void) ...@@ -833,6 +833,7 @@ buildop(void)
case ALOCALS: case ALOCALS:
case ACASE: case ACASE:
case ABCASE: case ABCASE:
case ATYPE:
break; break;
case AADDF: case AADDF:
oprange[AADDD] = oprange[r]; oprange[AADDD] = oprange[r];
......
...@@ -25,6 +25,9 @@ void ...@@ -25,6 +25,9 @@ void
markautoused(Prog* p) markautoused(Prog* p)
{ {
for (; p; p = p->link) { for (; p; p = p->link) {
if (p->as == ATYPE)
continue;
if (p->from.type == D_AUTO && p->from.node) if (p->from.type == D_AUTO && p->from.node)
p->from.node->used = 1; p->from.node->used = 1;
...@@ -35,14 +38,22 @@ markautoused(Prog* p) ...@@ -35,14 +38,22 @@ markautoused(Prog* p)
// Fixup instructions after compactframe has moved all autos around. // Fixup instructions after compactframe has moved all autos around.
void void
fixautoused(Prog* p) fixautoused(Prog *p)
{ {
for (; p; p = p->link) { Prog **lp;
for (lp=&p; (p=*lp) != P; ) {
if (p->as == ATYPE && p->from.node && p->from.type == D_AUTO && !p->from.node->used) {
*lp = p->link;
continue;
}
if (p->from.type == D_AUTO && p->from.node) if (p->from.type == D_AUTO && p->from.node)
p->from.offset += p->from.node->stkdelta; p->from.offset += p->from.node->stkdelta;
if (p->to.type == D_AUTO && p->to.node) if (p->to.type == D_AUTO && p->to.node)
p->to.offset += p->to.node->stkdelta; p->to.offset += p->to.node->stkdelta;
lp = &p->link;
} }
} }
......
...@@ -190,15 +190,16 @@ gjmp(Prog *to) ...@@ -190,15 +190,16 @@ gjmp(Prog *to)
} }
void void
ggloblnod(Node *nam, int32 width) ggloblnod(Node *nam)
{ {
Prog *p; Prog *p;
p = gins(AGLOBL, nam, N); p = gins(AGLOBL, nam, N);
p->lineno = nam->lineno; p->lineno = nam->lineno;
p->from.gotype = ngotype(nam);
p->to.sym = S; p->to.sym = S;
p->to.type = D_CONST; p->to.type = D_CONST;
p->to.offset = width; p->to.offset = nam->type->width;
if(nam->readonly) if(nam->readonly)
p->from.scale = RODATA; p->from.scale = RODATA;
if(nam->type != T && !haspointers(nam->type)) if(nam->type != T && !haspointers(nam->type))
...@@ -1179,10 +1180,8 @@ naddr(Node *n, Addr *a, int canemitcode) ...@@ -1179,10 +1180,8 @@ naddr(Node *n, Addr *a, int canemitcode)
case ONAME: case ONAME:
a->etype = 0; a->etype = 0;
if(n->type != T) { if(n->type != T)
a->etype = simtype[n->type->etype]; a->etype = simtype[n->type->etype];
a->gotype = ngotype(n);
}
a->offset = n->xoffset; a->offset = n->xoffset;
a->sym = n->sym; a->sym = n->sym;
a->node = n->orig; a->node = n->orig;
......
...@@ -161,7 +161,10 @@ brk: ...@@ -161,7 +161,10 @@ brk:
strcat(str, s); strcat(str, s);
} }
conv: conv:
return fmtstrcpy(fp, str); fmtstrcpy(fp, str);
if(a->gotype)
fmtprint(fp, "{%s}", a->gotype->name);
return 0;
} }
static char* regstr[] = static char* regstr[] =
......
...@@ -132,6 +132,8 @@ peep(void) ...@@ -132,6 +132,8 @@ peep(void)
case AGLOBL: case AGLOBL:
case ANAME: case ANAME:
case ASIGNAME: case ASIGNAME:
case ALOCALS:
case ATYPE:
p = p->link; p = p->link;
} }
} }
......
...@@ -224,6 +224,8 @@ regopt(Prog *firstp) ...@@ -224,6 +224,8 @@ regopt(Prog *firstp)
case AGLOBL: case AGLOBL:
case ANAME: case ANAME:
case ASIGNAME: case ASIGNAME:
case ALOCALS:
case ATYPE:
continue; continue;
} }
r = rega(); r = rega();
......
...@@ -759,6 +759,7 @@ enum as ...@@ -759,6 +759,7 @@ enum as
AUSEFIELD, AUSEFIELD,
ALOCALS, ALOCALS,
ATYPE,
ALAST ALAST
}; };
......
...@@ -590,6 +590,10 @@ loop: ...@@ -590,6 +590,10 @@ loop:
cursym->locals = p->to.offset; cursym->locals = p->to.offset;
pc++; pc++;
goto loop; goto loop;
case ATYPE:
pc++;
goto loop;
case ATEXT: case ATEXT:
s = p->from.sym; s = p->from.sym;
......
...@@ -1318,6 +1318,7 @@ Optab optab[] = ...@@ -1318,6 +1318,7 @@ Optab optab[] =
{ AUSEFIELD, ynop, Px, 0,0 }, { AUSEFIELD, ynop, Px, 0,0 },
{ ALOCALS }, { ALOCALS },
{ ATYPE },
{ AEND }, { AEND },
0 0
......
...@@ -27,6 +27,9 @@ void ...@@ -27,6 +27,9 @@ void
markautoused(Prog* p) markautoused(Prog* p)
{ {
for (; p; p = p->link) { for (; p; p = p->link) {
if (p->as == ATYPE)
continue;
if (p->from.type == D_AUTO && p->from.node) if (p->from.type == D_AUTO && p->from.node)
p->from.node->used = 1; p->from.node->used = 1;
...@@ -39,12 +42,21 @@ markautoused(Prog* p) ...@@ -39,12 +42,21 @@ markautoused(Prog* p)
void void
fixautoused(Prog* p) fixautoused(Prog* p)
{ {
for (; p; p = p->link) { Prog **lp;
for (lp=&p; (p=*lp) != P; ) {
if (p->as == ATYPE && p->from.node && p->from.type == D_AUTO && !p->from.node->used) {
*lp = p->link;
continue;
}
if (p->from.type == D_AUTO && p->from.node) if (p->from.type == D_AUTO && p->from.node)
p->from.offset += p->from.node->stkdelta; p->from.offset += p->from.node->stkdelta;
if (p->to.type == D_AUTO && p->to.node) if (p->to.type == D_AUTO && p->to.node)
p->to.offset += p->to.node->stkdelta; p->to.offset += p->to.node->stkdelta;
lp = &p->link;
} }
} }
......
...@@ -191,15 +191,16 @@ gjmp(Prog *to) ...@@ -191,15 +191,16 @@ gjmp(Prog *to)
} }
void void
ggloblnod(Node *nam, int32 width) ggloblnod(Node *nam)
{ {
Prog *p; Prog *p;
p = gins(AGLOBL, nam, N); p = gins(AGLOBL, nam, N);
p->lineno = nam->lineno; p->lineno = nam->lineno;
p->from.gotype = ngotype(nam);
p->to.sym = S; p->to.sym = S;
p->to.type = D_CONST; p->to.type = D_CONST;
p->to.offset = width; p->to.offset = nam->type->width;
if(nam->readonly) if(nam->readonly)
p->from.scale = RODATA; p->from.scale = RODATA;
if(nam->type != T && !haspointers(nam->type)) if(nam->type != T && !haspointers(nam->type))
...@@ -2260,7 +2261,6 @@ naddr(Node *n, Addr *a, int canemitcode) ...@@ -2260,7 +2261,6 @@ naddr(Node *n, Addr *a, int canemitcode)
a->etype = simtype[n->type->etype]; a->etype = simtype[n->type->etype];
dowidth(n->type); dowidth(n->type);
a->width = n->type->width; a->width = n->type->width;
a->gotype = ngotype(n);
} }
a->offset = n->xoffset; a->offset = n->xoffset;
a->sym = n->sym; a->sym = n->sym;
......
...@@ -158,7 +158,10 @@ brk: ...@@ -158,7 +158,10 @@ brk:
strcat(str, s); strcat(str, s);
} }
conv: conv:
return fmtstrcpy(fp, str); fmtstrcpy(fp, str);
if(a->gotype)
fmtprint(fp, "{%s}", a->gotype->name);
return 0;
} }
static char* regstr[] = static char* regstr[] =
......
...@@ -126,6 +126,8 @@ peep(void) ...@@ -126,6 +126,8 @@ peep(void)
case AGLOBL: case AGLOBL:
case ANAME: case ANAME:
case ASIGNAME: case ASIGNAME:
case ALOCALS:
case ATYPE:
p = p->link; p = p->link;
} }
} }
......
...@@ -195,6 +195,8 @@ regopt(Prog *firstp) ...@@ -195,6 +195,8 @@ regopt(Prog *firstp)
case AGLOBL: case AGLOBL:
case ANAME: case ANAME:
case ASIGNAME: case ASIGNAME:
case ALOCALS:
case ATYPE:
continue; continue;
} }
r = rega(); r = rega();
......
...@@ -569,6 +569,7 @@ enum as ...@@ -569,6 +569,7 @@ enum as
AUSEFIELD, AUSEFIELD,
ALOCALS, ALOCALS,
ATYPE,
ALAST ALAST
}; };
......
...@@ -600,6 +600,10 @@ loop: ...@@ -600,6 +600,10 @@ loop:
pc++; pc++;
goto loop; goto loop;
case ATYPE:
pc++;
goto loop;
case ATEXT: case ATEXT:
s = p->from.sym; s = p->from.sym;
if(s->text != nil) { if(s->text != nil) {
......
...@@ -963,6 +963,7 @@ Optab optab[] = ...@@ -963,6 +963,7 @@ Optab optab[] =
{ AUSEFIELD, ynop, Px, 0,0 }, { AUSEFIELD, ynop, Px, 0,0 },
{ ALOCALS }, { ALOCALS },
{ ATYPE },
0 0
}; };
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
void void
closurehdr(Node *ntype) closurehdr(Node *ntype)
{ {
Node *n, *name, *a, *orig; Node *n, *name, *a;
NodeList *l; NodeList *l;
n = nod(OCLOSURE, N, N); n = nod(OCLOSURE, N, N);
...@@ -43,11 +43,8 @@ closurehdr(Node *ntype) ...@@ -43,11 +43,8 @@ closurehdr(Node *ntype)
} }
for(l=n->rlist; l; l=l->next) { for(l=n->rlist; l; l=l->next) {
name = l->n->left; name = l->n->left;
if(name) { if(name)
orig = name->orig; // preserve the meaning of orig == N (anonymous PPARAMOUT)
name = newname(name->sym); name = newname(name->sym);
name->orig = orig;
}
ntype->rlist = list(ntype->rlist, nod(ODCLFIELD, name, l->n->right)); ntype->rlist = list(ntype->rlist, nod(ODCLFIELD, name, l->n->right));
} }
} }
......
...@@ -640,8 +640,7 @@ funcargs(Node *nt) ...@@ -640,8 +640,7 @@ funcargs(Node *nt)
// give it a name so escape analysis has nodes to work with // give it a name so escape analysis has nodes to work with
snprint(namebuf, sizeof(namebuf), "~anon%d", gen++); snprint(namebuf, sizeof(namebuf), "~anon%d", gen++);
n->left = newname(lookup(namebuf)); n->left = newname(lookup(namebuf));
n->left->orig = N; // signal that the original was absent // TODO: n->left->missing = 1;
} }
n->left->op = ONAME; n->left->op = ONAME;
...@@ -815,7 +814,7 @@ structfield(Node *n) ...@@ -815,7 +814,7 @@ structfield(Node *n)
break; break;
} }
if(n->left && n->left->op == ONAME && n->left->orig != N) { if(n->left && n->left->op == ONAME) {
f->nname = n->left; f->nname = n->left;
f->embedded = n->embedded; f->embedded = n->embedded;
f->sym = f->nname->sym; f->sym = f->nname->sym;
...@@ -1177,6 +1176,7 @@ functype(Node *this, NodeList *in, NodeList *out) ...@@ -1177,6 +1176,7 @@ functype(Node *this, NodeList *in, NodeList *out)
{ {
Type *t; Type *t;
NodeList *rcvr; NodeList *rcvr;
Sym *s;
t = typ(TFUNC); t = typ(TFUNC);
...@@ -1194,7 +1194,12 @@ functype(Node *this, NodeList *in, NodeList *out) ...@@ -1194,7 +1194,12 @@ functype(Node *this, NodeList *in, NodeList *out)
t->thistuple = 1; t->thistuple = 1;
t->outtuple = count(out); t->outtuple = count(out);
t->intuple = count(in); t->intuple = count(in);
t->outnamed = t->outtuple > 0 && out->n->left != N && out->n->left->orig != N; t->outnamed = 0;
if(t->outtuple > 0 && out->n->left != N && out->n->left->orig != N) {
s = out->n->left->orig->sym;
if(s != S && s->name[0] != '~')
t->outnamed = 1;
}
return t; return t;
} }
......
...@@ -723,12 +723,15 @@ typefmt(Fmt *fp, Type *t) ...@@ -723,12 +723,15 @@ typefmt(Fmt *fp, Type *t)
if(!(fp->flags&FmtShort)) { if(!(fp->flags&FmtShort)) {
s = t->sym; s = t->sym;
// Take the name from the original, lest we substituted it with .anon%d // Take the name from the original, lest we substituted it with ~anon%d
if ((fmtmode == FErr || fmtmode == FExp) && t->nname != N) if ((fmtmode == FErr || fmtmode == FExp) && t->nname != N) {
if(t->nname->orig != N) if(t->nname->orig != N) {
s = t->nname->orig->sym; s = t->nname->orig->sym;
else if(s != S && s->name[0] == '~')
s = S;
} else
s = S; s = S;
}
if(s != S && !t->embedded) { if(s != S && !t->embedded) {
if(t->funarg) if(t->funarg)
......
...@@ -271,7 +271,6 @@ struct Node ...@@ -271,7 +271,6 @@ struct Node
// most nodes // most nodes
Type* type; Type* type;
Type* realtype; // as determined by typecheck
Node* orig; // original form, for printing, and tracking copies of ONAMEs Node* orig; // original form, for printing, and tracking copies of ONAMEs
// func // func
...@@ -1438,7 +1437,7 @@ void gdata(Node*, Node*, int); ...@@ -1438,7 +1437,7 @@ void gdata(Node*, Node*, int);
void gdatacomplex(Node*, Mpcplx*); void gdatacomplex(Node*, Mpcplx*);
void gdatastring(Node*, Strlit*); void gdatastring(Node*, Strlit*);
void genembedtramp(Type*, Type*, Sym*, int iface); void genembedtramp(Type*, Type*, Sym*, int iface);
void ggloblnod(Node *nam, int32 width); void ggloblnod(Node *nam);
void ggloblsym(Sym *s, int32 width, int dupok, int rodata); void ggloblsym(Sym *s, int32 width, int dupok, int rodata);
Prog* gjmp(Prog*); Prog* gjmp(Prog*);
void gused(Node*); void gused(Node*);
......
...@@ -59,7 +59,7 @@ dumpglobls(void) ...@@ -59,7 +59,7 @@ dumpglobls(void)
continue; continue;
dowidth(n->type); dowidth(n->type);
ggloblnod(n, n->type->width); ggloblnod(n);
} }
for(l=funcsyms; l; l=l->next) { for(l=funcsyms; l; l=l->next) {
......
...@@ -14,11 +14,12 @@ compile(Node *fn) ...@@ -14,11 +14,12 @@ compile(Node *fn)
{ {
Plist *pl; Plist *pl;
Node nod1, *n; Node nod1, *n;
Prog *plocals, *ptxt; Prog *plocals, *ptxt, *p, *p1;
int32 lno; int32 lno;
Type *t; Type *t;
Iter save; Iter save;
vlong oldstksize; vlong oldstksize;
NodeList *l;
if(newproc == N) { if(newproc == N) {
newproc = sysfunc("newproc"); newproc = sysfunc("newproc");
...@@ -92,12 +93,25 @@ compile(Node *fn) ...@@ -92,12 +93,25 @@ compile(Node *fn)
for(t=curfn->paramfld; t; t=t->down) for(t=curfn->paramfld; t; t=t->down)
gtrack(tracksym(t->type)); gtrack(tracksym(t->type));
for(l=fn->dcl; l; l=l->next) {
n = l->n;
if(n->op != ONAME) // might be OTYPE or OLITERAL
continue;
switch(n->class) {
case PAUTO:
case PPARAM:
case PPARAMOUT:
nodconst(&nod1, types[TUINTPTR], l->n->type->width);
p = gins(ATYPE, l->n, &nod1);
p->from.gotype = ngotype(l->n);
break;
}
}
genlist(curfn->enter); genlist(curfn->enter);
retpc = nil; retpc = nil;
if(hasdefer || curfn->exit) { if(hasdefer || curfn->exit) {
Prog *p1;
p1 = gjmp(nil); p1 = gjmp(nil);
retpc = gjmp(nil); retpc = gjmp(nil);
patch(p1, pc); patch(p1, pc);
......
...@@ -3555,10 +3555,8 @@ umagic(Magic *m) ...@@ -3555,10 +3555,8 @@ umagic(Magic *m)
Sym* Sym*
ngotype(Node *n) ngotype(Node *n)
{ {
if(n->sym != S && n->realtype != T) if(n->type != T)
if(strncmp(n->sym->name, "autotmp_", 8) != 0) return typenamesym(n->type);
return typenamesym(n->realtype);
return S; return S;
} }
......
...@@ -296,7 +296,6 @@ typecheck1(Node **np, int top) ...@@ -296,7 +296,6 @@ typecheck1(Node **np, int top)
} }
typecheckdef(n); typecheckdef(n);
n->realtype = n->type;
if(n->op == ONONAME) if(n->op == ONONAME)
goto error; goto error;
} }
......
...@@ -2217,6 +2217,8 @@ paramstoheap(Type **argin, int out) ...@@ -2217,6 +2217,8 @@ paramstoheap(Type **argin, int out)
nn = nil; nn = nil;
for(t = structfirst(&savet, argin); t != T; t = structnext(&savet)) { for(t = structfirst(&savet, argin); t != T; t = structnext(&savet)) {
v = t->nname; v = t->nname;
if(v && v->sym && v->sym->name[0] == '~')
v = N;
if(v == N && out && hasdefer) { if(v == N && out && hasdefer) {
// Defer might stop a panic and show the // Defer might stop a panic and show the
// return values as they exist at the time of panic. // return values as they exist at the time of panic.
......
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