mirror of
https://github.com/golang/go
synced 2024-11-22 06:44:40 -07:00
interface speedups and fixes.
more caching, better hash functions, proper locking. fixed a bug in interface comparison too. R=ken DELTA=177 (124 added, 10 deleted, 43 changed) OCL=23491 CL=23493
This commit is contained in:
parent
7859ae8a2f
commit
9b6d385cb5
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
CC=quietgcc
|
CC=quietgcc
|
||||||
LD=quietgcc
|
LD=quietgcc
|
||||||
CFLAGS=-ggdb -I$(GOROOT)/include
|
CFLAGS=-ggdb -I$(GOROOT)/include -O1
|
||||||
BIN=$(HOME)/bin
|
BIN=$(HOME)/bin
|
||||||
O=o
|
O=o
|
||||||
YFLAGS=-d
|
YFLAGS=-d
|
||||||
|
@ -635,12 +635,14 @@ dumpsigt(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s)
|
|||||||
Iter savet;
|
Iter savet;
|
||||||
Prog *oldlist;
|
Prog *oldlist;
|
||||||
Sym *method;
|
Sym *method;
|
||||||
|
uint32 sighash;
|
||||||
|
|
||||||
at.sym = s;
|
at.sym = s;
|
||||||
|
|
||||||
a = nil;
|
a = nil;
|
||||||
o = 0;
|
o = 0;
|
||||||
oldlist = nil;
|
oldlist = nil;
|
||||||
|
sighash = 0;
|
||||||
for(f=methodt->method; f!=T; f=f->down) {
|
for(f=methodt->method; f!=T; f=f->down) {
|
||||||
if(f->type->etype != TFUNC)
|
if(f->type->etype != TFUNC)
|
||||||
continue;
|
continue;
|
||||||
@ -662,6 +664,8 @@ dumpsigt(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s)
|
|||||||
a->hash += PRIME10*stringhash(package);
|
a->hash += PRIME10*stringhash(package);
|
||||||
a->perm = o;
|
a->perm = o;
|
||||||
a->sym = methodsym(method, rcvrt);
|
a->sym = methodsym(method, rcvrt);
|
||||||
|
|
||||||
|
sighash = sighash*100003 + a->hash;
|
||||||
|
|
||||||
if(!a->sym->siggen) {
|
if(!a->sym->siggen) {
|
||||||
a->sym->siggen = 1;
|
a->sym->siggen = 1;
|
||||||
@ -709,7 +713,7 @@ dumpsigt(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s)
|
|||||||
ot = 0;
|
ot = 0;
|
||||||
ot = rnd(ot, maxround); // base structure
|
ot = rnd(ot, maxround); // base structure
|
||||||
|
|
||||||
// sigi[0].name = ""
|
// sigt[0].name = ""
|
||||||
ginsatoa(widthptr, stringo);
|
ginsatoa(widthptr, stringo);
|
||||||
|
|
||||||
// save type name for runtime error message.
|
// save type name for runtime error message.
|
||||||
@ -718,10 +722,10 @@ dumpsigt(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s)
|
|||||||
|
|
||||||
// first field of an type signature contains
|
// first field of an type signature contains
|
||||||
// the element parameters and is not a real entry
|
// the element parameters and is not a real entry
|
||||||
// sigi[0].hash = elemalg
|
// sigt[0].hash = elemalg + sighash<<8
|
||||||
gensatac(wi, algtype(progt));
|
gensatac(wi, algtype(progt) + (sighash<<8));
|
||||||
|
|
||||||
// sigi[0].offset = width
|
// sigt[0].offset = width
|
||||||
gensatac(wi, progt->width);
|
gensatac(wi, progt->width);
|
||||||
|
|
||||||
// skip the function
|
// skip the function
|
||||||
@ -730,10 +734,10 @@ dumpsigt(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s)
|
|||||||
for(b=a; b!=nil; b=b->link) {
|
for(b=a; b!=nil; b=b->link) {
|
||||||
ot = rnd(ot, maxround); // base structure
|
ot = rnd(ot, maxround); // base structure
|
||||||
|
|
||||||
// sigx[++].name = "fieldname"
|
// sigt[++].name = "fieldname"
|
||||||
ginsatoa(widthptr, stringo);
|
ginsatoa(widthptr, stringo);
|
||||||
|
|
||||||
// sigx[++].hash = hashcode
|
// sigt[++].hash = hashcode
|
||||||
gensatac(wi, b->hash);
|
gensatac(wi, b->hash);
|
||||||
|
|
||||||
// sigt[++].offset = of embedded struct
|
// sigt[++].offset = of embedded struct
|
||||||
@ -770,11 +774,13 @@ dumpsigi(Type *t, Sym *s)
|
|||||||
Sig *a, *b;
|
Sig *a, *b;
|
||||||
Prog *p;
|
Prog *p;
|
||||||
char buf[NSYMB];
|
char buf[NSYMB];
|
||||||
|
uint32 sighash;
|
||||||
|
|
||||||
at.sym = s;
|
at.sym = s;
|
||||||
|
|
||||||
a = nil;
|
a = nil;
|
||||||
o = 0;
|
o = 0;
|
||||||
|
sighash = 0;
|
||||||
for(f=t->type; f!=T; f=f->down) {
|
for(f=t->type; f!=T; f=f->down) {
|
||||||
if(f->type->etype != TFUNC)
|
if(f->type->etype != TFUNC)
|
||||||
continue;
|
continue;
|
||||||
@ -797,6 +803,8 @@ dumpsigi(Type *t, Sym *s)
|
|||||||
a->perm = o;
|
a->perm = o;
|
||||||
a->sym = methodsym(f->sym, t);
|
a->sym = methodsym(f->sym, t);
|
||||||
a->offset = 0;
|
a->offset = 0;
|
||||||
|
|
||||||
|
sighash = sighash*100003 + a->hash;
|
||||||
|
|
||||||
o++;
|
o++;
|
||||||
}
|
}
|
||||||
@ -815,8 +823,8 @@ dumpsigi(Type *t, Sym *s)
|
|||||||
// first field of an interface signature
|
// first field of an interface signature
|
||||||
// contains the count and is not a real entry
|
// contains the count and is not a real entry
|
||||||
|
|
||||||
// sigi[0].hash = 0
|
// sigi[0].hash = sighash
|
||||||
gensatac(wi, 0);
|
gensatac(wi, sighash);
|
||||||
|
|
||||||
// sigi[0].offset = count
|
// sigi[0].offset = count
|
||||||
o = 0;
|
o = 0;
|
||||||
|
@ -40,6 +40,7 @@ struct Itype
|
|||||||
|
|
||||||
static Iface niliface;
|
static Iface niliface;
|
||||||
static Itype* hash[1009];
|
static Itype* hash[1009];
|
||||||
|
static Lock ifacelock;
|
||||||
|
|
||||||
Sigi sigi·empty[2] = { (byte*)"interface { }" };
|
Sigi sigi·empty[2] = { (byte*)"interface { }" };
|
||||||
|
|
||||||
@ -113,32 +114,48 @@ printiface(Iface i)
|
|||||||
static Itype*
|
static Itype*
|
||||||
itype(Sigi *si, Sigt *st, int32 canfail)
|
itype(Sigi *si, Sigt *st, int32 canfail)
|
||||||
{
|
{
|
||||||
|
int32 locked;
|
||||||
int32 nt, ni;
|
int32 nt, ni;
|
||||||
uint32 ihash, h;
|
uint32 ihash, h;
|
||||||
byte *sname, *iname;
|
byte *sname, *iname;
|
||||||
Itype *m;
|
Itype *m;
|
||||||
|
|
||||||
h = ((uint32)(uint64)si + (uint32)(uint64)st) % nelem(hash);
|
// compiler has provided some good hash codes for us.
|
||||||
for(m=hash[h]; m!=nil; m=m->link) {
|
h = 0;
|
||||||
if(m->sigi == si && m->sigt == st) {
|
if(si)
|
||||||
if(m->bad) {
|
h += si->hash;
|
||||||
m = nil;
|
if(st)
|
||||||
if(!canfail) {
|
h += st->hash >> 8;
|
||||||
// this can only happen if the conversion
|
h %= nelem(hash);
|
||||||
// was already done once using the , ok form
|
|
||||||
// and we have a cached negative result.
|
// look twice - once without lock, once with.
|
||||||
// the cached result doesn't record which
|
// common case will be no lock contention.
|
||||||
// interface function was missing, so jump
|
for(locked=0; locked<2; locked++) {
|
||||||
// down to the interface check, which will
|
if(locked)
|
||||||
// give a better error.
|
lock(&ifacelock);
|
||||||
goto throw;
|
for(m=hash[h]; m!=nil; m=m->link) {
|
||||||
|
if(m->sigi == si && m->sigt == st) {
|
||||||
|
if(m->bad) {
|
||||||
|
m = nil;
|
||||||
|
if(!canfail) {
|
||||||
|
// this can only happen if the conversion
|
||||||
|
// was already done once using the , ok form
|
||||||
|
// and we have a cached negative result.
|
||||||
|
// the cached result doesn't record which
|
||||||
|
// interface function was missing, so jump
|
||||||
|
// down to the interface check, which will
|
||||||
|
// give a better error.
|
||||||
|
goto throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// prints("old itype\n");
|
||||||
|
if(locked)
|
||||||
|
unlock(&ifacelock);
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
// prints("old itype\n");
|
|
||||||
return m;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ni = si[0].perm; // first entry has size
|
ni = si[0].perm; // first entry has size
|
||||||
m = mal(sizeof(*m) + ni*sizeof(m->fun[0]));
|
m = mal(sizeof(*m) + ni*sizeof(m->fun[0]));
|
||||||
m->sigi = si;
|
m->sigi = si;
|
||||||
@ -180,6 +197,8 @@ throw:
|
|||||||
m->bad = 1;
|
m->bad = 1;
|
||||||
m->link = hash[h];
|
m->link = hash[h];
|
||||||
hash[h] = m;
|
hash[h] = m;
|
||||||
|
if(locked)
|
||||||
|
unlock(&ifacelock);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if(ihash == st[nt].hash && strcmp(sname, iname) == 0)
|
if(ihash == st[nt].hash && strcmp(sname, iname) == 0)
|
||||||
@ -190,6 +209,8 @@ throw:
|
|||||||
m->link = hash[h];
|
m->link = hash[h];
|
||||||
hash[h] = m;
|
hash[h] = m;
|
||||||
// printf("new itype %p\n", m);
|
// printf("new itype %p\n", m);
|
||||||
|
if(locked)
|
||||||
|
unlock(&ifacelock);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +237,7 @@ sys·ifaceT2I(Sigi *si, Sigt *st, ...)
|
|||||||
prints("\n");
|
prints("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
alg = st->hash;
|
alg = st->hash & 0xFF;
|
||||||
wid = st->offset;
|
wid = st->offset;
|
||||||
if(wid <= sizeof ret->data)
|
if(wid <= sizeof ret->data)
|
||||||
algarray[alg].copy(wid, &ret->data, elem);
|
algarray[alg].copy(wid, &ret->data, elem);
|
||||||
@ -272,7 +293,7 @@ sys·ifaceI2T(Sigt *st, Iface i, ...)
|
|||||||
throw("interface conversion");
|
throw("interface conversion");
|
||||||
}
|
}
|
||||||
|
|
||||||
alg = st->hash;
|
alg = st->hash & 0xFF;
|
||||||
wid = st->offset;
|
wid = st->offset;
|
||||||
if(wid <= sizeof i.data)
|
if(wid <= sizeof i.data)
|
||||||
algarray[alg].copy(wid, ret, &i.data);
|
algarray[alg].copy(wid, ret, &i.data);
|
||||||
@ -297,7 +318,7 @@ sys·ifaceI2T2(Sigt *st, Iface i, ...)
|
|||||||
int32 alg, wid;
|
int32 alg, wid;
|
||||||
|
|
||||||
ret = (byte*)(&i+1);
|
ret = (byte*)(&i+1);
|
||||||
alg = st->hash;
|
alg = st->hash & 0xFF;
|
||||||
wid = st->offset;
|
wid = st->offset;
|
||||||
ok = (bool*)(ret+rnd(wid, 8));
|
ok = (bool*)(ret+rnd(wid, 8));
|
||||||
|
|
||||||
@ -411,7 +432,7 @@ ifacehash(Iface a)
|
|||||||
|
|
||||||
if(a.type == nil)
|
if(a.type == nil)
|
||||||
return 0;
|
return 0;
|
||||||
alg = a.type->sigt->hash;
|
alg = a.type->sigt->hash & 0xFF;
|
||||||
wid = a.type->sigt->offset;
|
wid = a.type->sigt->offset;
|
||||||
if(algarray[alg].hash == nohash) {
|
if(algarray[alg].hash == nohash) {
|
||||||
// calling nohash will throw too,
|
// calling nohash will throw too,
|
||||||
@ -450,14 +471,12 @@ ifaceeq(Iface i1, Iface i2)
|
|||||||
if(i2.type == nil)
|
if(i2.type == nil)
|
||||||
goto no;
|
goto no;
|
||||||
|
|
||||||
// value
|
// are they the same type?
|
||||||
alg = i1.type->sigt->hash;
|
if(i1.type->sigt != i2.type->sigt)
|
||||||
if(alg != i2.type->sigt->hash)
|
|
||||||
goto no;
|
goto no;
|
||||||
|
|
||||||
|
alg = i1.type->sigt->hash & 0xFF;
|
||||||
wid = i1.type->sigt->offset;
|
wid = i1.type->sigt->offset;
|
||||||
if(wid != i2.type->sigt->offset)
|
|
||||||
goto no;
|
|
||||||
|
|
||||||
if(algarray[alg].equal == noequal) {
|
if(algarray[alg].equal == noequal) {
|
||||||
// calling noequal will throw too,
|
// calling noequal will throw too,
|
||||||
@ -553,20 +572,53 @@ extern int32 ngotypesigs;
|
|||||||
// for .([]int) instead of .(string) above, then there would be a
|
// for .([]int) instead of .(string) above, then there would be a
|
||||||
// signature with type string "[]int" in gotypesigs, and unreflect
|
// signature with type string "[]int" in gotypesigs, and unreflect
|
||||||
// wouldn't call fakesigt.
|
// wouldn't call fakesigt.
|
||||||
|
|
||||||
|
static Sigt *fake[1009];
|
||||||
|
static int32 nfake;
|
||||||
|
|
||||||
static Sigt*
|
static Sigt*
|
||||||
fakesigt(string type, bool indir)
|
fakesigt(string type, bool indir)
|
||||||
{
|
{
|
||||||
// TODO(rsc): Cache these by type string.
|
|
||||||
Sigt *sigt;
|
Sigt *sigt;
|
||||||
|
uint32 h;
|
||||||
|
int32 i, locked;
|
||||||
|
|
||||||
|
if(type == nil)
|
||||||
|
type = emptystring;
|
||||||
|
|
||||||
|
h = 0;
|
||||||
|
for(i=0; i<type->len; i++)
|
||||||
|
h = h*37 + type->str[i];
|
||||||
|
h += indir;
|
||||||
|
h %= nelem(fake);
|
||||||
|
|
||||||
|
for(locked=0; locked<2; locked++) {
|
||||||
|
if(locked)
|
||||||
|
lock(&ifacelock);
|
||||||
|
for(sigt = fake[h]; sigt != nil; sigt = (Sigt*)sigt->fun) {
|
||||||
|
// don't need to compare indir.
|
||||||
|
// same type string but different indir will have
|
||||||
|
// different hashes.
|
||||||
|
if(mcmp(sigt->name, type->str, type->len) == 0)
|
||||||
|
if(sigt->name[type->len] == '\0') {
|
||||||
|
if(locked)
|
||||||
|
unlock(&ifacelock);
|
||||||
|
return sigt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sigt = mal(2*sizeof sigt[0]);
|
sigt = mal(2*sizeof sigt[0]);
|
||||||
sigt[0].name = mal(type->len + 1);
|
sigt[0].name = mal(type->len + 1);
|
||||||
mcpy(sigt[0].name, type->str, type->len);
|
mcpy(sigt[0].name, type->str, type->len);
|
||||||
sigt[0].hash = AMEM; // alg
|
sigt[0].hash = AFAKE; // alg
|
||||||
if(indir)
|
if(indir)
|
||||||
sigt[0].offset = 2*sizeof(niliface.data); // big width
|
sigt[0].offset = 2*sizeof(niliface.data); // big width
|
||||||
else
|
else
|
||||||
sigt[0].offset = 1; // small width
|
sigt[0].offset = 1; // small width
|
||||||
|
sigt->fun = (void*)fake[h];
|
||||||
|
fake[h] = sigt;
|
||||||
|
unlock(&ifacelock);
|
||||||
return sigt;
|
return sigt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -574,31 +626,40 @@ static int32
|
|||||||
cmpstringchars(string a, uint8 *b)
|
cmpstringchars(string a, uint8 *b)
|
||||||
{
|
{
|
||||||
int32 i;
|
int32 i;
|
||||||
|
byte c1, c2;
|
||||||
|
|
||||||
for(i=0;; i++) {
|
for(i=0;; i++) {
|
||||||
if(i == a->len) {
|
if(i == a->len)
|
||||||
if(b[i] == 0)
|
c1 = 0;
|
||||||
return 0;
|
else
|
||||||
|
c1 = a->str[i];
|
||||||
|
c2 = b[i];
|
||||||
|
if(c1 < c2)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
if(c1 > c2)
|
||||||
if(b[i] == 0)
|
return +1;
|
||||||
return 1;
|
if(c1 == 0)
|
||||||
if(a->str[i] != b[i]) {
|
return 0;
|
||||||
if((uint8)a->str[i] < (uint8)b[i])
|
|
||||||
return -1;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Sigt*
|
static Sigt*
|
||||||
findtype(string type, bool indir)
|
findtype(string type, bool indir)
|
||||||
{
|
{
|
||||||
int32 i;
|
int32 i, lo, hi, m;
|
||||||
|
|
||||||
for(i=0; i<ngotypesigs; i++)
|
lo = 0;
|
||||||
if(cmpstringchars(type, gotypesigs[i]->name) == 0)
|
hi = ngotypesigs;
|
||||||
return gotypesigs[i];
|
while(lo < hi) {
|
||||||
|
m = lo + (hi - lo)/2;
|
||||||
|
i = cmpstringchars(type, gotypesigs[m]->name);
|
||||||
|
if(i == 0)
|
||||||
|
return gotypesigs[m];
|
||||||
|
if(i < 0)
|
||||||
|
hi = m;
|
||||||
|
else
|
||||||
|
lo = m+1;
|
||||||
|
}
|
||||||
return fakesigt(type, indir);
|
return fakesigt(type, indir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +69,24 @@ mcpy(byte *t, byte *f, uint32 n)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32
|
||||||
|
mcmp(byte *s1, byte *s2, uint32 n)
|
||||||
|
{
|
||||||
|
uint32 i;
|
||||||
|
byte c1, c2;
|
||||||
|
|
||||||
|
for(i=0; i<n; i++) {
|
||||||
|
c1 = s1[i];
|
||||||
|
c2 = s2[i];
|
||||||
|
if(c1 < c2)
|
||||||
|
return -1;
|
||||||
|
if(c1 > c2)
|
||||||
|
return +1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
mmov(byte *t, byte *f, uint32 n)
|
mmov(byte *t, byte *f, uint32 n)
|
||||||
{
|
{
|
||||||
@ -368,6 +386,23 @@ noequal(uint32 s, void *a, void *b)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
noprint(uint32 s, void *a)
|
||||||
|
{
|
||||||
|
USED(s);
|
||||||
|
USED(a);
|
||||||
|
throw("print of unprintable type");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
nocopy(uint32 s, void *a, void *b)
|
||||||
|
{
|
||||||
|
USED(s);
|
||||||
|
USED(a);
|
||||||
|
USED(b);
|
||||||
|
throw("copy of uncopyable type");
|
||||||
|
}
|
||||||
|
|
||||||
Alg
|
Alg
|
||||||
algarray[] =
|
algarray[] =
|
||||||
{
|
{
|
||||||
@ -375,5 +410,6 @@ algarray[] =
|
|||||||
[ANOEQ] { nohash, noequal, memprint, memcopy },
|
[ANOEQ] { nohash, noequal, memprint, memcopy },
|
||||||
[ASTRING] { strhash, strequal, strprint, memcopy },
|
[ASTRING] { strhash, strequal, strprint, memcopy },
|
||||||
[AINTER] { interhash, interequal, interprint, memcopy },
|
[AINTER] { interhash, interequal, interprint, memcopy },
|
||||||
|
[AFAKE] { nohash, noequal, noprint, nocopy },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -230,6 +230,7 @@ enum
|
|||||||
ANOEQ,
|
ANOEQ,
|
||||||
ASTRING,
|
ASTRING,
|
||||||
AINTER,
|
AINTER,
|
||||||
|
AFAKE,
|
||||||
Amax
|
Amax
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -269,6 +270,7 @@ void prints(int8*);
|
|||||||
void printf(int8*, ...);
|
void printf(int8*, ...);
|
||||||
byte* mchr(byte*, byte, byte*);
|
byte* mchr(byte*, byte, byte*);
|
||||||
void mcpy(byte*, byte*, uint32);
|
void mcpy(byte*, byte*, uint32);
|
||||||
|
int32 mcmp(byte*, byte*, uint32);
|
||||||
void mmov(byte*, byte*, uint32);
|
void mmov(byte*, byte*, uint32);
|
||||||
void* mal(uint32);
|
void* mal(uint32);
|
||||||
uint32 cmpstring(string, string);
|
uint32 cmpstring(string, string);
|
||||||
|
@ -41,7 +41,7 @@ func main()
|
|||||||
var ic interface{} = c;
|
var ic interface{} = c;
|
||||||
var id interface{} = d;
|
var id interface{} = d;
|
||||||
var ie interface{} = e;
|
var ie interface{} = e;
|
||||||
|
|
||||||
// these comparisons are okay because
|
// these comparisons are okay because
|
||||||
// string compare is okay and the others
|
// string compare is okay and the others
|
||||||
// are comparisons where the types differ.
|
// are comparisons where the types differ.
|
||||||
@ -53,6 +53,13 @@ func main()
|
|||||||
istrue(ic == id);
|
istrue(ic == id);
|
||||||
istrue(ie == ie);
|
istrue(ie == ie);
|
||||||
|
|
||||||
|
// 6g used to let this go through as true.
|
||||||
|
var g uint64 = 123;
|
||||||
|
var h int64 = 123;
|
||||||
|
var ig interface{} = g;
|
||||||
|
var ih interface{} = h;
|
||||||
|
isfalse(ig == ih);
|
||||||
|
|
||||||
// map of interface should use == on interface values,
|
// map of interface should use == on interface values,
|
||||||
// not memory.
|
// not memory.
|
||||||
// TODO: should m[c], m[d] be valid here?
|
// TODO: should m[c], m[d] be valid here?
|
||||||
|
Loading…
Reference in New Issue
Block a user