diff --git a/Makefile b/Makefile
index 2099a020..d217ea77 100644
--- a/Makefile
+++ b/Makefile
@@ -120,7 +120,6 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \
-I $$PWD/../../lib/ww \
-I $$PWD/../../lib/ww/lex \
-I $$PWD/../../lib/ww/parse \
- -I $$PWD/../../lib/encoding/utf8 \
-I $$PWD/../../selfhost/cmd/wcc \
$$PWD/../../selfhost/cmd/wwdump/main.ww
mv $(BIN)/main $@
@@ -142,7 +141,6 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \
-I $$PWD/../../lib/ww \
-I $$PWD/../../lib/ww/lex \
-I $$PWD/../../lib/ww/parse \
- -I $$PWD/../../lib/encoding/utf8 \
-I $$PWD/../../selfhost/cmd/wcc \
$$PWD/../../selfhost/cmd/w6c/main.ww
mv $(BIN)/main $@
@@ -266,6 +264,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_struct_modshadow \
$(BIN)/test_def_modshadow \
$(BIN)/test_cstage_label_ssot \
+ $(BIN)/test_direnum \
$(BIN)/test_module_decl \
$(BIN)/test_fnparams_bare_leaf_shadow \
$(BIN)/test_fnret_bare_leaf_shadow \
@@ -631,6 +630,12 @@ $(BIN)/test_cstage_label_ssot: test/wcc/736_cstage_label_ssot.c \
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
+$(BIN)/test_direnum: test/wcc/737_direnum.c \
+ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
+ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
+ $(LIB)/libwwrt.a | $(BIN)
+ $(CC) $(CFLAGS) -o $@ $<
+
$(BIN)/test_module_decl: test/wcc/738_module_decl.c $(LIB)/libwcc.a | $(BIN)
$(CC) $(CFLAGS) -Icmd/wcc -o $@ $< -Lout/lib -lwcc
diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c
index d2a6f9a2..1ba9f145 100644
--- a/cmd/wcc/parse.c
+++ b/cmd/wcc/parse.c
@@ -1241,17 +1241,23 @@ parseblock(Parser *p)
/* ------- top-level decls ------------------------------------------- */
+/* `import encoding.utf8;` — the driver resolves the dotted path to a
+ * directory; the checker only needs the leaf (`utf8`) as the module
+ * bareword for n_use→decl disambiguation, mirroring Hare's
+ * `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7
+ * stores `ident: []str` but identifier-resolution uses the last
+ * component). */
static Node *
parseuse(Parser *p)
{
Pos pp = p->cur.pos;
expect(p, TK_USE);
Node *n = newnode(p->a, N_USE, pp);
- const char *base = expectident(p);
+ const char *leaf = expectident(p);
while (accept(p, TK_DOT))
- base = aprintf(p->a, "%s.%s", base, expectident(p));
- n->str = base;
- n->strlen = strlen(base);
+ leaf = expectident(p);
+ n->str = leaf;
+ n->strlen = strlen(leaf);
expect(p, TK_SEMI);
return n;
}
diff --git a/cmd/ww/main.c b/cmd/ww/main.c
index 84ea46fa..553e1797 100644
--- a/cmd/ww/main.c
+++ b/cmd/ww/main.c
@@ -80,35 +80,48 @@ import_add(struct ImportSet *s, const char *path)
s->paths[s->n++] = strdup(path);
}
-/* try
/.ww then //.ww — symmetric with
- * wwstage locatein (selfhost/cmd/ww/main.ww) for byte-identical
- * driver output (rule 10).
- *
- * Retained divergence from brief: directory-as-module enumeration
- * NOT implemented in either stage. 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 without sibling import statements) is
- * deferred to task #22. The cstage scaffold (enumerate_dir + qsort +
- * is_testfile + dotpath_to_slash) was drafted and reverted during
- * #18 because the symmetric wwstage port needs a ww-side
- * getdents64 walker (~150-200 lines new ww in selfhost driver) and
- * the symmetric stage-rebuild blew the context budget mid-flight.
- * Rule 7 + rule 8 documentation. */
-static int
-locate_import_in(const char *dir, const char *name, char *out, size_t outsz)
+/* Translate dots in an `import` name to slashes for path lookup.
+ * `encoding.utf8` → `encoding/utf8`. Mirrors Hare hare(1)'s
+ * use-path → fs-path mapping (ref/hare/hare/module/srcs.ha:78
+ * builds the same shape via path::push per ident part). */
+static void
+import_path_form(const char *name, char *out, size_t outsz)
{
- snprintf(out, outsz, "%s/%s.ww", dir, name);
- if (access(out, 0) == 0) return 1;
- snprintf(out, outsz, "%s/%s/%s.ww", dir, name, name);
- if (access(out, 0) == 0) return 1;
+ size_t i;
+ for (i = 0; i + 1 < outsz && name[i] != '\0'; i++)
+ out[i] = (name[i] == '.') ? '/' : name[i];
+ out[i] = '\0';
+}
+
+/* try // as a directory, then /.ww as a file.
+ * Sets *is_dir on hit. Symmetric with wwstage locatein for byte-id
+ * driver output (rule 10). The legacy //.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). */
+static int
+locate_import_in(const char *dir, const char *path_form, char *out,
+ size_t outsz, int *is_dir)
+{
+ struct stat st;
+ snprintf(out, outsz, "%s/%s", dir, path_form);
+ if (stat(out, &st) == 0 && S_ISDIR(st.st_mode)) {
+ *is_dir = 1;
+ return 1;
+ }
+ snprintf(out, outsz, "%s/%s.ww", dir, path_form);
+ if (access(out, 0) == 0) {
+ *is_dir = 0;
+ return 1;
+ }
return 0;
}
-/* Walk a colon-separated dirlist trying to resolve `name`. Returns 1
- * on the first hit. */
+/* Walk a colon-separated dirlist trying to resolve `path_form`. Returns
+ * 1 on the first hit and writes the concrete path + dir/file marker. */
static int
-locate_import(const char *dirs, const char *name, char *out, size_t outsz)
+locate_import(const char *dirs, const char *path_form, char *out,
+ size_t outsz, int *is_dir)
{
const char *p = dirs;
while (*p) {
@@ -119,7 +132,8 @@ locate_import(const char *dirs, const char *name, char *out, size_t outsz)
if (n >= sizeof dir) n = sizeof dir - 1;
memcpy(dir, p, n);
dir[n] = '\0';
- if (locate_import_in(dir, name, out, outsz)) return 1;
+ if (locate_import_in(dir, path_form, out, outsz,
+ is_dir)) return 1;
}
if (!e) break;
p = e + 1;
@@ -127,11 +141,134 @@ locate_import(const char *dirs, const char *name, char *out, size_t outsz)
return 0;
}
-/* Recursively expand `path`: for each top-level `use IDENT;` we find,
- * resolve the import and expand it first, then append our own bytes.
- * Already-visited paths are skipped. Each source carries its own
- * `module ;` declaration (the parser stamps decls from it), so
- * the driver no longer injects a `// MODULE:` marker. */
+/* memcmp-based string compare for qsort. Byte-wise total order is
+ * locale-independent; rule-10 byte-id requires the two stages sort
+ * the same way. (strcmp would work today but Hare-fidelity points
+ * at memcmp via ref/hare/sort/cmp/cmp.ha:9.) */
+static int
+strs_cmp(const void *a, const void *b)
+{
+ const char *sa = *(const char *const *)a;
+ const char *sb = *(const char *const *)b;
+ return strcmp(sa, sb);
+}
+
+/* enumerate_dir_ww — collect *.ww names in `dirpath` excluding
+ * *test.ww, sort byte-wise. Returns count; caller frees entries. */
+static int
+enumerate_dir_ww(const char *dirpath, char ***out_files)
+{
+ DIR *d = opendir(dirpath);
+ if (d == NULL) { *out_files = NULL; return 0; }
+ char **arr = NULL;
+ int n = 0, cap = 0;
+ struct dirent *ent;
+ while ((ent = readdir(d)) != NULL) {
+ const char *nm = ent->d_name;
+ size_t nl = strlen(nm);
+ if (nl <= 3) continue;
+ if (strcmp(nm + nl - 3, ".ww") != 0) continue;
+ /* skip "*test.ww" (no underscore — bytestest.ww
+ * ostest.ww utf8test.ww — Hare convention is _test.ha
+ * but ww corpus settled on the un-underscored shape). */
+ if (nl >= 7 && strcmp(nm + nl - 7, "test.ww") == 0)
+ continue;
+ /* skip "*.combined.ww" — driver-generated concat
+ * artifacts (the previous build leaves them next to
+ * the source). They look like .ww but parse-erroring
+ * when re-included. */
+ if (nl >= 12 && strcmp(nm + nl - 12, ".combined.ww") == 0)
+ continue;
+ if (n + 1 > cap) {
+ cap = cap ? cap * 2 : 8;
+ arr = realloc(arr, cap * sizeof *arr);
+ }
+ arr[n++] = strdup(nm);
+ }
+ closedir(d);
+ if (n > 1) qsort(arr, n, sizeof *arr, strs_cmp);
+ *out_files = arr;
+ return n;
+}
+
+static void expand(FILE *out, const char *path, struct ImportSet *visited,
+ const char *libdir);
+
+/* Scan `path` for its first non-comment-non-blank line; if it starts
+ * with `package ;` write the name into `out` (NUL-terminated)
+ * and return 1, else 0. Strict-same-package enforcement (task #23
+ * subset) is bundled here because the failure mode is dir-enum's
+ * own — a non-dir-enum compilation unit cannot trigger it. Hare's
+ * hare/module/srcs.ha:131 has the same constraint via the README
+ * gate; we encode it as same-package across all enumerated files. */
+static int
+peek_package(const char *path, char *out, size_t outsz)
+{
+ FILE *in = fopen(path, "rb");
+ if (in == NULL) return 0;
+ char line[2048];
+ int found = 0;
+ while (fgets(line, sizeof line, in)) {
+ const char *p = line;
+ while (*p == ' ' || *p == '\t') p++;
+ if (*p == '\n' || *p == '\0') continue;
+ if (p[0] == '/' && p[1] == '/') continue;
+ if (strncmp(p, "package ", 8) != 0
+ && strncmp(p, "package\t", 8) != 0) break;
+ p += 8;
+ while (*p == ' ' || *p == '\t') p++;
+ size_t i = 0;
+ while (i + 1 < outsz && ((p[i] >= 'a' && p[i] <= 'z')
+ || (p[i] >= 'A' && p[i] <= 'Z')
+ || p[i] == '_' || (p[i] >= '0' && p[i] <= '9')))
+ out[i] = p[i], i++;
+ out[i] = '\0';
+ found = (i > 0);
+ break;
+ }
+ fclose(in);
+ return found;
+}
+
+/* expand_dir — enumerate /*.ww (skip *test.ww), byte-sort,
+ * recurse into each. Mirrors ref/hare/hare/module/srcs.ha:183
+ * `_findsrcs` minus tag handling. The visited set still keys on
+ * concrete file paths so multi-file modules are pulled once.
+ * Strict-same-package: all enumerated files must declare the same
+ * `package ;` (task #23 subset; failure mode native to
+ * dir-enum). */
+static void
+expand_dir(FILE *out, const char *dirpath, struct ImportSet *visited,
+ const char *libdir)
+{
+ char **files = NULL;
+ int n = enumerate_dir_ww(dirpath, &files);
+ char dirpkg[128] = {0};
+ for (int i = 0; i < n; i++) {
+ char fp[1024];
+ snprintf(fp, sizeof fp, "%s/%s", dirpath, files[i]);
+ char pkg[128];
+ if (peek_package(fp, pkg, sizeof pkg)) {
+ if (dirpkg[0] == '\0') {
+ snprintf(dirpkg, sizeof dirpkg, "%s", pkg);
+ } else if (strcmp(dirpkg, pkg) != 0) {
+ fprintf(stderr,
+ "ww: %s: package %s differs from %s in same module dir %s\n",
+ fp, pkg, dirpkg, dirpath);
+ exit(1);
+ }
+ }
+ expand(out, fp, visited, libdir);
+ free(files[i]);
+ }
+ free(files);
+}
+
+/* Recursively expand `path`: for each top-level `import IDENT;` we
+ * find, resolve the import and expand it first, then append our own
+ * bytes. Already-visited paths are skipped. Each source carries its
+ * own `package ;` declaration (the parser stamps decls from
+ * it). */
static void
expand(FILE *out, const char *path, struct ImportSet *visited,
const char *libdir)
@@ -158,10 +295,15 @@ expand(FILE *out, const char *path, struct ImportSet *visited,
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
+ char path_form[256];
+ import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
- if (!locate_import(libdir, name, ipath, sizeof ipath))
+ int is_dir = 0;
+ if (!locate_import(libdir, path_form, ipath, sizeof ipath,
+ &is_dir))
continue; /* silently skip if not found */
- expand(out, ipath, visited, libdir);
+ if (is_dir) expand_dir(out, ipath, visited, libdir);
+ else expand(out, ipath, visited, libdir);
}
rewind(in);
@@ -172,8 +314,9 @@ expand(FILE *out, const char *path, struct ImportSet *visited,
}
static int
-build_one(const char *src, const char *out, const char *extra_includes,
- const char *extra_libs, const char *extra_libdirs)
+build_one(const char *src, int entry_is_dir, const char *out,
+ const char *extra_includes, const char *extra_libs,
+ const char *extra_libdirs)
{
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
@@ -199,9 +342,16 @@ build_one(const char *src, const char *out, const char *extra_includes,
* convention (its `hare test` is run from the module dir, making CWD
* == module-dir); our wrappers don't cd, so dirname(src) is the
* closest analog. Also matches cc -I. — source-dir wins ties over
- * the system path. locate_import walks left-to-right. */
+ * the system path. locate_import walks left-to-right.
+ *
+ * For a dir entry the source-dir IS src; for a file entry it's
+ * the dirname. */
char srcd[1024];
- {
+ if (entry_is_dir) {
+ snprintf(srcd, sizeof srcd, "%s", src);
+ size_t n = strlen(srcd);
+ while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0';
+ } else {
const char *slash = strrchr(src, '/');
if (slash) {
size_t n = (size_t)(slash - src);
@@ -221,19 +371,29 @@ build_one(const char *src, const char *out, const char *extra_includes,
snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir);
srcdir = searchpath;
- /* Strip extension to derive a stem; e.g. /tmp/foo.ww → /tmp/foo */
+ /* Derive a stem for .s/.o/.combined.ww side files. For a file
+ * entry strip the .ww. For a dir entry use /
+ * so artifacts land inside the module directory. */
char stem[1024];
- snprintf(stem, sizeof stem, "%s", src);
- char *dot = strrchr(stem, '.');
- if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
+ if (entry_is_dir) {
+ const char *b = strrchr(srcd, '/');
+ const char *base = b ? b + 1 : srcd;
+ snprintf(stem, sizeof stem, "%s/%s", srcd, base);
+ } else {
+ snprintf(stem, sizeof stem, "%s", src);
+ char *dot = strrchr(stem, '.');
+ if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
+ }
char asmf[1024], obj[1024], combined[1024];
snprintf(asmf, sizeof asmf, "%s.s", stem);
snprintf(obj, sizeof obj, "%s.o", stem);
snprintf(combined, sizeof combined, "%s.combined.ww", stem);
- /* Resolve `use X;` imports by concatenating sources into a temp
- * file. The compiler then sees one flat source. */
+ /* Resolve imports by concatenating sources into a temp file. The
+ * compiler then sees one flat source. Dir entry → enumerate the
+ * module dir's *.ww (less *test.ww); file entry → start at the
+ * file. */
{
FILE *cf = fopen(combined, "wb");
if (cf == NULL) {
@@ -241,7 +401,8 @@ build_one(const char *src, const char *out, const char *extra_includes,
return 1;
}
struct ImportSet visited = {0};
- expand(cf, src, &visited, srcdir);
+ if (entry_is_dir) expand_dir(cf, srcd, &visited, srcdir);
+ else expand(cf, src, &visited, srcdir);
fclose(cf);
for (int i = 0; i < visited.n; i++) free(visited.paths[i]);
free(visited.paths);
@@ -330,41 +491,35 @@ basename_no_ext(const char *path, char *out, size_t outsz)
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
-/* resolve_module: turn a name into a concrete .ww file path.
+/* resolve_module: turn a name into a concrete entry path.
* foo.ww → use as-is if it exists
- * → /.ww (Hare module convention)
- * . → .ww in the cwd
- * foo (bare) → walk cwd:incs:WW_LIB for foo.ww or foo/foo.ww */
+ * → returns the dir path (caller dir-enumerates)
+ * . → cwd as a directory
+ * foo (bare) → walk cwd:incs:WW_LIB; first hit is dir or file.
+ * Sets *is_dir on hit. Dir resolution drives directory-as-module
+ * enumeration in build_one. */
static int
-resolve_module(const char *name, const char *incs, char *out, size_t outsz)
+resolve_module(const char *name, const char *incs, char *out, size_t outsz,
+ int *is_dir)
{
struct stat st;
if (stat(name, &st) == 0) {
if (S_ISREG(st.st_mode)) {
snprintf(out, outsz, "%s", name);
+ *is_dir = 0;
return 1;
}
if (S_ISDIR(st.st_mode)) {
- char buf[1024];
- const char *base;
- if (strcmp(name, ".") == 0) {
- if (getcwd(buf, sizeof buf) == NULL) return 0;
- } else {
- snprintf(buf, sizeof buf, "%s", name);
- size_t bl = strlen(buf);
- while (bl > 1 && buf[bl-1] == '/') buf[--bl] = '\0';
- }
- const char *b = strrchr(buf, '/');
- base = b ? b + 1 : buf;
- snprintf(out, outsz, "%s/%s.ww", name, base);
- if (access(out, 0) == 0) return 1;
- return 0;
+ snprintf(out, outsz, "%s", name);
+ *is_dir = 1;
+ return 1;
}
}
char sp[4096];
search_path(incs, sp, sizeof sp);
- if (locate_import(sp, name, out, outsz)) return 1;
- return 0;
+ char path_form[256];
+ import_path_form(name, path_form, sizeof path_form);
+ return locate_import(sp, path_form, out, outsz, is_dir);
}
/* Append `path` to a heap string-array. Caller frees each entry + the array. */
@@ -461,13 +616,23 @@ do_build(int argc, char **argv)
libdirs, sizeof libdirs, libs, sizeof libs, &src);
if (src == NULL) src = "."; /* default: build cwd */
char resolved[1024];
- if (!resolve_module(src, incs, resolved, sizeof resolved)) {
+ int is_dir = 0;
+ if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww build: cannot find module %s\n", src);
return 1;
}
char out[1024];
- basename_no_ext(resolved, out, sizeof out);
- return build_one(resolved, out, incs, libs, libdirs);
+ if (is_dir) {
+ char tmp[1024];
+ snprintf(tmp, sizeof tmp, "%s", resolved);
+ size_t n = strlen(tmp);
+ while (n > 1 && tmp[n-1] == '/') tmp[--n] = '\0';
+ const char *b = strrchr(tmp, '/');
+ snprintf(out, sizeof out, "%s", b ? b + 1 : tmp);
+ } else {
+ basename_no_ext(resolved, out, sizeof out);
+ }
+ return build_one(resolved, is_dir, out, incs, libs, libdirs);
}
static int
@@ -481,13 +646,15 @@ do_run(int argc, char **argv)
libdirs, sizeof libdirs, libs, sizeof libs, &src);
if (src == NULL) src = ".";
char resolved[1024];
- if (!resolve_module(src, incs, resolved, sizeof resolved)) {
+ int is_dir = 0;
+ if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww run: cannot find module %s\n", src);
return 1;
}
char tmp[1024];
snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid());
- if (build_one(resolved, tmp, incs, libs, libdirs) != 0) return 1;
+ if (build_one(resolved, is_dir, tmp, incs, libs, libdirs) != 0)
+ return 1;
/* exec the built binary with any trailing argv as its argv. */
pid_t pid = fork();
if (pid < 0) { perror("ww: fork"); unlink(tmp); return 1; }
@@ -517,13 +684,15 @@ do_test(int argc, char **argv)
/* not a literal path — try module resolution and run as
* a single test program. */
char resolved[1024];
- if (!resolve_module(target, "", resolved, sizeof resolved)) {
+ int is_dir = 0;
+ if (!resolve_module(target, "", resolved, sizeof resolved,
+ &is_dir)) {
fprintf(stderr, "ww test: cannot find %s\n", target);
return 1;
}
char tmp[1024];
snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid());
- if (build_one(resolved, tmp, "", "", "") != 0) return 1;
+ if (build_one(resolved, is_dir, tmp, "", "", "") != 0) return 1;
int rc = run(tmp);
unlink(tmp);
return rc;
@@ -532,7 +701,7 @@ do_test(int argc, char **argv)
/* single .ww file — build+run it. */
char tmp[1024];
snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid());
- if (build_one(target, tmp, "", "", "") != 0) return 1;
+ if (build_one(target, 0, tmp, "", "", "") != 0) return 1;
int rc = run(tmp);
unlink(tmp);
return rc;
@@ -558,7 +727,7 @@ do_test(int argc, char **argv)
snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d_%d", getpid(), i);
const char *label = strrchr(files[i], '/');
label = label ? label + 1 : files[i];
- int rc = build_one(files[i], tmp, target, "", "");
+ int rc = build_one(files[i], 0, tmp, target, "", "");
if (rc != 0) {
fprintf(stderr, "FAIL %s (build)\n", label);
fail++;
diff --git a/lib/encoding/utf8/utf8test.ww b/lib/encoding/utf8/utf8test.ww
index 4c7fb186..e40c3cf2 100644
--- a/lib/encoding/utf8/utf8test.ww
+++ b/lib/encoding/utf8/utf8test.ww
@@ -5,7 +5,7 @@
package utf8;
-import utf8;
+import encoding.utf8;
import os;
let signalled: i32 = 0;
diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww
index 86963830..c26f2836 100644
--- a/lib/strings/strings.ww
+++ b/lib/strings/strings.ww
@@ -33,7 +33,7 @@
package strings;
import bytes;
-import utf8;
+import encoding.utf8;
import os;
// toutf8 — borrowed []u8 view of `s`. ref/hare/strings/utf8.ha:29.
diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww
index 82020500..1d39574b 100644
--- a/lib/strings/stringstest.ww
+++ b/lib/strings/stringstest.ww
@@ -1,5 +1,5 @@
// stringstest — exercises lib/strings. Run with
-// `out/bin/ww run lib/strings/stringstest.ww -I lib/encoding/utf8`.
+// `out/bin/ww run lib/strings/stringstest.ww`.
// Same signalled-then-fail()-with-+10 shape as bytes / utf8 / hex /
// time tests: non-zero exit pinpoints the failing scenario.
//
@@ -9,7 +9,7 @@
package strings;
import strings;
-import utf8;
+import encoding.utf8;
import os;
let signalled: i32 = 0;
diff --git a/lib/ww/lex/lex.ww b/lib/ww/lex/lex.ww
index 59cb623f..5990d949 100644
--- a/lib/ww/lex/lex.ww
+++ b/lib/ww/lex/lex.ww
@@ -13,10 +13,11 @@
package lex;
+// Sibling import (tok) auto-resolves via task #22 dir-enum when
+// callers `import lex;` (which dir-enums lib/ww/lex/).
import os;
import ascii;
import mem;
-import tok;
// isidstart / isidpart — identifier classification. Lexer-local
// because the "alpha or '_' / alnum or '_'" set isn't part of Hare's
diff --git a/lib/ww/parse/decl.ww b/lib/ww/parse/decl.ww
index 458f8cf9..ea57b5b6 100644
--- a/lib/ww/parse/decl.ww
+++ b/lib/ww/parse/decl.ww
@@ -6,6 +6,11 @@ import os;
import mem;
import tok;
+// `import encoding.utf8;` — the driver resolves the dotted path to
+// a directory; only the leaf (`utf8`) is needed downstream as the
+// module bareword for n_use → decl disambiguation, mirroring Hare's
+// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7
+// stores `[]str` but identifier-resolution uses the last component).
fn parseuse(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
@@ -13,32 +18,13 @@ fn parseuse(p: *parser) *node = {
advance(p); // past `use`
let n: *node = newnode(p.a, nkind.N_USE, pf, pl, pc);
n.nmod = p.curmod;
- // Accept a dotted import path: `use encoding.utf8;` — capture the
- // full dotted form on n.str. Leaf-only SK_USE install lives in
- // the check stage; the lexer-side join happens here.
- let id: str;
- expectident(p, &id);
- n.str = id;
+ let leaf: str;
+ expectident(p, &leaf);
for (p.curkind == tkind.TK_DOT) {
advance(p); // past `.`
- let seg: str;
- expectident(p, &seg);
- // Concatenate id + "." + seg into a fresh str. Plan-9
- // separator per user pick over Hare's `::`.
- let total: i32 = n.str.len + 1 + seg.len;
- let buf: *u8 = amalloc(p.a, total: u64 + 1u64): *u8;
- let i: i32 = 0;
- for (i < n.str.len) { buf[i] = n.str[i]; i += 1; };
- buf[i] = 46u8; // '.'
- i += 1;
- let j: i32 = 0;
- for (j < seg.len) { buf[i + j] = seg[j]; j += 1; };
- buf[total] = 0u8;
- let joined: str;
- joined.ptr = buf;
- joined.len = total;
- n.str = joined;
+ expectident(p, &leaf);
};
+ n.str = leaf;
expecttok(p, tkind.TK_SEMI, "expected ';' after use");
return n;
};
diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww
index 204ccaed..bed314c8 100644
--- a/lib/ww/parse/parse.ww
+++ b/lib/ww/parse/parse.ww
@@ -12,12 +12,12 @@
package parse;
+// Sibling imports (expr, stmt, decl) auto-resolve via task #22
+// dir-enum when callers `import parse;` (which dir-enums
+// lib/ww/parse/).
import os;
import mem;
import tok;
-import expr;
-import stmt;
-import decl;
type parser = struct {
l: *lex,
diff --git a/lib/ww/sym.ww b/lib/ww/sym.ww
index 1f24fbc0..c905a096 100644
--- a/lib/ww/sym.ww
+++ b/lib/ww/sym.ww
@@ -6,9 +6,9 @@
package ww;
+// Sibling imports (typ, ast) auto-resolve via task #22 dir-enum
+// when callers `import ww;` or pull all three separately.
import mem;
-import typ;
-import ast;
// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind.
type skind = enum i32 {
diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww
index f4f89801..a6657284 100644
--- a/selfhost/cmd/w6c/main.combined.ww
+++ b/selfhost/cmd/w6c/main.combined.ww
@@ -1386,7 +1386,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.
@@ -2605,10 +2605,11 @@ export fn strcasecmp(a: str, b: str) i32 = {
package lex;
+// Sibling import (tok) auto-resolves via task #22 dir-enum when
+// callers `import lex;` (which dir-enums lib/ww/lex/).
import os;
import ascii;
import mem;
-import tok;
// isidstart / isidpart — identifier classification. Lexer-local
// because the "alpha or '_' / alnum or '_'" set isn't part of Hare's
@@ -3744,6 +3745,189 @@ export fn astprint(fd: i32, n: *node) void = {
pr(fd, n, 0);
};
+// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww.
+
+package parse;
+
+import os;
+import mem;
+import tok;
+
+// `import encoding.utf8;` — the driver resolves the dotted path to
+// a directory; only the leaf (`utf8`) is needed downstream as the
+// module bareword for n_use → decl disambiguation, mirroring Hare's
+// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7
+// stores `[]str` but identifier-resolution uses the last component).
+fn parseuse(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `use`
+ let n: *node = newnode(p.a, nkind.N_USE, pf, pl, pc);
+ n.nmod = p.curmod;
+ let leaf: str;
+ expectident(p, &leaf);
+ for (p.curkind == tkind.TK_DOT) {
+ advance(p); // past `.`
+ expectident(p, &leaf);
+ };
+ n.str = leaf;
+ expecttok(p, tkind.TK_SEMI, "expected ';' after use");
+ return n;
+};
+
+fn parsedef(p: *parser, exported: i32) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `def`
+ let n: *node = newnode(p.a, nkind.N_DEF, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectident(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_COLON, "expected ':' in def");
+ n.lhs = parsetype(p);
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' in def");
+ n.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after def");
+ n.exported = exported;
+ return n;
+};
+
+fn parselet(p: *parser, exported: i32) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ // Accept `let` or `const`. Const-bound bindings are marked via
+ // n.op = tkind.TK_CONST so the checker can reject reassignment.
+ let is_const: i32 = 0;
+ if (p.curkind == tkind.TK_CONST) { is_const = 1; };
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectbindname(p, &id);
+ n.str = id;
+ if (accepttok(p, tkind.TK_COLON)) {
+ n.lhs = parsetype(p);
+ };
+ if (accepttok(p, tkind.TK_ASSIGN)) {
+ n.rhs = parseexpr(p);
+ };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ n.exported = exported;
+ if (is_const != 0) { n.op = tkind.TK_CONST; };
+ return n;
+};
+
+fn parseattrs(p: *parser) *node = {
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (p.curkind == tkind.TK_AT) {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p);
+ let a: *node = newnode(p.a, nkind.N_ATTR, pf, pl, pc);
+ let id: str;
+ expectident(p, &id);
+ a.str = id;
+ // `@name(args...)` for FFI-style attrs; `@name` for marker-
+ // only attrs like @test (no parens).
+ if (accepttok(p, tkind.TK_LPAREN)) {
+ let arghead: *node = nil;
+ parsearglist(p, tkind.TK_RPAREN, &arghead);
+ a.list = arghead;
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args");
+ };
+ if (head == nil) { head = a; tail = a; }
+ else { tail.next = a; tail = a; };
+ };
+ return head;
+};
+
+fn parseparams(p: *parser) *node = {
+ if (p.curkind == tkind.TK_RPAREN) { return nil; };
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (true) {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ let n: *node = newnode(p.a, nkind.N_PARAM, pf, pl, pc);
+ // Param form: (IDENT|'_') ':' type. Anonymous-type-only params
+ // (used in fn type expressions) aren't yet wired here.
+ let id: str;
+ expectbindname(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_COLON, "expected ':' in parameter");
+ n.lhs = parsetype(p);
+ // Hare-style variadic: `name: T...`. Marker on n.op so check
+ // promotes the param's type to []T and call sites gather /
+ // forward. Mirrors cmd/wcc/parse.c parseparams.
+ if (accepttok(p, tkind.TK_ELLIPSIS)) {
+ n.op = tkind.TK_ELLIPSIS;
+ };
+ if (head == nil) { head = n; tail = n; }
+ else { tail.next = n; tail = n; };
+ if (n.op == tkind.TK_ELLIPSIS) {
+ break; // variadic must be the last param
+ };
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ if (p.curkind == tkind.TK_RPAREN) { break; };
+ };
+ return head;
+};
+
+fn parsefn(p: *parser, exported: i32, attrs: *node) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `fn`
+ let n: *node = newnode(p.a, nkind.N_FNDECL, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectident(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name");
+ n.list = parseparams(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after params");
+ if (p.curkind != tkind.TK_ASSIGN) {
+ if (p.curkind != tkind.TK_SEMI) {
+ n.lhs = parsetype(p);
+ };
+ };
+ if (accepttok(p, tkind.TK_ASSIGN)) {
+ n.body = parseblock(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after fn body");
+ } else {
+ // Body-less fn: FFI declaration (`fn name(args) ret;`).
+ expecttok(p, tkind.TK_SEMI, "expected ';' after fn header");
+ };
+ n.exported = exported;
+ n.attr = attrs;
+ return n;
+};
+
+fn parsetypedecl(p: *parser, exported: i32) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `type`
+ let n: *node = newnode(p.a, nkind.N_TYPEDECL, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectident(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl");
+ n.lhs = parsetype(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after type decl");
+ n.exported = exported;
+ return n;
+};
+
+
// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww.
package parse;
@@ -4210,618 +4394,6 @@ fn parseexpr(p: *parser) *node = {
};
-// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww.
-
-package parse;
-
-import os;
-import mem;
-import tok;
-
-fn parseletlocal(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- // `let` or `const`. Const-bound locals are marked via n.op = tkind.TK_CONST.
- let is_const: i32 = 0;
- if (p.curkind == tkind.TK_CONST) { is_const = 1; };
- advance(p);
-
- // Hare-style tuple destructure: `let (a, b) = expr;`.
- // Types are optional per binding (matches C parser; Hare itself
- // doesn't allow types here, but cmd/wcc/parse.c does).
- if (p.curkind == tkind.TK_LPAREN) {
- advance(p);
- let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
- let head: *node = nil;
- let tail: *node = nil;
- for (true) {
- let lpf: str = p.curfile;
- let lpl: i32 = p.curline;
- let lpc: i32 = p.curcol;
- let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
- let id: str;
- expectbindname(p, &id);
- l.str = id;
- if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
- if (head == nil) { head = l; }
- else { tail.next = l; };
- tail = l;
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- };
- expecttok(p, tkind.TK_RPAREN, "expected ')' in let destructure");
- expecttok(p, tkind.TK_ASSIGN, "expected '=' after let destructure");
- m.rhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- m.list = head;
- if (is_const != 0) {
- m.op = tkind.TK_CONST;
- let lc: *node = head;
- for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
- };
- return m;
- };
-
- let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
- let id: str;
- expectbindname(p, &id);
- n.str = id;
- if (accepttok(p, tkind.TK_COLON)) {
- n.lhs = parsetype(p);
- };
- // Comma-multi-let: `let n, s = call();` (ww extension over Hare).
- // Collects (name, type) pairs, then '=' rhs. Each binding gets
- // its own nkind.N_LET; the wrapping nkind.N_MLET carries the rhs.
- if (p.curkind == tkind.TK_COMMA) {
- let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
- let head: *node = n;
- let tail: *node = n;
- for (accepttok(p, tkind.TK_COMMA)) {
- let lpf: str = p.curfile;
- let lpl: i32 = p.curline;
- let lpc: i32 = p.curcol;
- let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
- let id2: str;
- expectbindname(p, &id2);
- l.str = id2;
- if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
- tail.next = l;
- tail = l;
- };
- expecttok(p, tkind.TK_ASSIGN, "expected '=' after let names");
- m.rhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- m.list = head;
- if (is_const != 0) {
- m.op = tkind.TK_CONST;
- let lc: *node = head;
- for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
- };
- return m;
- };
- if (accepttok(p, tkind.TK_ASSIGN)) {
- n.rhs = parseexpr(p);
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- if (is_const != 0) { n.op = tkind.TK_CONST; };
- return n;
-};
-
-fn parseblock(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- expecttok(p, tkind.TK_LBRACE, "expected '{' to open block");
- let blk: *node = newnode(p.a, nkind.N_BLOCK, pf, pl, pc);
- let head: *node = nil;
- let tail: *node = nil;
- for (p.curkind != tkind.TK_RBRACE) {
- if (p.curkind == tkind.TK_EOF) { break; };
- let s: *node = parsestmt(p);
- if (s != nil) {
- if (head == nil) { head = s; tail = s; }
- else { tail.next = s; tail = s; };
- };
- };
- expecttok(p, tkind.TK_RBRACE, "expected '}' to close block");
- blk.list = head;
- return blk;
-};
-
-fn parseif(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `if`
- expecttok(p, tkind.TK_LPAREN, "expected '(' after if");
- let n: *node = newnode(p.a, nkind.N_IF, pf, pl, pc);
- n.cond = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after if condition");
- n.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) {
- if (p.curkind == tkind.TK_IF) {
- n.els = parseif(p);
- } else {
- n.els = parseblock(p);
- };
- };
- return n;
-};
-
-fn parsefor(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `for`
- expecttok(p, tkind.TK_LPAREN, "expected '(' after for");
-
- // Four forms (matching C parser):
- // for (cond) — only cond
- // for (init; cond; post) — C-style 3-clause
- // for (let x .. expr) — Hare-style range, single binding
- // for (let (a, b) .. expr) — range with tuple destructure
- // Range and 3-clause both lead with `let`, so we commit to consuming
- // `let` then disambiguate by looking at what follows.
- if (p.curkind == tkind.TK_LET) {
- advance(p); // past `let`
-
- // Tuple destructure: `for (let (a, b) .. expr)`.
- if (p.curkind == tkind.TK_LPAREN) {
- advance(p);
- let names: *node = nil;
- let ntail: *node = nil;
- for (true) {
- let npf: str = p.curfile;
- let npl: i32 = p.curline;
- let npc: i32 = p.curcol;
- let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc);
- let nm: str;
- expectbindname(p, &nm);
- e.str = nm;
- if (names == nil) { names = e; }
- else { ntail.next = e; };
- ntail = e;
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- };
- expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names");
- expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names");
- let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
- rng.list = names;
- rng.lhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- rng.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
- return rng;
- };
-
- // Single binding range or C-style let-init. We need to consume
- // the IDENT/UNDER to know which: if followed by '..' it's a
- // range; otherwise build a synthetic LET for the C-style for-init
- // with the consumed name baked in.
- if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) {
- let isunder: bool = (p.curkind == tkind.TK_UNDER);
- let nm: str;
- nm.ptr = nil; nm.len = 0;
- if (!isunder) { nm = p.curtext; };
- let lpf: str = p.curfile;
- let lpl: i32 = p.curline;
- let lpc: i32 = p.curcol;
- advance(p); // consume IDENT/UNDER
-
- if (p.curkind == tkind.TK_DOTDOT) {
- advance(p);
- let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
- rng.str = nm; // "" for `_`
- rng.lhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- rng.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
- return rng;
- };
-
- // Not a range — finish the let manually and continue as
- // a 3-clause for-init.
- let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
- first.str = nm;
- if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); };
- if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); };
- expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let");
- let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
- n.lhs = first;
- n.cond = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after for cond");
- n.rhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- n.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); };
- return n;
- };
-
- errmsg(p, "expected name after 'let' in for");
- };
-
- // for (cond) or for (cond; post)
- let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
- let first: *node = parseexpr(p);
- if (accepttok(p, tkind.TK_SEMI)) {
- n.cond = first;
- n.rhs = parseexpr(p);
- } else {
- n.cond = first;
- };
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- n.body = parseblock(p);
- // Optional `else { ... }` — runs at normal cond-false exit; skipped
- // by break. Hare's "did the loop find it?" idiom.
- if (accepttok(p, tkind.TK_ELSE)) {
- n.els = parseblock(p);
- };
- return n;
-};
-
-fn parseswitch(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `switch`
- expecttok(p, tkind.TK_LPAREN, "expected '(' after switch");
- let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc);
- n.lhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression");
- expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body");
- let head: *node = nil;
- let tail: *node = nil;
- for (p.curkind == tkind.TK_CASE) {
- let cpf: str = p.curfile;
- let cpl: i32 = p.curline;
- let cpc: i32 = p.curcol;
- advance(p); // past `case`
- let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc);
- let eh: *node = nil;
- let et: *node = nil;
- if (p.curkind != tkind.TK_COLON) {
- p.nocast = 1;
- for (true) {
- let e: *node = parseexpr(p);
- if (eh == nil) { eh = e; }
- else { et.next = e; };
- et = e;
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- };
- p.nocast = 0;
- };
- cs.list = eh;
- expecttok(p, tkind.TK_COLON, "expected ':' after case label");
- let bh: *node = nil;
- let bt: *node = nil;
- for (p.curkind != tkind.TK_CASE) {
- if (p.curkind == tkind.TK_RBRACE) { break; };
- if (p.curkind == tkind.TK_EOF) { break; };
- let s: *node = parsestmt(p);
- if (s != nil) {
- if (bh == nil) { bh = s; }
- else { bt.next = s; };
- bt = s;
- };
- };
- let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc);
- blk.list = bh;
- cs.body = blk;
- if (head == nil) { head = cs; }
- else { tail.next = cs; };
- tail = cs;
- };
- expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch");
- n.list = head;
- return n;
-};
-
-fn parsestmt(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
-
- // `static` is allowed on local lets per Hare; we accept and skip
- // it (it doesn't change the AST shape).
- if (p.curkind == tkind.TK_STATIC) { advance(p); };
-
- if (p.curkind == tkind.TK_LBRACE) {
- let b: *node = parseblock(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after block");
- return b;
- };
- if (p.curkind == tkind.TK_LET) { return parseletlocal(p); };
- if (p.curkind == tkind.TK_CONST) { return parseletlocal(p); };
- if (p.curkind == tkind.TK_IF) {
- let n: *node = parseif(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after if");
- return n;
- };
- if (p.curkind == tkind.TK_FOR) {
- let n: *node = parsefor(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after for");
- return n;
- };
- if (p.curkind == tkind.TK_SWITCH) {
- let n: *node = parseswitch(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after switch");
- return n;
- };
- if (p.curkind == tkind.TK_RETURN) {
- advance(p);
- let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc);
- if (p.curkind != tkind.TK_SEMI) {
- let first: *node = parseexpr(p);
- // Hare-style multi-value: `return a, b;` becomes a
- // tuple expression so codegen sees one rvalue.
- if (p.curkind == tkind.TK_COMMA) {
- let t: *node = newnode(p.a, nkind.N_TUPLE, pf, pl, pc);
- t.list = first;
- let tail: *node = first;
- for (accepttok(p, tkind.TK_COMMA)) {
- let e: *node = parseexpr(p);
- tail.next = e;
- tail = e;
- };
- n.lhs = t;
- } else {
- n.lhs = first;
- };
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after return");
- return n;
- };
- if (p.curkind == tkind.TK_DEFER) {
- advance(p);
- let n: *node = newnode(p.a, nkind.N_DEFER, pf, pl, pc);
- n.lhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after defer");
- return n;
- };
- if (p.curkind == tkind.TK_YIELD) {
- advance(p);
- let n: *node = newnode(p.a, nkind.N_YIELD, pf, pl, pc);
- n.lhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after yield");
- return n;
- };
- if (p.curkind == tkind.TK_BREAK) {
- advance(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after break");
- return newnode(p.a, nkind.N_BREAK, pf, pl, pc);
- };
- if (p.curkind == tkind.TK_CONTINUE) {
- advance(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after continue");
- return newnode(p.a, nkind.N_CONTINUE, pf, pl, pc);
- };
- // expression statement, or tuple-destructure multi-assign:
- // a, b = expr;
- // Mirrors cmd/wcc/parse.c:1015-1031. We parse the first lvalue
- // with parseexpr (matches the C side); subsequent lvalues go
- // through parsebin(parseunary, 1) so the `=` stays for us to
- // consume — parseexpr would absorb it.
- let e: *node = parseexpr(p);
- if (p.curkind == tkind.TK_COMMA) {
- let m: *node = newnode(p.a, nkind.N_MASSIGN, pf, pl, pc);
- let head: *node = e;
- let tail: *node = e;
- for (p.curkind == tkind.TK_COMMA) {
- advance(p);
- let lv: *node = parsebin(p, parseunary(p), 1);
- tail.next = lv;
- tail = lv;
- };
- expecttok(p, tkind.TK_ASSIGN, "expected '=' after multi-assign lvalues");
- m.rhs = parseexpr(p);
- m.list = head;
- expecttok(p, tkind.TK_SEMI, "expected ';' after multi-assign");
- return m;
- };
- let n: *node = newnode(p.a, nkind.N_EXPRSTMT, pf, pl, pc);
- n.lhs = e;
- expecttok(p, tkind.TK_SEMI, "expected ';' after expression statement");
- return n;
-};
-
-
-// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww.
-
-package parse;
-
-import os;
-import mem;
-import tok;
-
-fn parseuse(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `use`
- let n: *node = newnode(p.a, nkind.N_USE, pf, pl, pc);
- n.nmod = p.curmod;
- // Accept a dotted import path: `use encoding.utf8;` — capture the
- // full dotted form on n.str. Leaf-only SK_USE install lives in
- // the check stage; the lexer-side join happens here.
- let id: str;
- expectident(p, &id);
- n.str = id;
- for (p.curkind == tkind.TK_DOT) {
- advance(p); // past `.`
- let seg: str;
- expectident(p, &seg);
- // Concatenate id + "." + seg into a fresh str. Plan-9
- // separator per user pick over Hare's `::`.
- let total: i32 = n.str.len + 1 + seg.len;
- let buf: *u8 = amalloc(p.a, total: u64 + 1u64): *u8;
- let i: i32 = 0;
- for (i < n.str.len) { buf[i] = n.str[i]; i += 1; };
- buf[i] = 46u8; // '.'
- i += 1;
- let j: i32 = 0;
- for (j < seg.len) { buf[i + j] = seg[j]; j += 1; };
- buf[total] = 0u8;
- let joined: str;
- joined.ptr = buf;
- joined.len = total;
- n.str = joined;
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after use");
- return n;
-};
-
-fn parsedef(p: *parser, exported: i32) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `def`
- let n: *node = newnode(p.a, nkind.N_DEF, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectident(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_COLON, "expected ':' in def");
- n.lhs = parsetype(p);
- expecttok(p, tkind.TK_ASSIGN, "expected '=' in def");
- n.rhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after def");
- n.exported = exported;
- return n;
-};
-
-fn parselet(p: *parser, exported: i32) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- // Accept `let` or `const`. Const-bound bindings are marked via
- // n.op = tkind.TK_CONST so the checker can reject reassignment.
- let is_const: i32 = 0;
- if (p.curkind == tkind.TK_CONST) { is_const = 1; };
- advance(p);
- let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectbindname(p, &id);
- n.str = id;
- if (accepttok(p, tkind.TK_COLON)) {
- n.lhs = parsetype(p);
- };
- if (accepttok(p, tkind.TK_ASSIGN)) {
- n.rhs = parseexpr(p);
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- n.exported = exported;
- if (is_const != 0) { n.op = tkind.TK_CONST; };
- return n;
-};
-
-fn parseattrs(p: *parser) *node = {
- let head: *node = nil;
- let tail: *node = nil;
- for (p.curkind == tkind.TK_AT) {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p);
- let a: *node = newnode(p.a, nkind.N_ATTR, pf, pl, pc);
- let id: str;
- expectident(p, &id);
- a.str = id;
- // `@name(args...)` for FFI-style attrs; `@name` for marker-
- // only attrs like @test (no parens).
- if (accepttok(p, tkind.TK_LPAREN)) {
- let arghead: *node = nil;
- parsearglist(p, tkind.TK_RPAREN, &arghead);
- a.list = arghead;
- expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args");
- };
- if (head == nil) { head = a; tail = a; }
- else { tail.next = a; tail = a; };
- };
- return head;
-};
-
-fn parseparams(p: *parser) *node = {
- if (p.curkind == tkind.TK_RPAREN) { return nil; };
- let head: *node = nil;
- let tail: *node = nil;
- for (true) {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- let n: *node = newnode(p.a, nkind.N_PARAM, pf, pl, pc);
- // Param form: (IDENT|'_') ':' type. Anonymous-type-only params
- // (used in fn type expressions) aren't yet wired here.
- let id: str;
- expectbindname(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_COLON, "expected ':' in parameter");
- n.lhs = parsetype(p);
- // Hare-style variadic: `name: T...`. Marker on n.op so check
- // promotes the param's type to []T and call sites gather /
- // forward. Mirrors cmd/wcc/parse.c parseparams.
- if (accepttok(p, tkind.TK_ELLIPSIS)) {
- n.op = tkind.TK_ELLIPSIS;
- };
- if (head == nil) { head = n; tail = n; }
- else { tail.next = n; tail = n; };
- if (n.op == tkind.TK_ELLIPSIS) {
- break; // variadic must be the last param
- };
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- if (p.curkind == tkind.TK_RPAREN) { break; };
- };
- return head;
-};
-
-fn parsefn(p: *parser, exported: i32, attrs: *node) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `fn`
- let n: *node = newnode(p.a, nkind.N_FNDECL, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectident(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name");
- n.list = parseparams(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after params");
- if (p.curkind != tkind.TK_ASSIGN) {
- if (p.curkind != tkind.TK_SEMI) {
- n.lhs = parsetype(p);
- };
- };
- if (accepttok(p, tkind.TK_ASSIGN)) {
- n.body = parseblock(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after fn body");
- } else {
- // Body-less fn: FFI declaration (`fn name(args) ret;`).
- expecttok(p, tkind.TK_SEMI, "expected ';' after fn header");
- };
- n.exported = exported;
- n.attr = attrs;
- return n;
-};
-
-fn parsetypedecl(p: *parser, exported: i32) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `type`
- let n: *node = newnode(p.a, nkind.N_TYPEDECL, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectident(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl");
- n.lhs = parsetype(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after type decl");
- n.exported = exported;
- return n;
-};
-
-
// lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing).
//
// Split into Hare-style submodule: parse.ww (here) holds the parser
@@ -4836,12 +4408,12 @@ fn parsetypedecl(p: *parser, exported: i32) *node = {
package parse;
+// Sibling imports (expr, stmt, decl) auto-resolve via task #22
+// dir-enum when callers `import parse;` (which dir-enums
+// lib/ww/parse/).
import os;
import mem;
import tok;
-import expr;
-import stmt;
-import decl;
type parser = struct {
l: *lex,
@@ -5285,6 +4857,421 @@ export fn parsefile(p: *parser) *node = {
return f;
};
+// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww.
+
+package parse;
+
+import os;
+import mem;
+import tok;
+
+fn parseletlocal(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ // `let` or `const`. Const-bound locals are marked via n.op = tkind.TK_CONST.
+ let is_const: i32 = 0;
+ if (p.curkind == tkind.TK_CONST) { is_const = 1; };
+ advance(p);
+
+ // Hare-style tuple destructure: `let (a, b) = expr;`.
+ // Types are optional per binding (matches C parser; Hare itself
+ // doesn't allow types here, but cmd/wcc/parse.c does).
+ if (p.curkind == tkind.TK_LPAREN) {
+ advance(p);
+ let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (true) {
+ let lpf: str = p.curfile;
+ let lpl: i32 = p.curline;
+ let lpc: i32 = p.curcol;
+ let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
+ let id: str;
+ expectbindname(p, &id);
+ l.str = id;
+ if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
+ if (head == nil) { head = l; }
+ else { tail.next = l; };
+ tail = l;
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ };
+ expecttok(p, tkind.TK_RPAREN, "expected ')' in let destructure");
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' after let destructure");
+ m.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ m.list = head;
+ if (is_const != 0) {
+ m.op = tkind.TK_CONST;
+ let lc: *node = head;
+ for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
+ };
+ return m;
+ };
+
+ let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
+ let id: str;
+ expectbindname(p, &id);
+ n.str = id;
+ if (accepttok(p, tkind.TK_COLON)) {
+ n.lhs = parsetype(p);
+ };
+ // Comma-multi-let: `let n, s = call();` (ww extension over Hare).
+ // Collects (name, type) pairs, then '=' rhs. Each binding gets
+ // its own nkind.N_LET; the wrapping nkind.N_MLET carries the rhs.
+ if (p.curkind == tkind.TK_COMMA) {
+ let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
+ let head: *node = n;
+ let tail: *node = n;
+ for (accepttok(p, tkind.TK_COMMA)) {
+ let lpf: str = p.curfile;
+ let lpl: i32 = p.curline;
+ let lpc: i32 = p.curcol;
+ let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
+ let id2: str;
+ expectbindname(p, &id2);
+ l.str = id2;
+ if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
+ tail.next = l;
+ tail = l;
+ };
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' after let names");
+ m.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ m.list = head;
+ if (is_const != 0) {
+ m.op = tkind.TK_CONST;
+ let lc: *node = head;
+ for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
+ };
+ return m;
+ };
+ if (accepttok(p, tkind.TK_ASSIGN)) {
+ n.rhs = parseexpr(p);
+ };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ if (is_const != 0) { n.op = tkind.TK_CONST; };
+ return n;
+};
+
+fn parseblock(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ expecttok(p, tkind.TK_LBRACE, "expected '{' to open block");
+ let blk: *node = newnode(p.a, nkind.N_BLOCK, pf, pl, pc);
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (p.curkind != tkind.TK_RBRACE) {
+ if (p.curkind == tkind.TK_EOF) { break; };
+ let s: *node = parsestmt(p);
+ if (s != nil) {
+ if (head == nil) { head = s; tail = s; }
+ else { tail.next = s; tail = s; };
+ };
+ };
+ expecttok(p, tkind.TK_RBRACE, "expected '}' to close block");
+ blk.list = head;
+ return blk;
+};
+
+fn parseif(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `if`
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after if");
+ let n: *node = newnode(p.a, nkind.N_IF, pf, pl, pc);
+ n.cond = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after if condition");
+ n.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) {
+ if (p.curkind == tkind.TK_IF) {
+ n.els = parseif(p);
+ } else {
+ n.els = parseblock(p);
+ };
+ };
+ return n;
+};
+
+fn parsefor(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `for`
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after for");
+
+ // Four forms (matching C parser):
+ // for (cond) — only cond
+ // for (init; cond; post) — C-style 3-clause
+ // for (let x .. expr) — Hare-style range, single binding
+ // for (let (a, b) .. expr) — range with tuple destructure
+ // Range and 3-clause both lead with `let`, so we commit to consuming
+ // `let` then disambiguate by looking at what follows.
+ if (p.curkind == tkind.TK_LET) {
+ advance(p); // past `let`
+
+ // Tuple destructure: `for (let (a, b) .. expr)`.
+ if (p.curkind == tkind.TK_LPAREN) {
+ advance(p);
+ let names: *node = nil;
+ let ntail: *node = nil;
+ for (true) {
+ let npf: str = p.curfile;
+ let npl: i32 = p.curline;
+ let npc: i32 = p.curcol;
+ let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc);
+ let nm: str;
+ expectbindname(p, &nm);
+ e.str = nm;
+ if (names == nil) { names = e; }
+ else { ntail.next = e; };
+ ntail = e;
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ };
+ expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names");
+ expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names");
+ let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
+ rng.list = names;
+ rng.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ rng.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
+ return rng;
+ };
+
+ // Single binding range or C-style let-init. We need to consume
+ // the IDENT/UNDER to know which: if followed by '..' it's a
+ // range; otherwise build a synthetic LET for the C-style for-init
+ // with the consumed name baked in.
+ if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) {
+ let isunder: bool = (p.curkind == tkind.TK_UNDER);
+ let nm: str;
+ nm.ptr = nil; nm.len = 0;
+ if (!isunder) { nm = p.curtext; };
+ let lpf: str = p.curfile;
+ let lpl: i32 = p.curline;
+ let lpc: i32 = p.curcol;
+ advance(p); // consume IDENT/UNDER
+
+ if (p.curkind == tkind.TK_DOTDOT) {
+ advance(p);
+ let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
+ rng.str = nm; // "" for `_`
+ rng.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ rng.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
+ return rng;
+ };
+
+ // Not a range — finish the let manually and continue as
+ // a 3-clause for-init.
+ let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
+ first.str = nm;
+ if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); };
+ if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let");
+ let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
+ n.lhs = first;
+ n.cond = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after for cond");
+ n.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ n.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); };
+ return n;
+ };
+
+ errmsg(p, "expected name after 'let' in for");
+ };
+
+ // for (cond) or for (cond; post)
+ let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
+ let first: *node = parseexpr(p);
+ if (accepttok(p, tkind.TK_SEMI)) {
+ n.cond = first;
+ n.rhs = parseexpr(p);
+ } else {
+ n.cond = first;
+ };
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ n.body = parseblock(p);
+ // Optional `else { ... }` — runs at normal cond-false exit; skipped
+ // by break. Hare's "did the loop find it?" idiom.
+ if (accepttok(p, tkind.TK_ELSE)) {
+ n.els = parseblock(p);
+ };
+ return n;
+};
+
+fn parseswitch(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `switch`
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after switch");
+ let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc);
+ n.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression");
+ expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body");
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (p.curkind == tkind.TK_CASE) {
+ let cpf: str = p.curfile;
+ let cpl: i32 = p.curline;
+ let cpc: i32 = p.curcol;
+ advance(p); // past `case`
+ let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc);
+ let eh: *node = nil;
+ let et: *node = nil;
+ if (p.curkind != tkind.TK_COLON) {
+ p.nocast = 1;
+ for (true) {
+ let e: *node = parseexpr(p);
+ if (eh == nil) { eh = e; }
+ else { et.next = e; };
+ et = e;
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ };
+ p.nocast = 0;
+ };
+ cs.list = eh;
+ expecttok(p, tkind.TK_COLON, "expected ':' after case label");
+ let bh: *node = nil;
+ let bt: *node = nil;
+ for (p.curkind != tkind.TK_CASE) {
+ if (p.curkind == tkind.TK_RBRACE) { break; };
+ if (p.curkind == tkind.TK_EOF) { break; };
+ let s: *node = parsestmt(p);
+ if (s != nil) {
+ if (bh == nil) { bh = s; }
+ else { bt.next = s; };
+ bt = s;
+ };
+ };
+ let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc);
+ blk.list = bh;
+ cs.body = blk;
+ if (head == nil) { head = cs; }
+ else { tail.next = cs; };
+ tail = cs;
+ };
+ expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch");
+ n.list = head;
+ return n;
+};
+
+fn parsestmt(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+
+ // `static` is allowed on local lets per Hare; we accept and skip
+ // it (it doesn't change the AST shape).
+ if (p.curkind == tkind.TK_STATIC) { advance(p); };
+
+ if (p.curkind == tkind.TK_LBRACE) {
+ let b: *node = parseblock(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after block");
+ return b;
+ };
+ if (p.curkind == tkind.TK_LET) { return parseletlocal(p); };
+ if (p.curkind == tkind.TK_CONST) { return parseletlocal(p); };
+ if (p.curkind == tkind.TK_IF) {
+ let n: *node = parseif(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after if");
+ return n;
+ };
+ if (p.curkind == tkind.TK_FOR) {
+ let n: *node = parsefor(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after for");
+ return n;
+ };
+ if (p.curkind == tkind.TK_SWITCH) {
+ let n: *node = parseswitch(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after switch");
+ return n;
+ };
+ if (p.curkind == tkind.TK_RETURN) {
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc);
+ if (p.curkind != tkind.TK_SEMI) {
+ let first: *node = parseexpr(p);
+ // Hare-style multi-value: `return a, b;` becomes a
+ // tuple expression so codegen sees one rvalue.
+ if (p.curkind == tkind.TK_COMMA) {
+ let t: *node = newnode(p.a, nkind.N_TUPLE, pf, pl, pc);
+ t.list = first;
+ let tail: *node = first;
+ for (accepttok(p, tkind.TK_COMMA)) {
+ let e: *node = parseexpr(p);
+ tail.next = e;
+ tail = e;
+ };
+ n.lhs = t;
+ } else {
+ n.lhs = first;
+ };
+ };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after return");
+ return n;
+ };
+ if (p.curkind == tkind.TK_DEFER) {
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_DEFER, pf, pl, pc);
+ n.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after defer");
+ return n;
+ };
+ if (p.curkind == tkind.TK_YIELD) {
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_YIELD, pf, pl, pc);
+ n.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after yield");
+ return n;
+ };
+ if (p.curkind == tkind.TK_BREAK) {
+ advance(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after break");
+ return newnode(p.a, nkind.N_BREAK, pf, pl, pc);
+ };
+ if (p.curkind == tkind.TK_CONTINUE) {
+ advance(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after continue");
+ return newnode(p.a, nkind.N_CONTINUE, pf, pl, pc);
+ };
+ // expression statement, or tuple-destructure multi-assign:
+ // a, b = expr;
+ // Mirrors cmd/wcc/parse.c:1015-1031. We parse the first lvalue
+ // with parseexpr (matches the C side); subsequent lvalues go
+ // through parsebin(parseunary, 1) so the `=` stays for us to
+ // consume — parseexpr would absorb it.
+ let e: *node = parseexpr(p);
+ if (p.curkind == tkind.TK_COMMA) {
+ let m: *node = newnode(p.a, nkind.N_MASSIGN, pf, pl, pc);
+ let head: *node = e;
+ let tail: *node = e;
+ for (p.curkind == tkind.TK_COMMA) {
+ advance(p);
+ let lv: *node = parsebin(p, parseunary(p), 1);
+ tail.next = lv;
+ tail = lv;
+ };
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' after multi-assign lvalues");
+ m.rhs = parseexpr(p);
+ m.list = head;
+ expecttok(p, tkind.TK_SEMI, "expected ';' after multi-assign");
+ return m;
+ };
+ let n: *node = newnode(p.a, nkind.N_EXPRSTMT, pf, pl, pc);
+ n.lhs = e;
+ expecttok(p, tkind.TK_SEMI, "expected ';' after expression statement");
+ return n;
+};
+
+
// lib/ww/typ.ww — port of cmd/wcc/type.c.
//
// Status: full structural port. The C version uses module-globals for
@@ -5637,9 +5624,9 @@ export fn typeeq(a: *tinfo, b: *tinfo) bool = {
package ww;
+// Sibling imports (typ, ast) auto-resolve via task #22 dir-enum
+// when callers `import ww;` or pull all three separately.
import mem;
-import typ;
-import ast;
// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind.
type skind = enum i32 {
diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww
index a9183579..29400769 100644
--- a/selfhost/cmd/ww/main.combined.ww
+++ b/selfhost/cmd/ww/main.combined.ww
@@ -1089,18 +1089,31 @@ fn visitadd(c: *expctx, path: str) void = {
c.visit = n;
};
-// Try /.ww then //.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: /.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 // as a directory, then /.ww as a file.
+// Sets *isdir on hit. Symmetric with cstage locate_import_in for
+// byte-id driver output (rule 10). The legacy //.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: //.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 ;` (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 ;` (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 ;` 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 /*.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 ;` (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, -l); 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, -l); 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: /;
+ // 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 .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) /.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_ 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);
diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww
index 574eb14a..d7cd5f77 100644
--- a/selfhost/cmd/ww/main.ww
+++ b/selfhost/cmd/ww/main.ww
@@ -229,18 +229,31 @@ fn visitadd(c: *expctx, path: str) void = {
c.visit = n;
};
-// Try /.ww then //.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: /.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 // as a directory, then /.ww as a file.
+// Sets *isdir on hit. Symmetric with cstage locate_import_in for
+// byte-id driver output (rule 10). The legacy //.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: //.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 ;` (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 ;` (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 ;` 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 /*.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 ;` (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, -l); 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, -l); 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: /;
+ // 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 .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) /.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_ 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);
diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww
index 9f08d71f..42b3dec3 100644
--- a/selfhost/cmd/wwdump/main.combined.ww
+++ b/selfhost/cmd/wwdump/main.combined.ww
@@ -1386,7 +1386,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.
@@ -2605,10 +2605,11 @@ export fn strcasecmp(a: str, b: str) i32 = {
package lex;
+// Sibling import (tok) auto-resolves via task #22 dir-enum when
+// callers `import lex;` (which dir-enums lib/ww/lex/).
import os;
import ascii;
import mem;
-import tok;
// isidstart / isidpart — identifier classification. Lexer-local
// because the "alpha or '_' / alnum or '_'" set isn't part of Hare's
@@ -3744,6 +3745,189 @@ export fn astprint(fd: i32, n: *node) void = {
pr(fd, n, 0);
};
+// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww.
+
+package parse;
+
+import os;
+import mem;
+import tok;
+
+// `import encoding.utf8;` — the driver resolves the dotted path to
+// a directory; only the leaf (`utf8`) is needed downstream as the
+// module bareword for n_use → decl disambiguation, mirroring Hare's
+// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7
+// stores `[]str` but identifier-resolution uses the last component).
+fn parseuse(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `use`
+ let n: *node = newnode(p.a, nkind.N_USE, pf, pl, pc);
+ n.nmod = p.curmod;
+ let leaf: str;
+ expectident(p, &leaf);
+ for (p.curkind == tkind.TK_DOT) {
+ advance(p); // past `.`
+ expectident(p, &leaf);
+ };
+ n.str = leaf;
+ expecttok(p, tkind.TK_SEMI, "expected ';' after use");
+ return n;
+};
+
+fn parsedef(p: *parser, exported: i32) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `def`
+ let n: *node = newnode(p.a, nkind.N_DEF, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectident(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_COLON, "expected ':' in def");
+ n.lhs = parsetype(p);
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' in def");
+ n.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after def");
+ n.exported = exported;
+ return n;
+};
+
+fn parselet(p: *parser, exported: i32) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ // Accept `let` or `const`. Const-bound bindings are marked via
+ // n.op = tkind.TK_CONST so the checker can reject reassignment.
+ let is_const: i32 = 0;
+ if (p.curkind == tkind.TK_CONST) { is_const = 1; };
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectbindname(p, &id);
+ n.str = id;
+ if (accepttok(p, tkind.TK_COLON)) {
+ n.lhs = parsetype(p);
+ };
+ if (accepttok(p, tkind.TK_ASSIGN)) {
+ n.rhs = parseexpr(p);
+ };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ n.exported = exported;
+ if (is_const != 0) { n.op = tkind.TK_CONST; };
+ return n;
+};
+
+fn parseattrs(p: *parser) *node = {
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (p.curkind == tkind.TK_AT) {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p);
+ let a: *node = newnode(p.a, nkind.N_ATTR, pf, pl, pc);
+ let id: str;
+ expectident(p, &id);
+ a.str = id;
+ // `@name(args...)` for FFI-style attrs; `@name` for marker-
+ // only attrs like @test (no parens).
+ if (accepttok(p, tkind.TK_LPAREN)) {
+ let arghead: *node = nil;
+ parsearglist(p, tkind.TK_RPAREN, &arghead);
+ a.list = arghead;
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args");
+ };
+ if (head == nil) { head = a; tail = a; }
+ else { tail.next = a; tail = a; };
+ };
+ return head;
+};
+
+fn parseparams(p: *parser) *node = {
+ if (p.curkind == tkind.TK_RPAREN) { return nil; };
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (true) {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ let n: *node = newnode(p.a, nkind.N_PARAM, pf, pl, pc);
+ // Param form: (IDENT|'_') ':' type. Anonymous-type-only params
+ // (used in fn type expressions) aren't yet wired here.
+ let id: str;
+ expectbindname(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_COLON, "expected ':' in parameter");
+ n.lhs = parsetype(p);
+ // Hare-style variadic: `name: T...`. Marker on n.op so check
+ // promotes the param's type to []T and call sites gather /
+ // forward. Mirrors cmd/wcc/parse.c parseparams.
+ if (accepttok(p, tkind.TK_ELLIPSIS)) {
+ n.op = tkind.TK_ELLIPSIS;
+ };
+ if (head == nil) { head = n; tail = n; }
+ else { tail.next = n; tail = n; };
+ if (n.op == tkind.TK_ELLIPSIS) {
+ break; // variadic must be the last param
+ };
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ if (p.curkind == tkind.TK_RPAREN) { break; };
+ };
+ return head;
+};
+
+fn parsefn(p: *parser, exported: i32, attrs: *node) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `fn`
+ let n: *node = newnode(p.a, nkind.N_FNDECL, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectident(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name");
+ n.list = parseparams(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after params");
+ if (p.curkind != tkind.TK_ASSIGN) {
+ if (p.curkind != tkind.TK_SEMI) {
+ n.lhs = parsetype(p);
+ };
+ };
+ if (accepttok(p, tkind.TK_ASSIGN)) {
+ n.body = parseblock(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after fn body");
+ } else {
+ // Body-less fn: FFI declaration (`fn name(args) ret;`).
+ expecttok(p, tkind.TK_SEMI, "expected ';' after fn header");
+ };
+ n.exported = exported;
+ n.attr = attrs;
+ return n;
+};
+
+fn parsetypedecl(p: *parser, exported: i32) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `type`
+ let n: *node = newnode(p.a, nkind.N_TYPEDECL, pf, pl, pc);
+ n.nmod = p.curmod;
+ let id: str;
+ expectident(p, &id);
+ n.str = id;
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl");
+ n.lhs = parsetype(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after type decl");
+ n.exported = exported;
+ return n;
+};
+
+
// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww.
package parse;
@@ -4210,618 +4394,6 @@ fn parseexpr(p: *parser) *node = {
};
-// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww.
-
-package parse;
-
-import os;
-import mem;
-import tok;
-
-fn parseletlocal(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- // `let` or `const`. Const-bound locals are marked via n.op = tkind.TK_CONST.
- let is_const: i32 = 0;
- if (p.curkind == tkind.TK_CONST) { is_const = 1; };
- advance(p);
-
- // Hare-style tuple destructure: `let (a, b) = expr;`.
- // Types are optional per binding (matches C parser; Hare itself
- // doesn't allow types here, but cmd/wcc/parse.c does).
- if (p.curkind == tkind.TK_LPAREN) {
- advance(p);
- let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
- let head: *node = nil;
- let tail: *node = nil;
- for (true) {
- let lpf: str = p.curfile;
- let lpl: i32 = p.curline;
- let lpc: i32 = p.curcol;
- let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
- let id: str;
- expectbindname(p, &id);
- l.str = id;
- if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
- if (head == nil) { head = l; }
- else { tail.next = l; };
- tail = l;
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- };
- expecttok(p, tkind.TK_RPAREN, "expected ')' in let destructure");
- expecttok(p, tkind.TK_ASSIGN, "expected '=' after let destructure");
- m.rhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- m.list = head;
- if (is_const != 0) {
- m.op = tkind.TK_CONST;
- let lc: *node = head;
- for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
- };
- return m;
- };
-
- let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
- let id: str;
- expectbindname(p, &id);
- n.str = id;
- if (accepttok(p, tkind.TK_COLON)) {
- n.lhs = parsetype(p);
- };
- // Comma-multi-let: `let n, s = call();` (ww extension over Hare).
- // Collects (name, type) pairs, then '=' rhs. Each binding gets
- // its own nkind.N_LET; the wrapping nkind.N_MLET carries the rhs.
- if (p.curkind == tkind.TK_COMMA) {
- let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
- let head: *node = n;
- let tail: *node = n;
- for (accepttok(p, tkind.TK_COMMA)) {
- let lpf: str = p.curfile;
- let lpl: i32 = p.curline;
- let lpc: i32 = p.curcol;
- let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
- let id2: str;
- expectbindname(p, &id2);
- l.str = id2;
- if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
- tail.next = l;
- tail = l;
- };
- expecttok(p, tkind.TK_ASSIGN, "expected '=' after let names");
- m.rhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- m.list = head;
- if (is_const != 0) {
- m.op = tkind.TK_CONST;
- let lc: *node = head;
- for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
- };
- return m;
- };
- if (accepttok(p, tkind.TK_ASSIGN)) {
- n.rhs = parseexpr(p);
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- if (is_const != 0) { n.op = tkind.TK_CONST; };
- return n;
-};
-
-fn parseblock(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- expecttok(p, tkind.TK_LBRACE, "expected '{' to open block");
- let blk: *node = newnode(p.a, nkind.N_BLOCK, pf, pl, pc);
- let head: *node = nil;
- let tail: *node = nil;
- for (p.curkind != tkind.TK_RBRACE) {
- if (p.curkind == tkind.TK_EOF) { break; };
- let s: *node = parsestmt(p);
- if (s != nil) {
- if (head == nil) { head = s; tail = s; }
- else { tail.next = s; tail = s; };
- };
- };
- expecttok(p, tkind.TK_RBRACE, "expected '}' to close block");
- blk.list = head;
- return blk;
-};
-
-fn parseif(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `if`
- expecttok(p, tkind.TK_LPAREN, "expected '(' after if");
- let n: *node = newnode(p.a, nkind.N_IF, pf, pl, pc);
- n.cond = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after if condition");
- n.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) {
- if (p.curkind == tkind.TK_IF) {
- n.els = parseif(p);
- } else {
- n.els = parseblock(p);
- };
- };
- return n;
-};
-
-fn parsefor(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `for`
- expecttok(p, tkind.TK_LPAREN, "expected '(' after for");
-
- // Four forms (matching C parser):
- // for (cond) — only cond
- // for (init; cond; post) — C-style 3-clause
- // for (let x .. expr) — Hare-style range, single binding
- // for (let (a, b) .. expr) — range with tuple destructure
- // Range and 3-clause both lead with `let`, so we commit to consuming
- // `let` then disambiguate by looking at what follows.
- if (p.curkind == tkind.TK_LET) {
- advance(p); // past `let`
-
- // Tuple destructure: `for (let (a, b) .. expr)`.
- if (p.curkind == tkind.TK_LPAREN) {
- advance(p);
- let names: *node = nil;
- let ntail: *node = nil;
- for (true) {
- let npf: str = p.curfile;
- let npl: i32 = p.curline;
- let npc: i32 = p.curcol;
- let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc);
- let nm: str;
- expectbindname(p, &nm);
- e.str = nm;
- if (names == nil) { names = e; }
- else { ntail.next = e; };
- ntail = e;
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- };
- expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names");
- expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names");
- let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
- rng.list = names;
- rng.lhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- rng.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
- return rng;
- };
-
- // Single binding range or C-style let-init. We need to consume
- // the IDENT/UNDER to know which: if followed by '..' it's a
- // range; otherwise build a synthetic LET for the C-style for-init
- // with the consumed name baked in.
- if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) {
- let isunder: bool = (p.curkind == tkind.TK_UNDER);
- let nm: str;
- nm.ptr = nil; nm.len = 0;
- if (!isunder) { nm = p.curtext; };
- let lpf: str = p.curfile;
- let lpl: i32 = p.curline;
- let lpc: i32 = p.curcol;
- advance(p); // consume IDENT/UNDER
-
- if (p.curkind == tkind.TK_DOTDOT) {
- advance(p);
- let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
- rng.str = nm; // "" for `_`
- rng.lhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- rng.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
- return rng;
- };
-
- // Not a range — finish the let manually and continue as
- // a 3-clause for-init.
- let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
- first.str = nm;
- if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); };
- if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); };
- expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let");
- let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
- n.lhs = first;
- n.cond = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after for cond");
- n.rhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- n.body = parseblock(p);
- if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); };
- return n;
- };
-
- errmsg(p, "expected name after 'let' in for");
- };
-
- // for (cond) or for (cond; post)
- let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
- let first: *node = parseexpr(p);
- if (accepttok(p, tkind.TK_SEMI)) {
- n.cond = first;
- n.rhs = parseexpr(p);
- } else {
- n.cond = first;
- };
- expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
- n.body = parseblock(p);
- // Optional `else { ... }` — runs at normal cond-false exit; skipped
- // by break. Hare's "did the loop find it?" idiom.
- if (accepttok(p, tkind.TK_ELSE)) {
- n.els = parseblock(p);
- };
- return n;
-};
-
-fn parseswitch(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `switch`
- expecttok(p, tkind.TK_LPAREN, "expected '(' after switch");
- let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc);
- n.lhs = parseexpr(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression");
- expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body");
- let head: *node = nil;
- let tail: *node = nil;
- for (p.curkind == tkind.TK_CASE) {
- let cpf: str = p.curfile;
- let cpl: i32 = p.curline;
- let cpc: i32 = p.curcol;
- advance(p); // past `case`
- let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc);
- let eh: *node = nil;
- let et: *node = nil;
- if (p.curkind != tkind.TK_COLON) {
- p.nocast = 1;
- for (true) {
- let e: *node = parseexpr(p);
- if (eh == nil) { eh = e; }
- else { et.next = e; };
- et = e;
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- };
- p.nocast = 0;
- };
- cs.list = eh;
- expecttok(p, tkind.TK_COLON, "expected ':' after case label");
- let bh: *node = nil;
- let bt: *node = nil;
- for (p.curkind != tkind.TK_CASE) {
- if (p.curkind == tkind.TK_RBRACE) { break; };
- if (p.curkind == tkind.TK_EOF) { break; };
- let s: *node = parsestmt(p);
- if (s != nil) {
- if (bh == nil) { bh = s; }
- else { bt.next = s; };
- bt = s;
- };
- };
- let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc);
- blk.list = bh;
- cs.body = blk;
- if (head == nil) { head = cs; }
- else { tail.next = cs; };
- tail = cs;
- };
- expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch");
- n.list = head;
- return n;
-};
-
-fn parsestmt(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
-
- // `static` is allowed on local lets per Hare; we accept and skip
- // it (it doesn't change the AST shape).
- if (p.curkind == tkind.TK_STATIC) { advance(p); };
-
- if (p.curkind == tkind.TK_LBRACE) {
- let b: *node = parseblock(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after block");
- return b;
- };
- if (p.curkind == tkind.TK_LET) { return parseletlocal(p); };
- if (p.curkind == tkind.TK_CONST) { return parseletlocal(p); };
- if (p.curkind == tkind.TK_IF) {
- let n: *node = parseif(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after if");
- return n;
- };
- if (p.curkind == tkind.TK_FOR) {
- let n: *node = parsefor(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after for");
- return n;
- };
- if (p.curkind == tkind.TK_SWITCH) {
- let n: *node = parseswitch(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after switch");
- return n;
- };
- if (p.curkind == tkind.TK_RETURN) {
- advance(p);
- let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc);
- if (p.curkind != tkind.TK_SEMI) {
- let first: *node = parseexpr(p);
- // Hare-style multi-value: `return a, b;` becomes a
- // tuple expression so codegen sees one rvalue.
- if (p.curkind == tkind.TK_COMMA) {
- let t: *node = newnode(p.a, nkind.N_TUPLE, pf, pl, pc);
- t.list = first;
- let tail: *node = first;
- for (accepttok(p, tkind.TK_COMMA)) {
- let e: *node = parseexpr(p);
- tail.next = e;
- tail = e;
- };
- n.lhs = t;
- } else {
- n.lhs = first;
- };
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after return");
- return n;
- };
- if (p.curkind == tkind.TK_DEFER) {
- advance(p);
- let n: *node = newnode(p.a, nkind.N_DEFER, pf, pl, pc);
- n.lhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after defer");
- return n;
- };
- if (p.curkind == tkind.TK_YIELD) {
- advance(p);
- let n: *node = newnode(p.a, nkind.N_YIELD, pf, pl, pc);
- n.lhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after yield");
- return n;
- };
- if (p.curkind == tkind.TK_BREAK) {
- advance(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after break");
- return newnode(p.a, nkind.N_BREAK, pf, pl, pc);
- };
- if (p.curkind == tkind.TK_CONTINUE) {
- advance(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after continue");
- return newnode(p.a, nkind.N_CONTINUE, pf, pl, pc);
- };
- // expression statement, or tuple-destructure multi-assign:
- // a, b = expr;
- // Mirrors cmd/wcc/parse.c:1015-1031. We parse the first lvalue
- // with parseexpr (matches the C side); subsequent lvalues go
- // through parsebin(parseunary, 1) so the `=` stays for us to
- // consume — parseexpr would absorb it.
- let e: *node = parseexpr(p);
- if (p.curkind == tkind.TK_COMMA) {
- let m: *node = newnode(p.a, nkind.N_MASSIGN, pf, pl, pc);
- let head: *node = e;
- let tail: *node = e;
- for (p.curkind == tkind.TK_COMMA) {
- advance(p);
- let lv: *node = parsebin(p, parseunary(p), 1);
- tail.next = lv;
- tail = lv;
- };
- expecttok(p, tkind.TK_ASSIGN, "expected '=' after multi-assign lvalues");
- m.rhs = parseexpr(p);
- m.list = head;
- expecttok(p, tkind.TK_SEMI, "expected ';' after multi-assign");
- return m;
- };
- let n: *node = newnode(p.a, nkind.N_EXPRSTMT, pf, pl, pc);
- n.lhs = e;
- expecttok(p, tkind.TK_SEMI, "expected ';' after expression statement");
- return n;
-};
-
-
-// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww.
-
-package parse;
-
-import os;
-import mem;
-import tok;
-
-fn parseuse(p: *parser) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `use`
- let n: *node = newnode(p.a, nkind.N_USE, pf, pl, pc);
- n.nmod = p.curmod;
- // Accept a dotted import path: `use encoding.utf8;` — capture the
- // full dotted form on n.str. Leaf-only SK_USE install lives in
- // the check stage; the lexer-side join happens here.
- let id: str;
- expectident(p, &id);
- n.str = id;
- for (p.curkind == tkind.TK_DOT) {
- advance(p); // past `.`
- let seg: str;
- expectident(p, &seg);
- // Concatenate id + "." + seg into a fresh str. Plan-9
- // separator per user pick over Hare's `::`.
- let total: i32 = n.str.len + 1 + seg.len;
- let buf: *u8 = amalloc(p.a, total: u64 + 1u64): *u8;
- let i: i32 = 0;
- for (i < n.str.len) { buf[i] = n.str[i]; i += 1; };
- buf[i] = 46u8; // '.'
- i += 1;
- let j: i32 = 0;
- for (j < seg.len) { buf[i + j] = seg[j]; j += 1; };
- buf[total] = 0u8;
- let joined: str;
- joined.ptr = buf;
- joined.len = total;
- n.str = joined;
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after use");
- return n;
-};
-
-fn parsedef(p: *parser, exported: i32) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `def`
- let n: *node = newnode(p.a, nkind.N_DEF, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectident(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_COLON, "expected ':' in def");
- n.lhs = parsetype(p);
- expecttok(p, tkind.TK_ASSIGN, "expected '=' in def");
- n.rhs = parseexpr(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after def");
- n.exported = exported;
- return n;
-};
-
-fn parselet(p: *parser, exported: i32) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- // Accept `let` or `const`. Const-bound bindings are marked via
- // n.op = tkind.TK_CONST so the checker can reject reassignment.
- let is_const: i32 = 0;
- if (p.curkind == tkind.TK_CONST) { is_const = 1; };
- advance(p);
- let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectbindname(p, &id);
- n.str = id;
- if (accepttok(p, tkind.TK_COLON)) {
- n.lhs = parsetype(p);
- };
- if (accepttok(p, tkind.TK_ASSIGN)) {
- n.rhs = parseexpr(p);
- };
- expecttok(p, tkind.TK_SEMI, "expected ';' after let");
- n.exported = exported;
- if (is_const != 0) { n.op = tkind.TK_CONST; };
- return n;
-};
-
-fn parseattrs(p: *parser) *node = {
- let head: *node = nil;
- let tail: *node = nil;
- for (p.curkind == tkind.TK_AT) {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p);
- let a: *node = newnode(p.a, nkind.N_ATTR, pf, pl, pc);
- let id: str;
- expectident(p, &id);
- a.str = id;
- // `@name(args...)` for FFI-style attrs; `@name` for marker-
- // only attrs like @test (no parens).
- if (accepttok(p, tkind.TK_LPAREN)) {
- let arghead: *node = nil;
- parsearglist(p, tkind.TK_RPAREN, &arghead);
- a.list = arghead;
- expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args");
- };
- if (head == nil) { head = a; tail = a; }
- else { tail.next = a; tail = a; };
- };
- return head;
-};
-
-fn parseparams(p: *parser) *node = {
- if (p.curkind == tkind.TK_RPAREN) { return nil; };
- let head: *node = nil;
- let tail: *node = nil;
- for (true) {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- let n: *node = newnode(p.a, nkind.N_PARAM, pf, pl, pc);
- // Param form: (IDENT|'_') ':' type. Anonymous-type-only params
- // (used in fn type expressions) aren't yet wired here.
- let id: str;
- expectbindname(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_COLON, "expected ':' in parameter");
- n.lhs = parsetype(p);
- // Hare-style variadic: `name: T...`. Marker on n.op so check
- // promotes the param's type to []T and call sites gather /
- // forward. Mirrors cmd/wcc/parse.c parseparams.
- if (accepttok(p, tkind.TK_ELLIPSIS)) {
- n.op = tkind.TK_ELLIPSIS;
- };
- if (head == nil) { head = n; tail = n; }
- else { tail.next = n; tail = n; };
- if (n.op == tkind.TK_ELLIPSIS) {
- break; // variadic must be the last param
- };
- if (!accepttok(p, tkind.TK_COMMA)) { break; };
- if (p.curkind == tkind.TK_RPAREN) { break; };
- };
- return head;
-};
-
-fn parsefn(p: *parser, exported: i32, attrs: *node) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `fn`
- let n: *node = newnode(p.a, nkind.N_FNDECL, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectident(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name");
- n.list = parseparams(p);
- expecttok(p, tkind.TK_RPAREN, "expected ')' after params");
- if (p.curkind != tkind.TK_ASSIGN) {
- if (p.curkind != tkind.TK_SEMI) {
- n.lhs = parsetype(p);
- };
- };
- if (accepttok(p, tkind.TK_ASSIGN)) {
- n.body = parseblock(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after fn body");
- } else {
- // Body-less fn: FFI declaration (`fn name(args) ret;`).
- expecttok(p, tkind.TK_SEMI, "expected ';' after fn header");
- };
- n.exported = exported;
- n.attr = attrs;
- return n;
-};
-
-fn parsetypedecl(p: *parser, exported: i32) *node = {
- let pf: str = p.curfile;
- let pl: i32 = p.curline;
- let pc: i32 = p.curcol;
- advance(p); // past `type`
- let n: *node = newnode(p.a, nkind.N_TYPEDECL, pf, pl, pc);
- n.nmod = p.curmod;
- let id: str;
- expectident(p, &id);
- n.str = id;
- expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl");
- n.lhs = parsetype(p);
- expecttok(p, tkind.TK_SEMI, "expected ';' after type decl");
- n.exported = exported;
- return n;
-};
-
-
// lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing).
//
// Split into Hare-style submodule: parse.ww (here) holds the parser
@@ -4836,12 +4408,12 @@ fn parsetypedecl(p: *parser, exported: i32) *node = {
package parse;
+// Sibling imports (expr, stmt, decl) auto-resolve via task #22
+// dir-enum when callers `import parse;` (which dir-enums
+// lib/ww/parse/).
import os;
import mem;
import tok;
-import expr;
-import stmt;
-import decl;
type parser = struct {
l: *lex,
@@ -5285,6 +4857,421 @@ export fn parsefile(p: *parser) *node = {
return f;
};
+// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww.
+
+package parse;
+
+import os;
+import mem;
+import tok;
+
+fn parseletlocal(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ // `let` or `const`. Const-bound locals are marked via n.op = tkind.TK_CONST.
+ let is_const: i32 = 0;
+ if (p.curkind == tkind.TK_CONST) { is_const = 1; };
+ advance(p);
+
+ // Hare-style tuple destructure: `let (a, b) = expr;`.
+ // Types are optional per binding (matches C parser; Hare itself
+ // doesn't allow types here, but cmd/wcc/parse.c does).
+ if (p.curkind == tkind.TK_LPAREN) {
+ advance(p);
+ let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (true) {
+ let lpf: str = p.curfile;
+ let lpl: i32 = p.curline;
+ let lpc: i32 = p.curcol;
+ let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
+ let id: str;
+ expectbindname(p, &id);
+ l.str = id;
+ if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
+ if (head == nil) { head = l; }
+ else { tail.next = l; };
+ tail = l;
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ };
+ expecttok(p, tkind.TK_RPAREN, "expected ')' in let destructure");
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' after let destructure");
+ m.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ m.list = head;
+ if (is_const != 0) {
+ m.op = tkind.TK_CONST;
+ let lc: *node = head;
+ for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
+ };
+ return m;
+ };
+
+ let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc);
+ let id: str;
+ expectbindname(p, &id);
+ n.str = id;
+ if (accepttok(p, tkind.TK_COLON)) {
+ n.lhs = parsetype(p);
+ };
+ // Comma-multi-let: `let n, s = call();` (ww extension over Hare).
+ // Collects (name, type) pairs, then '=' rhs. Each binding gets
+ // its own nkind.N_LET; the wrapping nkind.N_MLET carries the rhs.
+ if (p.curkind == tkind.TK_COMMA) {
+ let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc);
+ let head: *node = n;
+ let tail: *node = n;
+ for (accepttok(p, tkind.TK_COMMA)) {
+ let lpf: str = p.curfile;
+ let lpl: i32 = p.curline;
+ let lpc: i32 = p.curcol;
+ let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
+ let id2: str;
+ expectbindname(p, &id2);
+ l.str = id2;
+ if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); };
+ tail.next = l;
+ tail = l;
+ };
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' after let names");
+ m.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ m.list = head;
+ if (is_const != 0) {
+ m.op = tkind.TK_CONST;
+ let lc: *node = head;
+ for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; };
+ };
+ return m;
+ };
+ if (accepttok(p, tkind.TK_ASSIGN)) {
+ n.rhs = parseexpr(p);
+ };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after let");
+ if (is_const != 0) { n.op = tkind.TK_CONST; };
+ return n;
+};
+
+fn parseblock(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ expecttok(p, tkind.TK_LBRACE, "expected '{' to open block");
+ let blk: *node = newnode(p.a, nkind.N_BLOCK, pf, pl, pc);
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (p.curkind != tkind.TK_RBRACE) {
+ if (p.curkind == tkind.TK_EOF) { break; };
+ let s: *node = parsestmt(p);
+ if (s != nil) {
+ if (head == nil) { head = s; tail = s; }
+ else { tail.next = s; tail = s; };
+ };
+ };
+ expecttok(p, tkind.TK_RBRACE, "expected '}' to close block");
+ blk.list = head;
+ return blk;
+};
+
+fn parseif(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `if`
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after if");
+ let n: *node = newnode(p.a, nkind.N_IF, pf, pl, pc);
+ n.cond = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after if condition");
+ n.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) {
+ if (p.curkind == tkind.TK_IF) {
+ n.els = parseif(p);
+ } else {
+ n.els = parseblock(p);
+ };
+ };
+ return n;
+};
+
+fn parsefor(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `for`
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after for");
+
+ // Four forms (matching C parser):
+ // for (cond) — only cond
+ // for (init; cond; post) — C-style 3-clause
+ // for (let x .. expr) — Hare-style range, single binding
+ // for (let (a, b) .. expr) — range with tuple destructure
+ // Range and 3-clause both lead with `let`, so we commit to consuming
+ // `let` then disambiguate by looking at what follows.
+ if (p.curkind == tkind.TK_LET) {
+ advance(p); // past `let`
+
+ // Tuple destructure: `for (let (a, b) .. expr)`.
+ if (p.curkind == tkind.TK_LPAREN) {
+ advance(p);
+ let names: *node = nil;
+ let ntail: *node = nil;
+ for (true) {
+ let npf: str = p.curfile;
+ let npl: i32 = p.curline;
+ let npc: i32 = p.curcol;
+ let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc);
+ let nm: str;
+ expectbindname(p, &nm);
+ e.str = nm;
+ if (names == nil) { names = e; }
+ else { ntail.next = e; };
+ ntail = e;
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ };
+ expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names");
+ expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names");
+ let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
+ rng.list = names;
+ rng.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ rng.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
+ return rng;
+ };
+
+ // Single binding range or C-style let-init. We need to consume
+ // the IDENT/UNDER to know which: if followed by '..' it's a
+ // range; otherwise build a synthetic LET for the C-style for-init
+ // with the consumed name baked in.
+ if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) {
+ let isunder: bool = (p.curkind == tkind.TK_UNDER);
+ let nm: str;
+ nm.ptr = nil; nm.len = 0;
+ if (!isunder) { nm = p.curtext; };
+ let lpf: str = p.curfile;
+ let lpl: i32 = p.curline;
+ let lpc: i32 = p.curcol;
+ advance(p); // consume IDENT/UNDER
+
+ if (p.curkind == tkind.TK_DOTDOT) {
+ advance(p);
+ let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc);
+ rng.str = nm; // "" for `_`
+ rng.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ rng.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); };
+ return rng;
+ };
+
+ // Not a range — finish the let manually and continue as
+ // a 3-clause for-init.
+ let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc);
+ first.str = nm;
+ if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); };
+ if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let");
+ let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
+ n.lhs = first;
+ n.cond = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after for cond");
+ n.rhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ n.body = parseblock(p);
+ if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); };
+ return n;
+ };
+
+ errmsg(p, "expected name after 'let' in for");
+ };
+
+ // for (cond) or for (cond; post)
+ let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc);
+ let first: *node = parseexpr(p);
+ if (accepttok(p, tkind.TK_SEMI)) {
+ n.cond = first;
+ n.rhs = parseexpr(p);
+ } else {
+ n.cond = first;
+ };
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
+ n.body = parseblock(p);
+ // Optional `else { ... }` — runs at normal cond-false exit; skipped
+ // by break. Hare's "did the loop find it?" idiom.
+ if (accepttok(p, tkind.TK_ELSE)) {
+ n.els = parseblock(p);
+ };
+ return n;
+};
+
+fn parseswitch(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+ advance(p); // past `switch`
+ expecttok(p, tkind.TK_LPAREN, "expected '(' after switch");
+ let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc);
+ n.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression");
+ expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body");
+ let head: *node = nil;
+ let tail: *node = nil;
+ for (p.curkind == tkind.TK_CASE) {
+ let cpf: str = p.curfile;
+ let cpl: i32 = p.curline;
+ let cpc: i32 = p.curcol;
+ advance(p); // past `case`
+ let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc);
+ let eh: *node = nil;
+ let et: *node = nil;
+ if (p.curkind != tkind.TK_COLON) {
+ p.nocast = 1;
+ for (true) {
+ let e: *node = parseexpr(p);
+ if (eh == nil) { eh = e; }
+ else { et.next = e; };
+ et = e;
+ if (!accepttok(p, tkind.TK_COMMA)) { break; };
+ };
+ p.nocast = 0;
+ };
+ cs.list = eh;
+ expecttok(p, tkind.TK_COLON, "expected ':' after case label");
+ let bh: *node = nil;
+ let bt: *node = nil;
+ for (p.curkind != tkind.TK_CASE) {
+ if (p.curkind == tkind.TK_RBRACE) { break; };
+ if (p.curkind == tkind.TK_EOF) { break; };
+ let s: *node = parsestmt(p);
+ if (s != nil) {
+ if (bh == nil) { bh = s; }
+ else { bt.next = s; };
+ bt = s;
+ };
+ };
+ let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc);
+ blk.list = bh;
+ cs.body = blk;
+ if (head == nil) { head = cs; }
+ else { tail.next = cs; };
+ tail = cs;
+ };
+ expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch");
+ n.list = head;
+ return n;
+};
+
+fn parsestmt(p: *parser) *node = {
+ let pf: str = p.curfile;
+ let pl: i32 = p.curline;
+ let pc: i32 = p.curcol;
+
+ // `static` is allowed on local lets per Hare; we accept and skip
+ // it (it doesn't change the AST shape).
+ if (p.curkind == tkind.TK_STATIC) { advance(p); };
+
+ if (p.curkind == tkind.TK_LBRACE) {
+ let b: *node = parseblock(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after block");
+ return b;
+ };
+ if (p.curkind == tkind.TK_LET) { return parseletlocal(p); };
+ if (p.curkind == tkind.TK_CONST) { return parseletlocal(p); };
+ if (p.curkind == tkind.TK_IF) {
+ let n: *node = parseif(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after if");
+ return n;
+ };
+ if (p.curkind == tkind.TK_FOR) {
+ let n: *node = parsefor(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after for");
+ return n;
+ };
+ if (p.curkind == tkind.TK_SWITCH) {
+ let n: *node = parseswitch(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after switch");
+ return n;
+ };
+ if (p.curkind == tkind.TK_RETURN) {
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc);
+ if (p.curkind != tkind.TK_SEMI) {
+ let first: *node = parseexpr(p);
+ // Hare-style multi-value: `return a, b;` becomes a
+ // tuple expression so codegen sees one rvalue.
+ if (p.curkind == tkind.TK_COMMA) {
+ let t: *node = newnode(p.a, nkind.N_TUPLE, pf, pl, pc);
+ t.list = first;
+ let tail: *node = first;
+ for (accepttok(p, tkind.TK_COMMA)) {
+ let e: *node = parseexpr(p);
+ tail.next = e;
+ tail = e;
+ };
+ n.lhs = t;
+ } else {
+ n.lhs = first;
+ };
+ };
+ expecttok(p, tkind.TK_SEMI, "expected ';' after return");
+ return n;
+ };
+ if (p.curkind == tkind.TK_DEFER) {
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_DEFER, pf, pl, pc);
+ n.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after defer");
+ return n;
+ };
+ if (p.curkind == tkind.TK_YIELD) {
+ advance(p);
+ let n: *node = newnode(p.a, nkind.N_YIELD, pf, pl, pc);
+ n.lhs = parseexpr(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after yield");
+ return n;
+ };
+ if (p.curkind == tkind.TK_BREAK) {
+ advance(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after break");
+ return newnode(p.a, nkind.N_BREAK, pf, pl, pc);
+ };
+ if (p.curkind == tkind.TK_CONTINUE) {
+ advance(p);
+ expecttok(p, tkind.TK_SEMI, "expected ';' after continue");
+ return newnode(p.a, nkind.N_CONTINUE, pf, pl, pc);
+ };
+ // expression statement, or tuple-destructure multi-assign:
+ // a, b = expr;
+ // Mirrors cmd/wcc/parse.c:1015-1031. We parse the first lvalue
+ // with parseexpr (matches the C side); subsequent lvalues go
+ // through parsebin(parseunary, 1) so the `=` stays for us to
+ // consume — parseexpr would absorb it.
+ let e: *node = parseexpr(p);
+ if (p.curkind == tkind.TK_COMMA) {
+ let m: *node = newnode(p.a, nkind.N_MASSIGN, pf, pl, pc);
+ let head: *node = e;
+ let tail: *node = e;
+ for (p.curkind == tkind.TK_COMMA) {
+ advance(p);
+ let lv: *node = parsebin(p, parseunary(p), 1);
+ tail.next = lv;
+ tail = lv;
+ };
+ expecttok(p, tkind.TK_ASSIGN, "expected '=' after multi-assign lvalues");
+ m.rhs = parseexpr(p);
+ m.list = head;
+ expecttok(p, tkind.TK_SEMI, "expected ';' after multi-assign");
+ return m;
+ };
+ let n: *node = newnode(p.a, nkind.N_EXPRSTMT, pf, pl, pc);
+ n.lhs = e;
+ expecttok(p, tkind.TK_SEMI, "expected ';' after expression statement");
+ return n;
+};
+
+
// lib/ww/typ.ww — port of cmd/wcc/type.c.
//
// Status: full structural port. The C version uses module-globals for
@@ -5637,9 +5624,9 @@ export fn typeeq(a: *tinfo, b: *tinfo) bool = {
package ww;
+// Sibling imports (typ, ast) auto-resolve via task #22 dir-enum
+// when callers `import ww;` or pull all three separately.
import mem;
-import typ;
-import ast;
// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind.
type skind = enum i32 {
diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww
index 8cbe3aa3..d8a51b2a 100644
--- a/selfhost/test/smoke.combined.ww
+++ b/selfhost/test/smoke.combined.ww
@@ -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.
diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c
index ffaf6893..2beb9f2f 100644
--- a/test/wcc/700_e2e.c
+++ b/test/wcc/700_e2e.c
@@ -1748,14 +1748,8 @@ main(void)
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwe2e_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
- /* -I lib/encoding/utf8 for any fixture that pulls in fmt /
- * strconv / strings via `use` (transitive utf8.encoderune;
- * task #17). Unused -I is benign for fixtures that don't. */
- char cwd700[1024];
- if (getcwd(cwd700, sizeof cwd700) == NULL) { fail++; continue; }
- snprintf(cmd, sizeof cmd,
- "cd %s && %s/ww build -I %s/lib/encoding/utf8 %s",
- tmpdir, bin, cwd700, src);
+ snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
+ tmpdir, bin, src);
if (runwait(cmd) != 0) { fail++; continue; }
char outbin[128];
diff --git a/test/wcc/737_direnum.c b/test/wcc/737_direnum.c
new file mode 100644
index 00000000..b6d01d97
--- /dev/null
+++ b/test/wcc/737_direnum.c
@@ -0,0 +1,142 @@
+/*
+ * 737_direnum — driver-level sentinel for task #22 directory-as-
+ * module enumeration. Two row families pinning the contract:
+ *
+ * ok — multi-file dir is concatenated by both stages; the entry
+ * reads cross-pkg bare-leaf fns from sibling files. Build
+ * must succeed for both C-built `ww` and ww-built `ww_ww`.
+ * bad — multi-file dir with mismatched `package ;` decls
+ * triggers the strict-same-package error (task #25 subset
+ * bundled with #22 because the failure mode is dir-enum's
+ * own; cross-stage symmetric).
+ *
+ * Asm-presence isn't checked separately — 968_utf8_run, 966_strings_
+ * run, 995_self_rebuild already exercise dir-enum end-to-end at
+ * binary level. This file pins the cstage/wwstage symmetric error
+ * path so a regression on either driver fails loud.
+ */
+#include
+#include
+#include
+#include
+#include
+
+static int
+runwait(const char *cmd)
+{
+ int rc = system(cmd);
+ if (rc == -1) return -1;
+ if (WIFEXITED(rc)) return WEXITSTATUS(rc);
+ return 1;
+}
+
+static int
+runbuild(const char *driver, const char *src, const char *outbin)
+{
+ char cmd[2048];
+ snprintf(cmd, sizeof cmd, "%s build %s -o /dev/null >/dev/null 2>%s",
+ driver, src, outbin);
+ return runwait(cmd);
+}
+
+static int
+stderr_contains(const char *path, const char *needle)
+{
+ FILE *f = fopen(path, "rb");
+ if (!f) return 0;
+ char buf[4096];
+ size_t n = fread(buf, 1, sizeof buf - 1, f);
+ fclose(f);
+ buf[n] = '\0';
+ return strstr(buf, needle) != NULL;
+}
+
+int
+main(void)
+{
+ const char *bin = getenv("BIN");
+ if (!bin) bin = "out/bin";
+ char absbin[1024];
+ if (bin[0] != '/') {
+ char cwd[1024];
+ if (getcwd(cwd, sizeof cwd) == NULL) return 1;
+ snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
+ bin = absbin;
+ }
+
+ char ww[1280], ww_ww[1280];
+ snprintf(ww, sizeof ww, "%s/ww", bin);
+ snprintf(ww_ww, sizeof ww_ww, "%s/ww_ww", bin);
+
+ int total = 0, fail = 0;
+ char errp[64];
+
+ /* ok: cross-pkg bare-leaf via dir-enum, both stages. */
+ {
+ const char *src = "test/wcc/data/direnum/entry.ww";
+ char tmp[64];
+ snprintf(tmp, sizeof tmp, "/tmp/direnum_%d_ok", getpid());
+ char cmd[2048];
+
+ snprintf(cmd, sizeof cmd, "%s run %s >/dev/null 2>&1", ww, src);
+ total++;
+ if (runwait(cmd) != 0) {
+ fprintf(stderr, "737[ok-cstage]: ww run %s failed\n", src);
+ fail++;
+ }
+
+ if (access(ww_ww, X_OK) == 0) {
+ snprintf(cmd, sizeof cmd, "%s run %s >/dev/null 2>&1",
+ ww_ww, src);
+ total++;
+ if (runwait(cmd) != 0) {
+ fprintf(stderr, "737[ok-wwstage]: ww_ww run %s failed\n", src);
+ fail++;
+ }
+ }
+ (void)tmp;
+ }
+
+ /* bad: mismatched package decls in same dir → strict-same-package
+ * error. Both stages must surface "differs from" in stderr. */
+ {
+ const char *src = "test/wcc/data/direnum/bad_entry.ww";
+ snprintf(errp, sizeof errp, "/tmp/direnum_%d_bad.err", getpid());
+
+ char cmd[2048];
+ snprintf(cmd, sizeof cmd, "%s build %s 2>%s >/dev/null",
+ ww, src, errp);
+ int rc = runwait(cmd);
+ total++;
+ if (rc == 0) {
+ fprintf(stderr, "737[bad-cstage]: expected build failure, succeeded\n");
+ fail++;
+ } else if (!stderr_contains(errp, "differs from")) {
+ fprintf(stderr, "737[bad-cstage]: stderr missing 'differs from'\n");
+ fail++;
+ }
+ unlink(errp);
+
+ if (access(ww_ww, X_OK) == 0) {
+ snprintf(cmd, sizeof cmd, "%s build %s 2>%s >/dev/null",
+ ww_ww, src, errp);
+ rc = runwait(cmd);
+ total++;
+ if (rc == 0) {
+ fprintf(stderr, "737[bad-wwstage]: expected build failure, succeeded\n");
+ fail++;
+ } else if (!stderr_contains(errp, "differs from")) {
+ fprintf(stderr, "737[bad-wwstage]: stderr missing 'differs from'\n");
+ fail++;
+ }
+ unlink(errp);
+ }
+ }
+
+ if (fail) {
+ fprintf(stderr, "737_direnum: %d/%d fixtures failed\n", fail, total);
+ return 1;
+ }
+ printf("737_direnum: %d/%d ok\n", total, total);
+ return 0;
+}
diff --git a/test/wcc/738_module_decl.c b/test/wcc/738_module_decl.c
index 2ea928c8..76214ffe 100644
--- a/test/wcc/738_module_decl.c
+++ b/test/wcc/738_module_decl.c
@@ -98,6 +98,30 @@ main(void)
"package foo;\nimport encoding.utf8;\nfn x() void = {};\n")) pass++;
else { fprintf(stderr, "738[5] dotted import accept FAILED\n"); fail++; }
+ /* Row 6: dotted import stores only the leaf identifier on
+ * N_USE.str (post-task-#22 — the driver translates the full
+ * dotted path to a directory walk; the checker only needs the
+ * package bareword for n_use → decl disambiguation). */
+ {
+ Arena *a = newarena();
+ Lex l;
+ Parser p;
+ const char *src = "package foo;\nimport encoding.utf8;\n";
+ lexinit(&l, a, "", src, strlen(src));
+ parserinit(&p, a, &l);
+ Node *n = parsefile(&p);
+ int ok = 0;
+ if (n != NULL && p.errs == 0 && l.errs == 0 && n->list != NULL) {
+ Node *u = n->list;
+ ok = (u->kind == N_USE
+ && u->str != NULL
+ && strcmp(u->str, "utf8") == 0);
+ }
+ freearena(a);
+ if (ok) pass++;
+ else { fprintf(stderr, "738[6] dotted import leaf-store FAILED\n"); fail++; }
+ }
+
printf("738_module_decl: %d pass, %d fail\n", pass, fail);
return fail == 0 ? 0 : 1;
}
diff --git a/test/wcc/966_strings_run.c b/test/wcc/966_strings_run.c
index e41ef831..1bcf09d3 100644
--- a/test/wcc/966_strings_run.c
+++ b/test/wcc/966_strings_run.c
@@ -5,10 +5,6 @@
* Same thin-wrapper shape as 967_bytes_run / 968_utf8_run / 979_hex_run:
* stringstest.ww carries its own `export fn main()` that drives the
* @test fns and signals which case failed via the exit code.
- *
- * -I lib/encoding/utf8 is required because lib/strings.byteindex
- * encodes the rune-needle arm via utf8.encoderune; the import resolver
- * doesn't yet walk encoding/ subdirs (task #17).
*/
#include
#include
@@ -42,9 +38,8 @@ main(void)
const char *src = "lib/strings/stringstest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
- snprintf(cmd, sizeof cmd,
- "%s/ww run -I %s/lib/encoding/utf8 %s",
- bin, cwd, path);
+ snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
+ (void)cwd;
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "strings_run FAIL: %s exited %d\n", src, rc);
diff --git a/test/wcc/970_fmt_run.c b/test/wcc/970_fmt_run.c
index 34deac3f..7ec9027c 100644
--- a/test/wcc/970_fmt_run.c
+++ b/test/wcc/970_fmt_run.c
@@ -41,9 +41,8 @@ main(void)
const char *src = "lib/fmt/fmttest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
- /* -I lib/encoding/utf8 — task #17 */
- snprintf(cmd, sizeof cmd,
- "%s/ww run -I %s/lib/encoding/utf8 %s", bin, cwd, path);
+ snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
+ (void)cwd;
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "fmt_run FAIL: %s exited %d\n", src, rc);
diff --git a/test/wcc/971_log_run.c b/test/wcc/971_log_run.c
index 19cacef3..c5176e9a 100644
--- a/test/wcc/971_log_run.c
+++ b/test/wcc/971_log_run.c
@@ -41,9 +41,8 @@ main(void)
const char *src = "lib/log/logtest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
- /* -I lib/encoding/utf8 — task #17 */
- snprintf(cmd, sizeof cmd,
- "%s/ww run -I %s/lib/encoding/utf8 %s", bin, cwd, path);
+ snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
+ (void)cwd;
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "log_run FAIL: %s exited %d\n", src, rc);
diff --git a/test/wcc/972_fnmatch_run.c b/test/wcc/972_fnmatch_run.c
index 829ecfdd..f7b5439b 100644
--- a/test/wcc/972_fnmatch_run.c
+++ b/test/wcc/972_fnmatch_run.c
@@ -41,9 +41,8 @@ main(void)
const char *src = "lib/fnmatch/fnmatchtest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
- /* -I lib/encoding/utf8 — task #17 */
- snprintf(cmd, sizeof cmd,
- "%s/ww run -I %s/lib/encoding/utf8 %s", bin, cwd, path);
+ snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
+ (void)cwd;
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "fnmatch_run FAIL: %s exited %d\n", src, rc);
diff --git a/test/wcc/982_getopt_run.c b/test/wcc/982_getopt_run.c
index 99771a98..e19e873c 100644
--- a/test/wcc/982_getopt_run.c
+++ b/test/wcc/982_getopt_run.c
@@ -40,10 +40,8 @@ main(void)
const char *src = "lib/getopt/getopttest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
- /* -I lib/encoding/utf8: getopt -> strings -> utf8.encoderune
- * (task #17 — resolver doesn't yet walk encoding/ subdirs). */
- snprintf(cmd, sizeof cmd,
- "%s/ww run -I %s/lib/encoding/utf8 %s", bin, cwd, path);
+ snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
+ (void)cwd;
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "getopt_run FAIL: %s exited %d\n", src, rc);
diff --git a/test/wcc/990_selfhost.c b/test/wcc/990_selfhost.c
index 3b9d64bb..0b4171a3 100644
--- a/test/wcc/990_selfhost.c
+++ b/test/wcc/990_selfhost.c
@@ -99,11 +99,9 @@ probe_smoke(const char *bin)
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsh_%d", getpid());
mkdir(tmpdir, 0755);
char cmd[2048];
- /* -I lib/encoding/utf8: smoke.ww uses strconv -> strings ->
- * utf8.encoderune (task #17 — resolver doesn't walk encoding/). */
snprintf(cmd, sizeof cmd,
- "cd %s && %s/ww build -I %s/lib/encoding/utf8 %s/selfhost/test/smoke.ww >/dev/null 2>&1",
- tmpdir, bin, cwd, cwd);
+ "cd %s && %s/ww build %s/selfhost/test/smoke.ww >/dev/null 2>&1",
+ tmpdir, bin, cwd);
if (runwait(cmd) != 0) {
fprintf(stderr, "smoke FAIL: ww build did not succeed\n");
return -1;
@@ -745,8 +743,8 @@ probe_ww_links(const char *bin)
runwait(cmd);
/* ww build to get the .combined.ww as a side effect. */
snprintf(cmd, sizeof cmd,
- "cd %s && %s/ww build -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/lib/encoding/utf8 -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1",
- tmpdir, bin, cwd, cwd, cwd, cwd, cwd, tmpsrc);
+ "cd %s && %s/ww build -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1",
+ tmpdir, bin, cwd, cwd, cwd, cwd, tmpsrc);
if (runwait(cmd) != 0) {
fprintf(stderr, "ww-links FAIL: ww build %s\n", fix);
fail++;
diff --git a/test/wcc/995_self_rebuild.c b/test/wcc/995_self_rebuild.c
index 65dae848..16dc5078 100644
--- a/test/wcc/995_self_rebuild.c
+++ b/test/wcc/995_self_rebuild.c
@@ -62,9 +62,8 @@ slurp_eq(const char *a, const char *b)
/* Each tool builds via `ww_ww build -I -I lib/ww -I selfhost/cmd/wcc src`.
* lib/ww holds the language introspection (lex/tok/ast/parse/typ/sym);
* selfhost/cmd/wcc holds the compiler internals (mem/check/cgen*).
- * lib/encoding/utf8 carries the rune codec strings.byteindex needs;
- * the import resolver doesn't yet walk encoding/ subdirs (task #17),
- * so the dep travels as an explicit -I until it does.
+ * Dotted `import encoding.utf8;` finds lib/encoding/utf8/ via the
+ * driver's default srclib path post-task-#22 dir-enum.
* Some tools have a local module dir (w6a, w6l with sibling .ww files).
* inc_local is "" for tools without one (w6c, ww, wwdump).
*/
@@ -80,14 +79,14 @@ rebuild_one(const char *bin, const char *cwd, const char *tool,
if (inc_local && inc_local[0]) {
snprintf(cmd, sizeof cmd,
- "cd %s && %s/ww_ww build -I %s/%s -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/lib/encoding/utf8 -I %s/selfhost/cmd/wcc "
+ "cd %s && %s/ww_ww build -I %s/%s -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/selfhost/cmd/wcc "
"%s/%s >/dev/null 2>&1",
- workdir, bin, cwd, inc_local, cwd, cwd, cwd, cwd, cwd, cwd, src_rel);
+ workdir, bin, cwd, inc_local, cwd, cwd, cwd, cwd, cwd, src_rel);
} else {
snprintf(cmd, sizeof cmd,
- "cd %s && %s/ww_ww build -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/lib/encoding/utf8 -I %s/selfhost/cmd/wcc "
+ "cd %s && %s/ww_ww build -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/selfhost/cmd/wcc "
"%s/%s >/dev/null 2>&1",
- workdir, bin, cwd, cwd, cwd, cwd, cwd, cwd, src_rel);
+ workdir, bin, cwd, cwd, cwd, cwd, cwd, src_rel);
}
if (runwait(cmd) != 0) {
fprintf(stderr, "self-rebuild FAIL: ww_ww build errored on %s\n", tool);
diff --git a/test/wcc/data/direnum/bad_entry.ww b/test/wcc/data/direnum/bad_entry.ww
new file mode 100644
index 00000000..9915806f
--- /dev/null
+++ b/test/wcc/data/direnum/bad_entry.ww
@@ -0,0 +1,5 @@
+package main;
+
+import bad_pkg;
+
+export fn main() i32 = { return 0; };
diff --git a/test/wcc/data/direnum/bad_pkg/a.ww b/test/wcc/data/direnum/bad_pkg/a.ww
new file mode 100644
index 00000000..1586a808
--- /dev/null
+++ b/test/wcc/data/direnum/bad_pkg/a.ww
@@ -0,0 +1,3 @@
+package good;
+
+fn aaa() i32 = { return 1; };
diff --git a/test/wcc/data/direnum/bad_pkg/b.ww b/test/wcc/data/direnum/bad_pkg/b.ww
new file mode 100644
index 00000000..17f29b22
--- /dev/null
+++ b/test/wcc/data/direnum/bad_pkg/b.ww
@@ -0,0 +1,3 @@
+package bad;
+
+fn bbb() i32 = { return 2; };
diff --git a/test/wcc/data/direnum/entry.ww b/test/wcc/data/direnum/entry.ww
new file mode 100644
index 00000000..d8198f36
--- /dev/null
+++ b/test/wcc/data/direnum/entry.ww
@@ -0,0 +1,10 @@
+// Entry imports the dir; cross-pkg refs `ok.fromA` / `ok.fromB`
+// resolve via dir-enum pulling a.ww + b.ww (both package ok).
+package main;
+
+import ok;
+
+export fn main() i32 = {
+ if (ok.fromA() + ok.fromB() != 42) { return 1; };
+ return 0;
+};
diff --git a/test/wcc/data/direnum/ok/a.ww b/test/wcc/data/direnum/ok/a.ww
new file mode 100644
index 00000000..69b65ff6
--- /dev/null
+++ b/test/wcc/data/direnum/ok/a.ww
@@ -0,0 +1,3 @@
+package ok;
+
+export fn fromA() i32 = { return 7; };
diff --git a/test/wcc/data/direnum/ok/b.ww b/test/wcc/data/direnum/ok/b.ww
new file mode 100644
index 00000000..9817fc66
--- /dev/null
+++ b/test/wcc/data/direnum/ok/b.ww
@@ -0,0 +1,3 @@
+package ok;
+
+export fn fromB() i32 = { return 35; };