cmd+selfhost+lib+test: directory-as-module enumeration in driver (#22)

Replace the cmd/ww + selfhost driver's file-walk import resolver
with true directory enumeration. `import encoding.utf8;` now finds
the lib/encoding/utf8/ directory and concatenates every *.ww file
in it (excluding *test.ww and the driver's *.combined.ww artifacts)
in byte-wise sorted order, instead of just finding the single
lib/encoding/utf8/utf8.ww file. Mirrors Hare's
hare/module/srcs.ha:183 _findsrcs minus tag handling.

Lookup order in both stages: (1) <dir>/<dot-as-slash>/ as directory
→ enumerate. (2) <dir>/<dot-as-slash>.ww as file. The legacy
<dir>/<name>/<name>.ww shape from #18's retained divergence is
dropped per rule-9 Hare-fidelity — Hare has no foo/foo.ha fallback;
a module IS the directory.

Symmetric across cstage (cmd/ww/main.c via opendir+qsort+stat) and
wwstage (selfhost/cmd/ww/main.ww via existing lib/os.getdents64 +
os.stat — no new lib/os surface needed; the rundirtests() walker
in main.ww from #18 was the model). Bootstrap ww2.s==ww3.s==ww4.s
byte-identical post-change.

Bundling justification (rule 11): strict-same-package validation is
bundled because the failure mode is dir-enum's own (a non-dir-enum
compilation unit cannot trigger mismatch across enumerated files).
The natural enforcement site is the driver — the parser can't
distinguish dir-enum concat from file-walk concat. Both stages
peek each file's first `package <name>;` line in expand_dir /
expanddir and exit(1) on mismatch with a precise error pointing
at the offending file. Hare's hare/module/srcs.ha:131 has the
same constraint via its README gate. Other half of #23 (strict
missing-package error tightening — 63 inline-source test wrappers
blocker) stays deferred per its filing.

Parser side (cmd/wcc/parse.c parseuse + lib/ww/parse/decl.ww
parseuse): n->str now carries only the LEAF identifier from a
dotted import. With the driver translating the full dotted path
to a directory walk, the checker only needs the package bareword
(last component) for the N_USE → decl disambiguation walk in
check.c's src_imports / decl_mod. Mirrors Hare's
`use encoding::utf8;` → `utf8::name` semantics
(ref/hare/hare/ast/import.ha:7).

Migration: lib/ww/sym.ww drops `import typ; import ast;`;
lib/ww/parse/parse.ww drops `import expr; import stmt; import
decl;`; lib/ww/lex/lex.ww drops `import tok;` — all sibling
imports auto-resolve via the new dir-enum when callers import the
package directory. lib/strings/, lib/encoding/utf8/utf8test.ww
migrate `import utf8;` → `import encoding.utf8;`. Makefile drops
-I lib/encoding/utf8 stopgap from wwdump_ww + w6c_ww. Seven test
wrappers (700_e2e, 966_strings_run, 970_fmt_run, 971_log_run,
972_fnmatch_run, 982_getopt_run, 990_selfhost) and 995_self_rebuild
drop the -I lib/encoding/utf8 runtime stopgap.

Tests: new 737_direnum C wrapper + test/wcc/data/direnum/ fixtures
pin (a) cross-pkg multi-file dir-enum build at runtime (both stages
must succeed) and (b) strict-same-package mismatch error (both
stages must surface "differs from" + exit non-zero). 738_module_decl
gains row 6 pinning the n_use->str leaf-only storage post-parser
change.

Retained workaround at selfhost/cmd/ww/main.ww expanddir loop:
`names[i][k]` nested-deref-then-index split into
`let nm: *u8 = names[i]; nm[k]` because wwstage cgen miscompiles
the chained form (treats inner u8 element as 8B sizeof *u8 instead
of 1B sizeof u8: extra MOVQ $8 + IMULQ on the inner index, MOVQ
instead of MOVZBQ load). Inline rule-8 WHY comment cites task #24
(wwstage cgen chained-index inner element size on **T). Two-step
form routes through the bare-pointer index path which both stages
handle byte-identically.

Class A wwstage cgen UNDER (chained-index inner element size on
**T) surfaced first time the codebase exercises the **T[i][k]
shape via enumeratedir() — corpus-coverage-blind landmine pattern,
same family as the trio (#27/#28/#31) from STATUS-5.

112/112 ok. ww2 == ww3 == ww4 byte-id holds.
This commit is contained in:
2026-05-18 19:22:27 +09:00
parent 79d9528a00
commit 9e0816e199
31 changed files with 2521 additions and 1564 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1089,18 +1089,31 @@ fn visitadd(c: *expctx, path: str) void = {
c.visit = n;
};
// Try <dir>/<name>.ww then <dir>/<name>/<name>.ww. Returns NUL-terminated
// arena-resident path if found, else nil.
//
// Retained divergence from brief: directory-as-module enumeration is
// NOT implemented here. The user's "module IS directory" mental model
// is partially honored via the `package` keyword + file-walk + sibling
// `import` chain. True dir enumeration (lib/foo/*.ww concatenated
// atomically, no sibling-import boilerplate) is deferred to task #22
// and needs a lib/os opendir/readdir wrapper around getdents64 first.
// Rule 7 + rule 8 documentation.
fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = {
// candidate 1: <dir>/<name>.ww
// Translate dots in an `import` name to slashes for path lookup.
// `encoding.utf8` → `encoding/utf8`. Mirrors Hare's hare(1)
// use-path → fs-path mapping
// (ref/hare/hare/module/srcs.ha:78 builds the same shape via
// path::push per ident part).
fn importpathform(a: *arena, name: *u8, namelen: u64) *u8 = {
let buf: *u8 = amalloc(a, namelen + 1u64): *u8;
let i: u64 = 0u64;
for (i < namelen) {
if (name[i] == 46u8) { buf[i] = 47u8; } // '.' -> '/'
else { buf[i] = name[i]; };
i += 1u64;
};
buf[namelen] = 0u8;
return buf;
};
// Try <dir>/<path>/ as a directory, then <dir>/<path>.ww as a file.
// Sets *isdir on hit. Symmetric with cstage locate_import_in 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
// `foo/foo.ha` fallback; a module IS the directory).
fn locatein(a: *arena, dir: *u8, dirlen: u64,
pathform: *u8, pflen: u64, isdir: *i32) *u8 = {
let buf: *u8 = amalloc(a, PATH_MAX): *u8;
let off: u64 = 0u64;
let i: u64 = 0u64;
@@ -1108,15 +1121,24 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = {
off += dirlen;
buf[off] = 47u8; off += 1u64; // '/'
i = 0u64;
for (i < namelen) { buf[off + i] = name[i]; i += 1u64; };
off += namelen;
buf[off] = 46u8; off += 1u64; // '.'
buf[off] = 119u8; off += 1u64; // 'w'
buf[off] = 119u8; off += 1u64; // 'w'
for (i < pflen) { buf[off + i] = pathform[i]; i += 1u64; };
off += pflen;
buf[off] = 0u8;
if (os.access(pathstr(buf), 0i32) == 0) { return buf; };
let fi: os.filestat;
let r: (void | os.oserror) = os.stat(&fi, pathstr(buf));
let isdirhit: bool = false;
match (r) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) { isdirhit = true; };
};
case let e: os.oserror => void;
};
if (isdirhit) {
*isdir = 1;
return buf;
};
// candidate 2: <dir>/<name>/<name>.ww
let buf2: *u8 = amalloc(a, PATH_MAX): *u8;
off = 0u64;
i = 0u64;
@@ -1124,22 +1146,25 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = {
off += dirlen;
buf2[off] = 47u8; off += 1u64;
i = 0u64;
for (i < namelen) { buf2[off + i] = name[i]; i += 1u64; };
off += namelen;
buf2[off] = 47u8; off += 1u64;
i = 0u64;
for (i < namelen) { buf2[off + i] = name[i]; i += 1u64; };
off += namelen;
buf2[off] = 46u8; off += 1u64;
buf2[off] = 119u8; off += 1u64;
buf2[off] = 119u8; off += 1u64;
for (i < pflen) { buf2[off + i] = pathform[i]; i += 1u64; };
off += pflen;
buf2[off] = 46u8; off += 1u64; // '.'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 0u8;
if (os.access(pathstr(buf2), 0i32) == 0) { return buf2; };
if (os.access(pathstr(buf2), 0i32) == 0) {
*isdir = 0;
return buf2;
};
return nil;
};
// Walk a colon-separated dirlist, return first hit or nil.
fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = {
// Walk a colon-separated dirlist, return first hit or nil. Sets
// *isdir on hit.
fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64,
isdir: *i32) *u8 = {
let pathform: *u8 = importpathform(a, name, namelen);
let pflen: u64 = cstrlen(pathform);
let total: u64 = cstrlen(dirs);
let p: u64 = 0u64;
for (p < total) {
@@ -1150,7 +1175,8 @@ fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = {
};
let seglen: u64 = q - p;
if (seglen > 0u64) {
let hit: *u8 = locatein(a, dirs + p, seglen, name, namelen);
let hit: *u8 = locatein(a, dirs + p, seglen,
pathform, pflen, isdir);
if (hit != nil) { return hit; };
};
p = q + 1u64;
@@ -1158,6 +1184,123 @@ fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = {
return nil;
};
// Filter for dir enumeration: keep `*.ww` minus `*test.ww` and the
// `*.combined.ww` driver-generated concat artifacts (the previous
// build leaves them in the source tree; they parse-error when
// re-included). Returns true to keep.
fn dirfilekeep(name: *u8, nlen: u64) bool = {
if (nlen <= 3u64) { return false; };
if (name[nlen - 3u64] != 46u8) { return false; }; // '.'
if (name[nlen - 2u64] != 119u8) { return false; }; // 'w'
if (name[nlen - 1u64] != 119u8) { return false; }; // 'w'
if (nlen >= 7u64) {
if (name[nlen - 7u64] == 116u8) { // 't'
if (name[nlen - 6u64] == 101u8) { // 'e'
if (name[nlen - 5u64] == 115u8) { // 's'
if (name[nlen - 4u64] == 116u8) { // 't'
return false;
};
};
};
};
};
if (nlen >= 12u64) {
// ".combined.ww"
if (name[nlen - 12u64] == 46u8) { // '.'
if (name[nlen - 11u64] == 99u8) { // 'c'
if (name[nlen - 10u64] == 111u8) { // 'o'
if (name[nlen - 9u64] == 109u8) { // 'm'
if (name[nlen - 8u64] == 98u8) { // 'b'
return false;
};
};
};
};
};
};
return true;
};
// Byte-wise memcmp returning < 0, 0, > 0. Rule-10 byte-id requires
// cstage and wwstage sort the same way; memcmp is the
// locale-independent total order (mirrors ref/hare/sort/cmp/cmp.ha
// strs).
fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = {
let n: u64 = alen;
if (blen < n) { n = blen; };
let i: u64 = 0u64;
for (i < n) {
let av: i32 = (a[i]): i32;
let bv: i32 = (b[i]): i32;
if (av < bv) { return -1; };
if (av > bv) { return 1; };
i += 1u64;
};
if (alen < blen) { return -1; };
if (alen > blen) { return 1; };
return 0;
};
// enumeratedir — list *.ww entries of `dirpath` (less *test.ww and
// *.combined.ww), byte-sort. Returns (names[], nnames) with each
// name a NUL-terminated arena copy.
fn enumeratedir(a: *arena, dirpath: *u8) (**u8, i32) = {
let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil: **u8, 0; };
let maxnames: i32 = 256;
let names: **u8 = amalloc(a, (maxnames: u64) * 8u64): **u8;
let nlens: *u64 = amalloc(a, (maxnames: u64) * 8u64): *u64;
let n: i32 = 0;
let buf: *u8 = os.alloc(8192u64): *u8;
let r: i64 = os.getdents64(fd, buf, 8192u64);
for (r > 0i64) {
let off: u64 = 0u64;
let ru: u64 = r: u64;
for (off < ru) {
let blo: u64 = (buf[off + 16u64]): u64;
let bhi: u64 = (buf[off + 17u64]): u64;
let reclen: u64 = blo + (bhi * 256u64);
let nm: *u8 = buf + off + 19u64;
let nl: u64 = cstrlen(nm);
if (dirfilekeep(nm, nl)) {
if (n < maxnames) {
let cp: *u8 = amalloc(a, nl + 1u64): *u8;
let i: u64 = 0u64;
for (i < nl) { cp[i] = nm[i]; i += 1u64; };
cp[nl] = 0u8;
names[n] = cp;
nlens[n] = nl;
n += 1;
};
};
off += reclen;
};
r = os.getdents64(fd, buf, 8192u64);
};
os.close(fd);
// Insertion sort, byte-wise. n is small (≤16 in practice).
let i: i32 = 1;
for (i < n) {
let j: i32 = i;
for (j > 0) {
if (bytecmp(names[j - 1], nlens[j - 1],
names[j], nlens[j]) <= 0) { j = 0; }
else {
let t: *u8 = names[j];
names[j] = names[j - 1];
names[j - 1] = t;
let tl: u64 = nlens[j];
nlens[j] = nlens[j - 1];
nlens[j - 1] = tl;
j -= 1;
};
};
i += 1;
};
return names, n;
};
// ---- file slurp -------------------------------------------------------
fn slurp(pathcs: *u8) (*u8, u64) = {
@@ -1227,9 +1370,9 @@ fn scanuse(src: *u8, len: u64) (*u8, u64) = {
};
// expand — emit one file's bytes verbatim into the combined stream,
// after recursive-expanding its top-of-file `use X;` imports. Each
// source declares its own `module <name>;` (parser stamps decls);
// the driver no longer injects a `// MODULE:` marker.
// after recursive-expanding its top-of-file `import X;` imports.
// Each source declares its own `package <name>;` (parser stamps
// decls).
fn expand(c: *expctx, pathcs: *u8) void = {
let plen: u64 = cstrlen(pathcs);
let pathstr: str = astrndup(c.a, pathcs, plen);
@@ -1244,7 +1387,7 @@ fn expand(c: *expctx, pathcs: *u8) void = {
return;
};
// Pass 1: scan top-of-file `use X;` lines, recursively expand.
// Pass 1: scan top-of-file `import X;` lines, recursively expand.
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
@@ -1256,9 +1399,12 @@ fn expand(c: *expctx, pathcs: *u8) void = {
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let ipath: *u8 = locateimport(c.a, c.dirs, idp, idn);
let isdir: i32 = 0;
let ipath: *u8 = locateimport(c.a, c.dirs, idp, idn,
&isdir);
if (ipath != nil) {
expand(c, ipath);
if (isdir != 0) { expanddir(c, ipath); }
else { expand(c, ipath); };
};
};
i = j + 1u64;
@@ -1268,6 +1414,141 @@ fn expand(c: *expctx, pathcs: *u8) void = {
os.writeall(c.out, "\n".ptr, 1u64);
};
// Scan `pathcs` for its first non-comment-non-blank line; if it
// starts with `package <name>;` return the package name as a
// borrowed-arena str, else nil. Same shape as cstage peek_package.
fn peekpackage(a: *arena, pathcs: *u8) *u8 = {
let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil; };
let buf: *u8 = os.alloc(2048u64): *u8;
let n: i64 = os.read(fd, buf, 2048u64);
os.close(fd);
if (n <= 0i64) { return nil; };
let nu: u64 = n: u64;
let p: u64 = 0u64;
for (p < nu) {
let q: u64 = p;
for (q < nu) {
if (buf[q] == 10u8) { break; }; // '\n'
q += 1u64;
};
let s: u64 = p;
for (s < q) {
if (buf[s] != 32u8) {
if (buf[s] != 9u8) { break; };
};
s += 1u64;
};
if (s < q) {
if (s + 1u64 < q) {
if (buf[s] == 47u8) {
if (buf[s + 1u64] == 47u8) {
p = q + 1u64;
continue;
};
};
};
if (s + 8u64 <= q) {
if (buf[s] == 112u8) { // 'p'
if (buf[s + 1u64] == 97u8) { // 'a'
if (buf[s + 2u64] == 99u8) { // 'c'
if (buf[s + 3u64] == 107u8) { // 'k'
if (buf[s + 4u64] == 97u8) { // 'a'
if (buf[s + 5u64] == 103u8) { // 'g'
if (buf[s + 6u64] == 101u8) { // 'e'
let sep: u8 = buf[s + 7u64];
if (sep == 32u8) { }
else { if (sep != 9u8) { return nil; }; };
let t: u64 = s + 8u64;
for (t < q) {
if (buf[t] != 32u8) {
if (buf[t] != 9u8) { break; };
};
t += 1u64;
};
let start: u64 = t;
for (t < q) {
let ch: u8 = buf[t];
let isalpha: bool = false;
if (ch >= 97u8) { if (ch <= 122u8) { isalpha = true; }; };
if (ch >= 65u8) { if (ch <= 90u8) { isalpha = true; }; };
if (ch >= 48u8) { if (ch <= 57u8) { isalpha = true; }; };
if (ch == 95u8) { isalpha = true; };
if (!isalpha) { break; };
t += 1u64;
};
let plen: u64 = t - start;
if (plen == 0u64) { return nil; };
let r: *u8 = amalloc(a, plen + 1u64): *u8;
let k: u64 = 0u64;
for (k < plen) { r[k] = buf[start + k]; k += 1u64; };
r[plen] = 0u8;
return r;
}; }; }; }; }; }; };
};
return nil;
};
p = q + 1u64;
};
return nil;
};
// Strict-same-package error helper. Bundled here per task #22
// brief — failure mode is dir-enum's own.
fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = {
os.write(2, "ww: ".ptr, 4u64);
os.write(2, file, cstrlen(file));
os.write(2, ": package ".ptr, 10u64);
os.write(2, pkg, cstrlen(pkg));
os.write(2, " differs from ".ptr, 14u64);
os.write(2, dirpkg, cstrlen(dirpkg));
os.write(2, " in same module dir ".ptr, 20u64);
os.write(2, dirpath, cstrlen(dirpath));
os.write(2, "\n".ptr, 1u64);
os.exit(1);
};
// expanddir — enumerate <dirpath>/*.ww (skip *test.ww and
// *.combined.ww), byte-sort, recurse into each. Mirrors
// ref/hare/hare/module/srcs.ha:183 `_findsrcs` minus tag handling.
// The visited set keys on concrete file paths so multi-file modules
// are pulled once. Strict-same-package: all enumerated files must
// declare the same `package <name>;` (task #23 subset; failure
// mode native to dir-enum).
fn expanddir(c: *expctx, dirpath: *u8) void = {
let names: **u8;
let n: i32;
names, n = enumeratedir(c.a, dirpath);
let dlen: u64 = cstrlen(dirpath);
let dirpkg: *u8 = nil;
let i: i32 = 0;
for (i < n) {
// Two-step deref+index to avoid wwstage chained `names[i][k]`
// cgen UNDER (task #24 — wwstage cgen chained-index inner
// element size on **T). Wwstage treats inner element as 8B
// (sizeof *u8) instead of 1B (sizeof u8); cstage handles
// via typed-AST natively. Retire once the wwstage fix lands.
let nm: *u8 = names[i];
let nlen: u64 = cstrlen(nm);
let fp: *u8 = amalloc(c.a, dlen + 1u64 + nlen + 1u64): *u8;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = dirpath[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = nm[k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
let pkg: *u8 = peekpackage(c.a, fp);
if (pkg != nil) {
if (dirpkg == nil) { dirpkg = pkg; }
else { if (!cstreq(dirpkg, pkg)) {
strictpkgmismatch(fp, pkg, dirpkg, dirpath);
}; };
};
expand(c, fp);
i += 1;
};
};
// ---- Build pipeline ---------------------------------------------------
// Strip the trailing ".ww" off `src` (a NUL-terminated path) into
@@ -1308,19 +1589,21 @@ type lflags = struct {
nlibs: i32,
};
// buildone — compile `src` into the executable named `out`.
// selfdir: NUL-terminated dir containing this driver and the
// wwstage tools (w6c_ww/w6a_ww/w6l_ww)
// src: NUL-terminated path to the .ww file
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
// buildone — compile `src` (file or directory) into the executable
// named `out`.
// selfdir: NUL-terminated dir containing this driver and the
// wwstage tools (w6c_ww/w6a_ww/w6l_ww)
// src: NUL-terminated entry path (file or directory).
// entryisdir: non-zero when src is a module directory.
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
//
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
// still drives the C-built w6c/w6a/w6l. Test 993 pins the two
// pipelines to byte-identical output on a corpus.
fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, incs: *u8, lf: *lflags) i32 = {
let a: *arena = newarena();
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
@@ -1335,13 +1618,23 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
cstrseal(dotdotlib, off);
};
// Compute the source-file's directory: bytes of `src` up to the
// last '/'. If `src` has no '/', use ".". Hare's CWD-first
// Compute the source directory. For a file entry: bytes of `src`
// up to the last '/' (or "." when src has no '/'). For a dir
// entry: the dir itself (less trailing slashes). Hare's CWD-first
// convention assumes you're running from the module dir; our
// wrappers don't cd, so dirname(src) stands in as the closest
// analog. Source-dir wins ties over the system path (cc -I.).
let srcd: *u8 = os.alloc(PATH_MAX): *u8;
{
if (entryisdir != 0) {
let slen: u64 = cstrlen(src);
let k: u64 = 0u64;
for (k < slen) { srcd[k] = src[k]; k += 1u64; };
for (slen > 1u64) {
if (srcd[slen - 1u64] != 47u8) { break; };
slen -= 1u64;
};
srcd[slen] = 0u8;
} else {
let slen: u64 = cstrlen(src);
let last: u64 = slen;
let found: bool = false;
@@ -1377,9 +1670,20 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
cstrseal(searchpath, off);
};
// stem, .s, .o, .combined.ww, libwwrt.a
// Stem for .s/.o/.combined.ww side files. Dir entry: <dir>/<base>;
// file entry: src stripped of .ww.
let stem: *u8 = os.alloc(PATH_MAX): *u8;
makestem(stem, src);
if (entryisdir != 0) {
let dlen: u64 = cstrlen(srcd);
let bo: u64 = basenameoff(srcd, dlen);
let off: u64 = cstrinto(stem, 0u64, srcd);
stem[off] = 47u8; off += 1u64; // '/'
let i: u64 = bo;
for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; };
cstrseal(stem, off);
} else {
makestem(stem, src);
};
let asmf: *u8 = appendlit(stem, ".s");
let objf: *u8 = appendlit(stem, ".o");
let combined: *u8 = appendlit(stem, ".combined.ww");
@@ -1392,7 +1696,8 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
cstrseal(libwwrt, off);
};
// Step 1: expand `use`s into the combined file.
// Step 1: expand imports into the combined file. Dir entry →
// enumerate the module dir; file entry → start at the file.
let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (cf < 0) {
os.write(2, "ww: cannot open combined\n".ptr, 25u64);
@@ -1404,7 +1709,8 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
c.out = cf;
c.dirs = searchpath;
c.visit = nil;
expand(&c, src);
if (entryisdir != 0) { expanddir(&c, srcd); }
else { expand(&c, src); };
};
os.close(cf);
@@ -1566,43 +1872,41 @@ fn buildsearchpath(a: *arena, selfdir: *u8, incs: *u8) *u8 = {
return buf;
};
fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = {
// resolvemodule — map a name like "foo", "lib/foo", "foo.ww", or
// "." to a concrete entry path. Sets *isdir when the entry is a
// module directory (caller will dir-enumerate).
fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
let nlen: u64 = cstrlen(name);
// (1) literal .ww file that exists
// (1) Literal file that exists → use as-is.
if (cstrendswithlit(name, ".ww")) {
if (os.access(pathstr(name), 0i32) == 0) {
*isdir = 0;
return arenadupcstr(a, name, nlen);
};
};
// (2) "." → cwd's <basename>.ww
if (nlen == 1u64) {
if (name[0u64] == 46u8) { // '.'
let cwd: *u8 = amalloc(a, PATH_MAX): *u8;
let r: i64 = os.getcwd(cwd, PATH_MAX);
if (r <= 0i64) { return nil; };
let cwdlen: u64 = (r: u64) - 1u64; // strip trailing NUL
let bo: u64 = basenameoff(cwd, cwdlen);
let blen: u64 = cwdlen - bo;
let dot: *u8 = amalloc(a, 2u64): *u8;
dot[0] = 46u8; dot[1] = 0u8;
let probe: *u8 = builddirmodulepath(a, dot, 1u64,
cwd + bo, blen);
if (os.access(pathstr(probe), 0i32) == 0) { return probe; };
return nil;
};
// (2) Existing path → use as-is, dir vs file via stat.
let fi: os.filestat;
let sr: (void | os.oserror) = os.stat(&fi, pathstr(name));
let found: bool = false;
let foundisdir: i32 = 0;
match (sr) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) { foundisdir = 1; };
found = true;
};
case let e: os.oserror => void;
};
if (found) {
*isdir = foundisdir;
return arenadupcstr(a, name, nlen);
};
// (3) <name>/<basename(name)>.ww — directory-as-module
let bo: u64 = basenameoff(name, nlen);
let probe: *u8 = builddirmodulepath(a, name, nlen,
name + bo, nlen - bo);
if (os.access(pathstr(probe), 0i32) == 0) { return probe; };
// (4) search path lookup
// (3) Search-path lookup with dot-to-slash path translation.
let search: *u8 = buildsearchpath(a, selfdir, incs);
return locateimport(a, search, name, nlen);
return locateimport(a, search, name, nlen, isdir);
};
// ---- Subcommand handlers ----------------------------------------------
@@ -1736,18 +2040,34 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
dot[0] = 46u8; dot[1] = 0u8;
src = dot;
};
let resolved: *u8 = resolvemodule(a, selfdir, src, incs);
let isdir: i32 = 0;
let resolved: *u8 = resolvemodule(a, selfdir, src, incs, &isdir);
if (resolved == nil) {
os.write(2, "ww build: cannot find module\n".ptr, 29u64);
return 1;
};
let out: *u8 = defaultoutpath(resolved);
let out: *u8 = nil;
if (isdir != 0) {
let rlen: u64 = cstrlen(resolved);
for (rlen > 1u64) {
if (resolved[rlen - 1u64] != 47u8) { break; };
rlen -= 1u64;
};
let bo: u64 = basenameoff(resolved, rlen);
out = os.alloc(PATH_MAX): *u8;
let i: u64 = bo;
let off: u64 = 0u64;
for (i < rlen) { out[off] = resolved[i]; off += 1u64; i += 1u64; };
cstrseal(out, off);
} else {
out = defaultoutpath(resolved);
};
let lf: lflags;
lf.libdirs = libdirs;
lf.nlibdirs = nlibdirs;
lf.libs = libs;
lf.nlibs = nlibs;
return buildone(selfdir, resolved, out, incs, &lf);
return buildone(selfdir, resolved, isdir, out, incs, &lf);
};
// Format the scratch path /tmp/ww_run_<pid> into buf. Returns NUL-
@@ -1875,7 +2195,8 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
dot[0] = 46u8; dot[1] = 0u8;
src = dot;
};
let resolved: *u8 = resolvemodule(a, selfdir, src, incs);
let isdir: i32 = 0;
let resolved: *u8 = resolvemodule(a, selfdir, src, incs, &isdir);
if (resolved == nil) {
os.write(2, "ww run: cannot find module\n".ptr, 27u64);
return 1;
@@ -1888,7 +2209,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs;
lf.libs = libs;
lf.nlibs = nlibs;
if (buildone(selfdir, resolved, tmp, incs, &lf) != 0) {
if (buildone(selfdir, resolved, isdir, tmp, incs, &lf) != 0) {
os.remove(pathstr(tmp));
return 1;
};
@@ -1920,7 +2241,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
fn runsingletest(selfdir: *u8, src: *u8) i32 = {
let tmp: *u8 = os.alloc(PATH_MAX): *u8;
makeruntmp(tmp);
if (buildone(selfdir, src, tmp, "\0".ptr, nil) != 0) {
if (buildone(selfdir, src, 0, tmp, "\0".ptr, nil) != 0) {
os.remove(pathstr(tmp));
return 1;
};
@@ -1970,7 +2291,7 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = {
cstrseal(tincs, ic);
let tmp: *u8 = os.alloc(PATH_MAX): *u8;
makeruntmp(tmp);
let bres: i32 = buildone(selfdir, path, tmp, tincs, nil);
let bres: i32 = buildone(selfdir, path, 0, tmp, tincs, nil);
if (bres != 0) {
fail += 1;
os.write(2, "FAIL ".ptr, 5u64);

View File

@@ -229,18 +229,31 @@ fn visitadd(c: *expctx, path: str) void = {
c.visit = n;
};
// Try <dir>/<name>.ww then <dir>/<name>/<name>.ww. Returns NUL-terminated
// arena-resident path if found, else nil.
//
// Retained divergence from brief: directory-as-module enumeration is
// NOT implemented here. The user's "module IS directory" mental model
// is partially honored via the `package` keyword + file-walk + sibling
// `import` chain. True dir enumeration (lib/foo/*.ww concatenated
// atomically, no sibling-import boilerplate) is deferred to task #22
// and needs a lib/os opendir/readdir wrapper around getdents64 first.
// Rule 7 + rule 8 documentation.
fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = {
// candidate 1: <dir>/<name>.ww
// Translate dots in an `import` name to slashes for path lookup.
// `encoding.utf8` → `encoding/utf8`. Mirrors Hare's hare(1)
// use-path → fs-path mapping
// (ref/hare/hare/module/srcs.ha:78 builds the same shape via
// path::push per ident part).
fn importpathform(a: *arena, name: *u8, namelen: u64) *u8 = {
let buf: *u8 = amalloc(a, namelen + 1u64): *u8;
let i: u64 = 0u64;
for (i < namelen) {
if (name[i] == 46u8) { buf[i] = 47u8; } // '.' -> '/'
else { buf[i] = name[i]; };
i += 1u64;
};
buf[namelen] = 0u8;
return buf;
};
// Try <dir>/<path>/ as a directory, then <dir>/<path>.ww as a file.
// Sets *isdir on hit. Symmetric with cstage locate_import_in 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
// `foo/foo.ha` fallback; a module IS the directory).
fn locatein(a: *arena, dir: *u8, dirlen: u64,
pathform: *u8, pflen: u64, isdir: *i32) *u8 = {
let buf: *u8 = amalloc(a, PATH_MAX): *u8;
let off: u64 = 0u64;
let i: u64 = 0u64;
@@ -248,15 +261,24 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = {
off += dirlen;
buf[off] = 47u8; off += 1u64; // '/'
i = 0u64;
for (i < namelen) { buf[off + i] = name[i]; i += 1u64; };
off += namelen;
buf[off] = 46u8; off += 1u64; // '.'
buf[off] = 119u8; off += 1u64; // 'w'
buf[off] = 119u8; off += 1u64; // 'w'
for (i < pflen) { buf[off + i] = pathform[i]; i += 1u64; };
off += pflen;
buf[off] = 0u8;
if (os.access(pathstr(buf), 0i32) == 0) { return buf; };
let fi: os.filestat;
let r: (void | os.oserror) = os.stat(&fi, pathstr(buf));
let isdirhit: bool = false;
match (r) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) { isdirhit = true; };
};
case let e: os.oserror => void;
};
if (isdirhit) {
*isdir = 1;
return buf;
};
// candidate 2: <dir>/<name>/<name>.ww
let buf2: *u8 = amalloc(a, PATH_MAX): *u8;
off = 0u64;
i = 0u64;
@@ -264,22 +286,25 @@ fn locatein(a: *arena, dir: *u8, dirlen: u64, name: *u8, namelen: u64) *u8 = {
off += dirlen;
buf2[off] = 47u8; off += 1u64;
i = 0u64;
for (i < namelen) { buf2[off + i] = name[i]; i += 1u64; };
off += namelen;
buf2[off] = 47u8; off += 1u64;
i = 0u64;
for (i < namelen) { buf2[off + i] = name[i]; i += 1u64; };
off += namelen;
buf2[off] = 46u8; off += 1u64;
buf2[off] = 119u8; off += 1u64;
buf2[off] = 119u8; off += 1u64;
for (i < pflen) { buf2[off + i] = pathform[i]; i += 1u64; };
off += pflen;
buf2[off] = 46u8; off += 1u64; // '.'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 119u8; off += 1u64; // 'w'
buf2[off] = 0u8;
if (os.access(pathstr(buf2), 0i32) == 0) { return buf2; };
if (os.access(pathstr(buf2), 0i32) == 0) {
*isdir = 0;
return buf2;
};
return nil;
};
// Walk a colon-separated dirlist, return first hit or nil.
fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = {
// Walk a colon-separated dirlist, return first hit or nil. Sets
// *isdir on hit.
fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64,
isdir: *i32) *u8 = {
let pathform: *u8 = importpathform(a, name, namelen);
let pflen: u64 = cstrlen(pathform);
let total: u64 = cstrlen(dirs);
let p: u64 = 0u64;
for (p < total) {
@@ -290,7 +315,8 @@ fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = {
};
let seglen: u64 = q - p;
if (seglen > 0u64) {
let hit: *u8 = locatein(a, dirs + p, seglen, name, namelen);
let hit: *u8 = locatein(a, dirs + p, seglen,
pathform, pflen, isdir);
if (hit != nil) { return hit; };
};
p = q + 1u64;
@@ -298,6 +324,123 @@ fn locateimport(a: *arena, dirs: *u8, name: *u8, namelen: u64) *u8 = {
return nil;
};
// Filter for dir enumeration: keep `*.ww` minus `*test.ww` and the
// `*.combined.ww` driver-generated concat artifacts (the previous
// build leaves them in the source tree; they parse-error when
// re-included). Returns true to keep.
fn dirfilekeep(name: *u8, nlen: u64) bool = {
if (nlen <= 3u64) { return false; };
if (name[nlen - 3u64] != 46u8) { return false; }; // '.'
if (name[nlen - 2u64] != 119u8) { return false; }; // 'w'
if (name[nlen - 1u64] != 119u8) { return false; }; // 'w'
if (nlen >= 7u64) {
if (name[nlen - 7u64] == 116u8) { // 't'
if (name[nlen - 6u64] == 101u8) { // 'e'
if (name[nlen - 5u64] == 115u8) { // 's'
if (name[nlen - 4u64] == 116u8) { // 't'
return false;
};
};
};
};
};
if (nlen >= 12u64) {
// ".combined.ww"
if (name[nlen - 12u64] == 46u8) { // '.'
if (name[nlen - 11u64] == 99u8) { // 'c'
if (name[nlen - 10u64] == 111u8) { // 'o'
if (name[nlen - 9u64] == 109u8) { // 'm'
if (name[nlen - 8u64] == 98u8) { // 'b'
return false;
};
};
};
};
};
};
return true;
};
// Byte-wise memcmp returning < 0, 0, > 0. Rule-10 byte-id requires
// cstage and wwstage sort the same way; memcmp is the
// locale-independent total order (mirrors ref/hare/sort/cmp/cmp.ha
// strs).
fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = {
let n: u64 = alen;
if (blen < n) { n = blen; };
let i: u64 = 0u64;
for (i < n) {
let av: i32 = (a[i]): i32;
let bv: i32 = (b[i]): i32;
if (av < bv) { return -1; };
if (av > bv) { return 1; };
i += 1u64;
};
if (alen < blen) { return -1; };
if (alen > blen) { return 1; };
return 0;
};
// enumeratedir — list *.ww entries of `dirpath` (less *test.ww and
// *.combined.ww), byte-sort. Returns (names[], nnames) with each
// name a NUL-terminated arena copy.
fn enumeratedir(a: *arena, dirpath: *u8) (**u8, i32) = {
let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil: **u8, 0; };
let maxnames: i32 = 256;
let names: **u8 = amalloc(a, (maxnames: u64) * 8u64): **u8;
let nlens: *u64 = amalloc(a, (maxnames: u64) * 8u64): *u64;
let n: i32 = 0;
let buf: *u8 = os.alloc(8192u64): *u8;
let r: i64 = os.getdents64(fd, buf, 8192u64);
for (r > 0i64) {
let off: u64 = 0u64;
let ru: u64 = r: u64;
for (off < ru) {
let blo: u64 = (buf[off + 16u64]): u64;
let bhi: u64 = (buf[off + 17u64]): u64;
let reclen: u64 = blo + (bhi * 256u64);
let nm: *u8 = buf + off + 19u64;
let nl: u64 = cstrlen(nm);
if (dirfilekeep(nm, nl)) {
if (n < maxnames) {
let cp: *u8 = amalloc(a, nl + 1u64): *u8;
let i: u64 = 0u64;
for (i < nl) { cp[i] = nm[i]; i += 1u64; };
cp[nl] = 0u8;
names[n] = cp;
nlens[n] = nl;
n += 1;
};
};
off += reclen;
};
r = os.getdents64(fd, buf, 8192u64);
};
os.close(fd);
// Insertion sort, byte-wise. n is small (≤16 in practice).
let i: i32 = 1;
for (i < n) {
let j: i32 = i;
for (j > 0) {
if (bytecmp(names[j - 1], nlens[j - 1],
names[j], nlens[j]) <= 0) { j = 0; }
else {
let t: *u8 = names[j];
names[j] = names[j - 1];
names[j - 1] = t;
let tl: u64 = nlens[j];
nlens[j] = nlens[j - 1];
nlens[j - 1] = tl;
j -= 1;
};
};
i += 1;
};
return names, n;
};
// ---- file slurp -------------------------------------------------------
fn slurp(pathcs: *u8) (*u8, u64) = {
@@ -367,9 +510,9 @@ fn scanuse(src: *u8, len: u64) (*u8, u64) = {
};
// expand — emit one file's bytes verbatim into the combined stream,
// after recursive-expanding its top-of-file `use X;` imports. Each
// source declares its own `module <name>;` (parser stamps decls);
// the driver no longer injects a `// MODULE:` marker.
// after recursive-expanding its top-of-file `import X;` imports.
// Each source declares its own `package <name>;` (parser stamps
// decls).
fn expand(c: *expctx, pathcs: *u8) void = {
let plen: u64 = cstrlen(pathcs);
let pathstr: str = astrndup(c.a, pathcs, plen);
@@ -384,7 +527,7 @@ fn expand(c: *expctx, pathcs: *u8) void = {
return;
};
// Pass 1: scan top-of-file `use X;` lines, recursively expand.
// Pass 1: scan top-of-file `import X;` lines, recursively expand.
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
@@ -396,9 +539,12 @@ fn expand(c: *expctx, pathcs: *u8) void = {
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let ipath: *u8 = locateimport(c.a, c.dirs, idp, idn);
let isdir: i32 = 0;
let ipath: *u8 = locateimport(c.a, c.dirs, idp, idn,
&isdir);
if (ipath != nil) {
expand(c, ipath);
if (isdir != 0) { expanddir(c, ipath); }
else { expand(c, ipath); };
};
};
i = j + 1u64;
@@ -408,6 +554,141 @@ fn expand(c: *expctx, pathcs: *u8) void = {
os.writeall(c.out, "\n".ptr, 1u64);
};
// Scan `pathcs` for its first non-comment-non-blank line; if it
// starts with `package <name>;` return the package name as a
// borrowed-arena str, else nil. Same shape as cstage peek_package.
fn peekpackage(a: *arena, pathcs: *u8) *u8 = {
let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil; };
let buf: *u8 = os.alloc(2048u64): *u8;
let n: i64 = os.read(fd, buf, 2048u64);
os.close(fd);
if (n <= 0i64) { return nil; };
let nu: u64 = n: u64;
let p: u64 = 0u64;
for (p < nu) {
let q: u64 = p;
for (q < nu) {
if (buf[q] == 10u8) { break; }; // '\n'
q += 1u64;
};
let s: u64 = p;
for (s < q) {
if (buf[s] != 32u8) {
if (buf[s] != 9u8) { break; };
};
s += 1u64;
};
if (s < q) {
if (s + 1u64 < q) {
if (buf[s] == 47u8) {
if (buf[s + 1u64] == 47u8) {
p = q + 1u64;
continue;
};
};
};
if (s + 8u64 <= q) {
if (buf[s] == 112u8) { // 'p'
if (buf[s + 1u64] == 97u8) { // 'a'
if (buf[s + 2u64] == 99u8) { // 'c'
if (buf[s + 3u64] == 107u8) { // 'k'
if (buf[s + 4u64] == 97u8) { // 'a'
if (buf[s + 5u64] == 103u8) { // 'g'
if (buf[s + 6u64] == 101u8) { // 'e'
let sep: u8 = buf[s + 7u64];
if (sep == 32u8) { }
else { if (sep != 9u8) { return nil; }; };
let t: u64 = s + 8u64;
for (t < q) {
if (buf[t] != 32u8) {
if (buf[t] != 9u8) { break; };
};
t += 1u64;
};
let start: u64 = t;
for (t < q) {
let ch: u8 = buf[t];
let isalpha: bool = false;
if (ch >= 97u8) { if (ch <= 122u8) { isalpha = true; }; };
if (ch >= 65u8) { if (ch <= 90u8) { isalpha = true; }; };
if (ch >= 48u8) { if (ch <= 57u8) { isalpha = true; }; };
if (ch == 95u8) { isalpha = true; };
if (!isalpha) { break; };
t += 1u64;
};
let plen: u64 = t - start;
if (plen == 0u64) { return nil; };
let r: *u8 = amalloc(a, plen + 1u64): *u8;
let k: u64 = 0u64;
for (k < plen) { r[k] = buf[start + k]; k += 1u64; };
r[plen] = 0u8;
return r;
}; }; }; }; }; }; };
};
return nil;
};
p = q + 1u64;
};
return nil;
};
// Strict-same-package error helper. Bundled here per task #22
// brief — failure mode is dir-enum's own.
fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = {
os.write(2, "ww: ".ptr, 4u64);
os.write(2, file, cstrlen(file));
os.write(2, ": package ".ptr, 10u64);
os.write(2, pkg, cstrlen(pkg));
os.write(2, " differs from ".ptr, 14u64);
os.write(2, dirpkg, cstrlen(dirpkg));
os.write(2, " in same module dir ".ptr, 20u64);
os.write(2, dirpath, cstrlen(dirpath));
os.write(2, "\n".ptr, 1u64);
os.exit(1);
};
// expanddir — enumerate <dirpath>/*.ww (skip *test.ww and
// *.combined.ww), byte-sort, recurse into each. Mirrors
// ref/hare/hare/module/srcs.ha:183 `_findsrcs` minus tag handling.
// The visited set keys on concrete file paths so multi-file modules
// are pulled once. Strict-same-package: all enumerated files must
// declare the same `package <name>;` (task #23 subset; failure
// mode native to dir-enum).
fn expanddir(c: *expctx, dirpath: *u8) void = {
let names: **u8;
let n: i32;
names, n = enumeratedir(c.a, dirpath);
let dlen: u64 = cstrlen(dirpath);
let dirpkg: *u8 = nil;
let i: i32 = 0;
for (i < n) {
// Two-step deref+index to avoid wwstage chained `names[i][k]`
// cgen UNDER (task #24 — wwstage cgen chained-index inner
// element size on **T). Wwstage treats inner element as 8B
// (sizeof *u8) instead of 1B (sizeof u8); cstage handles
// via typed-AST natively. Retire once the wwstage fix lands.
let nm: *u8 = names[i];
let nlen: u64 = cstrlen(nm);
let fp: *u8 = amalloc(c.a, dlen + 1u64 + nlen + 1u64): *u8;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = dirpath[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = nm[k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
let pkg: *u8 = peekpackage(c.a, fp);
if (pkg != nil) {
if (dirpkg == nil) { dirpkg = pkg; }
else { if (!cstreq(dirpkg, pkg)) {
strictpkgmismatch(fp, pkg, dirpkg, dirpath);
}; };
};
expand(c, fp);
i += 1;
};
};
// ---- Build pipeline ---------------------------------------------------
// Strip the trailing ".ww" off `src` (a NUL-terminated path) into
@@ -448,19 +729,21 @@ type lflags = struct {
nlibs: i32,
};
// buildone — compile `src` into the executable named `out`.
// selfdir: NUL-terminated dir containing this driver and the
// wwstage tools (w6c_ww/w6a_ww/w6l_ww)
// src: NUL-terminated path to the .ww file
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
// buildone — compile `src` (file or directory) into the executable
// named `out`.
// selfdir: NUL-terminated dir containing this driver and the
// wwstage tools (w6c_ww/w6a_ww/w6l_ww)
// src: NUL-terminated entry path (file or directory).
// entryisdir: non-zero when src is a module directory.
// out: NUL-terminated desired output path
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
//
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
// still drives the C-built w6c/w6a/w6l. Test 993 pins the two
// pipelines to byte-identical output on a corpus.
fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, incs: *u8, lf: *lflags) i32 = {
let a: *arena = newarena();
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
@@ -475,13 +758,23 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
cstrseal(dotdotlib, off);
};
// Compute the source-file's directory: bytes of `src` up to the
// last '/'. If `src` has no '/', use ".". Hare's CWD-first
// Compute the source directory. For a file entry: bytes of `src`
// up to the last '/' (or "." when src has no '/'). For a dir
// entry: the dir itself (less trailing slashes). Hare's CWD-first
// convention assumes you're running from the module dir; our
// wrappers don't cd, so dirname(src) stands in as the closest
// analog. Source-dir wins ties over the system path (cc -I.).
let srcd: *u8 = os.alloc(PATH_MAX): *u8;
{
if (entryisdir != 0) {
let slen: u64 = cstrlen(src);
let k: u64 = 0u64;
for (k < slen) { srcd[k] = src[k]; k += 1u64; };
for (slen > 1u64) {
if (srcd[slen - 1u64] != 47u8) { break; };
slen -= 1u64;
};
srcd[slen] = 0u8;
} else {
let slen: u64 = cstrlen(src);
let last: u64 = slen;
let found: bool = false;
@@ -517,9 +810,20 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
cstrseal(searchpath, off);
};
// stem, .s, .o, .combined.ww, libwwrt.a
// Stem for .s/.o/.combined.ww side files. Dir entry: <dir>/<base>;
// file entry: src stripped of .ww.
let stem: *u8 = os.alloc(PATH_MAX): *u8;
makestem(stem, src);
if (entryisdir != 0) {
let dlen: u64 = cstrlen(srcd);
let bo: u64 = basenameoff(srcd, dlen);
let off: u64 = cstrinto(stem, 0u64, srcd);
stem[off] = 47u8; off += 1u64; // '/'
let i: u64 = bo;
for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; };
cstrseal(stem, off);
} else {
makestem(stem, src);
};
let asmf: *u8 = appendlit(stem, ".s");
let objf: *u8 = appendlit(stem, ".o");
let combined: *u8 = appendlit(stem, ".combined.ww");
@@ -532,7 +836,8 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
cstrseal(libwwrt, off);
};
// Step 1: expand `use`s into the combined file.
// Step 1: expand imports into the combined file. Dir entry →
// enumerate the module dir; file entry → start at the file.
let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (cf < 0) {
os.write(2, "ww: cannot open combined\n".ptr, 25u64);
@@ -544,7 +849,8 @@ fn buildone(selfdir: *u8, src: *u8, out: *u8, incs: *u8, lf: *lflags) i32 = {
c.out = cf;
c.dirs = searchpath;
c.visit = nil;
expand(&c, src);
if (entryisdir != 0) { expanddir(&c, srcd); }
else { expand(&c, src); };
};
os.close(cf);
@@ -706,43 +1012,41 @@ fn buildsearchpath(a: *arena, selfdir: *u8, incs: *u8) *u8 = {
return buf;
};
fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8) *u8 = {
// resolvemodule — map a name like "foo", "lib/foo", "foo.ww", or
// "." to a concrete entry path. Sets *isdir when the entry is a
// module directory (caller will dir-enumerate).
fn resolvemodule(a: *arena, selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
let nlen: u64 = cstrlen(name);
// (1) literal .ww file that exists
// (1) Literal file that exists → use as-is.
if (cstrendswithlit(name, ".ww")) {
if (os.access(pathstr(name), 0i32) == 0) {
*isdir = 0;
return arenadupcstr(a, name, nlen);
};
};
// (2) "." → cwd's <basename>.ww
if (nlen == 1u64) {
if (name[0u64] == 46u8) { // '.'
let cwd: *u8 = amalloc(a, PATH_MAX): *u8;
let r: i64 = os.getcwd(cwd, PATH_MAX);
if (r <= 0i64) { return nil; };
let cwdlen: u64 = (r: u64) - 1u64; // strip trailing NUL
let bo: u64 = basenameoff(cwd, cwdlen);
let blen: u64 = cwdlen - bo;
let dot: *u8 = amalloc(a, 2u64): *u8;
dot[0] = 46u8; dot[1] = 0u8;
let probe: *u8 = builddirmodulepath(a, dot, 1u64,
cwd + bo, blen);
if (os.access(pathstr(probe), 0i32) == 0) { return probe; };
return nil;
};
// (2) Existing path → use as-is, dir vs file via stat.
let fi: os.filestat;
let sr: (void | os.oserror) = os.stat(&fi, pathstr(name));
let found: bool = false;
let foundisdir: i32 = 0;
match (sr) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.DIR: u32) { foundisdir = 1; };
found = true;
};
case let e: os.oserror => void;
};
if (found) {
*isdir = foundisdir;
return arenadupcstr(a, name, nlen);
};
// (3) <name>/<basename(name)>.ww — directory-as-module
let bo: u64 = basenameoff(name, nlen);
let probe: *u8 = builddirmodulepath(a, name, nlen,
name + bo, nlen - bo);
if (os.access(pathstr(probe), 0i32) == 0) { return probe; };
// (4) search path lookup
// (3) Search-path lookup with dot-to-slash path translation.
let search: *u8 = buildsearchpath(a, selfdir, incs);
return locateimport(a, search, name, nlen);
return locateimport(a, search, name, nlen, isdir);
};
// ---- Subcommand handlers ----------------------------------------------
@@ -876,18 +1180,34 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
dot[0] = 46u8; dot[1] = 0u8;
src = dot;
};
let resolved: *u8 = resolvemodule(a, selfdir, src, incs);
let isdir: i32 = 0;
let resolved: *u8 = resolvemodule(a, selfdir, src, incs, &isdir);
if (resolved == nil) {
os.write(2, "ww build: cannot find module\n".ptr, 29u64);
return 1;
};
let out: *u8 = defaultoutpath(resolved);
let out: *u8 = nil;
if (isdir != 0) {
let rlen: u64 = cstrlen(resolved);
for (rlen > 1u64) {
if (resolved[rlen - 1u64] != 47u8) { break; };
rlen -= 1u64;
};
let bo: u64 = basenameoff(resolved, rlen);
out = os.alloc(PATH_MAX): *u8;
let i: u64 = bo;
let off: u64 = 0u64;
for (i < rlen) { out[off] = resolved[i]; off += 1u64; i += 1u64; };
cstrseal(out, off);
} else {
out = defaultoutpath(resolved);
};
let lf: lflags;
lf.libdirs = libdirs;
lf.nlibdirs = nlibdirs;
lf.libs = libs;
lf.nlibs = nlibs;
return buildone(selfdir, resolved, out, incs, &lf);
return buildone(selfdir, resolved, isdir, out, incs, &lf);
};
// Format the scratch path /tmp/ww_run_<pid> into buf. Returns NUL-
@@ -1015,7 +1335,8 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
dot[0] = 46u8; dot[1] = 0u8;
src = dot;
};
let resolved: *u8 = resolvemodule(a, selfdir, src, incs);
let isdir: i32 = 0;
let resolved: *u8 = resolvemodule(a, selfdir, src, incs, &isdir);
if (resolved == nil) {
os.write(2, "ww run: cannot find module\n".ptr, 27u64);
return 1;
@@ -1028,7 +1349,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs;
lf.libs = libs;
lf.nlibs = nlibs;
if (buildone(selfdir, resolved, tmp, incs, &lf) != 0) {
if (buildone(selfdir, resolved, isdir, tmp, incs, &lf) != 0) {
os.remove(pathstr(tmp));
return 1;
};
@@ -1060,7 +1381,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
fn runsingletest(selfdir: *u8, src: *u8) i32 = {
let tmp: *u8 = os.alloc(PATH_MAX): *u8;
makeruntmp(tmp);
if (buildone(selfdir, src, tmp, "\0".ptr, nil) != 0) {
if (buildone(selfdir, src, 0, tmp, "\0".ptr, nil) != 0) {
os.remove(pathstr(tmp));
return 1;
};
@@ -1110,7 +1431,7 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = {
cstrseal(tincs, ic);
let tmp: *u8 = os.alloc(PATH_MAX): *u8;
makeruntmp(tmp);
let bres: i32 = buildone(selfdir, path, tmp, tincs, nil);
let bres: i32 = buildone(selfdir, path, 0, tmp, tincs, nil);
if (bres != 0) {
fail += 1;
os.write(2, "FAIL ".ptr, 5u64);

File diff suppressed because it is too large Load Diff

View File

@@ -1277,7 +1277,7 @@ export fn encoderune(out: []u8, r: rune) i32 = {
package strings;
import bytes;
import utf8;
import encoding.utf8;
import os;
// toutf8 — borrowed []u8 view of `s`. ref/hare/strings/utf8.ha:29.