c: byte semantics — string-ref unsigned cast, pack_u64_slot bignum sentinel

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).
This commit is contained in:
russell@unturf.com 2026-06-07 20:19:09 -04:00
parent 269d3be756
commit 7b6643d1fc
No known key found for this signature in database

View file

@ -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);
}