ww: preserve command package identities

This commit is contained in:
2026-08-13 03:37:08 +09:00
parent 10d24a3237
commit 4377634bb9
10 changed files with 348 additions and 43 deletions

View File

@@ -226,6 +226,7 @@ $(BIN)/w6a_ww: selfhost/cmd/w6a/main.ww selfhost/cmd/w6a/opcodes.ww \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(WWBUILD)/w6a_ww @mkdir -p $(WWBUILD)/w6a_ww
@$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/w6a_ww \ @$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/w6a_ww \
-I $(CURDIR)/selfhost/cmd \
-o $(WWBUILD)/w6a_ww/main \ -o $(WWBUILD)/w6a_ww/main \
$(CURDIR)/selfhost/cmd/w6a $(CURDIR)/selfhost/cmd/w6a
@mv $(WWBUILD)/w6a_ww/main $@ @mv $(WWBUILD)/w6a_ww/main $@
@@ -244,6 +245,7 @@ $(BIN)/w6l_ww: selfhost/cmd/w6l/main.ww selfhost/cmd/w6l/sym.ww \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(WWBUILD)/w6l_ww @mkdir -p $(WWBUILD)/w6l_ww
@$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/w6l_ww \ @$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/w6l_ww \
-I $(CURDIR)/selfhost/cmd \
-o $(WWBUILD)/w6l_ww/main \ -o $(WWBUILD)/w6l_ww/main \
$(CURDIR)/selfhost/cmd/w6l $(CURDIR)/selfhost/cmd/w6l
@mv $(WWBUILD)/w6l_ww/main $@ @mv $(WWBUILD)/w6l_ww/main $@
@@ -961,9 +963,11 @@ nocc:
-I $(CURDIR)/lib/ww \ -I $(CURDIR)/lib/ww \
-I $(CURDIR)/selfhost/cmd \ -I $(CURDIR)/selfhost/cmd \
-I $(CURDIR)/lib $(CURDIR)/selfhost/cmd/w6c/main.ww && mv main w6c_ww1 -I $(CURDIR)/lib $(CURDIR)/selfhost/cmd/w6c/main.ww && mv main w6c_ww1
@cd $(NOCC_BIN) && ./ww build -o main -I $(CURDIR)/lib \ @cd $(NOCC_BIN) && ./ww build -o main \
-I $(CURDIR)/selfhost/cmd -I $(CURDIR)/lib \
$(CURDIR)/selfhost/cmd/w6a && mv main w6a_ww1 $(CURDIR)/selfhost/cmd/w6a && mv main w6a_ww1
@cd $(NOCC_BIN) && ./ww build -o main -I $(CURDIR)/lib \ @cd $(NOCC_BIN) && ./ww build -o main \
-I $(CURDIR)/selfhost/cmd -I $(CURDIR)/lib \
$(CURDIR)/selfhost/cmd/w6l && mv main w6l_ww1 $(CURDIR)/selfhost/cmd/w6l && mv main w6l_ww1
@cd $(NOCC_BIN) && ./ww build -o main -I $(CURDIR)/selfhost/cmd/wcc \ @cd $(NOCC_BIN) && ./ww build -o main -I $(CURDIR)/selfhost/cmd/wcc \
-I $(CURDIR)/lib $(CURDIR)/selfhost/cmd/ww/main.ww && mv main ww_ww1 -I $(CURDIR)/lib $(CURDIR)/selfhost/cmd/ww/main.ww && mv main ww_ww1

View File

