ww: separate package identity from declared name
This commit is contained in:
@@ -23,6 +23,8 @@ type checker = struct {
|
||||
istestpackage: i32, // validate/retain package-owned @test bodies and
|
||||
// export compiler-private metadata; no entry synth
|
||||
testmodule: str, // generated dispatcher support qualifier
|
||||
testtarget: str, // canonical target path used only as the
|
||||
// compiler-owned generated-main qualifier
|
||||
sepmode: i32, // -c package compilation: imported interfaces are
|
||||
// present, so absent members are hard export errors.
|
||||
synthtestrun: *syntax.node, // exact generated support.run DOT; its
|
||||
@@ -32,7 +34,9 @@ type checker = struct {
|
||||
curmod: str, // importing-module bareword for the decl
|
||||
// currently being walked; "" for primary
|
||||
// compilation unit. Drives same-module
|
||||
// preference in bare-leaf lookups.
|
||||
// preference in bare-leaf lookups.
|
||||
cursource: i32, // lexical source-file scope of the current decl;
|
||||
// selects only that file's import bindings.
|
||||
file: *syntax.node, // N_FILE root; used by checkmoduleshadow
|
||||
// to consult the declaring source's own
|
||||
// `use` directives.
|
||||
@@ -154,6 +158,7 @@ fn declmod(file: *syntax.node, d: *syntax.node) str = {
|
||||
if (d == nil) { return empty; };
|
||||
if (d.nmod.len == 0) { return empty; };
|
||||
if (file == nil) { return empty; };
|
||||
if (d.imported != 0) { return d.nmod; };
|
||||
let u: *syntax.node = file.list;
|
||||
for (u != nil) {
|
||||
// M1 #22: a decl is imported iff some `use` directive's full
|
||||
@@ -168,17 +173,16 @@ fn declmod(file: *syntax.node, d: *syntax.node) str = {
|
||||
return empty;
|
||||
};
|
||||
|
||||
// usepath — map a `use` alias (leaf bareword the user writes, `utf8`)
|
||||
// to the full dotted import path it binds (`encoding.utf8`), for the
|
||||
// module-qualified resolution and codegen hint (M1 #22). Single-level
|
||||
// packages have usepath == alias so the result is unchanged. The
|
||||
// Only an import owned by the referencing package is visible; a matching
|
||||
// usepath — map a source-file default qualifier (the imported package's
|
||||
// declared name) to the full canonical import path it binds, for
|
||||
// module-qualified resolution and codegen hint (M1 #22). Only an import owned
|
||||
// by the referencing file is visible; a matching
|
||||
// alias carried by a transitive interface is deliberately ignored.
|
||||
fn usepathfor(file: *syntax.node, modtag: str, alias: str) str = {
|
||||
fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
|
||||
let empty: str;
|
||||
if (file == nil) { return empty; };
|
||||
if (alias.len == 0) { return empty; };
|
||||
if (modtag.len != 0) {
|
||||
if (source == 0 && modtag.len != 0) {
|
||||
let (prefix, suffix) = strings.rcut(modtag, ".");
|
||||
let leaf: str = suffix;
|
||||
if (leaf.len == 0) { leaf = modtag; };
|
||||
@@ -186,14 +190,15 @@ fn usepathfor(file: *syntax.node, modtag: str, alias: str) str = {
|
||||
};
|
||||
let u: *syntax.node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == syntax.nkind.N_USE) {
|
||||
if (u.kind == syntax.nkind.N_USE && u.sourceid == source) {
|
||||
if (syntax.streq(u.str, alias)) {
|
||||
let um: str = declmod(file, u);
|
||||
let same: bool = false;
|
||||
if (modtag.len == 0) {
|
||||
if (um.len == 0) { same = true; };
|
||||
} else { if (syntax.streq(um, modtag)) { same = true; }; };
|
||||
if (same) {
|
||||
if (same) {
|
||||
u.used = 1;
|
||||
if (u.usepath.len != 0) { return u.usepath; };
|
||||
return u.str;
|
||||
};
|
||||
@@ -207,19 +212,19 @@ fn usepathfor(file: *syntax.node, modtag: str, alias: str) str = {
|
||||
// modkeyfor — the module key for a directly imported alias. Empty means
|
||||
// the referencing package did not itself declare that import.
|
||||
fn modkeyfor(c: *checker, alias: str) str = {
|
||||
return usepathfor(c.file, c.curmod, alias);
|
||||
return usepathfor(c.file, c.curmod, c.cursource, alias);
|
||||
};
|
||||
|
||||
// srcimports — does the source file that contributed decl-module
|
||||
// `modtag` carry `use <name>;`? Mirrors cstage's src_imports —
|
||||
// `modtag.len == 0` means primary, matching declmod's empty-str
|
||||
// return for primary-source decls.
|
||||
fn srcimports(file: *syntax.node, modtag: str, name: str) bool = {
|
||||
fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = {
|
||||
if (file == nil) { return false; };
|
||||
if (name.len == 0) { return false; };
|
||||
let u: *syntax.node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == syntax.nkind.N_USE) {
|
||||
if (u.kind == syntax.nkind.N_USE && u.sourceid == source) {
|
||||
// Skip self-imports: lib/fmt/fmt_test.ww carries
|
||||
// `use fmt;` while its module tag is also "fmt".
|
||||
// That directive doesn't introduce a foreign
|
||||
@@ -251,11 +256,23 @@ fn srcimports(file: *syntax.node, modtag: str, name: str) bool = {
|
||||
// symbol iff the referencing source package itself imported its module path.
|
||||
fn directmodvisible(c: *checker, mod: str) bool = {
|
||||
if (mod.len == 0) { return false; };
|
||||
let (prefix, suffix) = strings.rcut(mod, ".");
|
||||
let alias: str = suffix;
|
||||
if (alias.len == 0) { alias = mod; };
|
||||
let path: str = usepathfor(c.file, c.curmod, alias);
|
||||
return path.len != 0 && syntax.streq(path, mod);
|
||||
let u: *syntax.node = c.file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == syntax.nkind.N_USE && u.sourceid == c.cursource) {
|
||||
let um: str = declmod(c.file, u);
|
||||
let same: bool = false;
|
||||
if (c.curmod.len == 0) { same = um.len == 0; }
|
||||
else { same = syntax.streq(um, c.curmod); };
|
||||
let path: str = u.usepath;
|
||||
if (path.len == 0) { path = u.str; };
|
||||
if (same && syntax.streq(path, mod)) {
|
||||
u.used = 1;
|
||||
return true;
|
||||
};
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
fn lookupvisible(c: *checker, name: str) *syntax.sym = {
|
||||
@@ -268,7 +285,7 @@ fn lookupvisible(c: *checker, name: str) *syntax.sym = {
|
||||
// Flat scope installation coalesces same-leaf N_USE entries. The
|
||||
// source-owned alias map, not the retained marker's mod field, decides
|
||||
// whether this package can use the qualifier.
|
||||
if (usepathfor(c.file, c.curmod, name).len != 0) {
|
||||
if (usepathfor(c.file, c.curmod, c.cursource, name).len != 0) {
|
||||
let q: *syntax.scope = c.cur;
|
||||
for (q != nil) {
|
||||
let u: *syntax.sym = q.first;
|
||||
@@ -354,9 +371,11 @@ fn builtintypename(name: str) bool = {
|
||||
|
||||
fn packageaccesserr(c: *checker, e: *syntax.node, pkg: str, member: str,
|
||||
missing: bool) void = {
|
||||
cerr(e.file); cerr(":");
|
||||
cerr(strconv.i32tos(e.line, strconv.base.DEC)); cerr(":");
|
||||
cerr(strconv.i32tos(e.col, strconv.base.DEC));
|
||||
let at: *syntax.node = e;
|
||||
for (at.kind == syntax.nkind.N_DOT && at.lhs != nil) { at = at.lhs; };
|
||||
cerr(at.file); cerr(":");
|
||||
cerr(strconv.i32tos(at.line, strconv.base.DEC)); cerr(":");
|
||||
cerr(strconv.i32tos(at.col, strconv.base.DEC));
|
||||
cerr(": error: package '"); cerr(pkg);
|
||||
if (missing) {
|
||||
cerr("' has no exported declaration '"); cerr(member); cerr("'\n");
|
||||
@@ -396,7 +415,7 @@ fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
|
||||
if (s != nil) { s = s.parent; };
|
||||
};
|
||||
if (!seen) { return; };
|
||||
if (!srcimports(c.file, c.curmod, name)) { return; };
|
||||
if (!srcimports(c.file, c.curmod, c.cursource, name)) { return; };
|
||||
cerr(kindstr);
|
||||
cerr(" '");
|
||||
cerr(name);
|
||||
@@ -437,10 +456,8 @@ fn installdecl(c: *checker, file: *syntax.node, d: *syntax.node) void = {
|
||||
let k: syntax.nkind = d.kind;
|
||||
let nm: str = d.str;
|
||||
let mod: str = declmod(file, d);
|
||||
// check-(c) self-import: a package may not import itself. Pure
|
||||
// owner==leaf string compare, package-model-independent. check-(a)
|
||||
// unused + (b)/(d) membership DEFERRED to task #8 (filename-keyed
|
||||
// pulls lack import->file->symbol provenance). Message byte-identical
|
||||
// A package may not import its own canonical owner. Import usage and
|
||||
// membership are checked with source-file provenance. Message byte-identical
|
||||
// to cstage check.c.
|
||||
if (k == syntax.nkind.N_USE) {
|
||||
// M1 #22: self-import ⟺ the imported path equals the use's own
|
||||
@@ -2696,10 +2713,13 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = {
|
||||
// owns it. A direct dependency's fact may be demanded while
|
||||
// walking a consumer-owned type; keeping the consumer module
|
||||
// here can bind bare names in the fact to the wrong package.
|
||||
let savedmod: str = c.curmod;
|
||||
c.curmod = s.mod;
|
||||
let under: *syntax.tinfo = tinfofornode(c, body);
|
||||
c.curmod = savedmod;
|
||||
let savedmod: str = c.curmod;
|
||||
let savedsource: i32 = c.cursource;
|
||||
c.curmod = s.mod;
|
||||
if (s.decl != nil) { c.cursource = s.decl.sourceid; };
|
||||
let under: *syntax.tinfo = tinfofornode(c, body);
|
||||
c.curmod = savedmod;
|
||||
c.cursource = savedsource;
|
||||
// #62/#69: alias-root cycle (`type a = b;
|
||||
// type b = a` / `type a = a`) — checked
|
||||
// BEFORE clearing the flag so self-aliases
|
||||
@@ -3821,6 +3841,9 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
|
||||
return tn;
|
||||
};
|
||||
if (k == syntax.nkind.N_IDENT) {
|
||||
// resolvewalk visits the dot receiver before its parent. Preserve the
|
||||
// first causal undefined error just as cstage's cached cexpr does.
|
||||
if (e.type_ == c.tc.tyerr: *void) { return nil; };
|
||||
// #55: bare-leaf value-ident must prefer curmod. Flat-scope
|
||||
// scopelookup bucket-walks and can bind a same-leaf symbol from
|
||||
// the wrong module under a foreign curmod, dragging its decl's
|
||||
@@ -4406,7 +4429,12 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
|
||||
// name path stays primary because a fn-NAME callee node in wwstage
|
||||
// already carries its RETURN type (fn-decl.lhs), not its fn-type —
|
||||
// so a `fn make() fn() void` callee would otherwise mis-yield void.
|
||||
let ct: *syntax.node = resolvealias(c, unwrapbang(exprtype(c, callee, nil)));
|
||||
let calleetn: *syntax.node = exprtype(c, callee, nil);
|
||||
if (callee.type_ == c.tc.tyerr: *void) {
|
||||
e.type_ = c.tc.tyerr: *void;
|
||||
return nil;
|
||||
};
|
||||
let ct: *syntax.node = resolvealias(c, unwrapbang(calleetn));
|
||||
// #14/#181-cgen: ONE pointer-peel only, mirroring cstage
|
||||
// cmd/wcc/check.c:1947. #181-cgen lowers an indirect call by using the
|
||||
// callee VALUE as the target, the fn address for a single `*fn` but only
|
||||
@@ -4554,6 +4582,10 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
|
||||
// check.c:805-832. Peel one TPTR for `(*EnumT).MEMBER` (rare
|
||||
// but cstage handles it at L808).
|
||||
let basetn: *syntax.node = exprtype(c, lhsn, nil);
|
||||
if (lhsn != nil && lhsn.type_ == c.tc.tyerr: *void) {
|
||||
e.type_ = c.tc.tyerr: *void;
|
||||
return nil;
|
||||
};
|
||||
if (basetn != nil) {
|
||||
let bu: *syntax.node = resolvealias(c, unwrapbang(basetn));
|
||||
if (bu != nil) { if (bu.kind == syntax.nkind.N_TPTR) {
|
||||
@@ -7499,6 +7531,90 @@ fn asserttyped(c: *checker, n: *syntax.node, indot: bool) void = {
|
||||
};
|
||||
};
|
||||
|
||||
fn importdiagprefix(n: *syntax.node) void = {
|
||||
cerr(n.file); cerr(":");
|
||||
cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":");
|
||||
cerr(strconv.i32tos(n.col, strconv.base.DEC)); cerr(": error: ");
|
||||
};
|
||||
|
||||
fn importdiagalt(n: *syntax.node, name: str) void = {
|
||||
cerr("\t"); cerr(n.file); cerr(":");
|
||||
cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":");
|
||||
cerr(strconv.i32tos(n.col, strconv.base.DEC));
|
||||
cerr(": other declaration of "); cerr(name); cerr("\n");
|
||||
};
|
||||
|
||||
fn topdeclkind(d: *syntax.node) bool = {
|
||||
return d != nil && (d.kind == syntax.nkind.N_TYPEDECL
|
||||
|| d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_FNDECL
|
||||
|| d.kind == syntax.nkind.N_LET);
|
||||
};
|
||||
|
||||
fn checkimportredeclarations(c: *checker, file: *syntax.node) void = {
|
||||
if (c.sepmode == 0) { return; };
|
||||
let u: *syntax.node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == syntax.nkind.N_USE && u.imported == 0) {
|
||||
let v: *syntax.node = file.list;
|
||||
for (v != u) {
|
||||
if (v.kind == syntax.nkind.N_USE && v.imported == 0
|
||||
&& v.sourceid == u.sourceid && syntax.streq(v.str, u.str)) {
|
||||
importdiagprefix(u); cerr(u.str);
|
||||
cerr(" redeclared in this block\n");
|
||||
c.errs += 1;
|
||||
importdiagalt(v, u.str);
|
||||
break;
|
||||
};
|
||||
v = v.next;
|
||||
};
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
};
|
||||
|
||||
fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = {
|
||||
if (c.sepmode == 0) { return; };
|
||||
let u: *syntax.node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == syntax.nkind.N_USE && u.imported == 0 && u.used == 0) {
|
||||
let path: str = u.usepath;
|
||||
if (path.len == 0) { path = u.str; };
|
||||
let (prefix, suffix) = strings.rcut(path, ".");
|
||||
let leaf: str = suffix;
|
||||
if (leaf.len == 0) { leaf = path; };
|
||||
importdiagprefix(u); cerr("\""); cerr(path);
|
||||
if (syntax.streq(u.str, leaf)) {
|
||||
cerr("\" imported and not used\n");
|
||||
} else {
|
||||
cerr("\" imported as "); cerr(u.str);
|
||||
cerr(" and not used\n");
|
||||
};
|
||||
c.errs += 1;
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
let d: *syntax.node = file.list;
|
||||
for (d != nil) {
|
||||
if (d.imported == 0 && topdeclkind(d)) {
|
||||
u = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == syntax.nkind.N_USE && u.imported == 0
|
||||
&& syntax.streq(d.str, u.str)) {
|
||||
let path: str = u.usepath;
|
||||
if (path.len == 0) { path = u.str; };
|
||||
importdiagprefix(d); cerr(d.str);
|
||||
cerr(" already declared through import of package ");
|
||||
cerr(u.str); cerr(" (\""); cerr(path); cerr("\")\n");
|
||||
c.errs += 1;
|
||||
importdiagalt(u, d.str);
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
};
|
||||
d = d.next;
|
||||
};
|
||||
};
|
||||
|
||||
fn checkinit(c: *checker, tc: *syntax.tctx) void = {
|
||||
c.tc = tc;
|
||||
c.top = syntax.newscope(nil);
|
||||
@@ -7509,12 +7625,15 @@ fn checkinit(c: *checker, tc: *syntax.tctx) void = {
|
||||
c.istest = 0i32; // #15: caller (w6c main) sets it after init
|
||||
c.istestpackage = 0i32;
|
||||
c.testmodule = "test";
|
||||
let emptytesttarget: str;
|
||||
c.testtarget = emptytesttarget;
|
||||
c.sepmode = 0i32; // caller (w6c main) sets it from -c
|
||||
c.synthtestrun = nil;
|
||||
c.verbose = 0;
|
||||
c.fnret = nil;
|
||||
let empty: str;
|
||||
c.curmod = empty;
|
||||
c.cursource = 0;
|
||||
c.file = nil;
|
||||
c.allococtx = nil;
|
||||
seedprimitives(c);
|
||||
@@ -7538,25 +7657,35 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
let present: bool = false;
|
||||
let su: *syntax.node = file.list;
|
||||
for (su != nil) {
|
||||
if (c.testtarget.len > 0 && su.kind == syntax.nkind.N_USE
|
||||
&& su.imported == 0
|
||||
&& (syntax.streq(su.usepath, c.testtarget)
|
||||
|| syntax.streq(su.usepath, c.testmodule))) {
|
||||
su.used = 1i32;
|
||||
};
|
||||
if (su.kind == syntax.nkind.N_USE && su.imported == 0
|
||||
&& syntax.streq(su.usepath, c.testmodule)) {
|
||||
present = true;
|
||||
break;
|
||||
};
|
||||
su = su.next;
|
||||
};
|
||||
if (!present) {
|
||||
let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col);
|
||||
usenode.str = c.testmodule;
|
||||
usenode.usepath = c.testmodule;
|
||||
usenode.next = file.list;
|
||||
usenode.str = c.testmodule;
|
||||
usenode.usepath = c.testmodule;
|
||||
usenode.pkgname = file.pkgname;
|
||||
usenode.sourceid = file.sourceid;
|
||||
if (c.testtarget.len > 0) { usenode.used = 1i32; };
|
||||
usenode.next = file.list;
|
||||
file.list = usenode;
|
||||
};
|
||||
};
|
||||
};
|
||||
checkimportredeclarations(c, file);
|
||||
|
||||
// Pass 1: install all top-level names.
|
||||
let d: *syntax.node = file.list;
|
||||
for (d != nil) {
|
||||
c.cursource = d.sourceid;
|
||||
installdecl(c, file, d);
|
||||
d = d.next;
|
||||
};
|
||||
@@ -7693,11 +7822,14 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
};
|
||||
let nm: *syntax.node = syntax.newnode(syntax.nkind.N_STRLIT, pf, pl, pc);
|
||||
nm.str = t.str;
|
||||
let id: *syntax.node;
|
||||
if (t.imported != 0 && t.nmod.len > 0) {
|
||||
let (prefix, suffix) = strings.rcut(t.nmod, ".");
|
||||
let alias: str = suffix;
|
||||
let id: *syntax.node;
|
||||
if (t.imported != 0 && t.nmod.len > 0) {
|
||||
let alias: str = t.pkgname;
|
||||
if (alias.len == 0) { alias = t.nmod; };
|
||||
if (c.testtarget.len > 0
|
||||
&& syntax.streq(t.nmod, c.testtarget)) {
|
||||
alias = c.testtarget;
|
||||
};
|
||||
id = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc);
|
||||
id.lhs = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
|
||||
id.lhs.str = alias;
|
||||
@@ -7758,8 +7890,10 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
let arr: *syntax.node = syntax.newnode(syntax.nkind.N_ARRLIT, pf, pl, pc);
|
||||
arr.list = rhead;
|
||||
tab = syntax.newnode(syntax.nkind.N_LET, pf, pl, pc);
|
||||
tab.op = syntax.tkind.TK_CONST;
|
||||
tab.str = "__wwtests";
|
||||
tab.op = syntax.tkind.TK_CONST;
|
||||
tab.str = "__wwtests";
|
||||
tab.pkgname = file.pkgname;
|
||||
tab.sourceid = file.sourceid;
|
||||
tab.lhs = tsl;
|
||||
tab.rhs = arr;
|
||||
// pass 1 already ran; install the table name now so main
|
||||
@@ -7795,8 +7929,10 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
// the synthesized call type-resolves. Pass 1 installs the use.
|
||||
};
|
||||
let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc);
|
||||
m.str = "main";
|
||||
m.exported = 1i32;
|
||||
m.str = "main";
|
||||
m.exported = 1i32;
|
||||
m.pkgname = file.pkgname;
|
||||
m.sourceid = file.sourceid;
|
||||
let rety: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc);
|
||||
rety.str = "i32";
|
||||
m.lhs = rety;
|
||||
@@ -7824,6 +7960,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
d = file.list;
|
||||
for (d != nil) {
|
||||
c.curmod = declmod(file, d);
|
||||
c.cursource = d.sourceid;
|
||||
// A.6.2.1-pre — attr-subtree gap: top-level dispatch below walks
|
||||
// d.lhs / d.body per kind but never d.attr, leaving `@symbol("…")`
|
||||
// arg literals (N_STRLIT) outside the post-order exprtype
|
||||
@@ -7918,6 +8055,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
};
|
||||
d = d.next;
|
||||
};
|
||||
checkimportusageandcollisions(c, file);
|
||||
|
||||
// Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each
|
||||
// decl with its curmod set so asserttyped's gate lookups resolve
|
||||
@@ -7925,6 +8063,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
d = file.list;
|
||||
for (d != nil) {
|
||||
c.curmod = declmod(file, d);
|
||||
c.cursource = d.sourceid;
|
||||
asserttyped(c, d, false);
|
||||
d = d.next;
|
||||
};
|
||||
@@ -7967,5 +8106,6 @@ fn checkfile(c: *checker, file: *syntax.node) void = {
|
||||
|
||||
let empty: str;
|
||||
c.curmod = empty;
|
||||
c.cursource = 0;
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user