From 7b6643d1fcab2a9a180e44342eccbd3e6ebf731e Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 7 Jun 2026 20:19:09 -0400 Subject: [PATCH] =?UTF-8?q?c:=20byte=20semantics=20=E2=80=94=20string-ref?= =?UTF-8?q?=20unsigned=20cast,=20pack=5Fu64=5Fslot=20bignum=20sentinel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two C-tier defects surfaced when running foxhop's ecdsa Phase B emit through the lumbda C interpreter. Both produced wrong bytes in the generated QECCOPS1 binary; both Python tier handled correctly. 1. string-ref on a byte >= 0x80 returned a char with codepoint -1. The store was a `char` array (signed on x86_64); `s->data[idx]` sign-extends 0xFF into a negative int before VAL_CHAR wraps it. Round-tripping (char->integer (string-ref s 0)) for a 0xFF byte gave -1 instead of 255. The Scheme-level (u64-le n) packer uses (make-string 1 (integer->char (modulo v 256))) for each byte and reads them back; sign-extension corrupted the high-bit bytes. Cast through `unsigned char` in bi_string_ref. 2. pack_u64_slot treated bignum slots as raw fixnums. ecdsa's emit-stream binds (no-slot *no-slot*) where *no-slot* is 18446744073709551615 (u64 max). On Python tier that's a regular big-int. C tier carries it as a bignum NaN-box slot. The packer knew about VAL_FALSE → 0xFFFF... but called as_int(v) on bignums, reading the NaN-box payload bits (pointer-to-Bignum) and writing that pointer as the field's 64-bit LE value. Add an IS_BIGNUM branch that extracts the low 64 magnitude bits directly. Cross-tier emit-stream code stays unchanged. Tests: 88/88 c-test, 4/4 regression-named-let-leak, 205/205 functional. DIALOG_GCD smoke at p=11 n+1=5 now byte-identical to Python tier (1,941,816 bytes match exactly). --- c/builtins.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/c/builtins.c b/c/builtins.c index 4001cdc..16453ca 100644 --- a/c/builtins.c +++ b/c/builtins.c @@ -748,7 +748,12 @@ static Value bi_string_ref(Value *a, int n, Env *e) { int idx = (int)as_number_int(a[1]); ULString *s = AS_STRING(a[0]); if (idx < 0 || idx >= (int)s->len) lisp_error("string-ref: index out of range"); - return VAL_CHAR(s->data[idx]); + /* Cast through unsigned char so bytes 0x80..0xFF map to codepoints + * 128..255 rather than sign-extending to negative ints — emit-stream + * stores binary op bytes through (make-string 1 (integer->char b)) + * + (string-ref s 0); without this, char->integer of 0xFF returned + * -1, so u64-le's serialization produced a sentinel-shaped giant. */ + return VAL_CHAR((unsigned char)s->data[idx]); } static Value bi_substring(Value *a, int n, Env *e) { @@ -1595,6 +1600,13 @@ static void pack_u64_slot(char *buf, Value v) { uint64_t u; if (v == VAL_FALSE) { u = 0xFFFFFFFFFFFFFFFFULL; + } else if (IS_BIGNUM(v)) { + /* Bignum sentinel (e.g. *no-slot* = 2^64-1) — extract low 64 + * bits of magnitude. Python tier passes the literal big-int so + * cross-tier emit-stream code stays unchanged. */ + Bignum *b = AS_BIGNUM(v); + u = b->n_limbs > 0 ? b->limbs[0] : 0; + if (b->sign < 0) u = ~u + 1; } else { u = (uint64_t)as_int(v); }