From f138844737e9814a9f518627cfab6a25f7b87abe Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 21:25:16 +0900 Subject: [PATCH] ww: select sources by Go platform suffixes --- cmd/ww/main.c | 121 +++++++++++++++++-- internal/wwpackage/package.ww | 101 +++++++++++++++- selfhost/cmd/ww/main.ww | 216 ++++++++++++++++++++++++++++------ 3 files changed, 391 insertions(+), 47 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index ea13487a..5ba8db4f 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -372,6 +372,79 @@ strs_cmp(const void *a, const void *b) return strcmp(sa, sb); } +static int +sep_name_is(const char *s, size_t n, const char *word) +{ + return strlen(word) == n && memcmp(s, word, n) == 0; +} + +static int +sep_known_os(const char *s, size_t n) +{ + static const char *known[] = { + "aix", "android", "darwin", "dragonfly", "freebsd", "hurd", + "illumos", "ios", "js", "linux", "nacl", "netbsd", "openbsd", + "plan9", "solaris", "wasip1", "windows", "zos", + }; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) + if (sep_name_is(s, n, known[i])) return 1; + return 0; +} + +static int +sep_known_arch(const char *s, size_t n) +{ + static const char *known[] = { + "386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", + "loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", + "mips64p32le", "ppc", "ppc64", "ppc64le", "riscv", "riscv64", + "s390", "s390x", "sparc", "sparc64", "wasm", + }; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) + if (sep_name_is(s, n, known[i])) return 1; + return 0; +} + +static int +sep_target_tag(const char *s, size_t n) +{ + return sep_name_is(s, n, "linux") || sep_name_is(s, n, "amd64"); +} + +/* Go 1.26.5 build.go:1980-2027 filters known platform suffixes before + * opening a source. WW currently has one honest target, linux/amd64. */ +static int +sep_source_matches_target(const char *name) +{ + size_t stem = strcspn(name, "."); + if (memchr(name, '_', stem) == NULL) return 1; + + size_t end = stem; + size_t last = end; + while (last > 0 && name[last - 1] != '_') last--; + if (sep_name_is(name + last, end - last, "test")) { + if (last == 0) return 1; + end = last - 1; + last = end; + while (last > 0 && name[last - 1] != '_') last--; + } + if (last == 0) return 1; + + const char *final = name + last; + size_t nfinal = end - last; + size_t prevend = last - 1; + size_t prev = prevend; + while (prev > 0 && name[prev - 1] != '_') prev--; + if (prev < prevend + && sep_known_os(name + prev, prevend - prev) + && sep_known_arch(final, nfinal)) + return sep_target_tag(final, nfinal) + && sep_target_tag(name + prev, prevend - prev); + if (sep_known_os(final, nfinal) || sep_known_arch(final, nfinal)) + return sep_target_tag(final, nfinal); + return 1; +} + static int sep_slurp(const char*, char**, u64*); /* Go's contract: only *_test.ww is a test source. Ask the compiler parser, @@ -564,15 +637,39 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, { DIR *d = opendir(dirpath); if (d == NULL) { *out_files = NULL; return -1; } + char **names = NULL; + int nnames = 0, capnames = 0; char **prod = NULL, **tests = NULL; int nprod = 0, capprod = 0, ntests = 0, captests = 0; - struct dirent *ent; - while ((ent = readdir(d)) != NULL) { + struct dirent *ent = NULL; + for (;;) { + errno = 0; + ent = readdir(d); + if (ent == NULL) break; const char *nm = ent->d_name; size_t nl = strlen(nm); if (nl <= 3) continue; if (nm[0] == '.' || nm[0] == '_') continue; if (strcmp(nm + nl - 3, ".ww") != 0) continue; + if (source_list_add(&names, &nnames, &capnames, nm) < 0) { + source_list_free(names, nnames); + closedir(d); + *out_files = NULL; + return -2; + } + } + int direrr = errno; + closedir(d); + if (direrr != 0) { + source_list_free(names, nnames); + *out_files = NULL; + return -1; + } + if (nnames > 1) qsort(names, (size_t)nnames, sizeof *names, strs_cmp); + for (int ni = 0; ni < nnames; ni++) { + const char *nm = names[ni]; + size_t nl = strlen(nm); + if (!sep_source_matches_target(nm)) continue; int is_test = nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0; if (variant == SEP_VARIANT_PRODUCTION && is_test) continue; if (variant == SEP_VARIANT_EXTERNAL && !is_test) continue; @@ -580,9 +677,9 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, int pn = snprintf(path, sizeof path, "%s/%s", dirpath, nm); if (pn < 0 || (size_t)pn >= sizeof path) { fprintf(stderr, "ww: package source path is too long\n"); + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } @@ -590,9 +687,9 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, if (lstat(path, &st) != 0) { fprintf(stderr, "ww: %s: package source is not a regular file\n", path); + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } @@ -601,9 +698,9 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, fprintf(stderr, "ww: %s: package source is not a regular file\n", path); + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } @@ -613,17 +710,17 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, if (!S_ISREG(st.st_mode)) { fprintf(stderr, "ww: %s: package source is not a regular file\n", path); + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } int has_test = !is_test ? source_has_test_decl(path) : 0; if (has_test < 0) { + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } @@ -631,18 +728,18 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, fprintf(stderr, "ww: %s: @test declaration outside *_test.ww\n", path); + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } if (is_test) { char *package = NULL; if (source_package_name(path, &package) < 0) { + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } @@ -656,14 +753,14 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, int *n = is_test ? &ntests : &nprod; int *cap = is_test ? &captests : &capprod; if (source_list_add(list, n, cap, path) < 0) { + source_list_free(names, nnames); source_list_free(prod, nprod); source_list_free(tests, ntests); - closedir(d); *out_files = NULL; return -2; } } - closedir(d); + source_list_free(names, nnames); if (nprod > 1) qsort(prod, (size_t)nprod, sizeof *prod, strs_cmp); if (ntests > 1) qsort(tests, (size_t)ntests, sizeof *tests, strs_cmp); if (nprod > INT_MAX - ntests) { @@ -4161,7 +4258,7 @@ static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n", - is_test ? 16 : 17, is_test ? "test" : "build", emit_asm); + is_test ? 17 : 18, is_test ? "test" : "build", emit_asm); } static int diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index e5e50173..8fd01440 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -582,9 +582,108 @@ fn pkgisreg(path: str) bool = { }; }; +fn pkgnamerangeis(name: str, start: i32, end: i32, word: str) bool = { + if (end < start || end - start != word.len) { return false; }; + let i: i32 = 0; + for (i < end - start) { + if (name[start + i] != word[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn pkgknownos(name: str, start: i32, end: i32) bool = { + return pkgnamerangeis(name, start, end, "aix") + || pkgnamerangeis(name, start, end, "android") + || pkgnamerangeis(name, start, end, "darwin") + || pkgnamerangeis(name, start, end, "dragonfly") + || pkgnamerangeis(name, start, end, "freebsd") + || pkgnamerangeis(name, start, end, "hurd") + || pkgnamerangeis(name, start, end, "illumos") + || pkgnamerangeis(name, start, end, "ios") + || pkgnamerangeis(name, start, end, "js") + || pkgnamerangeis(name, start, end, "linux") + || pkgnamerangeis(name, start, end, "nacl") + || pkgnamerangeis(name, start, end, "netbsd") + || pkgnamerangeis(name, start, end, "openbsd") + || pkgnamerangeis(name, start, end, "plan9") + || pkgnamerangeis(name, start, end, "solaris") + || pkgnamerangeis(name, start, end, "wasip1") + || pkgnamerangeis(name, start, end, "windows") + || pkgnamerangeis(name, start, end, "zos"); +}; + +fn pkgknownarch(name: str, start: i32, end: i32) bool = { + return pkgnamerangeis(name, start, end, "386") + || pkgnamerangeis(name, start, end, "amd64") + || pkgnamerangeis(name, start, end, "amd64p32") + || pkgnamerangeis(name, start, end, "arm") + || pkgnamerangeis(name, start, end, "armbe") + || pkgnamerangeis(name, start, end, "arm64") + || pkgnamerangeis(name, start, end, "arm64be") + || pkgnamerangeis(name, start, end, "loong64") + || pkgnamerangeis(name, start, end, "mips") + || pkgnamerangeis(name, start, end, "mipsle") + || pkgnamerangeis(name, start, end, "mips64") + || pkgnamerangeis(name, start, end, "mips64le") + || pkgnamerangeis(name, start, end, "mips64p32") + || pkgnamerangeis(name, start, end, "mips64p32le") + || pkgnamerangeis(name, start, end, "ppc") + || pkgnamerangeis(name, start, end, "ppc64") + || pkgnamerangeis(name, start, end, "ppc64le") + || pkgnamerangeis(name, start, end, "riscv") + || pkgnamerangeis(name, start, end, "riscv64") + || pkgnamerangeis(name, start, end, "s390") + || pkgnamerangeis(name, start, end, "s390x") + || pkgnamerangeis(name, start, end, "sparc") + || pkgnamerangeis(name, start, end, "sparc64") + || pkgnamerangeis(name, start, end, "wasm"); +}; + +fn pkgtargettag(name: str, start: i32, end: i32) bool = { + return pkgnamerangeis(name, start, end, "linux") + || pkgnamerangeis(name, start, end, "amd64"); +}; + +// Recursive discovery must decide platform eligibility before a source can +// enter any production or test package graph. +fn pkgsourceplatform(name: str) bool = { + let stem: i32 = 0; + let hasunderscore: bool = false; + for (stem < name.len && name[stem] != '.') { + if (name[stem] == '_') { hasunderscore = true; }; + stem += 1; + }; + if (!hasunderscore) { return true; }; + + let end: i32 = stem; + let last: i32 = end; + for (last > 0 && name[last - 1] != '_') { last -= 1; }; + if (pkgnamerangeis(name, last, end, "test")) { + if (last == 0) { return true; }; + end = last - 1; + last = end; + for (last > 0 && name[last - 1] != '_') { last -= 1; }; + }; + if (last == 0) { return true; }; + + let prevend: i32 = last - 1; + let prev: i32 = prevend; + for (prev > 0 && name[prev - 1] != '_') { prev -= 1; }; + if (prev < prevend && pkgknownos(name, prev, prevend) + && pkgknownarch(name, last, end)) { + return pkgtargettag(name, last, end) + && pkgtargettag(name, prev, prevend); + }; + if (pkgknownos(name, last, end) || pkgknownarch(name, last, end)) { + return pkgtargettag(name, last, end); + }; + return true; +}; + fn pkgkeepfile(name: str) bool = { if (name.len <= 3 || name[0] == '.' || name[0] == '_') { return false; }; - return strings.hassuffix(name, ".ww"); + return strings.hassuffix(name, ".ww") && pkgsourceplatform(name); }; type pkgmatcher = struct { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 6d31da17..ac93f6ee 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -563,6 +563,105 @@ def SEP_LOAD_VENDOR: i32 = -4; def SEP_INITIAL_CAP: i32 = 8; def SEP_COUNT_MAX: i32 = 2147483647; +fn sepnamerangeis(name: *u8, start: u64, end: u64, word: str) bool = { + if (end < start || end - start != word.len: u64) { return false; }; + let i: u64 = 0u64; + for (i < end - start) { + if (name[start + i] != word.ptr[i]) { return false; }; + i += 1u64; + }; + return true; +}; + +fn sepknownos(name: *u8, start: u64, end: u64) bool = { + return sepnamerangeis(name, start, end, "aix") + || sepnamerangeis(name, start, end, "android") + || sepnamerangeis(name, start, end, "darwin") + || sepnamerangeis(name, start, end, "dragonfly") + || sepnamerangeis(name, start, end, "freebsd") + || sepnamerangeis(name, start, end, "hurd") + || sepnamerangeis(name, start, end, "illumos") + || sepnamerangeis(name, start, end, "ios") + || sepnamerangeis(name, start, end, "js") + || sepnamerangeis(name, start, end, "linux") + || sepnamerangeis(name, start, end, "nacl") + || sepnamerangeis(name, start, end, "netbsd") + || sepnamerangeis(name, start, end, "openbsd") + || sepnamerangeis(name, start, end, "plan9") + || sepnamerangeis(name, start, end, "solaris") + || sepnamerangeis(name, start, end, "wasip1") + || sepnamerangeis(name, start, end, "windows") + || sepnamerangeis(name, start, end, "zos"); +}; + +fn sepknownarch(name: *u8, start: u64, end: u64) bool = { + return sepnamerangeis(name, start, end, "386") + || sepnamerangeis(name, start, end, "amd64") + || sepnamerangeis(name, start, end, "amd64p32") + || sepnamerangeis(name, start, end, "arm") + || sepnamerangeis(name, start, end, "armbe") + || sepnamerangeis(name, start, end, "arm64") + || sepnamerangeis(name, start, end, "arm64be") + || sepnamerangeis(name, start, end, "loong64") + || sepnamerangeis(name, start, end, "mips") + || sepnamerangeis(name, start, end, "mipsle") + || sepnamerangeis(name, start, end, "mips64") + || sepnamerangeis(name, start, end, "mips64le") + || sepnamerangeis(name, start, end, "mips64p32") + || sepnamerangeis(name, start, end, "mips64p32le") + || sepnamerangeis(name, start, end, "ppc") + || sepnamerangeis(name, start, end, "ppc64") + || sepnamerangeis(name, start, end, "ppc64le") + || sepnamerangeis(name, start, end, "riscv") + || sepnamerangeis(name, start, end, "riscv64") + || sepnamerangeis(name, start, end, "s390") + || sepnamerangeis(name, start, end, "s390x") + || sepnamerangeis(name, start, end, "sparc") + || sepnamerangeis(name, start, end, "sparc64") + || sepnamerangeis(name, start, end, "wasm"); +}; + +fn septargettag(name: *u8, start: u64, end: u64) bool = { + return sepnamerangeis(name, start, end, "linux") + || sepnamerangeis(name, start, end, "amd64"); +}; + +// Go 1.26.5 build.go:1980-2027 filters known platform suffixes before +// opening a source. WW currently has one honest target, linux/amd64. +fn sepsourcematchestarget(name: *u8, nlen: u64) bool = { + let stem: u64 = 0u64; + let hasunderscore: bool = false; + for (stem < nlen && name[stem] != '.') { + if (name[stem] == '_') { hasunderscore = true; }; + stem += 1u64; + }; + if (!hasunderscore) { return true; }; + + let end: u64 = stem; + let last: u64 = end; + for (last > 0u64 && name[last - 1u64] != '_') { last -= 1u64; }; + if (sepnamerangeis(name, last, end, "test")) { + if (last == 0u64) { return true; }; + end = last - 1u64; + last = end; + for (last > 0u64 && name[last - 1u64] != '_') { last -= 1u64; }; + }; + if (last == 0u64) { return true; }; + + let prevend: u64 = last - 1u64; + let prev: u64 = prevend; + for (prev > 0u64 && name[prev - 1u64] != '_') { prev -= 1u64; }; + if (prev < prevend && sepknownos(name, prev, prevend) + && sepknownarch(name, last, end)) { + return septargettag(name, last, end) + && septargettag(name, prev, prevend); + }; + if (sepknownos(name, last, end) || sepknownarch(name, last, end)) { + return septargettag(name, last, end); + }; + return true; +}; + // Classify a selected directory entry: 1 production, 2 test, 0 skipped, // -1 @test outside *_test.ww, -2 non-regular source. fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64, @@ -575,10 +674,12 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64, s.ptr = name; s.len = nlen: i32; if (!strings.hassuffix(s, ".ww")) { return 0; }; + if (!sepsourcematchestarget(name, nlen)) { return 0; }; let istest: bool = strings.hassuffix(s, "_test.ww"); if (istest && variant == SEP_VARIANT_PRODUCTION) { return 0; }; if (!istest && variant == SEP_VARIANT_EXTERNAL) { return 0; }; - let source: *u8 = joinpath(dirpath, name); + let source: *u8 = sepjoinpath(dirpath, name); + if (source == nil) { return -2; }; let fi: os.filestat; let sr: (void | os.oserror) = os.lstat(&fi, pathstr(source)); let regular: bool = false; @@ -695,31 +796,11 @@ fn enumeratedir(dirpath: *u8, variant: i32, let reclen: u64 = blo + (bhi * 256u64); let nm: *u8 = buf.ptr + off + 19u64; let nl: u64 = cstrlen(nm); - let cls: i32 = dirfileclass(dirpath, nm, nl, variant); - if (cls == -1) { - cerr("ww: "); - cerr(pathstr(joinpath(dirpath, nm))); - cerr(": @test declaration outside *_test.ww\n"); - os.close(fd); - return nil: **u8, -2; - }; - if (cls == -2) { - os.close(fd); - return nil: **u8, -2; - }; - if (cls > 0) { - let full: *u8 = joinpath(dirpath, nm); - if (cls == 2) { - let pn: *u8 = dirpackagename(full); - if (pn == nil) { - os.close(fd); - return nil: **u8, -2; - }; - if (testpackage == nil || !cstreq(pn, testpackage)) { - off += reclen; - continue; - }; - }; + if (nl > 3u64 && nm[0] != '.' && nm[0] != '_') { + let view: str; + view.ptr = nm; + view.len = nl: i32; + if (strings.hassuffix(view, ".ww")) { if (n == SEP_COUNT_MAX) { sepfailsize(); os.close(fd); @@ -730,10 +811,16 @@ fn enumeratedir(dirpath: *u8, variant: i32, os.close(fd); return nil: **u8, -2; }; - names[n] = full; - nlens[n] = cstrlen(full); - kinds[n] = cls; + let owned: *u8 = sepdupcstr(nm, nl); + if (owned == nil) { + os.close(fd); + return nil: **u8, -2; + }; + names[n] = owned; + nlens[n] = nl; + kinds[n] = 0; n += 1; + }; }; off += reclen; }; @@ -749,8 +836,69 @@ fn enumeratedir(dirpath: *u8, variant: i32, return nil: **u8, -1; }; - // Insertion sort, byte-wise. n is small (≤16 in practice). + // Go's directory reader presents a byte-sorted name list to the loader. let i: i32 = 1; + for (i < n) { + let j: i32 = i; + for (j > 0) { + let c: i32 = bytecmp(names[j - 1], nlens[j - 1], + names[j], nlens[j]); + if (c <= 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; + let tk: i32 = kinds[j]; + kinds[j] = kinds[j - 1]; + kinds[j - 1] = tk; + j -= 1; + }; + }; + i += 1; + }; + let raw: i32 = n; + let selected: i32 = 0; + let ri: i32 = 0; + for (ri < raw) { + let nm: *u8 = names[ri]; + let nl: u64 = nlens[ri]; + let cls: i32 = dirfileclass(dirpath, nm, nl, variant); + if (cls == -1) { + let badpath: *u8 = sepjoinpath(dirpath, nm); + if (badpath == nil) { return nil: **u8, -2; }; + cerr("ww: "); + cerr(pathstr(badpath)); + cerr(": @test declaration outside *_test.ww\n"); + return nil: **u8, -2; + }; + if (cls == -2) { return nil: **u8, -2; }; + if (cls > 0) { + let full: *u8 = sepjoinpath(dirpath, nm); + if (full == nil) { return nil: **u8, -2; }; + let keep: bool = true; + if (cls == 2) { + let pn: *u8 = dirpackagename(full); + if (pn == nil) { return nil: **u8, -2; }; + if (testpackage == nil || !cstreq(pn, testpackage)) { + keep = false; + }; + }; + if (keep) { + names[selected] = full; + nlens[selected] = cstrlen(full); + kinds[selected] = cls; + selected += 1; + }; + }; + ri += 1; + }; + n = selected; + + // Production files precede test files; each partition remains byte-sorted. + i = 1; for (i < n) { let j: i32 = i; for (j > 0) { @@ -4981,14 +5129,14 @@ fn validatecommandoutputpath(out: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 16 mode test asm 1\n"; + return "ww workdir fmt 17 mode test asm 1\n"; }; - return "ww workdir fmt 16 mode test asm 0\n"; + return "ww workdir fmt 17 mode test asm 0\n"; }; if (emitasm != 0) { - return "ww workdir fmt 17 mode build asm 1\n"; + return "ww workdir fmt 18 mode build asm 1\n"; }; - return "ww workdir fmt 17 mode build asm 0\n"; + return "ww workdir fmt 18 mode build asm 0\n"; }; fn stampmatches(path: *u8, want: str) bool = {