From 9e0816e19964493ad606b988011588e5d525e700 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 19:22:27 +0900 Subject: [PATCH] cmd+selfhost+lib+test: directory-as-module enumeration in driver (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the cmd/ww + selfhost driver's file-walk import resolver with true directory enumeration. `import encoding.utf8;` now finds the lib/encoding/utf8/ directory and concatenates every *.ww file in it (excluding *test.ww and the driver's *.combined.ww artifacts) in byte-wise sorted order, instead of just finding the single lib/encoding/utf8/utf8.ww file. Mirrors Hare's hare/module/srcs.ha:183 _findsrcs minus tag handling. Lookup order in both stages: (1) // as directory → enumerate. (2) /.ww as file. The legacy //.ww shape from #18's retained divergence is dropped per rule-9 Hare-fidelity — Hare has no foo/foo.ha fallback; a module IS the directory. Symmetric across cstage (cmd/ww/main.c via opendir+qsort+stat) and wwstage (selfhost/cmd/ww/main.ww via existing lib/os.getdents64 + os.stat — no new lib/os surface needed; the rundirtests() walker in main.ww from #18 was the model). Bootstrap ww2.s==ww3.s==ww4.s byte-identical post-change. Bundling justification (rule 11): strict-same-package validation is bundled because the failure mode is dir-enum's own (a non-dir-enum compilation unit cannot trigger mismatch across enumerated files). The natural enforcement site is the driver — the parser can't distinguish dir-enum concat from file-walk concat. Both stages peek each file's first `package ;` line in expand_dir / expanddir and exit(1) on mismatch with a precise error pointing at the offending file. Hare's hare/module/srcs.ha:131 has the same constraint via its README gate. Other half of #23 (strict missing-package error tightening — 63 inline-source test wrappers blocker) stays deferred per its filing. Parser side (cmd/wcc/parse.c parseuse + lib/ww/parse/decl.ww parseuse): n->str now carries only the LEAF identifier from a dotted import. With the driver translating the full dotted path to a directory walk, the checker only needs the package bareword (last component) for the N_USE → decl disambiguation walk in check.c's src_imports / decl_mod. Mirrors Hare's `use encoding::utf8;` → `utf8::name` semantics (ref/hare/hare/ast/import.ha:7). Migration: lib/ww/sym.ww drops `import typ; import ast;`; lib/ww/parse/parse.ww drops `import expr; import stmt; import decl;`; lib/ww/lex/lex.ww drops `import tok;` — all sibling imports auto-resolve via the new dir-enum when callers import the package directory. lib/strings/, lib/encoding/utf8/utf8test.ww migrate `import utf8;` → `import encoding.utf8;`. Makefile drops -I lib/encoding/utf8 stopgap from wwdump_ww + w6c_ww. Seven test wrappers (700_e2e, 966_strings_run, 970_fmt_run, 971_log_run, 972_fnmatch_run, 982_getopt_run, 990_selfhost) and 995_self_rebuild drop the -I lib/encoding/utf8 runtime stopgap. Tests: new 737_direnum C wrapper + test/wcc/data/direnum/ fixtures pin (a) cross-pkg multi-file dir-enum build at runtime (both stages must succeed) and (b) strict-same-package mismatch error (both stages must surface "differs from" + exit non-zero). 738_module_decl gains row 6 pinning the n_use->str leaf-only storage post-parser change. Retained workaround at selfhost/cmd/ww/main.ww expanddir loop: `names[i][k]` nested-deref-then-index split into `let nm: *u8 = names[i]; nm[k]` because wwstage cgen miscompiles the chained form (treats inner u8 element as 8B sizeof *u8 instead of 1B sizeof u8: extra MOVQ $8 + IMULQ on the inner index, MOVQ instead of MOVZBQ load). Inline rule-8 WHY comment cites task #24 (wwstage cgen chained-index inner element size on **T). Two-step form routes through the bare-pointer index path which both stages handle byte-identically. Class A wwstage cgen UNDER (chained-index inner element size on **T) surfaced first time the codebase exercises the **T[i][k] shape via enumeratedir() — corpus-coverage-blind landmine pattern, same family as the trio (#27/#28/#31) from STATUS-5. 112/112 ok. ww2 == ww3 == ww4 byte-id holds. --- Makefile | 9 +- cmd/wcc/parse.c | 14 +- cmd/ww/main.c | 315 +++++-- lib/encoding/utf8/utf8test.ww | 2 +- lib/strings/strings.ww | 2 +- lib/strings/stringstest.ww | 4 +- lib/ww/lex/lex.ww | 3 +- lib/ww/parse/decl.ww | 32 +- lib/ww/parse/parse.ww | 6 +- lib/ww/sym.ww | 4 +- selfhost/cmd/w6c/main.combined.ww | 1225 +++++++++++++------------- selfhost/cmd/ww/main.combined.ww | 493 +++++++++-- selfhost/cmd/ww/main.ww | 493 +++++++++-- selfhost/cmd/wwdump/main.combined.ww | 1225 +++++++++++++------------- selfhost/test/smoke.combined.ww | 2 +- test/wcc/700_e2e.c | 10 +- test/wcc/737_direnum.c | 142 +++ test/wcc/738_module_decl.c | 24 + test/wcc/966_strings_run.c | 9 +- test/wcc/970_fmt_run.c | 5 +- test/wcc/971_log_run.c | 5 +- test/wcc/972_fnmatch_run.c | 5 +- test/wcc/982_getopt_run.c | 6 +- test/wcc/990_selfhost.c | 10 +- test/wcc/995_self_rebuild.c | 13 +- test/wcc/data/direnum/bad_entry.ww | 5 + test/wcc/data/direnum/bad_pkg/a.ww | 3 + test/wcc/data/direnum/bad_pkg/b.ww | 3 + test/wcc/data/direnum/entry.ww | 10 + test/wcc/data/direnum/ok/a.ww | 3 + test/wcc/data/direnum/ok/b.ww | 3 + 31 files changed, 2521 insertions(+), 1564 deletions(-) create mode 100644 test/wcc/737_direnum.c create mode 100644 test/wcc/data/direnum/bad_entry.ww create mode 100644 test/wcc/data/direnum/bad_pkg/a.ww create mode 100644 test/wcc/data/direnum/bad_pkg/b.ww create mode 100644 test/wcc/data/direnum/entry.ww create mode 100644 test/wcc/data/direnum/ok/a.ww create mode 100644 test/wcc/data/direnum/ok/b.ww 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; };