From 56ac53637b8213a41196630dcbc96e404a323e10 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 15:41:53 -0600 Subject: [PATCH] heap blobs --- source/quickjs-internal.h | 10 +- source/runtime.c | 690 +++++++++++++++++++++++++------------- vm_suite.ce | 374 +++++++++++++++++++++ 3 files changed, 837 insertions(+), 237 deletions(-) diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index f8487801..a4abe12a 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -898,12 +898,12 @@ typedef struct JSArray { JSValue values[]; /* inline flexible array member */ } JSArray; -/* JSBlob — not allocated on GC heap (blobs use JSRecord + opaque). - Struct kept for reference; gc_object_size/gc_scan_object do not handle OBJ_BLOB. */ +/* JSBlob — inline bit data on the GC heap. + cap56 = capacity in bits, S bit = stone (immutable). */ typedef struct JSBlob { - objhdr_t mist_hdr; - word_t length; - uint8_t bits[]; + objhdr_t mist_hdr; /* type=OBJ_BLOB, cap56=capacity_bits, S=stone */ + word_t length; /* used bits */ + word_t bits[]; /* inline bit data, ceil(cap56/64) words */ } JSBlob; typedef struct JSText { diff --git a/source/runtime.c b/source/runtime.c index 9313ad6b..b8d6eb1e 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -1315,6 +1315,11 @@ size_t gc_object_size (void *ptr) { size_t tab_size = sizeof (JSRecordEntry) * (cap + 1); return gc_align_up (sizeof (JSRecord) + tab_size); } + case OBJ_BLOB: { + /* JSBlob + inline bit data. cap56 = capacity in bits. */ + size_t word_count = (cap + 63) / 64; + return gc_align_up (sizeof (JSBlob) + word_count * sizeof (word_t)); + } case OBJ_FUNCTION: return gc_align_up (sizeof (JSFunction)); case OBJ_FRAME: { @@ -1363,7 +1368,7 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f continue; } - if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME) { + if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME && type != OBJ_BLOB) { fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx)\n", type, ptr, (unsigned long long)hdr); fflush (stderr); @@ -1446,6 +1451,7 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro break; } case OBJ_TEXT: + case OBJ_BLOB: /* No internal references to scan */ break; case OBJ_FRAME: { @@ -1465,7 +1471,7 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro objhdr_t sh = *(objhdr_t *)sp; uint8_t st = objhdr_type (sh); if (st != OBJ_FORWARD && st != OBJ_ARRAY && st != OBJ_TEXT && - st != OBJ_RECORD && st != OBJ_FUNCTION && st != OBJ_FRAME) { + st != OBJ_RECORD && st != OBJ_FUNCTION && st != OBJ_FRAME && st != OBJ_BLOB) { const char *fname = "?"; const char *ffile = "?"; uint16_t fnslots = 0; @@ -1571,7 +1577,7 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { } uint8_t tt = objhdr_type (th); if (tt != OBJ_FORWARD && tt != OBJ_ARRAY && tt != OBJ_TEXT && - tt != OBJ_RECORD && tt != OBJ_FUNCTION && tt != OBJ_FRAME) { + tt != OBJ_RECORD && tt != OBJ_FUNCTION && tt != OBJ_FRAME && tt != OBJ_BLOB) { const char *fn_name = "?"; JSValue fn_v = cf->function; if (JS_IsPtr (fn_v)) { @@ -3197,7 +3203,12 @@ JSValue JS_GetProperty (JSContext *ctx, JSValue obj, JSValue prop) { if (JS_IsException (obj)) return JS_EXCEPTION; if (unlikely (!JS_IsRecord (obj))) { - /* Primitives have no properties */ + if (mist_is_blob (obj)) { + JSValue proto = ctx->class_proto[JS_CLASS_BLOB]; + if (!JS_IsNull (proto) && JS_IsRecord (proto)) + return rec_get (ctx, JS_VALUE_GET_RECORD (proto), prop); + return JS_NULL; + } return JS_NULL; } @@ -4863,9 +4874,9 @@ __exception int js_get_length32 (JSContext *ctx, uint32_t *pres, JSValue obj) { return 0; } - blob *b = js_get_blob (ctx, obj); - if (b) { - *pres = b->length; + if (mist_is_blob (obj)) { + JSBlob *bd = (JSBlob *)chase (obj); + *pres = (uint32_t)bd->length; return 0; } @@ -6690,7 +6701,7 @@ static JSValue js_cell_format_number (JSContext *ctx, double num, const char *fo } /* Forward declaration for blob helper */ -blob *js_get_blob (JSContext *ctx, JSValue val); +static JSBlob *checked_get_blob (JSContext *ctx, JSValue val); /* modulo(dividend, divisor) - result has sign of divisor */ static JSValue js_cell_modulo (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { @@ -6836,9 +6847,9 @@ static JSValue js_cell_text (JSContext *ctx, JSValue this_val, int argc, JSValue } /* Handle blob - convert to text representation */ - blob *bd = js_get_blob (ctx, arg); - if (bd) { - if (!bd->is_stone) + if (mist_is_blob (arg)) { + JSBlob *bd = checked_get_blob (ctx, arg); + if (!objhdr_s (bd->mist_hdr)) return JS_ThrowTypeError (ctx, "text: blob must be stone"); char format = '\0'; @@ -6850,7 +6861,7 @@ static JSValue js_cell_text (JSContext *ctx, JSValue this_val, int argc, JSValue } size_t byte_len = (bd->length + 7) / 8; - const uint8_t *data = bd->data; + const uint8_t *data = (const uint8_t *)bd->bits; if (format == 'h') { static const char hex[] = "0123456789abcdef"; @@ -9322,102 +9333,154 @@ static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSV * ============================================================================ */ -/* Helper to check if JSValue is a blob */ -blob *js_get_blob (JSContext *ctx, JSValue val) { - /* Must be a record, not an array or other object type */ - if (!JS_IsRecord(val)) return NULL; - JSRecord *p = JS_VALUE_GET_OBJ (val); - if (REC_GET_CLASS_ID(p) != JS_CLASS_BLOB) return NULL; - return REC_GET_OPAQUE(p); +/* Get JSBlob* from a JSValue, chasing forwards. Returns NULL if not a blob. */ +static JSBlob *checked_get_blob (JSContext *ctx, JSValue val) { + if (!mist_is_blob (val)) return NULL; + return (JSBlob *)chase (val); } -/* Helper to create a new blob JSValue */ -JSValue js_new_blob (JSContext *ctx, blob *b) { - JSValue obj = JS_NewObjectClass (ctx, JS_CLASS_BLOB); - if (JS_IsException (obj)) { - blob_destroy (b); - return obj; +/* Allocate a new JSBlob on the GC heap with given capacity in bits. */ +static JSValue js_new_heap_blob (JSContext *ctx, size_t capacity_bits) { + size_t word_count = (capacity_bits + 63) / 64; + size_t total = sizeof (JSBlob) + word_count * sizeof (word_t); + JSBlob *bd = js_mallocz (ctx, total); + if (!bd) return JS_EXCEPTION; + bd->mist_hdr = objhdr_make (capacity_bits, OBJ_BLOB, false, false, false, false); + bd->length = 0; + return JS_MKPTR (bd); +} + +/* Grow a blob using the forward-pointer pattern. + *pblob is updated to point to the new, larger blob. */ +static int blob_grow (JSContext *ctx, JSValue *pblob, size_t need_bits) { + JSGCRef blob_ref; + JS_PushGCRef (ctx, &blob_ref); + blob_ref.val = *pblob; + + /* Growth: double until enough, minimum 64 bits */ + JSBlob *old = (JSBlob *)chase (blob_ref.val); + size_t old_cap = objhdr_cap56 (old->mist_hdr); + size_t new_cap = old_cap == 0 ? 64 : old_cap * 2; + while (new_cap < need_bits) new_cap *= 2; + + /* Allocate new blob — may trigger GC */ + size_t word_count = (new_cap + 63) / 64; + size_t total = sizeof (JSBlob) + word_count * sizeof (word_t); + JSBlob *nb = js_mallocz (ctx, total); + if (!nb) { + JS_PopGCRef (ctx, &blob_ref); + return -1; } - JS_SetOpaque (obj, b); - return obj; + + /* Re-derive old after potential GC */ + old = (JSBlob *)chase (blob_ref.val); + + /* Copy header, length, and bit data */ + nb->mist_hdr = objhdr_make (new_cap, OBJ_BLOB, false, false, false, false); + nb->length = old->length; + size_t old_words = (old_cap + 63) / 64; + if (old_words > 0) + memcpy (nb->bits, old->bits, old_words * sizeof (word_t)); + + /* Install forward pointer at old location */ + old->mist_hdr = objhdr_make_fwd (nb); + + /* Update caller's JSValue */ + *pblob = JS_MKPTR (nb); + JS_PopGCRef (ctx, &blob_ref); + return 0; } -/* Blob finalizer */ -static void js_blob_finalizer (JSRuntime *rt, JSValue val) { - blob *b = JS_GetOpaque (val, JS_CLASS_BLOB); - if (b) blob_destroy (b); +/* Ensure blob has capacity for total_need bits. May grow and update *pblob. */ +static int blob_ensure_cap (JSContext *ctx, JSValue *pblob, size_t total_need) { + JSBlob *bd = (JSBlob *)chase (*pblob); + size_t cap = objhdr_cap56 (bd->mist_hdr); + if (total_need <= cap) return 0; + return blob_grow (ctx, pblob, total_need); } /* blob() constructor */ static JSValue js_blob_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - blob *bd = NULL; - /* blob() - empty blob */ if (argc == 0) { - bd = blob_new (0); + return js_new_heap_blob (ctx, 0); } /* blob(capacity) - blob with initial capacity in bits */ - else if (argc == 1 && JS_IsNumber (argv[0])) { + if (argc == 1 && JS_IsNumber (argv[0])) { int64_t capacity_bits; if (JS_ToInt64 (ctx, &capacity_bits, argv[0]) < 0) return JS_EXCEPTION; if (capacity_bits < 0) capacity_bits = 0; - bd = blob_new ((size_t)capacity_bits); + return js_new_heap_blob (ctx, (size_t)capacity_bits); } /* blob(length, logical/random) - blob with fill or random */ - else if (argc == 2 && JS_IsNumber (argv[0])) { + if (argc == 2 && JS_IsNumber (argv[0])) { int64_t length_bits; if (JS_ToInt64 (ctx, &length_bits, argv[0]) < 0) return JS_EXCEPTION; if (length_bits < 0) length_bits = 0; if (JS_IsBool (argv[1])) { int is_one = JS_ToBool (ctx, argv[1]); - bd = blob_new_with_fill ((size_t)length_bits, is_one); - } else if (JS_IsFunction (argv[1])) { - /* Random function provided */ - size_t bytes = (length_bits + 7) / 8; - bd = blob_new ((size_t)length_bits); - if (bd) { - bd->length = length_bits; - memset (bd->data, 0, bytes); - - size_t bits_written = 0; - while (bits_written < (size_t)length_bits) { - JSValue randval = JS_Call (ctx, argv[1], JS_NULL, 0, NULL); - if (JS_IsException (randval)) { - blob_destroy (bd); - return JS_EXCEPTION; - } - - int64_t fitval; - JS_ToInt64 (ctx, &fitval, randval); - - size_t bits_to_use = length_bits - bits_written; - if (bits_to_use > 52) bits_to_use = 52; - - for (size_t j = 0; j < bits_to_use; j++) { - size_t bit_pos = bits_written + j; - size_t byte_idx = bit_pos / 8; - size_t bit_idx = bit_pos % 8; - - if (fitval & (1LL << j)) - bd->data[byte_idx] |= (uint8_t)(1 << bit_idx); - else - bd->data[byte_idx] &= (uint8_t)~(1 << bit_idx); - } - bits_written += bits_to_use; - } + JSValue bv = js_new_heap_blob (ctx, (size_t)length_bits); + if (JS_IsException (bv)) return bv; + JSBlob *bd = (JSBlob *)chase (bv); + bd->length = length_bits; + if (is_one && length_bits > 0) { + size_t bytes = (length_bits + 7) / 8; + memset (bd->bits, 0xFF, bytes); + size_t trail = length_bits & 7; + if (trail) ((uint8_t *)bd->bits)[bytes - 1] &= (1 << trail) - 1; } - } else { - return JS_ThrowTypeError ( - ctx, "Second argument must be boolean or random function"); + return bv; } + if (JS_IsFunction (argv[1])) { + JSGCRef bv_ref; + JS_PushGCRef (ctx, &bv_ref); + bv_ref.val = js_new_heap_blob (ctx, (size_t)length_bits); + if (JS_IsException (bv_ref.val)) { + JS_PopGCRef (ctx, &bv_ref); + return JS_EXCEPTION; + } + JSBlob *bd = (JSBlob *)chase (bv_ref.val); + bd->length = length_bits; + + size_t bits_written = 0; + while (bits_written < (size_t)length_bits) { + JSValue randval = JS_Call (ctx, argv[1], JS_NULL, 0, NULL); + if (JS_IsException (randval)) { + JS_PopGCRef (ctx, &bv_ref); + return JS_EXCEPTION; + } + + int64_t fitval; + JS_ToInt64 (ctx, &fitval, randval); + + size_t bits_to_use = length_bits - bits_written; + if (bits_to_use > 52) bits_to_use = 52; + + bd = (JSBlob *)chase (bv_ref.val); /* re-derive after JS_Call */ + uint8_t *data = (uint8_t *)bd->bits; + for (size_t j = 0; j < bits_to_use; j++) { + size_t bit_pos = bits_written + j; + size_t byte_idx = bit_pos / 8; + size_t bit_idx = bit_pos % 8; + if (fitval & (1LL << j)) + data[byte_idx] |= (uint8_t)(1 << bit_idx); + else + data[byte_idx] &= (uint8_t)~(1 << bit_idx); + } + bits_written += bits_to_use; + } + JSValue ret = bv_ref.val; + JS_PopGCRef (ctx, &bv_ref); + return ret; + } + return JS_ThrowTypeError (ctx, "Second argument must be boolean or random function"); } /* blob(blob, from, to) - copy from another blob */ - else if (argc >= 1 && mist_is_gc_object (argv[0]) && !JS_IsText (argv[0])) { - blob *src = js_get_blob (ctx, argv[0]); + if (argc >= 1 && mist_is_blob (argv[0])) { + JSBlob *src = checked_get_blob (ctx, argv[0]); if (!src) - return JS_ThrowTypeError (ctx, - "blob constructor: argument 1 not a blob"); + return JS_ThrowTypeError (ctx, "blob constructor: argument 1 not a blob"); int64_t from = 0, to = (int64_t)src->length; if (argc >= 2 && JS_IsNumber (argv[1])) { JS_ToInt64 (ctx, &from, argv[1]); @@ -9428,68 +9491,112 @@ static JSValue js_blob_constructor (JSContext *ctx, JSValue this_val, int argc, if (to < from) to = from; if (to > (int64_t)src->length) to = (int64_t)src->length; } - bd = blob_new_from_blob (src, (size_t)from, (size_t)to); + size_t copy_bits = (size_t)(to - from); + JSGCRef src_ref; + JS_PushGCRef (ctx, &src_ref); + src_ref.val = argv[0]; + JSValue bv = js_new_heap_blob (ctx, copy_bits); + if (JS_IsException (bv)) { + JS_PopGCRef (ctx, &src_ref); + return JS_EXCEPTION; + } + src = (JSBlob *)chase (src_ref.val); /* re-derive after alloc */ + JSBlob *dst = (JSBlob *)chase (bv); + if (copy_bits > 0) + copy_bits_fast (src->bits, dst->bits, (size_t)from, (size_t)to - 1, 0); + dst->length = copy_bits; + JS_PopGCRef (ctx, &src_ref); + return bv; } /* blob(text) - create blob from UTF-8 string */ - else if (argc == 1 && JS_IsText (argv[0])) { + if (argc == 1 && JS_IsText (argv[0])) { const char *str = JS_ToCString (ctx, argv[0]); if (!str) return JS_EXCEPTION; size_t len = strlen (str); - bd = blob_new (len * 8); - if (bd) { - memcpy (bd->data, str, len); - bd->length = len * 8; + JSValue bv = js_new_heap_blob (ctx, len * 8); + if (JS_IsException (bv)) { + JS_FreeCString (ctx, str); + return bv; } + JSBlob *bd = (JSBlob *)chase (bv); + if (len > 0) + memcpy (bd->bits, str, len); + bd->length = len * 8; JS_FreeCString (ctx, str); - } else { - return JS_ThrowTypeError (ctx, "blob constructor: invalid arguments"); + return bv; } - - if (!bd) return JS_ThrowOutOfMemory (ctx); - - return js_new_blob (ctx, bd); + return JS_ThrowTypeError (ctx, "blob constructor: invalid arguments"); } /* blob.write_bit(logical) */ static JSValue js_blob_write_bit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_ThrowTypeError (ctx, "write_bit(logical) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "write_bit: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "write_bit: cannot write (maybe stone or OOM)"); int bit_val; if (JS_IsNumber (argv[0])) { int32_t num; JS_ToInt32 (ctx, &num, argv[0]); if (num != 0 && num != 1) - return JS_ThrowTypeError ( - ctx, "write_bit: value must be true, false, 0, or 1"); + return JS_ThrowTypeError (ctx, "write_bit: value must be true, false, 0, or 1"); bit_val = num; } else { bit_val = JS_ToBool (ctx, argv[0]); } - if (blob_write_bit (bd, bit_val) < 0) - return JS_ThrowTypeError (ctx, - "write_bit: cannot write (maybe stone or OOM)"); + if (blob_ensure_cap (ctx, &this_val, bd->length + 1) < 0) + return JS_ThrowTypeError (ctx, "write_bit: cannot write (maybe stone or OOM)"); + bd = (JSBlob *)chase (this_val); + uint8_t *data = (uint8_t *)bd->bits; + size_t idx = bd->length; + if (bit_val) + data[idx >> 3] |= (1 << (idx & 7)); + else + data[idx >> 3] &= ~(1 << (idx & 7)); + bd->length++; return JS_NULL; } /* blob.write_blob(second_blob) */ static JSValue js_blob_write_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) - return JS_ThrowTypeError (ctx, - "write_blob(second_blob) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + return JS_ThrowTypeError (ctx, "write_blob(second_blob) requires 1 argument"); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "write_blob: not called on a blob"); - blob *second = js_get_blob (ctx, argv[0]); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "write_blob: cannot write to stone blob or OOM"); + JSBlob *second = checked_get_blob (ctx, argv[0]); if (!second) return JS_ThrowTypeError (ctx, "write_blob: argument must be a blob"); - if (blob_write_blob (bd, second) < 0) - return JS_ThrowTypeError (ctx, - "write_blob: cannot write to stone blob or OOM"); + size_t src_len = second->length; + if (src_len == 0) return JS_NULL; + /* Root both blobs across potential growth allocation */ + JSGCRef this_ref, arg_ref; + JS_PushGCRef (ctx, &this_ref); + JS_PushGCRef (ctx, &arg_ref); + this_ref.val = this_val; + arg_ref.val = argv[0]; + + bd = (JSBlob *)chase (this_ref.val); + if (blob_ensure_cap (ctx, &this_ref.val, bd->length + src_len) < 0) { + JS_PopGCRef (ctx, &arg_ref); + JS_PopGCRef (ctx, &this_ref); + return JS_ThrowTypeError (ctx, "write_blob: cannot write to stone blob or OOM"); + } + bd = (JSBlob *)chase (this_ref.val); + second = (JSBlob *)chase (arg_ref.val); + + copy_bits_fast (second->bits, bd->bits, 0, src_len - 1, bd->length); + bd->length += src_len; + + JS_PopGCRef (ctx, &arg_ref); + JS_PopGCRef (ctx, &this_ref); return JS_NULL; } @@ -9497,39 +9604,53 @@ static JSValue js_blob_write_blob (JSContext *ctx, JSValue this_val, int argc, J static JSValue js_blob_write_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_ThrowTypeError (ctx, "write_number(number) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "write_number: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "write_number: cannot write to stone blob or OOM"); double d; if (JS_ToFloat64 (ctx, &d, argv[0]) < 0) return JS_EXCEPTION; - if (blob_write_dec64 (bd, d) < 0) - return JS_ThrowTypeError ( - ctx, "write_number: cannot write to stone blob or OOM"); - + if (blob_ensure_cap (ctx, &this_val, bd->length + 64) < 0) + return JS_ThrowTypeError (ctx, "write_number: cannot write to stone blob or OOM"); + bd = (JSBlob *)chase (this_val); + copy_bits_fast (&d, bd->bits, 0, 63, bd->length); + bd->length += 64; return JS_NULL; } /* blob.write_fit(value, len) */ static JSValue js_blob_write_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) - return JS_ThrowTypeError (ctx, - "write_fit(value, len) requires 2 arguments"); + return JS_ThrowTypeError (ctx, "write_fit(value, len) requires 2 arguments"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "write_fit: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob"); int64_t value; int32_t len; - if (JS_ToInt64 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION; if (JS_ToInt32 (ctx, &len, argv[1]) < 0) return JS_EXCEPTION; + if (len < 1 || len > 64) + return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob"); - if (blob_write_fit (bd, value, len) < 0) - return JS_ThrowTypeError (ctx, - "write_fit: value doesn't fit or stone blob"); + /* Check if value fits in len bits with sign */ + if (len < 64) { + int64_t max = (1LL << (len - 1)) - 1; + int64_t min = -(1LL << (len - 1)); + if (value < min || value > max) + return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob"); + } + if (blob_ensure_cap (ctx, &this_val, bd->length + len) < 0) + return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob"); + bd = (JSBlob *)chase (this_val); + copy_bits_fast (&value, bd->bits, 0, len - 1, bd->length); + bd->length += len; return JS_NULL; } @@ -9538,16 +9659,31 @@ static JSValue js_blob_write_text (JSContext *ctx, JSValue this_val, int argc, J if (argc < 1) return JS_ThrowTypeError (ctx, "write_text(text) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "write_text: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "write_text: cannot write to stone blob or OOM"); const char *str = JS_ToCString (ctx, argv[0]); if (!str) return JS_EXCEPTION; + size_t slen = strlen (str); + size_t need = 64 + slen * 8; - if (blob_write_text (bd, str) < 0) { + if (blob_ensure_cap (ctx, &this_val, bd->length + need) < 0) { JS_FreeCString (ctx, str); - return JS_ThrowTypeError (ctx, - "write_text: cannot write to stone blob or OOM"); + return JS_ThrowTypeError (ctx, "write_text: cannot write to stone blob or OOM"); + } + bd = (JSBlob *)chase (this_val); + + /* Write 64-bit length prefix */ + int64_t text_len = (int64_t)slen; + copy_bits_fast (&text_len, bd->bits, 0, 63, bd->length); + bd->length += 64; + + /* Write raw text bytes */ + if (slen > 0) { + copy_bits_fast (str, bd->bits, 0, slen * 8 - 1, bd->length); + bd->length += slen * 8; } JS_FreeCString (ctx, str); @@ -9557,18 +9693,40 @@ static JSValue js_blob_write_text (JSContext *ctx, JSValue this_val, int argc, J /* blob.write_pad(block_size) */ static JSValue js_blob_write_pad (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) - return JS_ThrowTypeError (ctx, - "write_pad(block_size) requires 1 argument"); + return JS_ThrowTypeError (ctx, "write_pad(block_size) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "write_pad: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "write_pad: cannot write"); int32_t block_size; if (JS_ToInt32 (ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION; - - if (blob_write_pad (bd, block_size) < 0) + if (block_size <= 0) return JS_ThrowTypeError (ctx, "write_pad: cannot write"); + /* Write a 1 bit, then zeros to align to block_size */ + size_t after_one = bd->length + 1; + size_t rem = after_one % block_size; + size_t pad_zeros = rem > 0 ? block_size - rem : 0; + size_t total_pad = 1 + pad_zeros; + + if (blob_ensure_cap (ctx, &this_val, bd->length + total_pad) < 0) + return JS_ThrowTypeError (ctx, "write_pad: cannot write"); + bd = (JSBlob *)chase (this_val); + + uint8_t *data = (uint8_t *)bd->bits; + /* Write the 1 bit */ + size_t idx = bd->length; + data[idx >> 3] |= (1 << (idx & 7)); + bd->length++; + + /* Zero bits are already 0 from js_mallocz, but if we grew we need to be safe */ + for (size_t i = 0; i < pad_zeros; i++) { + idx = bd->length; + data[idx >> 3] &= ~(1 << (idx & 7)); + bd->length++; + } return JS_NULL; } @@ -9577,16 +9735,27 @@ static JSValue js_blob_w16 (JSContext *ctx, JSValue this_val, int argc, JSValue if (argc < 1) return JS_ThrowTypeError (ctx, "w16(value) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "w16: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "w16: cannot write"); int32_t value; if (JS_ToInt32 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION; int16_t short_val = (int16_t)value; - if (blob_write_bytes (bd, &short_val, sizeof (int16_t)) < 0) + if (blob_ensure_cap (ctx, &this_val, bd->length + 16) < 0) return JS_ThrowTypeError (ctx, "w16: cannot write"); + bd = (JSBlob *)chase (this_val); + uint8_t *data = (uint8_t *)bd->bits; + size_t bit_off = bd->length; + if ((bit_off & 7) == 0) { + memcpy (data + (bit_off >> 3), &short_val, sizeof (int16_t)); + } else { + copy_bits_fast (&short_val, bd->bits, 0, 15, bit_off); + } + bd->length += 16; return JS_NULL; } @@ -9595,15 +9764,26 @@ static JSValue js_blob_w32 (JSContext *ctx, JSValue this_val, int argc, JSValue if (argc < 1) return JS_ThrowTypeError (ctx, "w32(value) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "w32: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "w32: cannot write"); int32_t value; if (JS_ToInt32 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION; - if (blob_write_bytes (bd, &value, sizeof (int32_t)) < 0) + if (blob_ensure_cap (ctx, &this_val, bd->length + 32) < 0) return JS_ThrowTypeError (ctx, "w32: cannot write"); + bd = (JSBlob *)chase (this_val); + uint8_t *data = (uint8_t *)bd->bits; + size_t bit_off = bd->length; + if ((bit_off & 7) == 0) { + memcpy (data + (bit_off >> 3), &value, sizeof (int32_t)); + } else { + copy_bits_fast (&value, bd->bits, 0, 31, bit_off); + } + bd->length += 32; return JS_NULL; } @@ -9612,17 +9792,28 @@ static JSValue js_blob_wf (JSContext *ctx, JSValue this_val, JSValue arg0) { if (JS_IsNull (arg0)) return JS_ThrowTypeError (ctx, "wf(value) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "wf: not called on a blob"); + if (objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "wf: cannot write"); float f; double d; if (JS_ToFloat64 (ctx, &d, arg0) < 0) return JS_EXCEPTION; - f = d; + f = (float)d; - if (blob_write_bytes (bd, &f, sizeof (f)) < 0) + if (blob_ensure_cap (ctx, &this_val, bd->length + 32) < 0) return JS_ThrowTypeError (ctx, "wf: cannot write"); + bd = (JSBlob *)chase (this_val); + uint8_t *data = (uint8_t *)bd->bits; + size_t bit_off = bd->length; + if ((bit_off & 7) == 0) { + memcpy (data + (bit_off >> 3), &f, sizeof (f)); + } else { + copy_bits_fast (&f, bd->bits, 0, 31, bit_off); + } + bd->length += 32; return JS_NULL; } @@ -9630,31 +9821,30 @@ static JSValue js_blob_wf (JSContext *ctx, JSValue this_val, JSValue arg0) { static JSValue js_blob_read_logical (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_ThrowTypeError (ctx, "read_logical(from) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "read_logical: not called on a blob"); + if (!objhdr_s (bd->mist_hdr)) + return JS_ThrowTypeError (ctx, "read_logical: blob must be stone"); int64_t pos; if (JS_ToInt64 (ctx, &pos, argv[0]) < 0) return JS_ThrowInternalError (ctx, "must provide a positive bit"); - if (pos < 0) - return JS_ThrowRangeError (ctx, - "read_logical: position must be non-negative"); - int bit_val; - if (blob_read_bit (bd, (size_t)pos, &bit_val) < 0) - return JS_ThrowTypeError (ctx, "read_logical: blob must be stone"); + if (pos < 0 || (size_t)pos >= bd->length) + return JS_ThrowRangeError (ctx, "read_logical: position must be non-negative"); + uint8_t *data = (uint8_t *)bd->bits; + int bit_val = (data[pos >> 3] >> (pos & 7)) & 1; return JS_NewBool (ctx, bit_val); } /* blob.read_blob(from, to) */ JSValue js_blob_read_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "read_blob: not called on a blob"); - - if (!bd->is_stone) + if (!objhdr_s (bd->mist_hdr)) return JS_ThrowTypeError (ctx, "read_blob: blob must be stone"); int64_t from = 0; - int64_t to = bd->length; + int64_t to = (int64_t)bd->length; if (argc >= 1) { if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; @@ -9662,36 +9852,47 @@ JSValue js_blob_read_blob (JSContext *ctx, JSValue this_val, int argc, JSValue * } if (argc >= 2) { if (JS_ToInt64 (ctx, &to, argv[1]) < 0) return JS_EXCEPTION; - if (to > (int64_t)bd->length) to = bd->length; + if (to > (int64_t)bd->length) to = (int64_t)bd->length; } + if (from >= to) return js_new_heap_blob (ctx, 0); - blob *new_bd = blob_read_blob (bd, from, to); - if (!new_bd) return JS_ThrowOutOfMemory (ctx); + size_t copy_bits = (size_t)(to - from); - return js_new_blob (ctx, new_bd); + /* Root source blob across new allocation */ + JSGCRef src_ref; + JS_PushGCRef (ctx, &src_ref); + src_ref.val = this_val; + + JSValue bv = js_new_heap_blob (ctx, copy_bits); + if (JS_IsException (bv)) { + JS_PopGCRef (ctx, &src_ref); + return bv; + } + bd = (JSBlob *)chase (src_ref.val); + JSBlob *dst = (JSBlob *)chase (bv); + copy_bits_fast (bd->bits, dst->bits, (size_t)from, (size_t)to - 1, 0); + dst->length = copy_bits; + JS_PopGCRef (ctx, &src_ref); + return bv; } /* blob.read_number(from) */ static JSValue js_blob_read_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_ThrowTypeError (ctx, "read_number(from) requires 1 argument"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "read_number: not called on a blob"); - - if (!bd->is_stone) + if (!objhdr_s (bd->mist_hdr)) return JS_ThrowTypeError (ctx, "read_number: blob must be stone"); - double from; - if (JS_ToFloat64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; - - if (from < 0) - return JS_ThrowRangeError (ctx, - "read_number: position must be non-negative"); - - double d; - if (blob_read_dec64 (bd, from, &d) < 0) + double from_d; + if (JS_ToFloat64 (ctx, &from_d, argv[0]) < 0) return JS_EXCEPTION; + size_t from = (size_t)from_d; + if (from_d < 0 || from + 64 > bd->length) return JS_ThrowRangeError (ctx, "read_number: out of range"); + double d; + copy_bits_fast (bd->bits, &d, from, from + 63, 0); return JS_NewFloat64 (ctx, d); } @@ -9700,35 +9901,38 @@ static JSValue js_blob_read_fit (JSContext *ctx, JSValue this_val, int argc, JSV if (argc < 2) return JS_ThrowTypeError (ctx, "read_fit(from, len) requires 2 arguments"); - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "read_fit: not called on a blob"); - - if (!bd->is_stone) + if (!objhdr_s (bd->mist_hdr)) return JS_ThrowTypeError (ctx, "read_fit: blob must be stone"); int64_t from; int32_t len; - if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; if (JS_ToInt32 (ctx, &len, argv[1]) < 0) return JS_EXCEPTION; if (from < 0) return JS_ThrowRangeError (ctx, "read_fit: position must be non-negative"); + if (len < 1 || len > 64 || from + len > (int64_t)bd->length) + return JS_ThrowRangeError (ctx, "read_fit: out of range or invalid length"); - int64_t value; - if (blob_read_fit (bd, from, len, &value) < 0) - return JS_ThrowRangeError (ctx, - "read_fit: out of range or invalid length"); + int64_t value = 0; + copy_bits_fast (bd->bits, &value, (size_t)from, (size_t)(from + len - 1), 0); + + /* Sign extend if necessary */ + if (len < 64 && (value & (1LL << (len - 1)))) { + int64_t mask = ~((1LL << len) - 1); + value |= mask; + } return JS_NewInt64 (ctx, value); } /* blob.read_text(from) */ JSValue js_blob_read_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - blob *bd = js_get_blob (ctx, this_val); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "read_text: not called on a blob"); - - if (!bd->is_stone) + if (!objhdr_s (bd->mist_hdr)) return JS_ThrowTypeError (ctx, "read_text: blob must be stone"); int64_t from = 0; @@ -9736,36 +9940,60 @@ JSValue js_blob_read_text (JSContext *ctx, JSValue this_val, int argc, JSValue * if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; } - char *text; - size_t bits_read; - if (blob_read_text (bd, from, &text, &bits_read) < 0) - return JS_ThrowRangeError (ctx, - "read_text: out of range or invalid encoding"); + /* Need at least 64 bits for length prefix */ + if (from < 0 || (size_t)from + 64 > bd->length) + return JS_ThrowRangeError (ctx, "read_text: out of range or invalid encoding"); - JSValue result = JS_NewString (ctx, text); - /* Note: blob_read_text uses system malloc, so we use sys_free */ - sys_free (text); + int64_t raw_len = 0; + copy_bits_fast (bd->bits, &raw_len, (size_t)from, (size_t)from + 63, 0); + if (raw_len < 0) + return JS_ThrowRangeError (ctx, "read_text: out of range or invalid encoding"); + size_t slen = (size_t)raw_len; + size_t after_len = (size_t)from + 64; + if (after_len + slen * 8 > bd->length) + return JS_ThrowRangeError (ctx, "read_text: out of range or invalid encoding"); + + char *str = sys_malloc (slen + 1); + if (!str) return JS_ThrowOutOfMemory (ctx); + if (slen > 0) + copy_bits_fast (bd->bits, str, after_len, after_len + slen * 8 - 1, 0); + str[slen] = '\0'; + + JSValue result = JS_NewString (ctx, str); + sys_free (str); return result; } /* blob.pad?(from, block_size) */ static JSValue js_blob_pad_q (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) - return JS_ThrowTypeError (ctx, - "pad?(from, block_size) requires 2 arguments"); - blob *bd = js_get_blob (ctx, this_val); + return JS_ThrowTypeError (ctx, "pad?(from, block_size) requires 2 arguments"); + JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_ThrowTypeError (ctx, "pad?: not called on a blob"); - - if (!bd->is_stone) + if (!objhdr_s (bd->mist_hdr)) return JS_ThrowTypeError (ctx, "pad?: blob must be stone"); int64_t from; int32_t block_size; if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; if (JS_ToInt32 (ctx, &block_size, argv[1]) < 0) return JS_EXCEPTION; + if (block_size <= 0) return JS_FALSE; - return JS_NewBool (ctx, blob_pad_check (bd, from, block_size)); + /* Check: length is aligned to block_size, distance from..length is valid */ + if (bd->length % block_size != 0) return JS_FALSE; + int64_t diff = (int64_t)bd->length - from; + if (diff <= 0 || diff > block_size) return JS_FALSE; + + /* First bit must be 1, rest must be 0 */ + uint8_t *data = (uint8_t *)bd->bits; + int bit = (data[from >> 3] >> (from & 7)) & 1; + if (!bit) return JS_FALSE; + for (size_t i = (size_t)from + 1; i < bd->length; i++) { + bit = (data[i >> 3] >> (i & 7)) & 1; + if (bit) return JS_FALSE; + } + return JS_TRUE; } static const JSCFunctionListEntry js_blob_proto_funcs[] = { @@ -9802,76 +10030,73 @@ JSValue js_core_blob_use (JSContext *js) { /* Create a new blob from raw data, stone it, and return as JSValue */ JSValue js_new_blob_stoned_copy (JSContext *js, void *data, size_t bytes) { - blob *b = blob_new (bytes * 8); - if (!b) return JS_ThrowOutOfMemory (js); - memcpy (b->data, data, bytes); - b->length = bytes * 8; - blob_make_stone (b); - return js_new_blob (js, b); + size_t bits = bytes * 8; + JSValue bv = js_new_heap_blob (js, bits); + if (JS_IsException (bv)) return bv; + JSBlob *bd = (JSBlob *)chase (bv); + if (bytes > 0) + memcpy (bd->bits, data, bytes); + bd->length = bits; + bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true); + return bv; } -/* Get raw data pointer from a blob (must be stone) - returns byte count */ +/* Get raw data pointer from a blob (must be stone) - returns byte count. + WARNING: returned pointer is into GC heap — do NOT hold across allocations. */ void *js_get_blob_data (JSContext *js, size_t *size, JSValue v) { - blob *b = js_get_blob (js, v); - *size = (b->length + 7) / 8; - if (!b) { + JSBlob *bd = checked_get_blob (js, v); + if (!bd) { JS_ThrowReferenceError (js, "get_blob_data: not called on a blob"); return NULL; } + *size = (bd->length + 7) / 8; - if (!b->is_stone) { - JS_ThrowReferenceError (js, - "attempted to read data from a non-stone blob"); + if (!objhdr_s (bd->mist_hdr)) { + JS_ThrowReferenceError (js, "attempted to read data from a non-stone blob"); return NULL; } - if (b->length % 8 != 0) { + if (bd->length % 8 != 0) { JS_ThrowReferenceError ( js, - "attempted to read data from a non-byte aligned blob [length is %zu]", - b->length); + "attempted to read data from a non-byte aligned blob [length is %llu]", + (unsigned long long)bd->length); return NULL; } - return b->data; + return (void *)bd->bits; } -/* Get raw data pointer from a blob (must be stone) - returns bit count */ +/* Get raw data pointer from a blob (must be stone) - returns bit count. + WARNING: returned pointer is into GC heap — do NOT hold across allocations. */ void *js_get_blob_data_bits (JSContext *js, size_t *bits, JSValue v) { - blob *b = js_get_blob (js, v); - if (!b) { + JSBlob *bd = checked_get_blob (js, v); + if (!bd) { JS_ThrowReferenceError (js, "get_blob_data_bits: not called on a blob"); return NULL; } - if (!b->is_stone) { - JS_ThrowReferenceError (js, - "attempted to read data from a non-stone blob"); + if (!objhdr_s (bd->mist_hdr)) { + JS_ThrowReferenceError (js, "attempted to read data from a non-stone blob"); return NULL; } - if (!b->data) { + if (bd->length % 8 != 0) { + JS_ThrowReferenceError (js, "attempted to read data from a non-byte aligned blob"); + return NULL; + } + + if (bd->length == 0) { JS_ThrowReferenceError (js, "attempted to read data from an empty blob"); return NULL; } - if (b->length % 8 != 0) { - JS_ThrowReferenceError ( - js, "attempted to read data from a non-byte aligned blob"); - return NULL; - } - - if (b->length == 0) { - JS_ThrowReferenceError (js, "attempted to read data from an empty blob"); - return NULL; - } - - *bits = b->length; - return b->data; + *bits = bd->length; + return (void *)bd->bits; } /* Check if a value is a blob */ int js_is_blob (JSContext *js, JSValue v) { - return js_get_blob (js, v) != NULL; + return mist_is_blob (v); } /* ============================================================================ @@ -10061,9 +10286,9 @@ static JSValue js_cell_stone (JSContext *ctx, JSValue this_val, int argc, JSValu JSValue obj = argv[0]; - blob *bd = js_get_blob (ctx, obj); - if (bd) { - bd->is_stone = true; + if (mist_is_blob (obj)) { + JSBlob *bd = (JSBlob *)chase (obj); + bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true); return obj; } @@ -10128,8 +10353,7 @@ static JSValue js_cell_reverse (JSContext *ctx, JSValue this_val, int argc, JSVa } /* Handle blobs */ - blob *bd = js_get_blob (ctx, value); - if (bd) { + if (mist_is_blob (value)) { /* Blobs need proper blob reversal support - return null for now */ return JS_NULL; } @@ -10785,8 +11009,10 @@ static JSValue js_cell_length (JSContext *ctx, JSValue this_val, int argc, JSVal } /* Check for blob */ - blob *bd = js_get_blob (ctx, val); - if (bd) return JS_NewInt64 (ctx, bd->length); + if (mist_is_blob (val)) { + JSBlob *bd = (JSBlob *)chase (val); + return JS_NewInt64 (ctx, bd->length); + } /* Arrays return element count */ if (JS_IsArray (val)) { @@ -10866,7 +11092,7 @@ static JSValue js_cell_is_array (JSContext *ctx, JSValue this_val, int argc, JSV /* is_blob(val) */ static JSValue js_cell_is_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; - return JS_NewBool (ctx, js_get_blob (ctx, argv[0]) != NULL); + return JS_NewBool (ctx, mist_is_blob (argv[0])); } /* is_data(val) - check if object is a plain object (data record) */ @@ -10876,7 +11102,7 @@ static JSValue js_cell_is_data (JSContext *ctx, JSValue this_val, int argc, JSVa if (!mist_is_gc_object (val)) return JS_FALSE; if (JS_IsArray (val)) return JS_FALSE; if (JS_IsFunction (val)) return JS_FALSE; - if (js_get_blob (ctx, val)) return JS_FALSE; + if (mist_is_blob (val)) return JS_FALSE; /* Check if it's a plain object (prototype is Object.prototype or null) */ return JS_TRUE; } @@ -11079,7 +11305,7 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) { { JSClassDef blob_class = { .class_name = "blob", - .finalizer = js_blob_finalizer, + .finalizer = NULL, }; JS_NewClass (ctx, JS_CLASS_BLOB, &blob_class); ctx->class_proto[JS_CLASS_BLOB] = JS_NewObject (ctx); diff --git a/vm_suite.ce b/vm_suite.ce index ec6c566f..090bb490 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -5011,6 +5011,380 @@ run("nested function used after definition", function() { assert_eq(result[1], 'b = "hi"', "nested fn encode text") }) +// ============================================================================ +// BLOB - GC HEAP INTEGRATION +// ============================================================================ + +run("blob basic create and stone", function() { + var b = blob() + if (!is_blob(b)) fail("empty blob is not a blob") + b.write_bit(true) + b.write_bit(false) + b.write_bit(true) + stone(b) + if (length(b) != 3) fail("blob length should be 3, got " + text(length(b))) + if (!b.read_logical(0)) fail("bit 0 should be true") + if (b.read_logical(1)) fail("bit 1 should be false") + if (!b.read_logical(2)) fail("bit 2 should be true") +}) + +run("blob write_number read_number", function() { + var b = blob() + b.write_number(3.14) + b.write_number(2.718) + stone(b) + if (length(b) != 128) fail("expected 128 bits") + var pi = b.read_number(0) + var e = b.read_number(64) + if (pi != 3.14) fail("pi read back wrong: " + text(pi)) + if (e != 2.718) fail("e read back wrong: " + text(e)) +}) + +run("blob write_fit read_fit", function() { + var b = blob() + b.write_fit(42, 8) + b.write_fit(-7, 8) + stone(b) + if (b.read_fit(0, 8) != 42) fail("fit 42 failed") + if (b.read_fit(8, 8) != -7) fail("fit -7 failed") +}) + +run("blob write_text read_text", function() { + var b = blob() + b.write_text("hello world") + stone(b) + var t = b.read_text(0) + if (t != "hello world") fail("text roundtrip failed: " + t) +}) + +run("blob from text", function() { + var b = blob("abc") + stone(b) + if (length(b) != 24) fail("blob from text length wrong") + var t = text(b) + if (t != "abc") fail("blob to text failed: " + t) +}) + +run("blob copy slice", function() { + var b = blob() + b.write_fit(100, 16) + b.write_fit(200, 16) + b.write_fit(300, 16) + stone(b) + var slice = blob(b, 16, 32) + stone(slice) + if (slice.read_fit(0, 16) != 200) fail("blob slice failed") +}) + +run("blob write_blob", function() { + var a = blob() + a.write_fit(1, 8) + a.write_fit(2, 8) + var b = blob() + b.write_fit(3, 8) + b.write_blob(a) + stone(b) + if (b.read_fit(0, 8) != 3) fail("first byte wrong") + if (b.read_fit(8, 8) != 1) fail("second byte wrong") + if (b.read_fit(16, 8) != 2) fail("third byte wrong") +}) + +run("blob write_pad pad?", function() { + var b = blob() + b.write_fit(7, 4) + b.write_pad(8) + stone(b) + if (length(b) != 8) fail("pad didn't align to 8, got " + text(length(b))) + if (!b["pad?"](4, 8)) fail("pad? should be true") +}) + +run("blob w16 w32 wf", function() { + var b = blob() + b.w16(1000) + b.w32(100000) + b.wf(1.5) + stone(b) + if (length(b) != 80) fail("expected 80 bits, got " + text(length(b))) +}) + +run("blob is_data false for blob", function() { + var b = blob() + if (is_data(b)) fail("blob should not be is_data") +}) + +run("blob text hex format", function() { + var b = blob("AB") + stone(b) + var hex = text(b, "h") + if (hex != "4142") fail("hex encoding wrong: " + hex) +}) + +run("blob text binary format", function() { + var b = blob() + b.write_bit(true) + b.write_bit(false) + b.write_bit(true) + b.write_bit(true) + stone(b) + var bits = text(b, "b") + if (bits != "1011") fail("binary encoding wrong: " + bits) +}) + +run("blob(capacity) preallocates", function() { + var b = blob(1024) + if (!is_blob(b)) fail("capacity blob not a blob") + if (length(b) != 0) fail("capacity blob should start empty") + var i = 0 + for (i = 0; i < 128; i = i + 1) { + b.write_fit(i, 8) + } + if (length(b) != 1024) fail("after fill length wrong") +}) + +run("blob(length, bool) fill", function() { + var b = blob(16, true) + stone(b) + if (length(b) != 16) fail("filled blob length wrong") + var i = 0 + for (i = 0; i < 16; i = i + 1) { + if (!b.read_logical(i)) fail("bit " + text(i) + " should be true") + } + var z = blob(8, false) + stone(z) + for (i = 0; i < 8; i = i + 1) { + if (z.read_logical(i)) fail("zero bit " + text(i) + " should be false") + } +}) + +// --- GC stress tests: verify blobs survive collection and don't corrupt --- + +run("gc blob survives collection", function() { + var b = blob() + var garbage = null + var i = 0 + var v1 = null + var t = null + b.write_number(123.456) + b.write_text("test data") + // Trigger GC pressure by allocating many objects + for (i = 0; i < 500; i = i + 1) { + garbage = {a: i, b: text(i), c: [i, i+1, i+2]} + } + // blob should still be valid after GC + b.write_number(789.012) + stone(b) + v1 = b.read_number(0) + if (v1 != 123.456) fail("blob data corrupted after gc: " + text(v1)) + t = b.read_text(64) + if (t != "test data") fail("blob text corrupted after gc: " + t) +}) + +run("gc blob growth across collections", function() { + var b = blob() + var i = 0 + var junk = null + var v = null + for (i = 0; i < 200; i = i + 1) { + b.write_fit(i, 16) + junk = [text(i), {v: i}, text(i) + "_end"] + } + stone(b) + for (i = 0; i < 200; i = i + 1) { + v = b.read_fit(i * 16, 16) + if (v != i) fail("blob growth gc: slot " + text(i) + " = " + text(v)) + } +}) + +run("gc many blobs alive simultaneously", function() { + var blobs = [] + var i = 0 + var b = null + var trash = null + var v1 = null + var v2 = null + for (i = 0; i < 100; i = i + 1) { + b = blob() + b.write_fit(i * 7, 16) + b.write_fit(i * 13, 16) + stone(b) + blobs[i] = b + } + for (i = 0; i < 200; i = i + 1) { + trash = {x: text(i), y: [i]} + } + for (i = 0; i < 100; i = i + 1) { + v1 = blobs[i].read_fit(0, 16) + v2 = blobs[i].read_fit(16, 16) + if (v1 != i * 7) fail("multi blob " + text(i) + " v1 = " + text(v1)) + if (v2 != i * 13) fail("multi blob " + text(i) + " v2 = " + text(v2)) + } +}) + +run("gc blob not polluting other objects", function() { + var results = [] + var i = 0 + var b = null + var obj = null + var tmp = null + var entry = null + var bv = null + var bt = null + for (i = 0; i < 50; i = i + 1) { + b = blob() + b.write_fit(i, 16) + b.write_text("item" + text(i)) + stone(b) + obj = {index: i, name: "obj" + text(i)} + results[i] = {blob_val: b, obj_val: obj} + } + for (i = 0; i < 300; i = i + 1) { + tmp = {a: text(i), b: [i, i]} + } + for (i = 0; i < 50; i = i + 1) { + entry = results[i] + bv = entry.blob_val.read_fit(0, 16) + if (bv != i) fail("pollute test blob " + text(i) + " = " + text(bv)) + bt = entry.blob_val.read_text(16) + if (bt != "item" + text(i)) fail("pollute test text " + text(i)) + if (entry.obj_val.index != i) fail("pollute test obj index " + text(i)) + if (entry.obj_val.name != "obj" + text(i)) fail("pollute test obj name " + text(i)) + } +}) + +run("gc dead blobs are collected", function() { + // Verify that dead blobs don't cause leaks by checking heap stays bounded. + // We do two phases: allocate a batch, check heap, allocate another, check again. + // If blobs leaked, the second batch would cause unbounded growth. + var i = 0 + var b = null + var junk = null + var stats1 = null + var stats2 = null + gc_stats() + for (i = 0; i < 200; i = i + 1) { + b = blob(1024) + b.write_fit(i, 16) + junk = {a: text(i), b: [i]} + } + stats1 = gc_stats() + // Second identical batch — should reuse collected space + for (i = 0; i < 200; i = i + 1) { + b = blob(1024) + b.write_fit(i, 16) + junk = {a: text(i), b: [i]} + } + stats2 = gc_stats() + // Heap should not have grown more than 4x between phases + // (some growth is normal from doubling, but not unbounded) + if (stats2.heap_size > stats1.heap_size * 4) { + fail("heap grew too much: " + text(stats1.heap_size) + " -> " + text(stats2.heap_size)) + } +}) + +run("gc blob write_blob both survive", function() { + var src = blob() + var i = 0 + var v = null + var dst = null + for (i = 0; i < 100; i = i + 1) { + src.write_fit(i, 16) + } + dst = blob() + dst.write_fit(99, 16) + dst.write_blob(src) + stone(dst) + if (dst.read_fit(0, 16) != 99) fail("dst first word wrong") + for (i = 0; i < 100; i = i + 1) { + v = dst.read_fit(16 + i * 16, 16) + if (v != i) fail("write_blob gc: word " + text(i) + " = " + text(v)) + } +}) + +run("gc blob read_blob across allocation", function() { + var big = blob() + var i = 0 + var slices = [] + var s = null + var v = null + for (i = 0; i < 200; i = i + 1) { + big.write_fit(i, 16) + } + stone(big) + for (i = 0; i < 50; i = i + 1) { + s = big.read_blob(i * 16, (i + 1) * 16) + stone(s) + slices[i] = s + } + for (i = 0; i < 50; i = i + 1) { + v = slices[i].read_fit(0, 16) + if (v != i) fail("read_blob gc slice " + text(i) + " = " + text(v)) + } +}) + +run("gc blob with random fill", function() { + var b = blob(256, function() { return 42 }) + stone(b) + if (length(b) != 256) fail("random fill length wrong") +}) + +run("gc blob in record values", function() { + var rec = {} + var i = 0 + var b = null + var junk = null + var v = null + for (i = 0; i < 50; i = i + 1) { + b = blob() + b.write_fit(i * 3, 16) + stone(b) + rec["k" + text(i)] = b + junk = {x: text(i), y: [i, i+1]} + } + for (i = 0; i < 50; i = i + 1) { + v = rec["k" + text(i)].read_fit(0, 16) + if (v != i * 3) fail("blob in record " + text(i) + " = " + text(v)) + } +}) + +run("gc blob in array elements", function() { + var arr = [] + var i = 0 + var b = null + var garbage = null + var v = null + for (i = 0; i < 100; i = i + 1) { + b = blob() + b.write_number(i * 1.5) + stone(b) + arr[i] = b + } + for (i = 0; i < 500; i = i + 1) { + garbage = text(i) + text(i) + } + for (i = 0; i < 100; i = i + 1) { + v = arr[i].read_number(0) + if (v != i * 1.5) fail("blob in array " + text(i) + " = " + text(v)) + } +}) + +run("gc blob forward pointer chase", function() { + var b = blob() + var i = 0 + var junk = null + var v = null + for (i = 0; i < 500; i = i + 1) { + b.write_fit(i % 128, 8) + } + for (i = 0; i < 300; i = i + 1) { + junk = {a: [i, text(i)], b: text(i) + "!"} + } + stone(b) + for (i = 0; i < 500; i = i + 1) { + v = b.read_fit(i * 8, 8) + if (v != i % 128) fail("fwd chase " + text(i) + " = " + text(v)) + } +}) + // ============================================================================ // SUMMARY // ============================================================================