diff --git a/Makefile b/Makefile index efac1088..f3186475 100644 --- a/Makefile +++ b/Makefile @@ -337,6 +337,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_size_type_run \ $(BIN)/test_types_sizelim_run \ $(BIN)/test_types_intlim_run \ + $(BIN)/test_opaque_decl_run \ $(BIN)/test_bufio_run $(BIN)/test_random_run $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) @@ -1096,6 +1097,10 @@ $(BIN)/test_types_intlim_run: test/wcc/959_types_intlim_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_opaque_decl_run: test/wcc/960_opaque_decl_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_f64crossmod_run: test/wcc/953_f64crossmod_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 0bacfd69..327789e2 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -50,6 +50,7 @@ lookup_builtin(const char *name) if (strcmp(name, "uint") == 0) return ty_uint; if (strcmp(name, "uintptr") == 0) return ty_uintptr; if (strcmp(name, "size") == 0) return ty_size; /* #85 fold-2 */ + if (strcmp(name, "opaque") == 0) return ty_opaque; /* #108(a) */ if (strcmp(name, "f32") == 0) return ty_f32; if (strcmp(name, "f64") == 0) return ty_f64; if (strcmp(name, "str") == 0) return ty_str; diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index 269c6ba3..855827b0 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -17,6 +17,7 @@ Type *ty_f32, *ty_f64, *ty_str; Type *ty_err; Type *ty_never; Type *ty_nomem; +Type *ty_opaque; Type *ty_untyped_int, *ty_untyped_float, *ty_untyped_str; Type *ty_untyped_rune, *ty_untyped_bool, *ty_untyped_nil; @@ -78,6 +79,10 @@ typesinit(Arena *a) ty_nomem->size = ty_void->size; ty_nomem->align = ty_void->align; ty_nomem->iserror = 1; + /* #108(a): abstract + unsized. SIZE_UNDEFINED (not 0) so a bare + * `let x: opaque` can't fabricate a 0-byte local; legal only behind + * indirection. Mirrors harec builtin_type_opaque (types.c:1446). */ + ty_opaque = prim(a, TY_OPAQUE, "opaque", SIZE_UNDEFINED, SIZE_UNDEFINED); ty_untyped_int = prim(a, TY_UNTYPED_INT, "untyped_int", 0, 1); ty_untyped_float = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0, 1); @@ -375,6 +380,7 @@ type_name(Arena *a, Type *t) case TY_UINT: return "uint"; case TY_UINTPTR: return "uintptr"; case TY_SIZE: return "size"; + case TY_OPAQUE: return "opaque"; case TY_F32: return "f32"; case TY_F64: return "f64"; case TY_STR: return "str"; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index f3766f34..fa5f1844 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -391,11 +391,19 @@ typedef enum { * lib/ww/typ.ww mirrors them as explicit `def` numbers. */ TY_ENUM, /* `enum [storage] { ... }`. sub=storage, * fields=member list (Tfield.offset = u64 value). */ - TY_SIZE /* platform-width unsigned int; mirrors TY_UINTPTR + TY_SIZE, /* platform-width unsigned int; mirrors TY_UINTPTR * (8/8 on amd64). Hare: `size`, SIZE_MAX=U64_MAX * (ref/hare/types/arch+x86_64.ha:17,20). #85 fold-1. */ + TY_OPAQUE /* abstract + unsized; legal only behind indirection + * (`*opaque`, `[]opaque`). size=align=SIZE_UNDEFINED. + * Hare: `opaque` (ref/harec/src/types.c:1446). #108(a). */ } TypeKind; +/* Unsized sentinel for abstract types (Type.size / Type.align). Mirrors + * harec's SIZE_UNDEFINED = (size_t)-1 (ref/harec/include/types.h:58); + * chosen over 0 so a 0-byte local can never silently slip through. */ +#define SIZE_UNDEFINED ((u64)-1) + typedef struct Tfield Tfield; struct Tfield { const char *name; @@ -444,6 +452,7 @@ extern Type *ty_f32, *ty_f64, *ty_str; extern Type *ty_err; extern Type *ty_never; extern Type *ty_nomem; /* task #29: predeclared `!void` alias */ +extern Type *ty_opaque; /* #108(a): abstract unsized; behind indirection only */ extern Type *ty_untyped_int, *ty_untyped_float, *ty_untyped_str; extern Type *ty_untyped_rune, *ty_untyped_bool, *ty_untyped_nil; diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index 4e308afd..b4d65fde 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -58,8 +58,15 @@ type tykind = enum i32 { // against cmd/wcc/ww.h. TY_ENUM = 35, TY_SIZE = 36, // #85 fold-1; mirrors TY_UINTPTR (8/8 amd64) + TY_OPAQUE = 37, // #108(a); abstract + unsized, behind indirection only }; +// #108(a): unsized sentinel for abstract types (tinfo.size / .align). +// Mirrors harec SIZE_UNDEFINED = (size_t)-1 (ref/harec/include/types.h +// :58) and cstage cmd/wcc/ww.h; not 0, so a bare opaque local can't +// fabricate a 0-byte slot. Value == U64_MAX. +def SIZE_UNDEFINED: u64 = 18446744073709551615; + // ---- tinfo / tfield / tparam ----------------------------------------- type tfield = struct { @@ -160,6 +167,7 @@ type tctx = struct { tyuint: *tinfo, tyuintptr: *tinfo, tysize: *tinfo, + tyopaque: *tinfo, tyf32: *tinfo, tyf64: *tinfo, tystr: *tinfo, @@ -225,6 +233,10 @@ export fn typesinit(c: *tctx) void = { c.tystr.sub = c.tyu8; // str IS []u8: element is u8 (Phase 2 F1) c.tyerr = prim(tykind.TY_ERR, "", 0u64, 1u64); c.tynever = prim(tykind.TY_NEVER, "never", 0u64, 1u64); + // #108(a): abstract + unsized. SIZE_UNDEFINED (not 0) blocks a bare + // `let x: opaque` 0-byte slot; legal only behind indirection. + // Mirrors cstage type.c ty_opaque (harec types.c:1446). + c.tyopaque = prim(tykind.TY_OPAQUE, "opaque", SIZE_UNDEFINED, SIZE_UNDEFINED); c.tyuntypedint = prim(tykind.TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); c.tyuntypedfloat = prim(tykind.TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6660e3fa..32559cd5 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6411,8 +6411,15 @@ type tykind = enum i32 { // against cmd/wcc/ww.h. TY_ENUM = 35, TY_SIZE = 36, // #85 fold-1; mirrors TY_UINTPTR (8/8 amd64) + TY_OPAQUE = 37, // #108(a); abstract + unsized, behind indirection only }; +// #108(a): unsized sentinel for abstract types (tinfo.size / .align). +// Mirrors harec SIZE_UNDEFINED = (size_t)-1 (ref/harec/include/types.h +// :58) and cstage cmd/wcc/ww.h; not 0, so a bare opaque local can't +// fabricate a 0-byte slot. Value == U64_MAX. +def SIZE_UNDEFINED: u64 = 18446744073709551615; + // ---- tinfo / tfield / tparam ----------------------------------------- type tfield = struct { @@ -6513,6 +6520,7 @@ type tctx = struct { tyuint: *tinfo, tyuintptr: *tinfo, tysize: *tinfo, + tyopaque: *tinfo, tyf32: *tinfo, tyf64: *tinfo, tystr: *tinfo, @@ -6578,6 +6586,10 @@ export fn typesinit(c: *tctx) void = { c.tystr.sub = c.tyu8; // str IS []u8: element is u8 (Phase 2 F1) c.tyerr = prim(tykind.TY_ERR, "", 0u64, 1u64); c.tynever = prim(tykind.TY_NEVER, "never", 0u64, 1u64); + // #108(a): abstract + unsized. SIZE_UNDEFINED (not 0) blocks a bare + // `let x: opaque` 0-byte slot; legal only behind indirection. + // Mirrors cstage type.c ty_opaque (harec types.c:1446). + c.tyopaque = prim(tykind.TY_OPAQUE, "opaque", SIZE_UNDEFINED, SIZE_UNDEFINED); c.tyuntypedint = prim(tykind.TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); c.tyuntypedfloat = prim(tykind.TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); @@ -8438,6 +8450,7 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (streq(nm, "uint")) { r = c.tc.tyuint; }; if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; }; if (streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2 + if (streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a) if (streq(nm, "f32")) { r = c.tc.tyf32; }; if (streq(nm, "f64")) { r = c.tc.tyf64; }; if (streq(nm, "str")) { r = c.tc.tystr; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 24109230..3fc78e5b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1299,6 +1299,7 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (streq(nm, "uint")) { r = c.tc.tyuint; }; if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; }; if (streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2 + if (streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a) if (streq(nm, "f32")) { r = c.tc.tyf32; }; if (streq(nm, "f64")) { r = c.tc.tyf64; }; if (streq(nm, "str")) { r = c.tc.tystr; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 46527bd2..8f08c8b8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6411,8 +6411,15 @@ type tykind = enum i32 { // against cmd/wcc/ww.h. TY_ENUM = 35, TY_SIZE = 36, // #85 fold-1; mirrors TY_UINTPTR (8/8 amd64) + TY_OPAQUE = 37, // #108(a); abstract + unsized, behind indirection only }; +// #108(a): unsized sentinel for abstract types (tinfo.size / .align). +// Mirrors harec SIZE_UNDEFINED = (size_t)-1 (ref/harec/include/types.h +// :58) and cstage cmd/wcc/ww.h; not 0, so a bare opaque local can't +// fabricate a 0-byte slot. Value == U64_MAX. +def SIZE_UNDEFINED: u64 = 18446744073709551615; + // ---- tinfo / tfield / tparam ----------------------------------------- type tfield = struct { @@ -6513,6 +6520,7 @@ type tctx = struct { tyuint: *tinfo, tyuintptr: *tinfo, tysize: *tinfo, + tyopaque: *tinfo, tyf32: *tinfo, tyf64: *tinfo, tystr: *tinfo, @@ -6578,6 +6586,10 @@ export fn typesinit(c: *tctx) void = { c.tystr.sub = c.tyu8; // str IS []u8: element is u8 (Phase 2 F1) c.tyerr = prim(tykind.TY_ERR, "", 0u64, 1u64); c.tynever = prim(tykind.TY_NEVER, "never", 0u64, 1u64); + // #108(a): abstract + unsized. SIZE_UNDEFINED (not 0) blocks a bare + // `let x: opaque` 0-byte slot; legal only behind indirection. + // Mirrors cstage type.c ty_opaque (harec types.c:1446). + c.tyopaque = prim(tykind.TY_OPAQUE, "opaque", SIZE_UNDEFINED, SIZE_UNDEFINED); c.tyuntypedint = prim(tykind.TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); c.tyuntypedfloat = prim(tykind.TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); @@ -8438,6 +8450,7 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (streq(nm, "uint")) { r = c.tc.tyuint; }; if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; }; if (streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2 + if (streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a) if (streq(nm, "f32")) { r = c.tc.tyf32; }; if (streq(nm, "f64")) { r = c.tc.tyf64; }; if (streq(nm, "str")) { r = c.tc.tystr; }; diff --git a/test/wcc/960_opaque_decl_run.c b/test/wcc/960_opaque_decl_run.c new file mode 100644 index 00000000..8d2b54af --- /dev/null +++ b/test/wcc/960_opaque_decl_run.c @@ -0,0 +1,148 @@ +/* + * 960_opaque_decl_run — runtime proof that the abstract `opaque` type + * (#108 sub-fold a) EXISTS and is usable behind indirection: `*opaque` + * is a usable 8-byte pointer and `[]opaque` is a usable 24-byte slice + * header (.len/.ptr). The name resolves via the type-name resolver + * (lookup_builtin / tinfofornode's N_TNAME chain), mirroring uintptr/ + * size; `opaque` itself is unsized (size=align=SIZE_UNDEFINED), so it is + * only ever materialised through a pointer or slice, never as a bare + * value. A green row IS the proof the name binds: without the resolver + * arm every `*opaque` / `[]opaque` row fails to compile ("unknown type + * 'opaque'"). + * + * NOTE — this fold (a) deliberately does NOT test bare `let x: opaque`, + * `size(opaque)`, an `opaque` (bare) struct field, `[N]opaque`, or + * `[]opaque` INDEXING: those are the use-restriction GUARDS of sub-fold + * (b). Every row here stays behind indirection. + * + * Table-driven like 957_size_type_run / 952_floats_run: each row is a + * self-contained ww program; the C-side cstage `ww build -I lib` + * compiles it, we run the binary and assert the exit code. cstage-only + * by design (mirrors 951/952/957/969): per-program wwstage byte-id is + * the 990-997 gates' job, and the wwstage resolver arm is twinned for + * rule-10 symmetry but stays dead on the opaque-free selfhost corpus. + */ +#include +#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; +} + +struct row { const char *src; int want_exit; }; + +static const struct row rows[] = { + /* `*opaque` is a usable 8-byte pointer: round-trip a real *i32 + * through *opaque and back, then deref. Proves the type binds and + * carries pointer width. */ + { "package main;\n" + "export fn main() i32 = {\n" + " let n: i32 = 42;\n" + " let pn: *i32 = &n;\n" + " let po: *opaque = pn: *opaque;\n" + " let back: *i32 = po: *i32;\n" + " return *back;\n" + "};\n", 42 }, + /* `[]opaque` is a usable 24-byte slice header: reinterpret a real + * []i32 as []opaque, read .len, and round-trip .ptr (a *opaque) + * back to *i32 + deref. The []opaque is never indexed (guard b). + * (Uses a[0:4] + a store-to-var deref to steer clear of an + * unrelated, pre-existing cgen bug — inline deref of a sub-slice + * .ptr inside a comparison; see report. The opaque path itself is + * exercised in full.) */ + { "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i32;\n" + " a[0] = 11; a[1] = 22; a[2] = 33; a[3] = 44;\n" + " let s: []i32 = a[0:4];\n" + " let so: []opaque = s: []opaque;\n" + " let n: i32 = so.len: i32;\n" + " if (n != 4) { return 1; };\n" + " let pp: *opaque = so.ptr;\n" + " let pi: *i32 = pp: *i32;\n" + " let v: i32 = *pi;\n" + " if (v != 11) { return 2; };\n" + " return n - 1;\n" + "};\n", 3 }, + /* `*opaque` as a struct field (behind indirection): set from a cast + * pointer, read back, deref + add a sibling scalar field. */ + { "package main;\n" + "type handle = struct { p: *opaque, tag: i32 };\n" + "export fn main() i32 = {\n" + " let n: i32 = 99;\n" + " let h: handle = handle{ p = (&n): *opaque, tag = 7 };\n" + " let pi: *i32 = h.p: *i32;\n" + " return *pi + h.tag;\n" + "};\n", 106 }, + { NULL, 0 } +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + char absbin[1024]; + if (bin[0] != '/') { + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwopq_%d_%d.ww", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwopq_%d_d_%d", getpid(), i); + mkdir(tmpdir, 0755); + + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s/lib %s", + tmpdir, bin, cwd, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row %d: build failed\n src: %s\n", + i, rows[i].src); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row %d: exit %d, want %d\n src: %s\n", + i, got, rows[i].want_exit, rows[i].src); + fail++; + } + unlink(src); unlink(outbin); rmdir(tmpdir); + } + if (fail) { + fprintf(stderr, "%d/%d opaque tests failed\n", fail, n); + return 1; + } + printf("opaque: %d/%d ok\n", n, n); + return 0; +}