driver: ignore wrong-suffix files as named sources

This commit is contained in:
2026-08-23 03:18:19 +09:00
parent 59fc76964c
commit f6fabfc6ac
6 changed files with 1325 additions and 28 deletions

View File

@@ -7241,6 +7241,13 @@ basename_no_ext(const char *path, char *out, size_t outsz)
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
static int
source_operand_named(const char *path)
{
size_t n = strlen(path);
return n >= 3 && strcmp(path + n - 3, ".ww") == 0;
}
/* Go's named-file package still applies matchFile's unconditional basename
* exclusion before UseAllFiles, platform suffixes, source parsing, or test-file
* classification. Keep this predicate on the public operand spelling: a
@@ -7250,8 +7257,7 @@ static int
source_operand_ignored(const char *path)
{
struct stat st;
size_t n = strlen(path);
if (n < 3 || strcmp(path + n - 3, ".ww") != 0) return 0;
if (!source_operand_named(path)) return 0;
if (stat(path, &st) != 0 || S_ISDIR(st.st_mode)) return 0;
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
@@ -7342,7 +7348,7 @@ resolve_module(const char *name, const char *incs, char *out, size_t outsz,
{
struct stat st;
if (stat(name, &st) == 0) {
if (S_ISREG(st.st_mode)) {
if (S_ISREG(st.st_mode) && source_operand_named(name)) {
if (strlen(name) + 1 > outsz) return 0;
memcpy(out, name, strlen(name) + 1);
*is_dir = 0;
@@ -7578,7 +7584,9 @@ do_build(int argc, char **argv)
return exec_package_command(argc, argv, src, NULL, NULL, 0, 1);
}
struct stat requested;
int literal = stat(src, &requested) == 0;
int literal = stat(src, &requested) == 0
&& (S_ISDIR(requested.st_mode)
|| source_operand_named(src));
int requested_nondirectory = literal && !S_ISDIR(requested.st_mode);
if (source_operand_ignored(src)) {
source_operand_no_sources(src);
@@ -7705,10 +7713,12 @@ do_run(int argc, char **argv)
if (next < 0) { free(incs); return 2; }
if (src == NULL) src = ".";
struct stat requested;
int literal = stat(src, &requested) == 0;
int literal = stat(src, &requested) == 0
&& (S_ISDIR(requested.st_mode)
|| source_operand_named(src));
if (literal && S_ISDIR(requested.st_mode)) {
size_t n = strlen(src);
if (n >= 3 && strcmp(src + n - 3, ".ww") == 0) {
if (source_operand_named(src)) {
if (n >= 8 && strcmp(src + n - 8, "_test.ww") == 0)
fprintf(stderr,
"ww: cannot run *_test.ww files (%s)\n", src);
@@ -8188,10 +8198,14 @@ do_test(int argc, char **argv)
}
if (pattern != NULL) {
struct stat first;
if (stat(target, &first) != 0 || !S_ISREG(first.st_mode)) {
int first_found = stat(target, &first) == 0;
int first_logical = !first_found
|| (!S_ISDIR(first.st_mode) && !source_operand_named(target));
if (!first_found || !S_ISREG(first.st_mode)
|| !source_operand_named(target)) {
char first_resolved[PATH_MAX];
int first_is_dir = 0;
if (stat(target, &first) == 0
if (!first_logical
|| !resolve_module(target, incs, first_resolved,
sizeof first_resolved, &first_is_dir)
|| first_is_dir) {
@@ -8229,7 +8243,8 @@ do_test(int argc, char **argv)
return exec_package_command(argc, argv, src, NULL, NULL, 0, 0);
}
struct stat st;
if (stat(target, &st) != 0) {
if (stat(target, &st) != 0
|| (!S_ISDIR(st.st_mode) && !source_operand_named(target))) {
/* not a literal path — try module resolution and run as
* a single test program. */
char resolved[PATH_MAX];
@@ -8349,7 +8364,7 @@ do_test(int argc, char **argv)
if (test_failed) fputs("FAIL\n", stdout);
return rc;
}
if (S_ISREG(st.st_mode)) {
if (S_ISREG(st.st_mode) && source_operand_named(target)) {
if (nproducts != 0) {
fprintf(stderr,
"ww test: package-test variant needs one directory\n");

View File

@@ -10511,6 +10511,150 @@ serialized representation changes: build workdir format remains `18`, test
workdir format remains `19`, semantic storage format remains `3`, and no cache
or result record is added.
### 11.54 Implemented wrong-suffix physical-source exclusion
One public operand is a local named source only when its exact requested
spelling ends `.ww` and the command's existing file-kind rule admits it. An
existing non-directory object with any other suffix does not become source and
does not preempt the same operand's ordinary dotted lookup. Thus a physical
`foo.bar` is ignored as a source while request `foo.bar` continues to
`foo/bar.ww` or `foo/bar/`. An existing directory, including a symlink whose
target is a directory, remains a stat-first directory package regardless of
suffix.
#### Pinned authority and applicability
- **behavior directly implemented or asserted by pinned Go** — official Go
1.26.5 commit `c19862e5f8415b4f24b189d065ed739517c548ba` enters named-file
mode in `cmd/go/internal/load/pkg.go:28872932`, especially 29032918,
only when a requested spelling ends `.go`, `Stat` succeeds, and the result
is not a directory. `GoFilesPackage` independently rejects every non-`.go`
member at `pkg.go:32443318` before constructing its synthetic package.
Build and test call that loader at
`cmd/go/internal/work/build.go:459477` and
`cmd/go/internal/test/test.go:684719`. Run independently consumes only
leading `.go` spellings at `cmd/go/internal/run/run.go:73145`, especially
96123.
- **behavior directly implemented or asserted by pinned Go** — official
`cmd/go/testdata/script/list_test_non_go_files.txt:113` directly tests a
mixed named-file list: after a `.go` member selects named-file mode,
`GoFilesPackage` rejects the non-`.go` member. Official `run_hello.txt:110`
and `run_set_executable_name.txt:417` anchor ordinary named-file and package
run fronts. None directly tests one existing wrong-suffix object colliding
with a package request, and the official tree contains no such singular
build/run/test script.
- **behavior derived from the pinned implementation** — the singular
collision result follows from the pinned suffix-before-`Stat` build/test
gate and run's suffix-only scan. WW's honest local adaptation applies the
same positive spelling decision to `.ww` named sources before its existing
dotted search. It requires no module, manifest, registry, network lookup,
generalized import syntax, cache, database, CAS, lock, or source expression.
#### Ownership, selection, and identity
- **directly measured WW behavior** — before this change, both stages adopted
an existing physical `foo.bar` as a raw source. Build and run therefore used
its package, imports, main, and runtime status instead of `foo/bar.ww`;
raw, compile-only, and assembly-only test used its test descriptors and
retained its semantic/public bytes. Removing only `foo.bar` selected the
logical provider and changed all of those observations.
- **behavior derived from the pinned implementation** — the true shared
owners are `cmd/ww/main.c::resolve_module` and
`selfhost/cmd/ww/main.ww::resolvemodule`. Their direct non-directory adoption
now requires the exact `.ww` requested spelling. The mirrored spelling
predicate also owns build/run requested-literal bookkeeping and raw test's
second-positional classifier plus main stat/resolution branch. These command
fronts distinguish direct directory, eligible direct source, and logical
resolution without moving the rule into the compiler, enumerator, graph,
coordinator, producer, or runtime.
- **directly measured WW behavior** — a resolved logical single-file provider
retains the established command-line-file root family `__root.*`; selection
by a dotted request does not invent a dotted storage identity. A resolved
logical directory retains its dotted package/import/action family such as
`foo.bar.*`. The ignored physical pathname, object kind, containing
directory, bytes, mode, and timestamp create no package member, qualifier,
graph node or edge, action, symbol, `.wwi`, initializer, artifact,
publication destination, or persistence key. The logical requested spelling
remains canonical request identity where the existing directory route uses
it.
- **directly measured WW behavior** — requested suffix, not a symlink target's
basename, owns the positive gate. A wrong-suffix symlink to a regular or
non-directory special object is ignored as source; a wrong-suffix symlink to
a directory follows ordinary directory routing. A visible `.ww` symlink to a
regular source remains eligible. Each stage retains its prior visible `.ww`
special-file kind handling; this slice does not make FIFO/device loading a
shared new contract.
#### Build, test, package, and import effects
- **directly measured WW behavior** — with a logical provider, build produces
the same source set, import closure, initializer graph, producer calls,
runtime result, public output, and persistent artifacts whether the
wrong-suffix object is absent or present. Run executes that same provider.
Raw/running test, the historical second-positional test-name filter,
`test -c`, and `test -S` select the same logical test package, descriptors,
support closure, binary, and assembly. Cstage and WWstage outputs and every
comparable semantic artifact are byte-identical.
- **directly measured WW behavior** — the ignored object's package clause,
imports, malformed bytes, checker failures, abort/nonzero behavior, and
timestamps are not source input and cannot displace logical-provider
diagnostics. Package membership and import edges are exactly those of the
provider. A logical directory retains its dotted identity and a logical file
retains `__root`; physical collision state supplies neither.
- **directly measured WW behavior** — when no logical provider exists, a
collision matches the absent-physical control. An ordinary build or run
emits its existing `cannot find module` diagnostic; raw test emits its
existing `cannot find` diagnostic plus `FAIL` only when running; `-c` and
`-S` omit that marker. A second positional deliberately retains the historic
package-coordinator route and its exact canonicalization or usage result,
rather than being silently redefined as a direct cannot-find path. Existing
flag, output, tree, package-option, hidden-source, named `_test.ww`, and
`.ww` run-directory precedence remains unchanged.
- **behavior derived from the pinned implementation** — all four permanent
axes meet at this one source-eligibility decision. Build no longer constructs
or publishes the wrong action; test no longer constructs or runs the wrong
test package; package membership is not stolen by an ineligible physical
filename; and import binding/initialization comes only from the logical
provider. No axis receives a compatibility bypass or new identity model.
#### Lifecycle, parity, formats, and scope
- **directly measured WW behavior** — changing the collision among absent and
stat-successful non-directory states does not invalidate semantic actions or
alter semantic bytes or producer inputs. An unchanged warm command still
performs the established final link and success publication, producing the
same public bytes while its inode and mtime may change. Replacing the
collision with a directory, or retargeting a symlink to a directory, leaves
this case and follows ordinary stat-first directory behavior; no new atomic
snapshot promise is made for a concurrent kind change.
- **directly measured WW behavior** — logical producer failure preserves the
prior public and semantic generation. A retained running-test runtime failure
occurs after the complete logical build generation commits: deferred public
installation is skipped, so prior retained public bytes survive while the
newly built semantic generation remains committed and reusable. Restoring the
prior source requires a later successful rebuild and commit, not rollback of
the runtime-failing generation. Normal completion and controlled failures
remove request-owned scratch and transaction fragments. The spelling gate is
request-local and allocates no state before logical action or coordinator
start; concurrent requests use separate destinations and the existing
logical-action synchronization.
- **directly measured WW behavior** — external signal interruption after an
action starts is unchanged. In particular, the verified direct-driver fixed
`.new` leakage and later persistent-request poisoning remain open. This
source classifier neither prevents nor recovers that residue and makes no
signal-cleanup claim.
- **behavior derived from the pinned implementation** — the rule does not
complete the remaining suffix-first run front, multiple named sources,
finite `.ww` FIFO capture, shared test-process state/failure topology,
Go-compatible `-run` regular expressions, or `package documentation`
suppression. Existing `.ww` directory slices, hidden-source exclusion,
named `_test.ww` build omission, recursive/multiple-root coordination,
package syntax, and import syntax remain intact.
Build workdir format remains `18`, test workdir format remains `19`, and
semantic storage format remains `3`. No schema, action descriptor, cache/result
record, manifest, transaction protocol, or lock changes.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -395,6 +395,52 @@ ImportPath = ident { "." ident } .
persistence, process, or filesystem owner. Build and all test forms retain
their distinct stat-first directory behavior. Build workdir format remains
18, test workdir format remains 19, and semantic storage format remains 3.
- A public operand is eligible for direct named-source adoption only when its
exact requested spelling ends `.ww` and that command's existing file-kind
rule admits it. An existing non-directory object with another suffix is not
source: its bytes, package clause, imports, syntax, test declarations,
runtime behavior, mode, and timestamp are not read as source, and ordinary
dotted resolution continues exactly as though the colliding object were
absent. Thus request `foo.bar` may resolve `foo/bar.ww` or `foo/bar/` even
while a physical non-directory `foo.bar` exists. A wrong-suffix symlink to a
non-directory is the same ignored collision; a symlink to a directory remains
an ordinary stat-first directory request. A visible `.ww` symlink to a
regular source remains eligible, and the established stage-specific handling
of visible `.ww` special files is not broadened by this rule.
A resolved logical single file retains the established `__root` command-line
package/action/artifact identity. A resolved logical directory retains its
dotted package, import, graph, action, symbol, `.wwi`, initializer, artifact,
publication, and persistence identity. The ignored physical pathname/object
creates no membership, binding, edge, action, key, or alternate identity.
Build and run use only the logical provider's production/import/initializer
closure and runtime. Raw running test, its historical second-positional
test-name filter, `test -c`, and `test -S` use only the provider's test
package, descriptors, support closure, binary, and assembly.
When no provider exists, collision-present behavior is byte-for-byte the
existing collision-absent behavior. An ordinary single target retains its
direct build/run/test cannot-find result and creates no producer action. A
second positional retains the established package-coordinator route,
diagnostics, status, selection lifecycle, and cleanup; this source gate does
not reinterpret that positional form. Mutation invariance covers only absent
and stat-successful non-directory collision states. A transition to a
directory leaves this rule and follows ordinary directory routing, with no
new atomic-snapshot guarantee for concurrent kind changes.
Provider compilation failure preserves the prior public and semantic
generation. A retained running-test runtime failure occurs after the complete
logical build generation commits: it preserves prior retained public bytes by
skipping deferred installation, while that semantic generation remains
committed and reusable. Restoring prior source bytes requires a later
successful rebuild and commit, not runtime-failure rollback. Controlled-
failure cleanup otherwise remains unchanged. External signal
interruption after action start is unchanged, including the verified-open
fixed `.new` residue and later persistent-request poisoning. Multiple named
sources, remaining suffix-first run behavior, finite `.ww` FIFOs, test
process topology, and `-run` regular expressions are not completed here.
Build workdir format remains 18, test workdir format remains 19, and semantic
storage format remains 3.
- `import acme.codec;` loads the canonical package `acme.codec`. If that
package declares `package wire;`, the importing file sees its exported names
as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias

View File

@@ -302,6 +302,53 @@ multiple named sources, finite FIFOs, and hidden regular sources are not
claimed by this existing-directory slice. The general direct-driver `.new`
interruption residue also remains open.
The raw source front also applies one positive requested-spelling gate before
physical adoption. An existing non-directory operand whose exact requested
spelling does not end `.ww` is not a raw source and cannot hijack its ordinary
dotted provider. Build, run, raw test, the historical second-positional raw
test filter, `test -c`, and `test -S` therefore resolve and consume the same
logical file or directory they consume when the collision is absent. Logical
files keep the existing `__root` action/artifact family; logical directories
keep dotted package/import/action identity. The ignored object's bytes,
package, imports, tests, runtime status, mode, and timestamp supply no package
membership, edge, producer input, artifact, publication, or persistence key.
Requested suffix owns the gate through symlinks. Wrong-suffix links to regular
or special non-directories remain logical requests, while a link to a directory
retains stat-first direct-directory routing; a visible `.ww` link to a regular
source remains raw source. Each stage's existing visible `.ww` special-file
handling is outside this slice. Mutation/reuse invariance is limited to absent
or stat-successful non-directory collision states. A transition to a directory
uses the directory owner and receives no new concurrent-kind snapshot promise.
No-provider rows deliberately separate the ordinary single-target path from
the second-positional compatibility path. The first retains direct cannot-find
diagnostics, running-test `FAIL`, and producer/artifact absence. The second
retains the existing package-coordinator process, canonicalization or usage
diagnostic, status, selection state, and cleanup; the gate does not turn that
route into a direct raw-file error. Logical producer failure preserves prior
public and semantic bytes. A retained running-test runtime failure instead
keeps the complete newly built semantic generation committed and reusable but
skips deferred installation, preserving prior retained public bytes; restoring
the old source requires a later successful rebuild and commit. Normal and
controlled-failure cleanup is unchanged. External driver signals after action
start retain the open fixed
`.new` leakage and later-request poisoning behavior.
The focused `wrong_suffix_physical_files_do_not_hijack_dotted_requests`
package observer owns this boundary for both driver stages. It compares absent
controls with regular, symlinked-regular, and symlinked-special collisions;
checks build, run, raw/filter test, `-c`, and `-S` streams, statuses, runtime,
public bytes, complete comparable semantic artifacts, `__root` file identity,
and dotted directory identity; separates direct and coordinator no-provider
rows; exercises warm non-directory mutation, producer-failure rollback,
retained-runtime-failure public-byte preservation with committed semantic-
generation reuse and later successful restoration, request-local concurrent
builds, positive `.ww` and direct-directory controls, and normal residue
cleanup. It makes no finite-FIFO or signal-recovery claim.
The rule changes no package coordinator, test harness/process topology,
test-result caching, or build/test/semantic format (18/19/3).
List mode uses that same product process and initialization boundary but starts
no per-test child. The shared language harness emits only selected qualified
test names, one per line in descriptor order. A valid filter selecting no tests

View File

@@ -8660,13 +8660,17 @@ fn basenameoff(p: *u8, plen: u64) u64 = {
return start;
};
fn sourceoperandnamed(path: *u8) bool = {
return cstrendswithlit(path, ".ww");
};
// A named Go source still passes through matchFile's leading-dot/underscore
// exclusion even when UseAllFiles bypasses platform suffixes and build tags.
// Inspect the public operand spelling so a hidden parent is not source identity
// and a hidden symlink spelling cannot be resurrected by a non-directory
// target's name or file kind.
fn sourceoperandignored(path: *u8) bool = {
if (!cstrendswithlit(path, ".ww")) { return false; };
if (!sourceoperandnamed(path)) { return false; };
let fi: os.filestat;
match (os.stat(&fi, pathstr(path))) {
case void => {
@@ -8741,13 +8745,6 @@ fn buildsearchpath(selfdir: *u8, incs: *u8) *u8 = {
fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
let nlen: u64 = cstrlen(name);
if (cstrendswithlit(name, ".ww")) {
if (os.access(pathstr(name), 0i32) == 0) {
*isdir = 0;
return arenadupcstr(name, nlen);
};
};
let fi: os.filestat;
let sr: (void | os.oserror) = os.stat(&fi, pathstr(name));
let found: bool = false;
@@ -8760,10 +8757,16 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
};
case let e: os.oserror => void;
};
if (found) {
if (foundisdir != 0) {
*isdir = foundisdir;
return arenadupcstr(name, nlen);
};
if (sourceoperandnamed(name)) {
if (os.access(pathstr(name), 0i32) == 0 || found) {
*isdir = 0;
return arenadupcstr(name, nlen);
};
};
if (reservedimportpath(name)) { return nil; };
let search: *u8 = buildsearchpath(selfdir, incs);
@@ -9043,9 +9046,11 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let requestedstat: os.filestat;
match (os.stat(&requestedstat, pathstr(src))) {
case void => {
requestedliteral = true;
let typ: u32 = (requestedstat.mode: u32) & 61440u32;
requestednondirectory = typ != os.mode.DIR: u32;
requestedliteral = typ == os.mode.DIR: u32
|| sourceoperandnamed(src);
requestednondirectory = requestedliteral
&& typ != os.mode.DIR: u32;
};
case let e: os.oserror => void;
};
@@ -9071,7 +9076,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let isdir: i32 = 0;
let resolved: *u8 = nil;
if (requestedliteral && !requestednondirectory
&& cstrendswithlit(src, ".ww")) {
&& sourceoperandnamed(src)) {
isdir = 1;
resolved = src;
} else {
@@ -9318,12 +9323,16 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let requestedliteral: bool = false;
let requestedstat: os.filestat;
match (os.stat(&requestedstat, pathstr(src))) {
case void => requestedliteral = true;
case void => {
let typ: u32 = (requestedstat.mode: u32) & 61440u32;
requestedliteral = typ == os.mode.DIR: u32
|| sourceoperandnamed(src);
};
case let e: os.oserror => void;
};
if (requestedliteral
&& ((requestedstat.mode: u32) & 61440u32) == os.mode.DIR: u32
&& cstrendswithlit(src, ".ww")) {
&& sourceoperandnamed(src)) {
if (cstrendswithlit(src, "_test.ww")) {
cerrpath("ww: cannot run *_test.ww files (", src, ")\n");
} else {
@@ -9987,22 +9996,27 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let first: os.filestat;
let firstregular: bool = false;
let firstfound: bool = false;
let firstisdirphysical: bool = false;
match (os.stat(&first, pathstr(target))) {
case void => {
firstfound = true;
firstisdirphysical = ((first.mode: u32) & 61440u32)
== os.mode.DIR: u32;
firstregular = (((first.mode: u32) & 61440u32)
== (os.mode.REG: u32));
== (os.mode.REG: u32)) && sourceoperandnamed(target);
};
case let e: os.oserror => void;
};
if (!firstregular) {
let firstresolved: *u8 = nil;
let firstisdir: i32 = 0;
if (!firstfound) {
let firstlogical: bool = !firstfound
|| (!firstisdirphysical && !sourceoperandnamed(target));
if (firstlogical) {
firstresolved = resolvemodule(selfdir, target, incs.ptr,
&firstisdir);
};
if (firstfound || firstresolved == nil || firstisdir != 0) {
if (!firstlogical || firstresolved == nil || firstisdir != 0) {
let replacement: *u8 = nil;
let identity: *u8 = requestidentity;
if (firstisdir != 0) {
@@ -10049,7 +10063,9 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32;
if (t == os.mode.DIR: u32) { isdir = 1; found = true; }
else { if (t == os.mode.REG: u32) { found = true; }; };
else { if (t == os.mode.REG: u32 && sourceoperandnamed(target)) {
found = true;
}; };
};
case let e: os.oserror => void;
};

File diff suppressed because it is too large Load Diff