cmd: resolve source imports only as directories

Separate import lookup from raw CLI target compatibility in both production drivers. Package scans and test-support edges now create directory nodes only, while unit composition emits only the owning package's sorted sources after direct export data. Document the Go 1.26.5 ownership evidence and retained single-file root boundary.
This commit is contained in:
2026-08-12 17:12:49 +09:00
parent c7d9dc92de
commit 0af13ea501
3 changed files with 272 additions and 339 deletions

View File

@@ -178,65 +178,73 @@ import_path_form(const char *name, char *out, size_t outsz)
out[i] = '\0';
}
/* Symmetric with wwstage locatein for byte-id driver output (rule 10).
* The legacy <dir>/<name>/<name>.ww form was dropped in task #22 —
* directory-as-module enumeration replaces it, mirroring
* ref/hare/hare/module/srcs.ha (Hare has no fallback matching
* `foo/foo.ha`; a module IS the directory). */
/* An import path names one directory package. There is deliberately no
* <dir>/<path>.ww branch here: literal or searched single-file roots are a
* CLI compatibility concern handled by locate_module, never an import edge. */
static int
locate_import_in(const char *dir, const char *path_form, char *out,
size_t outsz, int *is_dir, int want_dir)
size_t outsz)
{
struct stat st;
if (want_dir) {
snprintf(out, outsz, "%s/%s", dir, path_form);
if (stat(out, &st) == 0 && S_ISDIR(st.st_mode)) {
*is_dir = 1;
return 1;
}
return 0;
}
snprintf(out, outsz, "%s/%s.ww", dir, path_form);
if (access(out, 0) == 0) {
*is_dir = 0;
snprintf(out, outsz, "%s/%s", dir, path_form);
if (stat(out, &st) == 0 && S_ISDIR(st.st_mode))
return 1;
return 0;
}
/* Walk every ordered root for <root>/<path>/ only. A decoy
* <earlier-root>/<path>.ww is neither a match nor a shadow: source imports
* always create canonical directory-package nodes. */
static int
locate_import(const char *dirs, const char *path_form, char *out,
size_t outsz)
{
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0 && n < outsz) {
char dir[1024];
if (n >= sizeof dir) n = sizeof dir - 1;
memcpy(dir, p, n);
dir[n] = '\0';
if (locate_import_in(dir, path_form, out, outsz))
return 1;
}
if (!e) break;
p = e + 1;
}
return 0;
}
/* #98: "a module IS the directory" — a directory-package on ANY entry
* wins over a same-named sibling FILE on an EARLIER entry. The driver
* builds the searchpath srcd-first; a co-located `lib/<mod>/<x>_test.ww`
* entry makes srcd = lib/<mod>, so a self-named `import <mod>` would
* else file-hit the sibling lib/<mod>/<mod>.ww and fold it
* inline under the wrong module-reset → "package <mod> does not match
* import path <importer>". Two passes — directories first, files only
* if no directory matches anywhere — let lib/<mod>/ resolve as the dir
* while a genuine leaf package with no directory (e.g. lib/encoding/hex
* imported bare as `hex`, reachable only via its file in srcd) still
* resolves in the file pass. Latent: a dir-package now beats an
* earlier-entry same-named sibling FILE — loud-failing, none in the
* corpus; tracked as #101. */
/* CLI target compatibility: directory packages still win globally, then a
* bare target may resolve to <root>/<path>.ww. This function is never used
* while loading a source import. */
static int
locate_import(const char *dirs, const char *path_form, char *out,
locate_module(const char *dirs, const char *path_form, char *out,
size_t outsz, int *is_dir)
{
for (int want_dir = 1; want_dir >= 0; want_dir--) {
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0 && n < outsz) {
char dir[1024];
if (n >= sizeof dir) n = sizeof dir - 1;
memcpy(dir, p, n);
dir[n] = '\0';
if (locate_import_in(dir, path_form, out,
outsz, is_dir, want_dir)) return 1;
if (locate_import(dirs, path_form, out, outsz)) {
*is_dir = 1;
return 1;
}
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0 && n < outsz) {
char dir[1024];
if (n >= sizeof dir) n = sizeof dir - 1;
memcpy(dir, p, n);
dir[n] = '\0';
snprintf(out, outsz, "%s/%s.ww", dir, path_form);
if (access(out, 0) == 0) {
*is_dir = 0;
return 1;
}
if (!e) break;
p = e + 1;
}
if (!e) break;
p = e + 1;
}
return 0;
}
@@ -521,7 +529,7 @@ struct seppkg {
int failed; /* discovery/compile failure reaches this action */
int test_support; /* compiler-generated -T support package */
int loaded; /* directory membership/name loaded exactly once */
int emit_context; /* resolution context used to compose file imports */
int emit_context; /* first verified resolution context */
unsigned char context_state[SEP_MAXCONTEXT]; /* 0 new, 1 active, 2 checked */
struct ImportSet bindings; /* first context's canonical import bindings */
int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */
@@ -882,8 +890,8 @@ sep_external_production_import(const struct seppkg *pkg, const char *path)
}
/* Canonical bindings make a shared package independent of which selected
* root reaches it first. Directory, folded-file, and inline bindings are all
* part of the package action's source meaning. */
* root reaches it first. Directory bindings and the raw-file inline
* compatibility binding are part of the package action's source meaning. */
static int
sep_binding_add(struct ImportSet *bindings, char kind, const char *name,
const char *target)
@@ -1019,28 +1027,27 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
break;
}
char ipath[1024];
int is_dir = 0;
int external_production = sep_external_production_import(
&g->pkg[pi], name);
int located = 0;
if (external_production) {
snprintf(ipath, sizeof ipath, "%s", g->pkg[pi].entry);
is_dir = 1;
located = 1;
} else {
located = locate_import(searchpath, path_form, ipath,
sizeof ipath, &is_dir);
sizeof ipath);
}
if (!located) {
const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name;
int inline_package = 0;
for (Node *package = imports->body; package;
package = package->next)
if (strcmp(package->module, leaf) == 0) {
inline_package = 1;
break;
}
if (!g->pkg[pi].is_dir)
for (Node *package = imports->body; package;
package = package->next)
if (strcmp(package->module, leaf) == 0) {
inline_package = 1;
break;
}
if (inline_package) {
if (sep_binding_add(bindings, 'I', name, NULL) < 0)
rc = -1;
@@ -1050,7 +1057,7 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
rc = -1;
break;
}
if (is_dir) {
{
char *canon = realpath(ipath, NULL);
if (canon == NULL) {
errorf(u->pos, "cannot canonicalize package '%s'", name);
@@ -1105,20 +1112,6 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; }
g->pkg[pi].deps[g->pkg[pi].ndeps++] = di;
}
} else {
char *canon = realpath(ipath, NULL);
if (canon == NULL
|| sep_binding_add(bindings, 'F', name, canon) < 0) {
free(canon);
rc = -1;
break;
}
free(canon);
if (sep_scan_file(g, pi, ipath, searchpath, filevisit,
bindings, 0) < 0) {
rc = -1;
break;
}
}
}
free(uses);
@@ -1322,65 +1315,18 @@ sep_validate_module_closure(struct sepgraph *g, const int *order, int n,
}
/* Emit one of pi's own source files into the sep-unit under the
* //ww:module-reset primary boundary (so -c emits its decls, imported
* ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the
* body); FILE imports fold in (intra-package split). */
* //ww:module-reset primary boundary. Imports were already resolved to
* directory-package edges and their direct `.wwi` files were prepended;
* no source outside pi's sorted owned-source set may enter this unit. */
static int
sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
const char *searchpath, const char *modpath, const struct seppkg *pkg)
sep_emit_body(FILE *out, const char *path, const char *modpath)
{
if (import_seen(visited, path)) return 0;
import_add(visited, path);
char *buf;
u64 len;
if (sep_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path);
return -1;
}
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, path, buf, len);
parserinit(&p, a, &l);
Node *imports = parseimports(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
int nuse = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) nuse++;
Node **uses = nuse ? malloc((size_t)nuse * sizeof *uses) : NULL;
if (nuse && uses == NULL) {
freearena(a);
free(buf);
return -1;
}
int ui = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) uses[ui++] = u;
if (nuse > 1) qsort(uses, (size_t)nuse, sizeof *uses, use_node_cmp);
for (int i = 0; i < nuse; i++) {
Node *u = uses[i];
const char *name = u->usepath ? u->usepath : u->str;
if (sep_external_production_import(pkg, name))
continue;
char path_form[1024];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
if (!is_dir && sep_emit_body(out, ipath, visited, searchpath,
modpath, pkg) < 0) {
free(uses);
freearena(a);
free(buf);
return -1;
}
}
/* #57: tag the primary body by its full dotted import path so the
* definer mangles == the importer reference; a root build (path "")
* stays a bare reset (keeps bare main). */
@@ -1394,8 +1340,6 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
if (fwrite(buf, 1, (size_t)len, out) != (size_t)len
|| fputc('\n', out) == EOF)
bad = 1;
free(uses);
freearena(a);
free(buf);
if (bad) {
fprintf(stderr, "ww: cannot write package unit\n");
@@ -1415,8 +1359,6 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
if (g->pkg[pi].emit_context < 0
|| g->pkg[pi].emit_context >= g->ncontext)
return -1;
const char *searchpath =
g->context[g->pkg[pi].emit_context].searchpath;
FILE *u = fopen(unitf, "wb");
if (u == NULL) {
fprintf(stderr, "ww: cannot open %s\n", unitf);
@@ -1444,18 +1386,14 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
return -1;
}
}
struct ImportSet bodyvisit = {0};
int bodyrc = 0;
if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit,
searchpath, g->pkg[pi].path, &g->pkg[pi]);
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i],
g->pkg[pi].path);
} else {
bodyrc = sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath,
g->pkg[pi].path, &g->pkg[pi]);
bodyrc = sep_emit_body(u, g->pkg[pi].entry, g->pkg[pi].path);
}
for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]);
free(bodyvisit.paths);
if (fclose(u) != 0) {
fprintf(stderr, "ww: cannot close package unit %s\n", unitf);
return -1;
@@ -1821,7 +1759,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
if (is_test) {
char tpath[1024];
int tdir = 0;
if (locate_import(toolsrcdir, "test", tpath, sizeof tpath, &tdir)) {
if (locate_import(toolsrcdir, "test", tpath, sizeof tpath)) {
tdir = 1;
g->support_context = sep_context_for(g, toolsrcdir, NULL,
toolsrcdir);
if (g->support_context < 0) return 1;
@@ -1840,7 +1779,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
char userpath[1024];
int userdir = 0;
if (locate_import(g->context[products[i].context].searchpath,
"test", userpath, sizeof userpath, &userdir)) {
"test", userpath, sizeof userpath)) {
userdir = 1;
(void)userdir;
char *uc = realpath(userpath, NULL);
if (tc != NULL && uc != NULL
@@ -2342,7 +2282,7 @@ resolve_module(const char *name, const char *incs, char *out, size_t outsz,
search_path(incs, sp, sizeof sp);
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
return locate_import(sp, path_form, out, outsz, is_dir);
return locate_module(sp, path_form, out, outsz, is_dir);
}
/* Returns the index past the last arg consumed for positionals (so callers