@@ -31,13 +31,14 @@ struct importin {
static Node * static Node *
parseinput(Arena *a, const char *file, char *buf, u64 len, parseinput(Arena *a, const char *file, char *buf, u64 len,
const char *mod, const char *testsupport, int *bad) const char *mod, const char *testsupport, int commandpackage, int *bad)
{ {
Lex l; Lex l;
Parser p; Parser p;
lexinit(&l, a, file, buf, len); lexinit(&l, a, file, buf, len);
parserinit(&p, a, &l); parserinit(&p, a, &l);
p.testmodule = testsupport; p.testmodule = testsupport;
p.commandpackage = commandpackage;
if (mod != NULL) { if (mod != NULL) {
p.pathmod = mod; p.pathmod = mod;
p.curmod = mod; p.curmod = mod;
@@ -66,6 +67,7 @@ main(int argc, char **argv)
const char *testsupport = NULL; const char *testsupport = NULL;
int testmode = 0; int testmode = 0;
int testpackage = 0; int testpackage = 0;
int commandpackage = 0;
int entrymode = 0; int entrymode = 0;
int sepmode = 0; /* -c: #22 M3 separate-compile / primary- int sepmode = 0; /* -c: #22 M3 separate-compile / primary-
* only codegen (emit imported==0 decls * only codegen (emit imported==0 decls
@@ -86,6 +88,8 @@ main(int argc, char **argv)
testmode = 1; testmode = 1;
} else if (strcmp(a, "--test-package") == 0) { } else if (strcmp(a, "--test-package") == 0) {
testpackage = 1; testpackage = 1;
} else if (strcmp(a, "--command-package") == 0) {
commandpackage = 1;
} else if (strcmp(a, "--entry") == 0) { } else if (strcmp(a, "--entry") == 0) {
entrymode = 1; entrymode = 1;
} else if (strcmp(a, "--test-support-module") == 0) { } else if (strcmp(a, "--test-support-module") == 0) {
@@ -115,7 +119,7 @@ main(int argc, char **argv)
} }
} }
if (src == NULL) { if (src == NULL) {
fputs("usage: w6c [-T|--test-package] [--entry] [-c] [-I out.wwi] " fputs("usage: w6c [-T|--test-package] [--command-package] [--entry] [-c] [-I out.wwi] "
"[--import path dep.wwi]... [-o out.s] file.ww\n", stderr); "[--import path dep.wwi]... [-o out.s] file.ww\n", stderr);
return 2; return 2;
} }
@@ -123,8 +127,8 @@ main(int argc, char **argv)
fputs("w6c: --import requires -c\n", stderr); fputs("w6c: --import requires -c\n", stderr);
return 2; return 2;
} }
if ((entrymode || testpackage) && !sepmode) { if ((entrymode || testpackage || commandpackage) && !sepmode) {
fputs("w6c: --entry and --test-package require -c\n", stderr); fputs("w6c: --entry, --test-package, and --command-package require -c\n", stderr);
return 2; return 2;
} }
if (testmode && testpackage) { if (testmode && testpackage) {
@@ -162,7 +166,7 @@ main(int argc, char **argv)
} }
int bad = 0; int bad = 0;
Node *f = parseinput(a, imports[i].file, imports[i].buf, Node *f = parseinput(a, imports[i].file, imports[i].buf,
imports[i].len, imports[i].path, testsupport, &bad); imports[i].len, imports[i].path, testsupport, 0, &bad);
if (bad) return 1; if (bad) return 1;
appendnodes(&head, &tail, f->list); appendnodes(&head, &tail, f->list);
} }
@@ -174,7 +178,11 @@ main(int argc, char **argv)
return 1; return 1;
} }
int bad = 0; int bad = 0;
Node *file = parseinput(a, src, buf, len, NULL, testsupport, &bad); /* --entry classifies an ordinary selected command. The separate
* --command-package marker carries the same parser-only fact for command
* test variants whose code generation must not expose bare main. */
Node *file = parseinput(a, src, buf, len, NULL, testsupport,
commandpackage || entrymode, &bad);
if (bad) return 1; if (bad) return 1;
if (head != NULL) { if (head != NULL) {
tail->next = file->list; tail->next = file->list;

View File

@@ -1575,7 +1575,12 @@ parsefile(Parser *p)
&& strcmp(p->testmodule, "__wwtest") == 0 && strcmp(p->testmodule, "__wwtest") == 0
&& strcmp(active, "__wwtest") == 0 && strcmp(active, "__wwtest") == 0
&& strcmp(name, "test") == 0; && strcmp(name, "test") == 0;
if (strcmp(name, last) != 0 && !testsupport) { int commandpackage = p->commandpackage
&& p->pathmod == NULL && p->resetmod != NULL
&& (strcmp(name, "main") == 0
|| strcmp(name, "main_test") == 0);
if (strcmp(name, last) != 0 && !testsupport
&& !commandpackage) {
errorf(p->cur.pos, errorf(p->cur.pos,
"package %s does not match import path %s", "package %s does not match import path %s",
name, active); name, active);

View File

@@ -381,6 +381,8 @@ struct Parser {
* component) instead of overwriting curmod. */ * component) instead of overwriting curmod. */
const char *testmodule; /* hidden package-driver alias for toolchain const char *testmodule; /* hidden package-driver alias for toolchain
* `package test`; NULL outside that compile */ * `package test`; NULL outside that compile */
int commandpackage; /* selected command family: package main/main_test
* validates kind without replacing resetmod identity */
}; };
void parserinit(Parser*, Arena*, Lex*); void parserinit(Parser*, Arena*, Lex*);

View File

@@ -638,6 +638,37 @@ sep_import_base_valid(const char *path)
return 0; return 0;
} }
/* Go command packages retain their canonical import identity while declaring
* package main. An external command-test variant declares main_test. These
* declarations classify package kind but never participate in interning. */
static int
sep_command_declared_name(const struct seppkg *p)
{
return p->variant == SEP_VARIANT_EXTERNAL
? strcmp(p->name, "main_test") == 0
: strcmp(p->name, "main") == 0;
}
static int
sep_forbidden_command_import(const struct sepgraph *g, int importer, int dep)
{
const struct seppkg *from = &g->pkg[importer];
const struct seppkg *to = &g->pkg[dep];
if (strcmp(to->name, "main") != 0 || to->role != SEP_ROLE_NORMAL)
return 0;
/* An external test's exact import of its colocated command production is
* test-variant wiring, not a general source-importable command edge. */
return !(from->variant == SEP_VARIANT_EXTERNAL
&& sep_command_declared_name(from)
&& strcmp(from->canon, to->canon) == 0);
}
static int
sep_command_compiler_marker(const struct sepgraph *g, int pi)
{
return sep_command_declared_name(&g->pkg[pi])
&& !g->pkg[pi].link_entry;
}
/* Bind the canonical ordinary import identity of one provisional directory /* Bind the canonical ordinary import identity of one provisional directory
* action. The action's compiler path is derived from that base and its semantic * action. The action's compiler path is derived from that base and its semantic
* variant; neither requested-root state nor artifact naming participates. */ * variant; neither requested-root state nor artifact naming participates. */
@@ -1223,7 +1254,9 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
} }
int self = strcmp(canon, g->pkg[pi].canon) == 0; int self = strcmp(canon, g->pkg[pi].canon) == 0;
free(canon); free(canon);
if (self && sep_external_production_name(&g->pkg[pi], name, 1)) if (self
&& (sep_external_production_name(&g->pkg[pi], name, 1)
|| sep_command_declared_name(&g->pkg[pi])))
external_production = 1; external_production = 1;
if (self && !external_production) { if (self && !external_production) {
const char *owner = g->pkg[pi].path[0] const char *owner = g->pkg[pi].path[0]
@@ -1409,7 +1442,8 @@ sep_load_pkg(struct sepgraph *g, int pi, int context)
&& !g->pkg[pi].test_support) { && !g->pkg[pi].test_support) {
const char *dot = strrchr(g->pkg[pi].path, '.'); const char *dot = strrchr(g->pkg[pi].path, '.');
const char *leaf = dot ? dot + 1 : g->pkg[pi].path; const char *leaf = dot ? dot + 1 : g->pkg[pi].path;
if (strcmp(g->pkg[pi].name, leaf) != 0) { if (strcmp(g->pkg[pi].name, leaf) != 0
&& !sep_command_declared_name(&g->pkg[pi])) {
fprintf(stderr, fprintf(stderr,
"ww: package %s does not match import path %s\n", "ww: package %s does not match import path %s\n",
g->pkg[pi].name, g->pkg[pi].path); g->pkg[pi].name, g->pkg[pi].path);
@@ -1461,11 +1495,20 @@ sep_load_pkg(struct sepgraph *g, int pi, int context)
/* Mark before recursion so a source cycle terminates here; topo emits the /* Mark before recursion so a source cycle terminates here; topo emits the
* stable cycle diagnostic after all direct bindings are known. */ * stable cycle diagnostic after all direct bindings are known. */
g->pkg[pi].context_state[context] = 2; g->pkg[pi].context_state[context] = 2;
for (int k = 0; k < g->pkg[pi].ndeps; k++) for (int k = 0; k < g->pkg[pi].ndeps; k++) {
if (sep_load_pkg(g, g->pkg[pi].deps[k], context) < 0) { int dep = g->pkg[pi].deps[k];
if (sep_load_pkg(g, dep, context) < 0) {
g->pkg[pi].failed = 1; g->pkg[pi].failed = 1;
return -1; return -1;
} }
if (dep != pi && sep_forbidden_command_import(g, pi, dep)) {
fprintf(stderr,
"ww: package %s is a program, not an importable package\n",
g->pkg[dep].path[0] ? g->pkg[dep].path : g->pkg[dep].canon);
g->pkg[pi].failed = 1;
return -1;
}
}
return 0; return 0;
} }
@@ -1677,7 +1720,8 @@ sep_finalize_directory_identities(struct sepgraph *g)
int support_alias = p->role == SEP_ROLE_TEST_SUPPORT int support_alias = p->role == SEP_ROLE_TEST_SUPPORT
&& strcmp(p->path, SEP_TEST_SUPPORT_MODULE) == 0 && strcmp(p->path, SEP_TEST_SUPPORT_MODULE) == 0
&& strcmp(p->name, "test") == 0; && strcmp(p->name, "test") == 0;
if (!support_alias && strcmp(p->name, leaf) != 0) { if (!support_alias && strcmp(p->name, leaf) != 0
&& !sep_command_declared_name(p)) {
fprintf(stderr, "ww: package %s does not match import path %s\n", fprintf(stderr, "ww: package %s does not match import path %s\n",
p->name, p->path); p->name, p->path);
return -1; return -1;
@@ -2437,6 +2481,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST
|| g->pkg[pi].variant == SEP_VARIANT_EXTERNAL) || g->pkg[pi].variant == SEP_VARIANT_EXTERNAL)
cargv[cpos++] = "--test-package"; cargv[cpos++] = "--test-package";
if (sep_command_compiler_marker(g, pi))
cargv[cpos++] = "--command-package";
if (g->pkg[pi].link_entry) if (g->pkg[pi].link_entry)
cargv[cpos++] = "--entry"; cargv[cpos++] = "--entry";
if (g->pkg[pi].test_support) { if (g->pkg[pi].test_support) {

View File

@@ -2805,9 +2805,14 @@ compilation.
A directory package consists of its immediate regular non-symlink `.ww` files, A directory package consists of its immediate regular non-symlink `.ww` files,
excluding `*_test.ww`, in byte-sorted filename order. Every selected file must excluding `*_test.ww`, in byte-sorted filename order. Every selected file must
declare the same package. An imported directory's declared package must equal declare the same package. An ordinary importable directory's declaration must
the final component of its import path; two logical identities for one physical equal the final component of its import path. A selected command directory
directory are rejected rather than compiled twice. instead declares `package main` to validate command kind while retaining its
complete canonical import identity. A source import of a command package from a
different directory is rejected; the one same-directory exception is an
external `main_test` variant's canonical import of its production action. Two
logical identities for one physical directory are rejected rather than compiled
twice.
Packages compile serially in dependency-first postorder. The compiler emits the Packages compile serially in dependency-first postorder. The compiler emits the
existing deterministic `.wwi` interface for every directory-package action, existing deterministic `.wwi` interface for every directory-package action,
@@ -2839,13 +2844,18 @@ boundary is live in production Cstage and WWstage compilers and drivers.
An ordinary executable directory root is one normal package action. Its An ordinary executable directory root is one normal package action. Its
finalized canonical import identity tags its owner-only unit; it receives only finalized canonical import identity tags its owner-only unit; it receives only
direct exports, emits `.wwi`, `.o`, and a deterministic `.a`, and is compiled direct exports, emits `.wwi`, `.o`, and a deterministic `.a`, and is compiled
exactly once. The declared package name only validates the last component of exactly once. The declared package name validates the last component of that
that identity. The narrow compiler `--entry` flag controls only bare `main` identity for ordinary importable packages. A selected command root may declare
codegen and is independent of export production. The linker receives that root `package main`; `main` validates its executable role and never replaces or
archive first, then the complete reachable package-archive closure and runtime truncates an identity such as `cmd.tool`. The narrow compiler `--entry` flag
archive; it never receives `.wwi`. The linkers seed `main` before archive both classifies that primary unit as a command and controls bare `main` codegen;
selection, so the existing WWAR member protocol needs no special root object or the package driver uses the parser-only `--command-package` marker for command
format change. test variants that must remain ordinary archive code. Neither flag changes
export identity, and imported interfaces remain under strict leaf validation.
The linker receives that root archive first, then the complete reachable
package-archive closure and runtime archive; it never receives `.wwi`. The
linkers seed `main` before archive selection, so the existing WWAR member
protocol needs no special root object or format change.
`ww build -p -o lib.a DIR` explicitly requests a non-main package product: it `ww build -p -o lib.a DIR` explicitly requests a non-main package product: it
emits a deterministic archive at `lib.a` and its compiler interface at emits a deterministic archive at `lib.a` and its compiler interface at
@@ -2941,7 +2951,11 @@ invocation, each still-unbound directory is finalized by this exact algorithm:
The selected full identity's final component is then validated against the The selected full identity's final component is then validated against the
ordinary declared package name (or against the ordinary leaf obtained by ordinary declared package name (or against the ordinary leaf obtained by
removing `_test` for the external variant). There is no fallback from an empty removing `_test` for the external variant). The one command-kind rule is that a
selected command family declares `main`/`main_test` while keeping the finalized
ordinary identity unchanged. A source import of that command from another
directory rejects as a program before tools; a colocated external command test
may reuse the canonical production action. There is no fallback from an empty
import path to a declaration name. Relative, absolute, and symlink spellings import path to a declaration name. Relative, absolute, and symlink spellings
converge through the canonical directory; two unrelated local directories with converge through the canonical directory; two unrelated local directories with
the same declaration therefore remain distinct. One bound import path mapping the same declaration therefore remain distinct. One bound import path mapping
@@ -3292,7 +3306,14 @@ and `b/foo` directories declaring the same `package foo` publish distinct
coexist in one command under distinct reversible local identities. Equivalent coexist in one command under distinct reversible local identities. Equivalent
recursive spellings reuse the same persistent request directory without new recursive spellings reuse the same persistent request directory without new
compilation, while source imports of the reserved local namespace reject before compilation, while source imports of the reserved local namespace reject before
tool invocation. tool invocation. The exact-argv command root declares `package main` but keeps
its non-`main` canonical identity in the unit, export, archive, compiler argv,
and linker argv; the bootstrap self-rebuild independently proves the same rule
for the real `w6a` and `w6l` command directories. A focused command-test row
proves distinct production, internal, external, and generated-main actions,
colocated external-production reuse, parser-only command markers, stage-equal
unit/export/archive bytes, both test binaries' runtime behavior, and pre-tool
rejection when another directory tries to import the command.
The exact-argv regression uses the real diamond The exact-argv regression uses the real diamond
`base -> {left,right} -> root`. It proves one compile per node; no input for `base -> {left,right} -> root`. It proves one compile per node; no input for
@@ -3350,6 +3371,10 @@ The pinned official Go 1.26.5 tag (commit
and the command-global package cache returns the existing package pointer for and the command-global package cache returns the existing package pointer for
a later root or import of the resolved identity a later root or import of the resolved identity
([lines 757775](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L757-L775)). ([lines 757775](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L757-L775)).
Go keeps a command-line `Name == "main"` package as the selected command and
rejects an import from a different directory, while permitting the
same-directory test-loader edge
([lines 799805](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L799-L805)).
The package's parsed import list becomes its direct package dependencies, The package's parsed import list becomes its direct package dependencies,
rather than a transitive flattening rather than a transitive flattening
([lines 433440](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L433-L440), ([lines 433440](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L433-L440),
@@ -3358,6 +3383,9 @@ The pinned official Go 1.26.5 tag (commit
pointer ([`action.go`, lines 202206](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L202-L206)) pointer ([`action.go`, lines 202206](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L202-L206))
and returns the already-interned action for that key and returns the already-interned action for that key
([lines 437447](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L437-L447)). ([lines 437447](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L437-L447)).
A selected package whose independent `Name` is `main` receives a link action
([lines 450455](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L450-L455))
while retaining its ordinary compiled archive action.
A compile action depends on only `p.Internal.Imports` A compile action depends on only `p.Internal.Imports`
([lines 628658](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L628-L658)); ([lines 628658](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L628-L658));
an executable link asks for that same cached root compile action an executable link asks for that same cached root compile action

View File

@@ -49,6 +49,9 @@ export type parser = struct {
// Hidden package-driver alias for the toolchain `package test` source. // Hidden package-driver alias for the toolchain `package test` source.
// Empty outside that one separate-compilation edge. // Empty outside that one separate-compilation edge.
testmodule: str, testmodule: str,
// Selected command-family validation: main/main_test classifies the
// package without replacing the resetmod canonical identity.
commandpackage: bool,
}; };
fn refill(p: *parser) void = { fn refill(p: *parser) void = {
@@ -71,6 +74,7 @@ export fn parserinit(p: *parser, l: *lex) void = {
p.pathmod = ""; p.pathmod = "";
p.resetmod = ""; p.resetmod = "";
p.testmodule = ""; p.testmodule = "";
p.commandpackage = false;
refill(p); refill(p);
}; };
@@ -619,7 +623,12 @@ export fn parsefile(p: *parser) *node = {
p.testmodule, "__wwtest") == 0 p.testmodule, "__wwtest") == 0
&& strings.compare(active, "__wwtest") == 0 && strings.compare(active, "__wwtest") == 0
&& strings.compare(name, "test") == 0; && strings.compare(name, "test") == 0;
if (strings.compare(name, last) != 0 && !testsupport) { let commandpackage: bool = p.commandpackage
&& p.pathmod.len == 0 && p.resetmod.len != 0
&& (strings.compare(name, "main") == 0
|| strings.compare(name, "main_test") == 0);
if (strings.compare(name, last) != 0 && !testsupport
&& !commandpackage) {
errmsg(p, "package does not match import path"); errmsg(p, "package does not match import path");
}; };
} else { } else {

View File

@@ -67,6 +67,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
let testsupport: *u8 = nil; let testsupport: *u8 = nil;
let testmode: i32 = 0i32; // #15: `-T` test-mode let testmode: i32 = 0i32; // #15: `-T` test-mode
let testpackage: i32 = 0i32; let testpackage: i32 = 0i32;
let commandpackage: i32 = 0i32;
let entrymode: i32 = 0i32; let entrymode: i32 = 0i32;
let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary-
// only codegen (emit imported==0 decls // only codegen (emit imported==0 decls
@@ -100,6 +101,8 @@ export fn main(argc: i32, argv: **u8) i32 = {
testmode = 1i32; testmode = 1i32;
} else { if (cstreq(a, "--test-package")) { } else { if (cstreq(a, "--test-package")) {
testpackage = 1i32; testpackage = 1i32;
} else { if (cstreq(a, "--command-package")) {
commandpackage = 1i32;
} else { if (cstreq(a, "--entry")) { } else { if (cstreq(a, "--entry")) {
entrymode = 1i32; entrymode = 1i32;
} else { if (cstreq(a, "--test-support-module")) { } else { if (cstreq(a, "--test-support-module")) {
@@ -133,12 +136,12 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2; return 2;
}; };
src = a; src = a;
}; }; }; }; }; }; }; }; }; }; }; }; }; }; }; }; }; }; };
i += 1; i += 1;
}; };
if (src == nil) { if (src == nil) {
let m: str = "usage: w6c_ww [-T|--test-package] [--entry] [-c] [-I out.wwi] [--import path dep.wwi]... [-o out.s] file.ww\n"; let m: str = "usage: w6c_ww [-T|--test-package] [--command-package] [--entry] [-c] [-I out.wwi] [--import path dep.wwi]... [-o out.s] file.ww\n";
os.write(2, m.ptr, m.len: u64); os.write(2, m.ptr, m.len: u64);
return 2; return 2;
}; };
@@ -147,8 +150,9 @@ export fn main(argc: i32, argv: **u8) i32 = {
os.write(2, m.ptr, m.len: u64); os.write(2, m.ptr, m.len: u64);
return 2; return 2;
}; };
if ((entrymode != 0 || testpackage != 0) && sepmode == 0) { if ((entrymode != 0 || testpackage != 0 || commandpackage != 0)
let m: str = "w6c: --entry and --test-package require -c\n"; && sepmode == 0) {
let m: str = "w6c: --entry, --test-package, and --command-package require -c\n";
os.write(2, m.ptr, m.len: u64); os.write(2, m.ptr, m.len: u64);
return 2; return 2;
}; };
@@ -233,6 +237,9 @@ export fn main(argc: i32, argv: **u8) i32 = {
let ps: parser; let ps: parser;
parserinit(&ps, &l); parserinit(&ps, &l);
if (testsupport != nil) { ps.testmodule = pathstr(testsupport); }; if (testsupport != nil) { ps.testmodule = pathstr(testsupport); };
// Entry compilation classifies an ordinary command package. The explicit
// marker carries only that parser fact for non-entry command test variants.
ps.commandpackage = commandpackage != 0 || entrymode != 0;
let f: *node = parsefile(&ps); let f: *node = parsefile(&ps);
// Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's // Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's
// `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches // `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches

View File

@@ -899,6 +899,33 @@ fn sepimportbasevalid(base: *u8) bool = {
return false; return false;
}; };
// Go-style command packages keep their canonical import identity while
// declaring main; an external command-test variant declares main_test. These
// declarations classify package kind but never enter action identity.
fn sepcommanddeclaredname(p: *seppkg) bool = {
if (p.name == nil) { return false; };
if (p.variant == SEP_VARIANT_EXTERNAL) {
return cstreqlit(p.name, "main_test");
};
return cstreqlit(p.name, "main");
};
fn sepforbiddencommandimport(g: *sepgraph, importer: i32, dep: i32) bool = {
let from: *seppkg = &g.pkg[importer];
let to: *seppkg = &g.pkg[dep];
if (to.name == nil || !cstreqlit(to.name, "main")
|| to.role != SEP_ROLE_NORMAL) { return false; };
// An external test's exact colocated production edge is variant wiring,
// not a general source-importable command-package alias.
return !(from.variant == SEP_VARIANT_EXTERNAL
&& sepcommanddeclaredname(from)
&& cstreq(from.canon, to.canon));
};
fn sepcommandcompilermarker(g: *sepgraph, pi: i32) bool = {
return sepcommanddeclaredname(&g.pkg[pi]) && !g.pkg[pi].linkentry;
};
// Bind a provisional directory action to its canonical ordinary import // Bind a provisional directory action to its canonical ordinary import
// identity. The compiler path is derived from that base and the semantic // identity. The compiler path is derived from that base and the semantic
// variant; root/product/artifact state never participates. // variant; root/product/artifact state never participates.
@@ -1458,7 +1485,8 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
sepbindadd(bindings, 'D': u8, u.usepath, ipath); sepbindadd(bindings, 'D': u8, u.usepath, ipath);
let self: bool = os.samefile(pathstr(ipath), let self: bool = os.samefile(pathstr(ipath),
pathstr(g.pkg[pi].entry)); pathstr(g.pkg[pi].entry));
if (self && sepexternalname(&g.pkg[pi], idp, idn, true)) { if (self && (sepexternalname(&g.pkg[pi], idp, idn, true)
|| sepcommanddeclaredname(&g.pkg[pi]))) {
externalproduction = true; externalproduction = true;
}; };
if (self && !externalproduction) { if (self && !externalproduction) {
@@ -1719,7 +1747,8 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = {
if (g.pkg[pi].path[j] == '.') { leaf = g.pkg[pi].path + j + 1u64; }; if (g.pkg[pi].path[j] == '.') { leaf = g.pkg[pi].path + j + 1u64; };
j += 1u64; j += 1u64;
}; };
if (!cstreq(g.pkg[pi].name, leaf)) { if (!cstreq(g.pkg[pi].name, leaf)
&& !sepcommanddeclaredname(&g.pkg[pi])) {
cerr("ww: package "); cerr("ww: package ");
cerr(pathstr(g.pkg[pi].name)); cerr(pathstr(g.pkg[pi].name));
cerr(" does not match import path "); cerr(" does not match import path ");
@@ -1765,7 +1794,17 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = {
g.pkg[pi].contextstate[context] = 2u8; g.pkg[pi].contextstate[context] = 2u8;
let k: i32 = 0; let k: i32 = 0;
for (k < g.pkg[pi].ndeps) { for (k < g.pkg[pi].ndeps) {
if (seploadpkg(g, g.pkg[pi].deps[k], context) < 0) { let dep: i32 = g.pkg[pi].deps[k];
if (seploadpkg(g, dep, context) < 0) {
g.pkg[pi].failed = true;
return -1;
};
if (dep != pi && sepforbiddencommandimport(g, pi, dep)) {
cerr("ww: package ");
if (g.pkg[dep].path[0u64] != 0u8) {
cerr(pathstr(g.pkg[dep].path));
} else { cerr(pathstr(g.pkg[dep].canon)); };
cerr(" is a program, not an importable package\n");
g.pkg[pi].failed = true; g.pkg[pi].failed = true;
return -1; return -1;
}; };
@@ -1997,7 +2036,8 @@ fn sepfinalizedirectoryidentities(g: *sepgraph) i32 = {
let supportalias: bool = p.role == SEP_ROLE_TEST_SUPPORT let supportalias: bool = p.role == SEP_ROLE_TEST_SUPPORT
&& cstreqlit(p.path, SEP_TEST_SUPPORT_MODULE) && cstreqlit(p.path, SEP_TEST_SUPPORT_MODULE)
&& cstreqlit(p.name, "test"); && cstreqlit(p.name, "test");
if (!supportalias && !cstreq(p.name, leaf)) { if (!supportalias && !cstreq(p.name, leaf)
&& !sepcommanddeclaredname(p)) {
cerr("ww: package "); cerr(pathstr(p.name)); cerr("ww: package "); cerr(pathstr(p.name));
cerr(" does not match import path "); cerr(pathstr(p.path)); cerr(" does not match import path "); cerr(pathstr(p.path));
cerr("\n"); cerr("\n");
@@ -2969,12 +3009,14 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let gent: bool = g.pkg[pi].generatedmain || rawtest; let gent: bool = g.pkg[pi].generatedmain || rawtest;
let testpkg: bool = g.pkg[pi].variant == SEP_VARIANT_SAME_TEST let testpkg: bool = g.pkg[pi].variant == SEP_VARIANT_SAME_TEST
|| g.pkg[pi].variant == SEP_VARIANT_EXTERNAL; || g.pkg[pi].variant == SEP_VARIANT_EXTERNAL;
let commandpkg: bool = sepcommandcompilermarker(g, pi);
let entry: bool = g.pkg[pi].linkentry; let entry: bool = g.pkg[pi].linkentry;
let supportpkg: bool = g.pkg[pi].testsupport; let supportpkg: bool = g.pkg[pi].testsupport;
let alen: u64 = 8u64; let alen: u64 = 8u64;
if (gent) { alen += 4u64; } if (gent) { alen += 4u64; }
else { else {
if (testpkg) { alen += 1u64; }; if (testpkg) { alen += 1u64; };
if (commandpkg) { alen += 1u64; };
if (entry) { alen += 1u64; }; if (entry) { alen += 1u64; };
if (supportpkg) { alen += 2u64; }; if (supportpkg) { alen += 2u64; };
}; };
@@ -2988,6 +3030,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
append(argv, testsupportmodule); append(argv, testsupportmodule);
} else { } else {
if (testpkg) { append(argv, "--test-package"); }; if (testpkg) { append(argv, "--test-package"); };
if (commandpkg) { append(argv, "--command-package"); };
if (entry) { append(argv, "--entry"); }; if (entry) { append(argv, "--entry"); };
if (supportpkg) { if (supportpkg) {
append(argv, "--test-support-module"); append(argv, "--test-support-module");

View File

@@ -2609,7 +2609,8 @@ fn localidentity(dir: str, leaf: str) str = {
let base: str = strings.concat(source, "/ww_root_parity_base_7f3"); let base: str = strings.concat(source, "/ww_root_parity_base_7f3");
let left: str = strings.concat(source, "/ww_root_parity_left_7f3"); let left: str = strings.concat(source, "/ww_root_parity_left_7f3");
let right: str = strings.concat(source, "/ww_root_parity_right_7f3"); let right: str = strings.concat(source, "/ww_root_parity_right_7f3");
let target: str = strings.concat(source, "/main"); let rootidentity: str = "ww_root_parity_target_7f3";
let target: str = strings.concat(source, "/", rootidentity);
let tools: str = strings.concat(root, "/tool wrappers"); let tools: str = strings.concat(root, "/tool wrappers");
assert(os.mkdir(source, 448i32) == 0); assert(os.mkdir(source, 448i32) == 0);
assert(os.mkdir(runtime, 448i32) == 0); assert(os.mkdir(runtime, 448i32) == 0);
@@ -2703,9 +2704,11 @@ fn localidentity(dir: str, leaf: str) str = {
let workroot: str = strings.concat(bin, ".sepwork"); let workroot: str = strings.concat(bin, ".sepwork");
let work: str = strings.concat(workroot, "/"); let work: str = strings.concat(workroot, "/");
let artifacts: []str = ["ww_root_parity_base_7f3", let artifacts: []str = ["ww_root_parity_base_7f3",
"ww_root_parity_left_7f3", "ww_root_parity_right_7f3", "main"]; "ww_root_parity_left_7f3", "ww_root_parity_right_7f3",
rootidentity];
let unitartifacts: []str = ["ww_root_parity_base_7f3", let unitartifacts: []str = ["ww_root_parity_base_7f3",
"ww_root_parity_left_7f3", "ww_root_parity_right_7f3", "main"]; "ww_root_parity_left_7f3", "ww_root_parity_right_7f3",
rootidentity];
let wantunits: []str = [strings.concat( let wantunits: []str = [strings.concat(
"//ww:module-reset ww_root_parity_base_7f3\n", basea, "//ww:module-reset ww_root_parity_base_7f3\n", basea,
"\n//ww:module-reset ww_root_parity_base_7f3\n", basez, "\n"), "\n//ww:module-reset ww_root_parity_base_7f3\n", basez, "\n"),
@@ -2713,7 +2716,8 @@ fn localidentity(dir: str, leaf: str) str = {
leftbody, "\n"), leftbody, "\n"),
strings.concat("//ww:module-reset ww_root_parity_right_7f3\n", strings.concat("//ww:module-reset ww_root_parity_right_7f3\n",
rightbody, "\n"), rightbody, "\n"),
strings.concat("//ww:module-reset main\n", rootbody, "\n")]; strings.concat("//ww:module-reset ", rootidentity, "\n", rootbody,
"\n")];
let referenceunits: []str = ["", "", "", ""]; let referenceunits: []str = ["", "", "", ""];
let referenceexports: []str = ["", "", "", ""]; let referenceexports: []str = ["", "", "", ""];
let referencearchives: []str = ["", "", "", ""]; let referencearchives: []str = ["", "", "", ""];
@@ -2807,6 +2811,11 @@ fn localidentity(dir: str, leaf: str) str = {
".wwi")); ".wwi"));
let archive: str = readfile(strings.concat(work, artifacts[ai], let archive: str = readfile(strings.concat(work, artifacts[ai],
".a")); ".a"));
if (ai == 3) {
assert(strings.hasprefix(exportf, strings.concat("package ",
rootidentity, ";\n")));
assert(!strings.hasprefix(exportf, "package main;\n"));
};
if (si == 0) { if (si == 0) {
referenceexports[ai] = strings.dup(exportf); referenceexports[ai] = strings.dup(exportf);
referencearchives[ai] = strings.dup(archive); referencearchives[ai] = strings.dup(archive);
@@ -2843,16 +2852,28 @@ fn localidentity(dir: str, leaf: str) str = {
"ww_root_parity_left_7f3.wwi><--import>", "ww_root_parity_left_7f3.wwi><--import>",
"<ww_root_parity_right_7f3><", work, "<ww_root_parity_right_7f3><", work,
"ww_root_parity_right_7f3.wwi><-I><", work, "ww_root_parity_right_7f3.wwi><-I><", work,
"main.wwi><-o><", work, rootidentity, ".wwi><-o><", work,
"main.s><", work, "main.unit.ww>"); rootidentity, ".s><", work, rootidentity, ".unit.ww>");
assert(same(linecontaining(ctrace, assert(same(linecontaining(ctrace,
"ww_root_parity_base_7f3.unit.ww"), baseline)); "ww_root_parity_base_7f3.unit.ww"), baseline));
assert(same(linecontaining(ctrace, assert(same(linecontaining(ctrace,
"ww_root_parity_left_7f3.unit.ww"), leftline)); "ww_root_parity_left_7f3.unit.ww"), leftline));
assert(same(linecontaining(ctrace, assert(same(linecontaining(ctrace,
"ww_root_parity_right_7f3.unit.ww"), rightline)); "ww_root_parity_right_7f3.unit.ww"), rightline));
assert(same(linecontaining(ctrace, "main.unit.ww"), rootline)); assert(same(linecontaining(ctrace, strings.concat(rootidentity,
".unit.ww")), rootline));
assert(!has(rootline, "ww_root_parity_base_7f3.wwi")); assert(!has(rootline, "ww_root_parity_base_7f3.wwi"));
// The primary command declaration is accepted only when the compiler
// receives command classification from --entry. Imported interfaces and
// ordinary package compiles retain strict identity-leaf validation.
let rejectwwi: str = strings.concat(root, "/reject-", tags[si], ".wwi");
let rejectasm: str = strings.concat(root, "/reject-", tags[si], ".s");
let rejectav: []str = [driver(compilers[si]), "-c", "-I", rejectwwi,
"-o", rejectasm, strings.concat(work, rootidentity, ".unit.ww")];
runcommand(root, strings.concat("command-without-entry-", tags[si]),
rejectav, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "does not match import path"));
assert(occurrences(atrace, "\n") == 4); assert(occurrences(atrace, "\n") == 4);
ai = 0; ai = 0;
for (ai < artifacts.len) { for (ai < artifacts.len) {
@@ -2864,7 +2885,7 @@ fn localidentity(dir: str, leaf: str) str = {
}; };
assert(occurrences(ltrace, "\n") == 1); assert(occurrences(ltrace, "\n") == 1);
assert(has(ltrace, strings.concat("BEGIN<-o><", bin, "><", work, assert(has(ltrace, strings.concat("BEGIN<-o><", bin, "><", work,
"main.a>"))); rootidentity, ".a>")));
ai = 0; ai = 0;
for (ai < artifacts.len) { for (ai < artifacts.len) {
assert(occurrences(ltrace, strings.concat("<", work, artifacts[ai], assert(occurrences(ltrace, strings.concat("<", work, artifacts[ai],
@@ -2935,6 +2956,138 @@ fn localidentity(dir: str, leaf: str) str = {
clean(root); clean(root);
}; };
@test fn command_package_test_variants_keep_canonical_identity() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let target: str = strings.concat(source, "/cmdtool");
let importer: str = strings.concat(source, "/importer");
let wrapper: str = strings.concat(root, "/command-w6c.sh");
assert(os.mkdir(source, 448i32) == 0);
assert(os.mkdir(target, 448i32) == 0);
assert(os.mkdir(importer, 448i32) == 0);
writefile(strings.concat(target, "/main.ww"), strings.concat(
"package main;\n",
"export fn value() i32 = { return 41; };\n",
"fn main() i32 = { return 0; };\n"));
writefile(strings.concat(target, "/internal_test.ww"), strings.concat(
"package main;\n",
"@test fn command_internal() void = { assert(value() == 41); };\n"));
writefile(strings.concat(target, "/external_test.ww"), strings.concat(
"package main_test;\n",
"import cmdtool;\n",
"@test fn command_external() void = { ",
"assert(cmdtool.value() == 41); };\n"));
writefile(strings.concat(importer, "/importer.ww"), strings.concat(
"package importer;\n",
"import cmdtool;\n",
"export fn value() i32 = { return cmdtool.value(); };\n"));
writeexecutable(wrapper, strings.concat(
"#!/bin/sh\n",
"printf 'BEGIN' >> \"$WW_COMMAND_COMPILER_TRACE\"\n",
"for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ",
"\"$WW_COMMAND_COMPILER_TRACE\"; done\n",
"printf '\\n' >> \"$WW_COMMAND_COMPILER_TRACE\"\n",
"exec \"$WW_COMMAND_REAL_COMPILER\" \"$@\"\n"));
let stages: []str = ["ww", "ww_ww"];
let compilers: []str = ["w6c", "w6c_ww"];
let artifacts: []str = ["cmdtool", "cmdtool-internal-test",
"cmdtool_test-external-test"];
let suffixes: []str = [".unit.ww", ".wwi", ".a"];
let reference: []str = ["", "", "", "", "", "", "", "", ""];
let samebin: str = strings.concat(target, "/main.test");
let externalbin: str = strings.concat(target, "/main_test.test");
let workroot: str = strings.concat(samebin, ".sepwork");
let work: str = strings.concat(workroot, "/");
let baseenv: []str = os.getenvs();
let si: i32 = 0;
for (si < stages.len) {
let trace: str = strings.concat(root, "/", stages[si], ".trace");
writefile(trace, "");
let env: []str = alloc([], (baseenv.len + 3): u64)!;
let ei: i32 = 0;
for (ei < baseenv.len) {
if (!strings.hasprefix(baseenv[ei], "WW_W6C=")
&& !strings.hasprefix(baseenv[ei],
"WW_COMMAND_COMPILER_TRACE=")
&& !strings.hasprefix(baseenv[ei],
"WW_COMMAND_REAL_COMPILER=")) {
append(env, baseenv[ei]);
};
ei += 1;
};
append(env, strings.concat("WW_W6C=", wrapper));
append(env, strings.concat("WW_COMMAND_COMPILER_TRACE=", trace));
append(env, strings.concat("WW_COMMAND_REAL_COMPILER=",
driver(compilers[si])));
let av: []str = [driver(stages[si]), "test", "-c", "-I", source,
target];
let out: commandout;
runcommandenv(root, strings.concat("command-variants-", stages[si]), av,
env, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0);
let ctrace: str = readfile(trace);
let ai: i32 = 0;
for (ai < artifacts.len) {
let line: str = linecontaining(ctrace,
strings.concat(artifacts[ai], ".unit.ww"));
assert(has(line, "<--command-package>"));
let sj: i32 = 0;
for (sj < suffixes.len) {
let body: str = readfile(strings.concat(work, artifacts[ai],
suffixes[sj]));
let ri: i32 = ai * suffixes.len + sj;
if (si == 0) { reference[ri] = strings.dup(body); }
else { assert(same(reference[ri], body)); };
sj += 1;
};
ai += 1;
};
assert(has(readfile(strings.concat(work, "cmdtool.unit.ww")),
"//ww:module-reset cmdtool\n"));
assert(has(readfile(strings.concat(work,
"cmdtool-internal-test.unit.ww")),
"//ww:module-reset cmdtool\n"));
assert(has(readfile(strings.concat(work,
"cmdtool_test-external-test.unit.ww")),
"//ww:module-reset cmdtool_test\n"));
let runav: []str = [samebin, "-package=main"];
runcommand(root, strings.concat("command-internal-run-", stages[si]),
runav, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "command_internal ... ok\n"));
let externalav: []str = [externalbin, "-package=main_test"];
runcommand(root, strings.concat("command-external-run-", stages[si]),
externalav, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "command_external ... ok\n"));
// The same command action is not generally source-importable. It is
// classified before leaf validation so this Go-like diagnostic is stable,
// and graph failure occurs before any compiler invocation.
clean(trace);
writefile(trace, "");
let importout: str = strings.concat(root, "/importer-", stages[si], ".a");
let importav: []str = [driver(stages[si]), "build", "-p", "-I", source,
"-o", importout, importer];
runcommandenv(root, strings.concat("command-import-", stages[si]),
importav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr,
"package cmdtool is a program, not an importable package\n"));
assert(readfile(trace).len == 0);
clean(workroot);
clean(samebin);
clean(externalbin);
si += 1;
};
clean(root);
};
@test fn package_graph_diagnostics_are_stable() void = { @test fn package_graph_diagnostics_are_stable() void = {
let root: str = fresh(); let root: str = fresh();
let missing: str = strings.concat(root, "/missing"); let missing: str = strings.concat(root, "/missing");