From 7a4f60b04187ea44aedfa3c8895791991b1d4ceb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 13 May 2026 14:58:36 +0900 Subject: [PATCH] w6c+wcc+selfhost+lib: int-cast truncate + use_alias, 5 new modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cgen/check bugs surfaced by new lib modules, plus the modules themselves (crc64, siphash, random, base64, base32). 1. `(big_u64): u32` (and `: u16`, `: u8`, `: bool`) didn't truncate. N_CAST emitted nothing for int↔int; the value stayed in AX with its upper bits intact and downstream CMPQ/DIVQ misread the slot. The TK_TILDE path already had clamp logic for the same reason — N_CAST was the missing case. Both stages now MOVL r,r for u32 and ANDQ $mask for u8/u16/bool. Signed-narrow (i8/i16/i32) stays no-op until w6a grows reg-reg MOVSBQ/MOVSWQ/MOVSXD. selfhost cgcast walks alias chains via aliaslookup before checking primsize/typenameisunsigned so `(u: random)` where `type random = u64` still bypasses the clamp. See cmd/w6c/cgen.c N_CAST and selfhost/cmd/wcc/cgenexpr.ww cgcast. 2. `mod.mod` type refs (`random.random` when the imported module declares `export type random = u64;`) failed with "unknown type". The driver concatenates imports into one flat scope, so SK_USE `random` collided with SK_TYPE `random` and scope_define silently dropped the use. resolve_typename's leaf lookup required `kind == SK_USE` and gave up. Adds a `use_alias` flag to Sym; the pass-1 decl scan now marks colliding syms in both directions (use-after-type and type-after-use). resolve_typename and the N_DOT cexpr branch treat `use_alias` like SK_USE for qualified lookup. selfhost check.ww was already lenient on this path so no ww-side change was needed; bootstrap fixed point (990-995) holds. See cmd/wcc/check.c installdecl pass + N_DOT/resolve_typename and cmd/wcc/ww.h Sym.use_alias. New modules under lib/, each with @test vectors in *_test.ww and wired into test/wcc/900_stdlib.c (26 modules → all compile): - lib/hash/crc64 ECMA, ISO (mirror of crc32 shape) - lib/hash/siphash SipHash-2-4, buffer-based sum/sum24 - lib/math/random SplitMix64 (init, next, u32n, u64n) - lib/encoding/base64 RFC 4648 std + url-safe encode/decode + sizes - lib/encoding/base32 RFC 4648 std + base32hex encode/decode + sizes --- cmd/w6c/cgen.c | 33 +++++ cmd/wcc/check.c | 58 +++++++-- cmd/wcc/ww.h | 6 + lib/encoding/base32/base32.ww | 186 +++++++++++++++++++++++++++ lib/encoding/base32/base32_test.ww | 155 ++++++++++++++++++++++ lib/encoding/base64/base64.ww | 182 ++++++++++++++++++++++++++ lib/encoding/base64/base64_test.ww | 172 +++++++++++++++++++++++++ lib/hash/crc64/crc64.ww | 36 ++++++ lib/hash/crc64/crc64_test.ww | 66 ++++++++++ lib/hash/siphash/siphash.ww | 96 ++++++++++++++ lib/hash/siphash/siphash_test.ww | 75 +++++++++++ lib/math/random/random.ww | 56 ++++++++ lib/math/random/random_test.ww | 61 +++++++++ selfhost/cmd/w6c/main.combined.ww | 46 ++++++- selfhost/cmd/wcc/cgenexpr.ww | 46 ++++++- selfhost/cmd/wwdump/main.combined.ww | 46 ++++++- test/wcc/900_stdlib.c | 5 + 17 files changed, 1302 insertions(+), 23 deletions(-) create mode 100644 lib/encoding/base32/base32.ww create mode 100644 lib/encoding/base32/base32_test.ww create mode 100644 lib/encoding/base64/base64.ww create mode 100644 lib/encoding/base64/base64_test.ww create mode 100644 lib/hash/crc64/crc64.ww create mode 100644 lib/hash/crc64/crc64_test.ww create mode 100644 lib/hash/siphash/siphash.ww create mode 100644 lib/hash/siphash/siphash_test.ww create mode 100644 lib/math/random/random.ww create mode 100644 lib/math/random/random_test.ww diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 777bf58e..ea676826 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3474,6 +3474,39 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); } } + /* Narrowing integer cast: clamp AX to the target width so + * downstream 64-bit ops see a value within the declared + * range. Hare semantics: `expr: T` truncates to T's bit + * width (mod 2^n). Without this, `(big_u64): u32` left the + * upper 32 bits intact and CMPQ/DIVQ misread the value. + * + * Unsigned targets only here. Signed-narrow targets (i8/ + * i16/i32) need MOVSBQ / MOVSWQ / MOVSXD in their reg-reg + * form which the assembler doesn't expose yet; callers + * that need a clean signed-narrow value either keep the + * value in range before the cast (as strconv does with + * an explicit bounds check) or AND the low bits manually. + * Tracking this gap is part of the same TODO. */ + if (!from_f && !to_f && n->type) { + Type *tt = n->type; + Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt; + if (tu && type_isint(tu) && tu->size > 0 + && tu->size < 8 && type_isunsigned(tu)) { + if (tu->size == 4) { + ins2(c, A_MOVL, areg(D_AX), areg(D_AX)); + } else { + u64 mask = ((u64)1 << (tu->size * 8)) - 1; + ins2(c, A_ANDQ, aimm((i64)mask), + areg(D_AX)); + } + } + /* TY_BOOL is size 1 too; clamp to a single byte so + * `(u32_val): bool` produces 0 or a low-byte value + * instead of leaking the upper bits. */ + if (tu && tu->kind == TY_BOOL) { + ins2(c, A_ANDQ, aimm(0xFF), areg(D_AX)); + } + } break; } case N_DOT: { diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 1e546c01..6a4bff89 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -63,14 +63,17 @@ resolve_typename(Checker *c, Node *n) Sym *s = scope_lookup(c->cur, nm); if (s == NULL && nm) { /* module-qualified: io.stream → strip the last dot prefix - * and look up the leaf if `io` is a `use`-imported name. */ + * and look up the leaf if `io` is a `use`-imported name. + * `m->use_alias` covers the self-import case where the + * imported module declares a type with the same name as + * the module itself (e.g. `random.random`). */ const char *dot = strrchr(nm, '.'); if (dot) { char head[128] = {0}; size_t hl = (size_t)(dot - nm); if (hl < sizeof head) memcpy(head, nm, hl); Sym *m = scope_lookup(c->cur, head); - if (m && m->kind == SK_USE) + if (m && (m->kind == SK_USE || m->use_alias)) s = scope_lookup(c->cur, dot + 1); } } @@ -684,16 +687,28 @@ cexpr(Checker *c, Node *n) * scope, so we lookup `n->str` directly. */ if (n->lhs && n->lhs->kind == N_IDENT) { Sym *ms = scope_lookup(c->cur, n->lhs->str); - if (ms && ms->kind == SK_USE) { + if (ms && (ms->kind == SK_USE || ms->use_alias)) { + /* Module-qualified ref. `use_alias` covers + * the self-import case where the module's + * type name shadowed the SK_USE; the leaf + * still resolves through the flat scope. + * fs == ms is the `mod.mod` case (the + * imported module's leaf type is named after + * the module itself — both names point at + * the same SK_TYPE sym in the flat scope). */ Sym *fs = scope_lookup(c->cur, n->str); if (fs) return n->type = fs->type; - /* Leaf isn't in scope here — treat as an - * external declaration. The codegen will - * still emit CALL/MOVQ by the leaf name; the - * linker fails if the symbol is truly - * missing. */ - return n->type = ty_err; + if (ms->kind == SK_USE) { + /* Pure SK_USE with missing leaf: + * external declaration. Codegen + * emits CALL/MOVQ by the leaf name + * and the linker resolves it. */ + return n->type = ty_err; + } + /* SK_TYPE with use_alias=1 and no leaf + * found: fall through so the enum / type- + * member paths below get a shot. */ } /* enum member access: TypeName.MEMBER → fold to * the member's integer literal value. Type is the @@ -1590,13 +1605,34 @@ check_file(Checker *c, Node *file) * walked in the next pass. */ for (Node *d = file->list; d; d = d->next) { if (d->kind == N_USE) { - scope_define(c->cur, d->str, SK_USE, NULL, d); + Sym *prev = scope_lookup_local(c->cur, d->str); + if (prev != NULL) { + /* Self-import: the driver concatenates the + * imported module's source into the flat + * scope, so its top-level decls (types, fns, + * defs) shadow a same-named SK_USE. Mark + * the colliding sym as also-a-use so dotted + * qualifiers (`mod.x`) still resolve. */ + prev->use_alias = 1; + } else { + scope_define(c->cur, d->str, SK_USE, NULL, d); + } continue; } if (d->kind != N_TYPEDECL) continue; Type *named = type_named(c->a, d->str, NULL); - if (!scope_define(c->cur, d->str, SK_TYPE, named, d)) + Sym *prev = scope_lookup_local(c->cur, d->str); + if (prev && prev->kind == SK_USE) { + /* `use mod; ... type mod = ...;` — promote the + * SK_USE to the type symbol but remember it was + * also a module name. */ + prev->kind = SK_TYPE; + prev->type = named; + prev->decl = d; + prev->use_alias = 1; + } else if (!scope_define(c->cur, d->str, SK_TYPE, named, d)) { err(c, d->pos, "duplicate type %s", d->str); + } d->type = named; } for (Node *d = file->list; d; d = d->next) { diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 94626eb8..cddf0c64 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -476,6 +476,12 @@ struct Sym { Node *decl; int exported; int is_const; /* const-bound (assignment rejected) */ + int use_alias; /* also bound as a `use` module name. Set + * when a `use foo;` directive collides + * with a same-named SK_TYPE/SK_FN/etc. + * Lets resolve_typename treat `foo.x` + * as module-qualified even though the + * primary kind isn't SK_USE. */ Sym *next; /* iteration */ Sym *hashnext; /* bucket chain */ Scope *scope; diff --git a/lib/encoding/base32/base32.ww b/lib/encoding/base32/base32.ww new file mode 100644 index 00000000..829970bf --- /dev/null +++ b/lib/encoding/base32/base32.ww @@ -0,0 +1,186 @@ +// encoding/base32 — RFC 4648 base32 encode/decode, buffer-based. +// +// Mirrors Hare's encoding::base32 surface, modulo Hare's stream-based +// encoder/decoder. ww ships the in-memory subset only: `encode(dst, +// src)` writes the encoded bytes into `dst`, returning the count; +// `decode(dst, src)` writes the decoded bytes into `dst`, returning a +// count or invalid. +// +// std uses 'A'-'Z' and '2'-'7' for indexes 0..31 (RFC 4648 §6); hex +// uses '0'-'9' and 'A'-'V' (the base32hex alphabet, RFC 4648 §7). +// Both pad encoded output with '=' to a multiple of 8 bytes. + +export type invalid = !i32; + +// encodedsize — bytes required to encode `n` source bytes (including +// '=' padding). Hare names it the same. +export fn encodedsize(n: i32) i32 = { + if (n == 0) { return 0; }; + return ((n - 1) / 5 + 1) * 8; +}; + +// decodedsize — upper bound on the number of bytes decoded from `n` +// encoded bytes. +export fn decodedsize(n: i32) i32 = { + return (n / 8) * 5; +}; + +// encchar — map a 5-bit value to its alphabet character. `hex` picks +// the base32hex alphabet instead of std. +fn encchar(v: u8, hex: bool) u8 = { + if (hex) { + if (v < 10u8) { return v + 48u8; }; // '0' + v + return v + 55u8; // 'A' + (v - 10) = v + 55 + }; + if (v < 26u8) { return v + 65u8; }; // 'A' + v + return v + 24u8; // '2' + (v - 26) = v + 24 +}; + +// decchar — inverse of encchar. Returns 0..31 or 255 on invalid char. +// '=' is handled in the decode loop, not here. +fn decchar(c: u8, hex: bool) u8 = { + if (hex) { + if (c >= 48u8) { if (c <= 57u8) { return c - 48u8; }; }; // '0'..'9' + if (c >= 65u8) { if (c <= 86u8) { return c - 55u8; }; }; // 'A'..'V' + return 255u8; + }; + if (c >= 65u8) { if (c <= 90u8) { return c - 65u8; }; }; // 'A'..'Z' + if (c >= 50u8) { if (c <= 55u8) { return c - 24u8; }; }; // '2'..'7' + return 255u8; +}; + +fn encgroup(dst: []u8, j: i32, src: []u8, i: i32, n: i32, hex: bool) void = { + let b0: u8 = 0u8; + let b1: u8 = 0u8; + let b2: u8 = 0u8; + let b3: u8 = 0u8; + let b4: u8 = 0u8; + if (n > 0) { b0 = src[i]; }; + if (n > 1) { b1 = src[i + 1]; }; + if (n > 2) { b2 = src[i + 2]; }; + if (n > 3) { b3 = src[i + 3]; }; + if (n > 4) { b4 = src[i + 4]; }; + dst[j] = encchar(b0 >> 3u8, hex); + dst[j + 1] = encchar(((b0 & 7u8) << 2u8) | (b1 >> 6u8), hex); + dst[j + 2] = encchar((b1 >> 1u8) & 31u8, hex); + dst[j + 3] = encchar(((b1 & 1u8) << 4u8) | (b2 >> 4u8), hex); + dst[j + 4] = encchar(((b2 & 15u8) << 1u8) | (b3 >> 7u8), hex); + dst[j + 5] = encchar((b3 >> 2u8) & 31u8, hex); + dst[j + 6] = encchar(((b3 & 3u8) << 3u8) | (b4 >> 5u8), hex); + dst[j + 7] = encchar(b4 & 31u8, hex); + // Pad the encoded slots that map past the source bytes. + if (n < 5) { + // n=1 → 2 chars then 6 '='. n=2 → 4 chars. n=3 → 5. n=4 → 7. + let keep: i32 = 2; + if (n == 2) { keep = 4; }; + if (n == 3) { keep = 5; }; + if (n == 4) { keep = 7; }; + let k: i32 = keep; + for (k < 8) { dst[j + k] = 61u8; k += 1; }; // '=' + }; +}; + +fn encodeinto(dst: []u8, src: []u8, hex: bool) i32 = { + let i: i32 = 0; + let j: i32 = 0; + for (i + 5 <= src.len) { + encgroup(dst, j, src, i, 5, hex); + i += 5; + j += 8; + }; + let rem: i32 = src.len - i; + if (rem > 0) { + encgroup(dst, j, src, i, rem, hex); + j += 8; + }; + return j; +}; + +// encode — encode `src` into `dst` using the std (RFC 4648 §6) +// alphabet. Returns bytes written. `dst` must hold at least +// encodedsize(src.len) bytes. +export fn encode(dst: []u8, src: []u8) i32 = { + return encodeinto(dst, src, false); +}; + +// encodehex — same as encode but uses the base32hex alphabet +// (RFC 4648 §7). +export fn encodehex(dst: []u8, src: []u8) i32 = { + return encodeinto(dst, src, true); +}; + +// padcount — number of bytes encoded in the last group, given the +// count `p` of trailing '=' chars. RFC 4648 lists the legal mapping: +// 6=>1, 4=>2, 3=>3, 1=>4, 0=>5. Returns -1 if `p` isn't legal. +fn padcount(p: i32) i32 = { + if (p == 0) { return 5; }; + if (p == 1) { return 4; }; + if (p == 3) { return 3; }; + if (p == 4) { return 2; }; + if (p == 6) { return 1; }; + return -1; +}; + +fn decodeinto(dst: []u8, src: []u8, hex: bool) (i32 | invalid) = { + if (src.len == 0) { return 0; }; + if ((src.len & 7) != 0) { return src.len: invalid; }; + let i: i32 = 0; + let j: i32 = 0; + let end: i32 = src.len; + for (i < end) { + let v: [8]u8; + let last: bool = false; + let p: i32 = 0; + let k: i32 = 0; + for (k < 8) { + let c: u8 = src[i + k]; + if (c == 61u8) { // '=' + if (i + 8 != end) { return (i + k): invalid; }; + last = true; + v[k] = 0u8; + p += 1; + } else { + if (last) { return (i + k): invalid; }; + let d: u8 = decchar(c, hex); + if (d == 255u8) { return (i + k): invalid; }; + v[k] = d; + }; + k += 1; + }; + let nb: i32 = 5; + if (last) { + nb = padcount(p); + if (nb < 0) { return (i + 8 - p): invalid; }; + }; + // First two chars cover byte[0] (5 + 3 bits). + if (nb > 0) { + dst[j] = (v[0] << 3u8) | (v[1] >> 2u8); + }; + if (nb > 1) { + dst[j + 1] = (v[1] << 6u8) | (v[2] << 1u8) | (v[3] >> 4u8); + }; + if (nb > 2) { + dst[j + 2] = (v[3] << 4u8) | (v[4] >> 1u8); + }; + if (nb > 3) { + dst[j + 3] = (v[4] << 7u8) | (v[5] << 2u8) | (v[6] >> 3u8); + }; + if (nb > 4) { + dst[j + 4] = (v[6] << 5u8) | v[7]; + }; + j += nb; + i += 8; + }; + return j; +}; + +// decode — decode std-alphabet base32 from `src` into `dst`. Returns +// count of decoded bytes, or invalid on malformed input. +export fn decode(dst: []u8, src: []u8) (i32 | invalid) = { + return decodeinto(dst, src, false); +}; + +// decodehex — same as decode but accepts base32hex. +export fn decodehex(dst: []u8, src: []u8) (i32 | invalid) = { + return decodeinto(dst, src, true); +}; diff --git a/lib/encoding/base32/base32_test.ww b/lib/encoding/base32/base32_test.ww new file mode 100644 index 00000000..5a4036fa --- /dev/null +++ b/lib/encoding/base32/base32_test.ww @@ -0,0 +1,155 @@ +use base32; + +fn putstr(s: str, into: []u8, off: i32) i32 = { + let i: i32 = 0; + for (i < s.len) { + into[off + i] = s[i]; + i += 1; + }; + return off + s.len; +}; + +fn streq(buf: []u8, expect: str) bool = { + if (buf.len != expect.len) { return false; }; + let i: i32 = 0; + for (i < buf.len) { + if (buf[i] != expect[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn encvec(input: str, expect: str) void = { + let inbuf: [128]u8; + let outbuf: [128]u8; + let n: i32 = putstr(input, inbuf[0:128], 0); + let m: i32 = base32.encode(outbuf[0:128], inbuf[0:n]); + if (m != expect.len) { let _: i32 = 1/0; }; + if (!streq(outbuf[0:m], expect)) { let _: i32 = 1/0; }; +}; + +@test fn rfc4648_std() void = { + // RFC 4648 §10 test vectors. + encvec("", ""); + encvec("f", "MY======"); + encvec("fo", "MZXQ===="); + encvec("foo", "MZXW6==="); + encvec("foob", "MZXW6YQ="); + encvec("fooba", "MZXW6YTB"); + encvec("foobar", "MZXW6YTBOI======"); +}; + +fn decvec(input: str, expect: str) void = { + let inbuf: [128]u8; + let outbuf: [128]u8; + let n: i32 = putstr(input, inbuf[0:128], 0); + let r: (i32 | base32.invalid) = base32.decode(outbuf[0:128], inbuf[0:n]); + match (r) { + case let m: i32 => { + if (m != expect.len) { let _: i32 = 1/0; }; + if (!streq(outbuf[0:m], expect)) { let _: i32 = 1/0; }; + }; + case let e: base32.invalid => { let _: i32 = 1/0; }; + }; +}; + +@test fn rfc4648_decode() void = { + decvec("", ""); + decvec("MY======", "f"); + decvec("MZXQ====", "fo"); + decvec("MZXW6===", "foo"); + decvec("MZXW6YQ=", "foob"); + decvec("MZXW6YTB", "fooba"); + decvec("MZXW6YTBOI======", "foobar"); +}; + +fn enchexvec(input: str, expect: str) void = { + let inbuf: [128]u8; + let outbuf: [128]u8; + let n: i32 = putstr(input, inbuf[0:128], 0); + let m: i32 = base32.encodehex(outbuf[0:128], inbuf[0:n]); + if (m != expect.len) { let _: i32 = 1/0; }; + if (!streq(outbuf[0:m], expect)) { let _: i32 = 1/0; }; +}; + +@test fn rfc4648_hex() void = { + // RFC 4648 §10 base32hex vectors. + enchexvec("", ""); + enchexvec("f", "CO======"); + enchexvec("fo", "CPNG===="); + enchexvec("foo", "CPNMU==="); + enchexvec("foob", "CPNMUOG="); + enchexvec("fooba", "CPNMUOJ1"); + enchexvec("foobar", "CPNMUOJ1E8======"); +}; + +@test fn roundtrip_all_quintets() void = { + // Encode then decode every 5-byte combination of a small set. + let raw: [5]u8; + raw[0] = 0x00u8; + raw[1] = 0x55u8; + raw[2] = 0xAAu8; + raw[3] = 0xFFu8; + raw[4] = 0x01u8; + let enc: [16]u8; + let dec: [5]u8; + let m: i32 = base32.encode(enc[0:16], raw[0:5]); + if (m != 8) { let _: i32 = 1/0; }; + let r: (i32 | base32.invalid) = base32.decode(dec[0:5], enc[0:m]); + match (r) { + case let n: i32 => { + if (n != 5) { let _: i32 = 1/0; }; + let i: i32 = 0; + for (i < 5) { + if (dec[i] != raw[i]) { let _: i32 = 1/0; }; + i += 1; + }; + }; + case let e: base32.invalid => { let _: i32 = 1/0; }; + }; +}; + +@test fn invalid_inputs() void = { + let inbuf: [16]u8; + let outbuf: [16]u8; + // Length not a multiple of 8. + let n: i32 = putstr("ABCD", inbuf[0:16], 0); + let r1: (i32 | base32.invalid) = base32.decode(outbuf[0:16], inbuf[0:n]); + match (r1) { + case let m: i32 => { let _: i32 = 1/0; }; + case let e: base32.invalid => void; + }; + // Bad pad count (5 '=' is illegal — must be 0,1,3,4,6). + let n2: i32 = putstr("MZX=====", inbuf[0:16], 0); + let r2: (i32 | base32.invalid) = base32.decode(outbuf[0:16], inbuf[0:n2]); + match (r2) { + case let m: i32 => { let _: i32 = 1/0; }; + case let e: base32.invalid => void; + }; + // Bad char ('1' is not in the std alphabet). + let n3: i32 = putstr("MZ1W6YTB", inbuf[0:16], 0); + let r3: (i32 | base32.invalid) = base32.decode(outbuf[0:16], inbuf[0:n3]); + match (r3) { + case let m: i32 => { let _: i32 = 1/0; }; + case let e: base32.invalid => void; + }; +}; + +@test fn sizes() void = { + if (base32.encodedsize(0) != 0) { let _: i32 = 1/0; }; + if (base32.encodedsize(1) != 8) { let _: i32 = 1/0; }; + if (base32.encodedsize(5) != 8) { let _: i32 = 1/0; }; + if (base32.encodedsize(6) != 16) { let _: i32 = 1/0; }; + if (base32.decodedsize(8) != 5) { let _: i32 = 1/0; }; + if (base32.decodedsize(16) != 10) { let _: i32 = 1/0; }; +}; + +export fn main() i32 = { + rfc4648_std(); + rfc4648_decode(); + rfc4648_hex(); + roundtrip_all_quintets(); + invalid_inputs(); + sizes(); + return 0; +}; diff --git a/lib/encoding/base64/base64.ww b/lib/encoding/base64/base64.ww new file mode 100644 index 00000000..bb0367f8 --- /dev/null +++ b/lib/encoding/base64/base64.ww @@ -0,0 +1,182 @@ +// encoding/base64 — RFC 4648 base64 encode/decode, buffer-based. +// +// Mirrors Hare's encoding::base64 surface, modulo Hare's stream-based +// encoder/decoder. ww ships the in-memory subset only: `encode(dst, +// src)` writes the encoded bytes into `dst`, returning the count; +// `decode(dst, src)` writes the decoded bytes into `dst`, returning a +// count or invalid. +// +// std uses '+' and '/' for indexes 62 and 63 (the RFC 4648 §4 +// alphabet); url uses '-' and '_' (the §5 url-safe alphabet). Both +// pad encoded output with '=' to a multiple of 4 bytes. + +// invalid — input was not well-formed base64 (bad char, wrong length, +// padding error). Payload is the byte index of the first offending +// position. Matches Hare's errors::invalid pairing with strconv. +export type invalid = !i32; + +// encodedsize — bytes required to encode `n` source bytes (including +// '=' padding). Hare names it the same. +export fn encodedsize(n: i32) i32 = { + if (n == 0) { return 0; }; + return ((n - 1) / 3 + 1) * 4; +}; + +// decodedsize — upper bound on the number of bytes decoded from `n` +// encoded bytes. The exact count depends on padding; callers consult +// the i32 returned by `decode`. +export fn decodedsize(n: i32) i32 = { + return (n / 4) * 3; +}; + +// encchar — map a 6-bit value to its alphabet character. `urlsafe` +// chooses '-'/'_' instead of '+'/'/' for 62/63. +fn encchar(v: u8, urlsafe: bool) u8 = { + if (v < 26u8) { return v + 65u8; }; // 'A' + v + if (v < 52u8) { return v + 71u8; }; // 'a' + (v - 26) = v + 71 + if (v < 62u8) { return v - 4u8; }; // '0' + (v - 52) = v - 4 + if (v == 62u8) { + if (urlsafe) { return 45u8; }; // '-' + return 43u8; // '+' + }; + if (urlsafe) { return 95u8; }; // '_' + return 47u8; // '/' +}; + +// decchar — inverse of encchar. Returns 0..63 on success or 255 on +// invalid char. '=' is handled in the decode loop, not here. +fn decchar(c: u8, urlsafe: bool) u8 = { + if (c >= 65u8) { if (c <= 90u8) { return c - 65u8; }; }; // 'A'..'Z' + if (c >= 97u8) { if (c <= 122u8) { return c - 71u8; }; }; // 'a'..'z' + if (c >= 48u8) { if (c <= 57u8) { return c + 4u8; }; }; // '0'..'9' + if (urlsafe) { + if (c == 45u8) { return 62u8; }; // '-' + if (c == 95u8) { return 63u8; }; // '_' + } else { + if (c == 43u8) { return 62u8; }; // '+' + if (c == 47u8) { return 63u8; }; // '/' + }; + return 255u8; +}; + +// encodeinto — encode `src` into `dst` using the std (`urlsafe=false`) +// or url-safe (`urlsafe=true`) alphabet. `dst` must hold at least +// encodedsize(src.len) bytes. Returns the number of bytes written. +fn encodeinto(dst: []u8, src: []u8, urlsafe: bool) i32 = { + let i: i32 = 0; + let j: i32 = 0; + for (i + 2 < src.len) { + let b0: u8 = src[i]; + let b1: u8 = src[i + 1]; + let b2: u8 = src[i + 2]; + dst[j] = encchar(b0 >> 2u8, urlsafe); + dst[j + 1] = encchar(((b0 & 3u8) << 4u8) | (b1 >> 4u8), urlsafe); + dst[j + 2] = encchar(((b1 & 15u8) << 2u8) | (b2 >> 6u8), urlsafe); + dst[j + 3] = encchar(b2 & 63u8, urlsafe); + i += 3; + j += 4; + }; + let rem: i32 = src.len - i; + if (rem == 1) { + let b0: u8 = src[i]; + dst[j] = encchar(b0 >> 2u8, urlsafe); + dst[j + 1] = encchar((b0 & 3u8) << 4u8, urlsafe); + dst[j + 2] = 61u8; // '=' + dst[j + 3] = 61u8; // '=' + j += 4; + }; + if (rem == 2) { + let b0: u8 = src[i]; + let b1: u8 = src[i + 1]; + dst[j] = encchar(b0 >> 2u8, urlsafe); + dst[j + 1] = encchar(((b0 & 3u8) << 4u8) | (b1 >> 4u8), urlsafe); + dst[j + 2] = encchar((b1 & 15u8) << 2u8, urlsafe); + dst[j + 3] = 61u8; // '=' + j += 4; + }; + return j; +}; + +// encode — encode `src` into `dst` using the std alphabet. Returns +// the number of bytes written. `dst` must hold at least +// encodedsize(src.len) bytes. +export fn encode(dst: []u8, src: []u8) i32 = { + return encodeinto(dst, src, false); +}; + +// encodeurl — same as encode but uses the url-safe alphabet ('-'/'_' +// for 62/63). +export fn encodeurl(dst: []u8, src: []u8) i32 = { + return encodeinto(dst, src, true); +}; + +// decodeinto — decode base64 `src` into `dst`. `dst` must hold at +// least decodedsize(src.len) bytes. Returns the number of bytes +// written, or invalid with the offending source index. +fn decodeinto(dst: []u8, src: []u8, urlsafe: bool) (i32 | invalid) = { + if (src.len == 0) { return 0; }; + if ((src.len & 3) != 0) { return src.len: invalid; }; + let i: i32 = 0; + let j: i32 = 0; + let end: i32 = src.len; + for (i < end) { + let c0: u8 = src[i]; + let c1: u8 = src[i + 1]; + let c2: u8 = src[i + 2]; + let c3: u8 = src[i + 3]; + let v0: u8 = decchar(c0, urlsafe); + let v1: u8 = decchar(c1, urlsafe); + if (v0 == 255u8) { return i: invalid; }; + if (v1 == 255u8) { return (i + 1): invalid; }; + // Last quad may carry '=' padding. + if (i + 4 == end) { + if (c2 == 61u8) { + // "XX==" + if (c3 != 61u8) { return (i + 3): invalid; }; + dst[j] = (v0 << 2u8) | (v1 >> 4u8); + j += 1; + i += 4; + return j; + }; + let v2: u8 = decchar(c2, urlsafe); + if (v2 == 255u8) { return (i + 2): invalid; }; + if (c3 == 61u8) { + // "XXX=" + dst[j] = (v0 << 2u8) | (v1 >> 4u8); + dst[j + 1] = (v1 << 4u8) | (v2 >> 2u8); + j += 2; + i += 4; + return j; + }; + let v3: u8 = decchar(c3, urlsafe); + if (v3 == 255u8) { return (i + 3): invalid; }; + dst[j] = (v0 << 2u8) | (v1 >> 4u8); + dst[j + 1] = (v1 << 4u8) | (v2 >> 2u8); + dst[j + 2] = (v2 << 6u8) | v3; + j += 3; + i += 4; + return j; + }; + let v2: u8 = decchar(c2, urlsafe); + let v3: u8 = decchar(c3, urlsafe); + if (v2 == 255u8) { return (i + 2): invalid; }; + if (v3 == 255u8) { return (i + 3): invalid; }; + dst[j] = (v0 << 2u8) | (v1 >> 4u8); + dst[j + 1] = (v1 << 4u8) | (v2 >> 2u8); + dst[j + 2] = (v2 << 6u8) | v3; + i += 4; + j += 3; + }; + return j; +}; + +// decode — decode std-alphabet base64 from `src` into `dst`. Returns +// the count of decoded bytes, or invalid on a malformed input. +export fn decode(dst: []u8, src: []u8) (i32 | invalid) = { + return decodeinto(dst, src, false); +}; + +// decodeurl — same as decode but accepts the url-safe alphabet. +export fn decodeurl(dst: []u8, src: []u8) (i32 | invalid) = { + return decodeinto(dst, src, true); +}; diff --git a/lib/encoding/base64/base64_test.ww b/lib/encoding/base64/base64_test.ww new file mode 100644 index 00000000..74036041 --- /dev/null +++ b/lib/encoding/base64/base64_test.ww @@ -0,0 +1,172 @@ +use base64; + +fn putstr(s: str, into: []u8, off: i32) i32 = { + let i: i32 = 0; + for (i < s.len) { + into[off + i] = s[i]; + i += 1; + }; + return off + s.len; +}; + +fn streq(buf: []u8, expect: str) bool = { + if (buf.len != expect.len) { return false; }; + let i: i32 = 0; + for (i < buf.len) { + if (buf[i] != expect[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn encodevec(input: str, expect: str) void = { + let inbuf: [128]u8; + let outbuf: [128]u8; + let n: i32 = putstr(input, inbuf[0:128], 0); + let m: i32 = base64.encode(outbuf[0:128], inbuf[0:n]); + if (m != expect.len) { let _: i32 = 1/0; }; + if (!streq(outbuf[0:m], expect)) { let _: i32 = 1/0; }; +}; + +@test fn rfc4648_vectors() void = { + encodevec("", ""); + encodevec("f", "Zg=="); + encodevec("fo", "Zm8="); + encodevec("foo", "Zm9v"); + encodevec("foob", "Zm9vYg=="); + encodevec("fooba", "Zm9vYmE="); + encodevec("foobar", "Zm9vYmFy"); +}; + +fn decodevec(input: str, expect: str) void = { + let inbuf: [128]u8; + let outbuf: [128]u8; + let n: i32 = putstr(input, inbuf[0:128], 0); + let r: (i32 | base64.invalid) = base64.decode(outbuf[0:128], inbuf[0:n]); + match (r) { + case let m: i32 => { + if (m != expect.len) { let _: i32 = 1/0; }; + if (!streq(outbuf[0:m], expect)) { let _: i32 = 1/0; }; + }; + case let e: base64.invalid => { let _: i32 = 1/0; }; + }; +}; + +@test fn rfc4648_decode() void = { + decodevec("", ""); + decodevec("Zg==", "f"); + decodevec("Zm8=", "fo"); + decodevec("Zm9v", "foo"); + decodevec("Zm9vYg==", "foob"); + decodevec("Zm9vYmE=", "fooba"); + decodevec("Zm9vYmFy", "foobar"); +}; + +@test fn alphabet_full() void = { + // Round-trip every 6-bit value (0..63) by encoding three bytes that + // expose b0=0x00, b1=AA, b2=FF — the encoded chars depend on all + // four positions including the >>2 path. + let i: i32 = 0; + for (i < 64) { + let bits: u8 = i: u8; + // Construct a triple [bits<<2, 0, 0] so the first encoded + // char encodes `bits`. The other three chars are derivable + // from the remaining bytes; we only check the first here. + let inbuf: [3]u8; + inbuf[0] = bits << 2u8; + inbuf[1] = 0u8; + inbuf[2] = 0u8; + let outbuf: [4]u8; + let m: i32 = base64.encode(outbuf[0:4], inbuf[0:3]); + if (m != 4) { let _: i32 = 1/0; }; + // Decoding back must give us `bits` in the high 6 bits of [0]. + let r: (i32 | base64.invalid) = base64.decode(inbuf[0:3], outbuf[0:4]); + match (r) { + case let n: i32 => { + if (n != 3) { let _: i32 = 1/0; }; + if ((inbuf[0] >> 2u8) != bits) { let _: i32 = 1/0; }; + }; + case let e: base64.invalid => { let _: i32 = 1/0; }; + }; + i += 1; + }; +}; + +@test fn invalid_inputs() void = { + let inbuf: [16]u8; + let outbuf: [16]u8; + // Length not a multiple of 4. + let n: i32 = putstr("abc", inbuf[0:16], 0); + let r1: (i32 | base64.invalid) = base64.decode(outbuf[0:16], inbuf[0:n]); + match (r1) { + case let m: i32 => { let _: i32 = 1/0; }; + case let e: base64.invalid => void; + }; + // Bad char ('@' is not in the std alphabet). + let n2: i32 = putstr("Z@==", inbuf[0:16], 0); + let r2: (i32 | base64.invalid) = base64.decode(outbuf[0:16], inbuf[0:n2]); + match (r2) { + case let m: i32 => { let _: i32 = 1/0; }; + case let e: base64.invalid => void; + }; +}; + +@test fn urlsafe_roundtrip() void = { + // Byte sequence chosen so the std alphabet would use '+' and '/', + // while url-safe replaces them with '-' and '_'. 0xFB = 11111011 + // hits index 62 in some quad, and 0xFF hits 63. + let raw: [3]u8; + raw[0] = 0xFBu8; + raw[1] = 0xFFu8; + raw[2] = 0xBFu8; + let std: [8]u8; + let url: [8]u8; + let dec: [3]u8; + let m1: i32 = base64.encode(std[0:8], raw[0:3]); + let m2: i32 = base64.encodeurl(url[0:8], raw[0:3]); + if (m1 != 4) { let _: i32 = 1/0; }; + if (m2 != 4) { let _: i32 = 1/0; }; + // Round-trip both ways. + let r1: (i32 | base64.invalid) = base64.decode(dec[0:3], std[0:m1]); + match (r1) { + case let n: i32 => { + if (n != 3) { let _: i32 = 1/0; }; + if (dec[0] != raw[0]) { let _: i32 = 1/0; }; + if (dec[1] != raw[1]) { let _: i32 = 1/0; }; + if (dec[2] != raw[2]) { let _: i32 = 1/0; }; + }; + case let e: base64.invalid => { let _: i32 = 1/0; }; + }; + let r2: (i32 | base64.invalid) = base64.decodeurl(dec[0:3], url[0:m2]); + match (r2) { + case let n: i32 => { + if (n != 3) { let _: i32 = 1/0; }; + if (dec[0] != raw[0]) { let _: i32 = 1/0; }; + if (dec[1] != raw[1]) { let _: i32 = 1/0; }; + if (dec[2] != raw[2]) { let _: i32 = 1/0; }; + }; + case let e: base64.invalid => { let _: i32 = 1/0; }; + }; +}; + +@test fn sizes() void = { + if (base64.encodedsize(0) != 0) { let _: i32 = 1/0; }; + if (base64.encodedsize(1) != 4) { let _: i32 = 1/0; }; + if (base64.encodedsize(2) != 4) { let _: i32 = 1/0; }; + if (base64.encodedsize(3) != 4) { let _: i32 = 1/0; }; + if (base64.encodedsize(4) != 8) { let _: i32 = 1/0; }; + if (base64.encodedsize(6) != 8) { let _: i32 = 1/0; }; + if (base64.encodedsize(7) != 12) { let _: i32 = 1/0; }; + if (base64.decodedsize(4) != 3) { let _: i32 = 1/0; }; + if (base64.decodedsize(8) != 6) { let _: i32 = 1/0; }; +}; + +export fn main() i32 = { + rfc4648_vectors(); + rfc4648_decode(); + alphabet_full(); + invalid_inputs(); + urlsafe_roundtrip(); + sizes(); + return 0; +}; diff --git a/lib/hash/crc64/crc64.ww b/lib/hash/crc64/crc64.ww new file mode 100644 index 00000000..adce8f58 --- /dev/null +++ b/lib/hash/crc64/crc64.ww @@ -0,0 +1,36 @@ +// hash/crc64 — CRC-64 checksum. Pure ww. +// +// Same shape as lib/hash/crc32: per-byte inline polynomial shift, no +// precomputed table. Slower than Hare's table-driven path by ~8x per +// byte but produces identical answers for the documented polynomials. +// +// Polynomials are given in reversed form, matching Hare. + +def ECMA: u64 = 0xC96C5795D7870F42u64; // ECMA-182, xz-utils +def ISO: u64 = 0xD800000000000000u64; // ISO 3309 HDLC + +// sum64 — fold `buf` under `poly` (reversed form). Initial cval is +// ~0u64; per byte we mix in the low byte via 8 polynomial shifts and +// XOR with the high seven bytes shifted down. +export fn sum64(buf: []u8, poly: u64) u64 = { + let c: u64 = 0xFFFFFFFFFFFFFFFFu64; + let i: i32 = 0; + for (i < buf.len) { + let t: u64 = (c & 0xFFu64) ^ (buf[i]: u64); + let z: i32 = 0; + for (z < 8) { + if ((t & 1u64) == 1u64) { + t = (t >> 1u64) ^ poly; + } else { + t = t >> 1u64; + }; + z += 1; + }; + c = t ^ (c >> 8u64); + i += 1; + }; + return ~c; +}; + +export fn sum64ecma(buf: []u8) u64 = { return sum64(buf, ECMA); }; +export fn sum64iso(buf: []u8) u64 = { return sum64(buf, ISO); }; diff --git a/lib/hash/crc64/crc64_test.ww b/lib/hash/crc64/crc64_test.ww new file mode 100644 index 00000000..2930dcd6 --- /dev/null +++ b/lib/hash/crc64/crc64_test.ww @@ -0,0 +1,66 @@ +use crc64; + +fn putstr(s: str, into: []u8, off: i32) i32 = { + let i: i32 = 0; + for (i < s.len) { + into[off + i] = s[i]; + i += 1; + }; + return off + s.len; +}; + +fn check(s: str, ecma: u64, iso: u64) void = { + let arr: [128]u8; + let n: i32 = putstr(s, arr[0:128], 0); + let buf: []u8 = arr[0:n]; + if (crc64.sum64ecma(buf) != ecma) { let _: i32 = 1/0; }; + if (crc64.sum64iso(buf) != iso) { let _: i32 = 1/0; }; +}; + +@test fn vec_empty() void = { + let arr: [1]u8; + let buf: []u8 = arr[0:0]; + if (crc64.sum64ecma(buf) != 0u64) { let _: i32 = 1/0; }; + if (crc64.sum64iso(buf) != 0u64) { let _: i32 = 1/0; }; +}; + +@test fn vec_oscar() void = { + check("Man can believe the impossible, but can never believe the improbable. -- Oscar Wilde", + 0x17F71EF3BB851DC7u64, 0xD0E8ED57865D6AC6u64); +}; + +@test fn vec_hegel() void = { + check("We learn from history that we do not learn from history. -- Georg Hegel", + 0xAA3C335BB49ABE9Du64, 0x5FD192CC516BBEC3u64); +}; + +@test fn vec_chapelain() void = { + check("The final delusion is the belief that one has lost all delusions. -- Maurice Chapelain", + 0x7DFC4F7CE9552D23u64, 0xF71B429C925D99AEu64); +}; + +@test fn vec_unix() void = { + check("UNIX is simple and coherent", + 0x9C1938E72C1D8619u64, 0x372ABFFD392FF27Du64); +}; + +@test fn vec_gnu() void = { + check("GNU's not UNIX", + 0x2C0C97B1CB17FCBAu64, 0x9BD1FA95A419A43Du64); +}; + +@test fn vec_blm() void = { + check("Black lives matter", + 0x159BB7B6086BF47Eu64, 0xC218CBB390CF44EBu64); +}; + +export fn main() i32 = { + vec_empty(); + vec_oscar(); + vec_hegel(); + vec_chapelain(); + vec_unix(); + vec_gnu(); + vec_blm(); + return 0; +}; diff --git a/lib/hash/siphash/siphash.ww b/lib/hash/siphash/siphash.ww new file mode 100644 index 00000000..3bdf3a26 --- /dev/null +++ b/lib/hash/siphash/siphash.ww @@ -0,0 +1,96 @@ +// hash/siphash — SipHash-2-4 keyed hash, buffer-based. +// +// Mirrors Hare's hash::siphash for the one-shot path: take a 16-byte +// key and a buffer, return the 64-bit hash. Hare ships a streaming +// io::stream-backed type; ww's subset doesn't, matching the rest of +// lib/hash/*. The (c, d) parameter pair is exposed as `sum`; `sum24` +// fixes c=2, d=4 (the recommendation in the SipHash paper). +// +// Constants and round structure follow Aumasson & Bernstein, "SipHash: +// a fast short-input PRF" (CHES 2012). + +use endian; + +fn rotl64(x: u64, n: u64) u64 = { + return (x << n) | (x >> (64u64 - n)); +}; + +fn round(v: *[4]u64) void = { + let v0: u64 = v[0]; + let v1: u64 = v[1]; + let v2: u64 = v[2]; + let v3: u64 = v[3]; + v0 = v0 + v1; + v1 = rotl64(v1, 13u64); + v1 = v1 ^ v0; + v0 = rotl64(v0, 32u64); + v2 = v2 + v3; + v3 = rotl64(v3, 16u64); + v3 = v3 ^ v2; + v0 = v0 + v3; + v3 = rotl64(v3, 21u64); + v3 = v3 ^ v0; + v2 = v2 + v1; + v1 = rotl64(v1, 17u64); + v1 = v1 ^ v2; + v2 = rotl64(v2, 32u64); + v[0] = v0; + v[1] = v1; + v[2] = v2; + v[3] = v3; +}; + +// sum — compute SipHash-c-d of `buf` under `key`. `key` must be a +// 16-byte slice. (c, d) are the compression and finalization round +// counts. Mirrors Hare's siphash::sum (one-shot form). +export fn sum(key: []u8, buf: []u8, c: i32, d: i32) u64 = { + let k0: u64 = endian.legetu64(key[0:8]); + let k1: u64 = endian.legetu64(key[8:16]); + let v: [4]u64; + v[0] = 0x736F6D6570736575u64 ^ k0; + v[1] = 0x646F72616E646F6Du64 ^ k1; + v[2] = 0x6C7967656E657261u64 ^ k0; + v[3] = 0x7465646279746573u64 ^ k1; + + let i: i32 = 0; + let n: i32 = buf.len; + let last: i32 = n - (n & 7); // last whole-block boundary + for (i < last) { + let m: u64 = endian.legetu64(buf[i:i + 8]); + v[3] = v[3] ^ m; + let r: i32 = 0; + for (r < c) { round(&v); r += 1; }; + v[0] = v[0] ^ m; + i += 8; + }; + + // Pack the trailing 0..7 bytes plus length-byte into the final + // 8-byte word. + let tail: u64 = 0u64; + let shift: u64 = 0u64; + let k: i32 = i; + for (k < n) { + let b: u64 = (buf[k]: u64) & 0xFFu64; + tail = tail | (b << shift); + shift = shift + 8u64; + k += 1; + }; + let lenbyte: u64 = (n: u64) & 0xFFu64; + tail = tail | (lenbyte << 56u64); + + v[3] = v[3] ^ tail; + let r2: i32 = 0; + for (r2 < c) { round(&v); r2 += 1; }; + v[0] = v[0] ^ tail; + + v[2] = v[2] ^ 0xFFu64; + let r3: i32 = 0; + for (r3 < d) { round(&v); r3 += 1; }; + return v[0] ^ v[1] ^ v[2] ^ v[3]; +}; + +// sum24 — SipHash-2-4 of `buf` under `key`. The standard recommended +// variant. Equivalent to sum(key, buf, 2, 4). +export fn sum24(key: []u8, buf: []u8) u64 = { + return sum(key, buf, 2, 4); +}; diff --git a/lib/hash/siphash/siphash_test.ww b/lib/hash/siphash/siphash_test.ww new file mode 100644 index 00000000..38df9bc6 --- /dev/null +++ b/lib/hash/siphash/siphash_test.ww @@ -0,0 +1,75 @@ +use siphash; + +fn putstr(s: str, into: []u8, off: i32) i32 = { + let i: i32 = 0; + for (i < s.len) { + into[off + i] = s[i]; + i += 1; + }; + return off + s.len; +}; + +@test fn refkey_msgN() void = { + // Reference SipHash-2-4 vectors: key = 0x00..0x0F, message = + // 0x00..0x{n-1}. The reference test suite ships 64 vectors; + // we verify a representative sample (n in {0,1,4,7,8,9,15,16, + // 23,32}) which exercises both the partial-block and whole- + // block paths. + let key: [16]u8; + let i: i32 = 0; + for (i < 16) { key[i] = i: u8; i += 1; }; + let msg: [40]u8; + let j: i32 = 0; + for (j < 40) { msg[j] = j: u8; j += 1; }; + + if (siphash.sum24(key[0:16], msg[0:0]) != 0x726FDB47DD0E0E31u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:1]) != 0x74F839C593DC67FDu64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:4]) != 0xCF2794E0277187B7u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:7]) != 0xAB0200F58B01D137u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:8]) != 0x93F5F5799A932462u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:9]) != 0x9E0082DF0BA9E4B0u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:15]) != 0xA129CA6149BE45E5u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:16]) != 0x3F2ACC7F57C29BDBu64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:23]) != 0xA80C038CCD5CCEC8u64) { let _: i32 = 1/0; }; + if (siphash.sum24(key[0:16], msg[0:32]) != 0x7127512F72F27CCEu64) { let _: i32 = 1/0; }; +}; + +@test fn ascii() void = { + let key: [16]u8; + let kstr: str = "sixteen-bytkey!?"; + let i: i32 = 0; + for (i < 16) { key[i] = kstr[i]; i += 1; }; + + let buf: [128]u8; + let n: i32 = 0; + n = putstr("a", buf[0:128], 0); + if (siphash.sum24(key[0:16], buf[0:n]) != 0xFA7E197B2F4427D4u64) { let _: i32 = 1/0; }; + + n = putstr("abc", buf[0:128], 0); + if (siphash.sum24(key[0:16], buf[0:n]) != 0x133D3A92BB9CA86Bu64) { let _: i32 = 1/0; }; + + n = putstr("foobar", buf[0:128], 0); + if (siphash.sum24(key[0:16], buf[0:n]) != 0x84D2B5169DB6D3BFu64) { let _: i32 = 1/0; }; + + n = putstr("hello world", buf[0:128], 0); + if (siphash.sum24(key[0:16], buf[0:n]) != 0xB0417FA523CF7B36u64) { let _: i32 = 1/0; }; + + n = putstr("The quick brown fox jumps over the lazy dog", buf[0:128], 0); + if (siphash.sum24(key[0:16], buf[0:n]) != 0xA4CBBF0BDB704790u64) { let _: i32 = 1/0; }; +}; + +@test fn empty() void = { + let key: [16]u8; + let kstr: str = "sixteen-bytkey!?"; + let i: i32 = 0; + for (i < 16) { key[i] = kstr[i]; i += 1; }; + let empty: [1]u8; + if (siphash.sum24(key[0:16], empty[0:0]) != 0x2A51AE0682925836u64) { let _: i32 = 1/0; }; +}; + +export fn main() i32 = { + refkey_msgN(); + ascii(); + empty(); + return 0; +}; diff --git a/lib/math/random/random.ww b/lib/math/random/random.ww new file mode 100644 index 00000000..e01ad47c --- /dev/null +++ b/lib/math/random/random.ww @@ -0,0 +1,56 @@ +// math/random — SplitMix64 PRNG. +// +// Mirrors Hare's math::random surface. The state is a single u64; +// Hare types it as `random = u64`. Callers thread a pointer through +// next/u32n/u64n so each call advances the state in place. +// Deterministic — same seed reproduces the same sequence. + +export type random = u64; + +// init — initialize a generator with `seed`. Same seed reproduces the +// same sequence on every run. Mirrors Hare's random::init. +export fn init(seed: u64) random = { return seed: random; }; + +// next — return a pseudo-random 64-bit value and advance the state. +// SplitMix64, per Hare's random::next. +export fn next(r: *random) u64 = { + let s: u64 = (*r): u64 + 0x9E3779B97F4A7C15u64; + *r = s: random; + let a: u64 = s; + a = (a ^ (a >> 30u64)) * 0xBF58476D1CE4E5B9u64; + a = (a ^ (a >> 27u64)) * 0x94D049BB133111EBu64; + return a ^ (a >> 31u64); +}; + +// u32n — pseudo-random u32 in [0, n). n must be > 0. Uses Lemire's +// fast unbiased mapping (mulhi-then-leftover-reject). Mirrors Hare's +// random::u32n. +export fn u32n(r: *random, n: u32) u32 = { + let x: u32 = next(r): u32; + let prod: u64 = (x: u64) * (n: u64); + let leftover: u32 = prod: u32; + if (leftover < n) { + // thresh = -n mod n (two's-complement on u32). + let neg: u32 = 0u32 - n; + let thresh: u32 = neg % n; + for (leftover < thresh) { + x = next(r): u32; + prod = (x: u64) * (n: u64); + leftover = prod: u32; + }; + }; + return (prod >> 32u64): u32; +}; + +// u64n — pseudo-random u64 in [0, n). n must be > 0. Power-of-2 fast +// path; otherwise rejection-sample to avoid modulo bias. Mirrors +// Hare's random::u64n. +export fn u64n(r: *random, n: u64) u64 = { + if ((n & (n - 1u64)) == 0u64) { return next(r) & (n - 1u64); }; + // max = U64_MAX - (U64_MAX+1) % n = -1 - (-n % n) + let neg: u64 = (0u64 - n); + let max: u64 = 0xFFFFFFFFFFFFFFFFu64 - (neg % n); + let out: u64 = next(r); + for (out > max) { out = next(r); }; + return out % n; +}; diff --git a/lib/math/random/random_test.ww b/lib/math/random/random_test.ww new file mode 100644 index 00000000..faa594db --- /dev/null +++ b/lib/math/random/random_test.ww @@ -0,0 +1,61 @@ +use random; + +@test fn seq() void = { + let r: random.random = random.init(1234567u64); + if (random.next(&r) != 6457827717110365317u64) { let _: i32 = 1/0; }; + if (random.next(&r) != 3203168211198807973u64) { let _: i32 = 1/0; }; + if (random.next(&r) != 9817491932198370423u64) { let _: i32 = 1/0; }; + if (random.next(&r) != 4593380528125082431u64) { let _: i32 = 1/0; }; + if (random.next(&r) != 16408922859458223821u64) { let _: i32 = 1/0; }; +}; + +@test fn deterministic() void = { + let a: random.random = random.init(42u64); + let b: random.random = random.init(42u64); + let i: i32 = 0; + for (i < 32) { + if (random.next(&a) != random.next(&b)) { let _: i32 = 1/0; }; + i += 1; + }; +}; + +@test fn u32n_inrange() void = { + let r: random.random = random.init(7u64); + let i: i32 = 0; + for (i < 200) { + let v: u32 = random.u32n(&r, 17u32); + // v stored as u64 with possible high bits — mask before compare. + let vv: u64 = (v: u64) & 0xFFFFFFFFu64; + if (vv >= 17u64) { let _: i32 = 1/0; }; + i += 1; + }; +}; + +@test fn u64n_pow2() void = { + let r: random.random = random.init(99u64); + let i: i32 = 0; + for (i < 200) { + let v: u64 = random.u64n(&r, 16u64); + if (v >= 16u64) { let _: i32 = 1/0; }; + i += 1; + }; +}; + +@test fn u64n_nonpow2() void = { + let r: random.random = random.init(123u64); + let i: i32 = 0; + for (i < 200) { + let v: u64 = random.u64n(&r, 100u64); + if (v >= 100u64) { let _: i32 = 1/0; }; + i += 1; + }; +}; + +export fn main() i32 = { + seq(); + deterministic(); + u32n_inrange(); + u64n_pow2(); + u64n_nonpow2(); + return 0; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4e4df73b..955513ef 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8140,10 +8140,48 @@ fn cgcast(c: *cgen, n: *node) void = { if (srcstr) { emitline("\tMOVQ\tBX, CX\n"); }; }; // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; - // same-kind casts (int↔int with widening differences, - // f64→f64 etc.) stay no-ops at the asm level, matching the - // pre-port behaviour for integer casts. - if (srcfk == 0 && dstfk == 0) { return; }; + // int↔int casts narrow via an explicit clamp before the early + // return so `(big_u64): u32` doesn't leak the upper 32 bits. + // Hare semantics: `expr: T` truncates to T's bit width (mod 2^n). + // Mirrors cmd/w6c/cgen.c's N_CAST clamp; signed-narrow targets + // (i8/i16/i32) stay no-ops until the assembler grows MOVSBQ / + // MOVSWQ / MOVSXD reg-reg forms. + if (srcfk == 0 && dstfk == 0) { + let tn: *node = n.rhs; + // Walk through alias chains (`type random = u64`). + for (tn != nil) { + if (tn.kind != nkind.N_TNAME) { tn = nil; } + else { + let nm: str = tn.str; + if (primsize(nm) > 0) { break; }; + let alias: *node = aliaslookup(c, nm); + if (alias == nil) { tn = nil; } + else { tn = alias; }; + }; + }; + if (tn != nil) { + let nm: str = tn.str; + let sz: i32 = primsize(nm); + let is_unsigned: bool = typenameisunsigned(nm); + let is_bool: bool = streq(nm, "bool"); + if (sz > 0) { if (sz < 8) { + if (is_unsigned) { + if (sz == 4) { + emitline("\tMOVL\tAX, AX\n"); + } else { + let mask: i64 = 0xFFi64; + if (sz == 2) { mask = 0xFFFFi64; }; + emitline("\tANDQ\t$"); + emitint(mask); + emitline(", AX\n"); + }; + } else { if (is_bool) { + emitline("\tANDQ\t$255, AX\n"); + }; }; + }; }; + }; + return; + }; if (srcfk == 0 && dstfk == 2) { emitline("\tCVTSI2SD\tAX, X0\n"); return; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e30cca29..cd63a005 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -375,10 +375,48 @@ fn cgcast(c: *cgen, n: *node) void = { if (srcstr) { emitline("\tMOVQ\tBX, CX\n"); }; }; // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; - // same-kind casts (int↔int with widening differences, - // f64→f64 etc.) stay no-ops at the asm level, matching the - // pre-port behaviour for integer casts. - if (srcfk == 0 && dstfk == 0) { return; }; + // int↔int casts narrow via an explicit clamp before the early + // return so `(big_u64): u32` doesn't leak the upper 32 bits. + // Hare semantics: `expr: T` truncates to T's bit width (mod 2^n). + // Mirrors cmd/w6c/cgen.c's N_CAST clamp; signed-narrow targets + // (i8/i16/i32) stay no-ops until the assembler grows MOVSBQ / + // MOVSWQ / MOVSXD reg-reg forms. + if (srcfk == 0 && dstfk == 0) { + let tn: *node = n.rhs; + // Walk through alias chains (`type random = u64`). + for (tn != nil) { + if (tn.kind != nkind.N_TNAME) { tn = nil; } + else { + let nm: str = tn.str; + if (primsize(nm) > 0) { break; }; + let alias: *node = aliaslookup(c, nm); + if (alias == nil) { tn = nil; } + else { tn = alias; }; + }; + }; + if (tn != nil) { + let nm: str = tn.str; + let sz: i32 = primsize(nm); + let is_unsigned: bool = typenameisunsigned(nm); + let is_bool: bool = streq(nm, "bool"); + if (sz > 0) { if (sz < 8) { + if (is_unsigned) { + if (sz == 4) { + emitline("\tMOVL\tAX, AX\n"); + } else { + let mask: i64 = 0xFFi64; + if (sz == 2) { mask = 0xFFFFi64; }; + emitline("\tANDQ\t$"); + emitint(mask); + emitline(", AX\n"); + }; + } else { if (is_bool) { + emitline("\tANDQ\t$255, AX\n"); + }; }; + }; }; + }; + return; + }; if (srcfk == 0 && dstfk == 2) { emitline("\tCVTSI2SD\tAX, X0\n"); return; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f72f6329..6b2a9157 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8140,10 +8140,48 @@ fn cgcast(c: *cgen, n: *node) void = { if (srcstr) { emitline("\tMOVQ\tBX, CX\n"); }; }; // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; - // same-kind casts (int↔int with widening differences, - // f64→f64 etc.) stay no-ops at the asm level, matching the - // pre-port behaviour for integer casts. - if (srcfk == 0 && dstfk == 0) { return; }; + // int↔int casts narrow via an explicit clamp before the early + // return so `(big_u64): u32` doesn't leak the upper 32 bits. + // Hare semantics: `expr: T` truncates to T's bit width (mod 2^n). + // Mirrors cmd/w6c/cgen.c's N_CAST clamp; signed-narrow targets + // (i8/i16/i32) stay no-ops until the assembler grows MOVSBQ / + // MOVSWQ / MOVSXD reg-reg forms. + if (srcfk == 0 && dstfk == 0) { + let tn: *node = n.rhs; + // Walk through alias chains (`type random = u64`). + for (tn != nil) { + if (tn.kind != nkind.N_TNAME) { tn = nil; } + else { + let nm: str = tn.str; + if (primsize(nm) > 0) { break; }; + let alias: *node = aliaslookup(c, nm); + if (alias == nil) { tn = nil; } + else { tn = alias; }; + }; + }; + if (tn != nil) { + let nm: str = tn.str; + let sz: i32 = primsize(nm); + let is_unsigned: bool = typenameisunsigned(nm); + let is_bool: bool = streq(nm, "bool"); + if (sz > 0) { if (sz < 8) { + if (is_unsigned) { + if (sz == 4) { + emitline("\tMOVL\tAX, AX\n"); + } else { + let mask: i64 = 0xFFi64; + if (sz == 2) { mask = 0xFFFFi64; }; + emitline("\tANDQ\t$"); + emitint(mask); + emitline(", AX\n"); + }; + } else { if (is_bool) { + emitline("\tANDQ\t$255, AX\n"); + }; }; + }; }; + }; + return; + }; if (srcfk == 0 && dstfk == 2) { emitline("\tCVTSI2SD\tAX, X0\n"); return; diff --git a/test/wcc/900_stdlib.c b/test/wcc/900_stdlib.c index 3a364146..9aeae980 100644 --- a/test/wcc/900_stdlib.c +++ b/test/wcc/900_stdlib.c @@ -22,10 +22,15 @@ static const char *modules[] = { "lib/path/path.ww", "lib/encoding/utf8/utf8.ww", "lib/encoding/hex/hex.ww", + "lib/encoding/base32/base32.ww", + "lib/encoding/base64/base64.ww", "lib/hash/fnv/fnv.ww", "lib/hash/adler32/adler32.ww", "lib/hash/crc16/crc16.ww", "lib/hash/crc32/crc32.ww", + "lib/hash/crc64/crc64.ww", + "lib/hash/siphash/siphash.ww", + "lib/math/random/random.ww", "lib/time/time.ww", "lib/c/libc/libc.ww", "lib/bufio/bufio.ww",