From f334a2ad565bc6c6b814d9e82084931622d2f8cd Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 28 May 2025 14:38:43 -0500 Subject: [PATCH] expand qjs_blob --- source/qjs_blob.c | 999 +++++++++++++++++++++++++++++++++++++--------- tests/blob.js | 402 +++++++++++++++++-- 2 files changed, 1178 insertions(+), 223 deletions(-) diff --git a/source/qjs_blob.c b/source/qjs_blob.c index a43f6290..fa472bde 100644 --- a/source/qjs_blob.c +++ b/source/qjs_blob.c @@ -3,6 +3,13 @@ #include #include "quickjs.h" #include "qjs_blob.h" +#include "qjs_macros.h" +#include "qjs_wota.h" + +// Get countof from macros if not defined +#ifndef countof +#define countof(x) (sizeof(x)/sizeof((x)[0])) +#endif // ----------------------------------------------------------------------------- // A simple blob structure that can be in two states: @@ -16,7 +23,7 @@ // memory or bit manipulation for performance. // ----------------------------------------------------------------------------- -typedef struct { +typedef struct blob { // The actual buffer holding the bits (in multiples of 8 bits). uint8_t *data; @@ -29,14 +36,85 @@ typedef struct { // 0 = antestone (mutable) // 1 = stone (immutable) int is_stone; -} JSBlobData; +} blob; -// Forward declaration of class ID and methods -static JSClassID js_blob_class_id; +// Free function for blob +void blob_free(JSRuntime *rt, blob *b) +{ + if (b) { + free(b->data); + free(b); + } +} + +// Use QJSCLASS macro to generate class boilerplate +QJSCLASS(blob,) +// Read one bit from a stone blob at position 'pos' +static int js_blob_read_bit_internal(blob *bd, size_t pos, int *out_bit) { + if (!bd->is_stone) { + return -1; // not stone + } + if (pos >= bd->bit_length) { + return -1; // out of range + } + size_t byte_index = pos >> 3; + size_t offset_in_byte = pos & 7; + *out_bit = (bd->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0; + return 0; +} + +// Read kim-encoded value from blob +static int read_kim_internal(blob *bd, size_t from, int64_t *out_value, size_t *bits_read) { + if (!bd->is_stone) return -1; + + size_t pos = from; + uint8_t bytes[10]; + int byte_count = 0; + + // Read bytes until we find one without continuation bit + while (byte_count < 10) { + if (pos + 8 > bd->bit_length) return -1; + + uint8_t byte = 0; + for (int b = 0; b < 8; b++) { + int bit; + if (js_blob_read_bit_internal(bd, pos + b, &bit) < 0) + return -1; + if (bit) byte |= (1 << b); + } + + bytes[byte_count++] = byte; + pos += 8; + + if (!(byte & 0x80)) break; // No continuation bit + } + + if (byte_count == 0) return -1; + + // Decode the kim value + int64_t value = 0; + int is_negative = (bytes[0] == 0x80); + + if (is_negative) { + // Skip the 0x80 byte and decode remaining + for (int i = 1; i < byte_count; i++) { + value = (value << 7) | (bytes[i] & 0x7F); + } + value = -value - 1; + } else { + for (int i = 0; i < byte_count; i++) { + value = (value << 7) | (bytes[i] & 0x7F); + } + } + + *out_value = value; + *bits_read = byte_count * 8; + return 0; +} // Helper to ensure capacity for writing // new_bits is additional bits to be appended -static int js_blob_ensure_capacity(JSContext *ctx, JSBlobData *bd, size_t new_bits) { +static int js_blob_ensure_capacity(JSContext *ctx, blob *bd, size_t new_bits) { size_t need_bits = bd->bit_length + new_bits; if (need_bits <= bd->bit_capacity) return 0; @@ -65,42 +143,28 @@ static int js_blob_ensure_capacity(JSContext *ctx, JSBlobData *bd, size_t new_bi return 0; } -// Finalizer for JSBlobData -static void js_blob_finalizer(JSRuntime *rt, JSValue val) { - JSBlobData *bd = JS_GetOpaque(val, js_blob_class_id); - if (bd) { - free(bd->data); - bd->data = NULL; - bd->bit_length = 0; - bd->bit_capacity = 0; - bd->is_stone = 0; - free(bd); - } -} -// Mark function: not used here, as we have no child JS objects in JSBlobData -static void js_blob_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { - // No child JS references to mark -} - -// A helper to create a new JSBlobData object, returning a JSValue wrapping it. -static JSValue js_blob_wrap(JSContext *ctx, JSBlobData *bd) { - JSValue obj = JS_NewObjectClass(ctx, js_blob_class_id); - if (JS_IsException(obj)) { - free(bd->data); - free(bd); - return obj; - } - JS_SetOpaque(obj, bd); - return obj; -} // ----------------------------------------------------------------------------- -// Helpers for reading/writing bits +// Kim encoding helpers // ----------------------------------------------------------------------------- +// Calculate the kim length in bits for a fit (int64) value +static int kim_length_for_fit(int64_t value) { + if (value >= -1 && value <= 127) return 8; + if (value >= -127 && value <= 16383) return 16; + if (value >= -16383 && value <= 2097151) return 24; + if (value >= -2097151 && value <= 268435455) return 32; + if (value >= -268435455 && value <= 34359738367LL) return 40; + if (value >= -34359738367LL && value <= 4398046511103LL) return 48; + if (value >= -4398046511103LL && value <= 562949953421311LL) return 56; + if (value >= -562949953421311LL && value <= 36028797018963967LL) return 64; + if (value >= -36028797018963967LL) return 72; + return 80; // Maximum kim length +} + // Write one bit (0 or 1) at the end of the blob -static int js_blob_write_bit_internal(JSContext *ctx, JSBlobData *bd, int bit_val) { +static int js_blob_write_bit_internal(JSContext *ctx, blob *bd, int bit_val) { if (bd->is_stone) { // Trying to write to an immutable blob -> throw return -1; @@ -123,24 +187,53 @@ static int js_blob_write_bit_internal(JSContext *ctx, JSBlobData *bd, int bit_va return 0; } -// Read one bit from a stone blob at position 'pos' -static int js_blob_read_bit_internal(JSBlobData *bd, size_t pos, int *out_bit) { - if (!bd->is_stone) { - // It's not stone -> reading might be out of the specification - // but we can allow or return error. Here we just return error. - return -1; +// Write kim-encoded value to blob +static int write_kim_internal(blob *bd, JSContext *ctx, int64_t value) { + int bits = kim_length_for_fit(value); + int bytes = bits / 8; + + uint8_t kim_bytes[10] = {0}; // Max 10 bytes for kim + + if (value < 0) { + // Negative number: first byte is 0x80, then encode -value-1 + kim_bytes[0] = 0x80; + value = -value - 1; + + // Encode remaining bytes + for (int i = bytes - 1; i > 0; i--) { + kim_bytes[i] = value & 0x7F; + value >>= 7; + if (i > 1) kim_bytes[i] |= 0x80; // Set continuation bit + } + } else { + // Positive number + for (int i = bytes - 1; i >= 0; i--) { + kim_bytes[i] = value & 0x7F; + value >>= 7; + if (i > 0) kim_bytes[i] |= 0x80; // Set continuation bit + } } - if (pos >= bd->bit_length) { - return -1; // out of range + + // Write the kim bytes as bits + for (int i = 0; i < bytes; i++) { + for (int b = 0; b < 8; b++) { + if (js_blob_write_bit_internal(ctx, bd, (kim_bytes[i] >> b) & 1) < 0) + return -1; + } } - size_t byte_index = pos >> 3; - size_t offset_in_byte = pos & 7; - *out_bit = (bd->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0; + return 0; } +// ----------------------------------------------------------------------------- +// Helpers for reading/writing bits +// ----------------------------------------------------------------------------- + + + + // Turn a blob into the "stone" state. This discards any extra capacity. -static void js_blob_make_stone(JSBlobData *bd) { +static void js_blob_make_stone(blob *bd) { bd->is_stone = 1; // Optionally shrink the buffer to exactly bit_length in size if (bd->bit_capacity > bd->bit_length) { @@ -164,19 +257,10 @@ static void js_blob_make_stone(JSBlobData *bd) { // JS Functions (blob.make, blob.write_bit, blob.read_logical, etc.) // ----------------------------------------------------------------------------- -// blob.make(...) -static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - // We'll implement a few typical signatures: - // blob.make() - // blob.make(capacity) - // blob.make(length, logical_value) - // blob.make(blob, from, to) (makes a copy) - // - // This is a simplified approach. The spec mentions random, partial copy, etc. - // We'll handle just these forms enough to demonstrate the concept. - - JSBlobData *bd = calloc(1, sizeof(*bd)); +// Constructor function for blob +static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target, + int argc, JSValueConst *argv) { + blob *bd = calloc(1, sizeof(*bd)); if (!bd) return JS_ThrowOutOfMemory(ctx); // default @@ -185,11 +269,11 @@ static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val, bd->bit_capacity = 0; bd->is_stone = 0; // initially antestone - // blob.make() + // new Blob() if (argc == 0) { // empty antestone blob } - // blob.make(capacity) + // new Blob(capacity) else if (argc == 1 && JS_IsNumber(argv[0])) { int64_t capacity_bits; if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) { @@ -210,15 +294,14 @@ static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val, } } } - // blob.make(length, logical) - else if (argc == 2 && JS_IsNumber(argv[0]) && JS_IsBool(argv[1])) { + // new Blob(length, logical/random) + else if (argc == 2 && JS_IsNumber(argv[0])) { int64_t length_bits; if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) { free(bd); return JS_EXCEPTION; } if (length_bits < 0) length_bits = 0; - int is_one = JS_ToBool(ctx, argv[1]); bd->bit_length = (size_t)length_bits; bd->bit_capacity = bd->bit_length; if (bd->bit_capacity % 8) { @@ -231,23 +314,54 @@ static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val, free(bd); return JS_ThrowOutOfMemory(ctx); } - memset(bd->data, is_one ? 0xff : 0x00, bytes); - // if length_bits isn't a multiple of 8, we need to clear the unused bits - size_t used_bits_in_last_byte = (size_t)length_bits & 7; - if (used_bits_in_last_byte && is_one) { - // clear top bits in the last byte - uint8_t mask = (1 << used_bits_in_last_byte) - 1; - bd->data[bytes - 1] &= mask; + + if (JS_IsBool(argv[1])) { + // Fill with all 0s or all 1s + int is_one = JS_ToBool(ctx, argv[1]); + memset(bd->data, is_one ? 0xff : 0x00, bytes); + // if length_bits isn't a multiple of 8, we need to clear the unused bits + size_t used_bits_in_last_byte = (size_t)length_bits & 7; + if (used_bits_in_last_byte && is_one) { + // clear top bits in the last byte + uint8_t mask = (1 << used_bits_in_last_byte) - 1; + bd->data[bytes - 1] &= mask; + } + } else if (JS_IsFunction(ctx, argv[1])) { + // Random function provided - call it for each bit + for (size_t i = 0; i < length_bits; i++) { + JSValue randval = JS_Call(ctx, argv[1], JS_UNDEFINED, 0, NULL); + if (JS_IsException(randval)) { + free(bd->data); + free(bd); + return JS_EXCEPTION; + } + int64_t fitval; + JS_ToInt64(ctx, &fitval, randval); + JS_FreeValue(ctx, randval); + + // Set bit based on random value + size_t byte_idx = i / 8; + size_t bit_idx = i % 8; + if (fitval & 1) { + bd->data[byte_idx] |= (1 << bit_idx); + } else { + bd->data[byte_idx] &= ~(1 << bit_idx); + } + } + } else { + free(bd->data); + free(bd); + return JS_ThrowTypeError(ctx, "Second argument must be boolean or random function"); } } } - // blob.make(blob, from, to) + // new Blob(blob, from, to) else if (argc >= 1 && JS_IsObject(argv[0])) { // we try copying from another blob if it's of the same class - JSBlobData *src = JS_GetOpaque(argv[0], js_blob_class_id); + blob *src = js2blob(ctx, argv[0]); if (!src) { free(bd); - return JS_ThrowTypeError(ctx, "blob.make: argument 1 not a blob"); + return JS_ThrowTypeError(ctx, "Blob constructor: argument 1 not a blob"); } int64_t from = 0, to = (int64_t)src->bit_length; if (argc >= 2 && JS_IsNumber(argv[1])) { @@ -292,41 +406,284 @@ static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val, // else fail else { free(bd); - return JS_ThrowTypeError(ctx, "blob.make: invalid arguments"); + return JS_ThrowTypeError(ctx, "Blob constructor: invalid arguments"); } - return js_blob_wrap(ctx, bd); + return blob2js(ctx, bd); } -// blob.write_bit(blob, logical) +// blob.write_bit(logical) static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 2) { - return JS_ThrowTypeError(ctx, "blob.write_bit(blob, logical) requires 2 arguments"); + if (argc < 1) { + return JS_ThrowTypeError(ctx, "write_bit(logical) requires 1 argument"); } - JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + blob *bd = js2blob(ctx, this_val); if (!bd) { - return JS_ThrowTypeError(ctx, "blob.write_bit: argument 1 not a blob"); + return JS_ThrowTypeError(ctx, "write_bit: not called on a blob"); } - int bit_val = JS_ToBool(ctx, argv[1]); // interpret any truthy as 1, else 0 + + // Handle numeric 0/1 or boolean + 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"); + } + bit_val = num; + } else { + bit_val = JS_ToBool(ctx, argv[0]); + } + if (js_blob_write_bit_internal(ctx, bd, bit_val) < 0) { - return JS_ThrowTypeError(ctx, "blob.write_bit: cannot write (maybe stone or OOM)"); + return JS_ThrowTypeError(ctx, "write_bit: cannot write (maybe stone or OOM)"); } return JS_UNDEFINED; } -// blob.read_logical(blob, from) +// blob.write_blob(second_blob) +static JSValue js_blob_write_blob(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "write_blob(second_blob) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "write_blob: not called on a blob"); + } + blob *second = js2blob(ctx, argv[0]); + if (!second) { + return JS_ThrowTypeError(ctx, "write_blob: argument must be a blob"); + } + + if (bd->is_stone) { + return JS_ThrowTypeError(ctx, "write_blob: cannot write to stone blob"); + } + + // Append all bits from second blob + for (size_t i = 0; i < second->bit_length; i++) { + size_t byte_idx = i / 8; + size_t bit_idx = i % 8; + int bit = (second->data[byte_idx] >> bit_idx) & 1; + if (js_blob_write_bit_internal(ctx, bd, bit) < 0) { + return JS_ThrowTypeError(ctx, "write_blob: out of memory"); + } + } + + return JS_UNDEFINED; +} + +// blob.write_dec64(number) +static JSValue js_blob_write_dec64(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "write_dec64(number) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "write_dec64: not called on a blob"); + } + + if (bd->is_stone) { + return JS_ThrowTypeError(ctx, "write_dec64: cannot write to stone blob"); + } + + // Get the number as a double and convert to DEC64 + double d; + if (JS_ToFloat64(ctx, &d, argv[0]) < 0) { + return JS_EXCEPTION; + } + + // Simple DEC64 encoding: store as IEEE 754 double (64 bits) + uint64_t bits; + memcpy(&bits, &d, sizeof(bits)); + + // Write 64 bits + for (int i = 0; i < 64; i++) { + if (js_blob_write_bit_internal(ctx, bd, (bits >> i) & 1) < 0) { + return JS_ThrowTypeError(ctx, "write_dec64: out of memory"); + } + } + + return JS_UNDEFINED; +} + +// blob.write_fit(fit, length) +static JSValue js_blob_write_fit(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 2) { + return JS_ThrowTypeError(ctx, "write_fit(fit, length) requires 2 arguments"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "write_fit: not called on a blob"); + } + + if (bd->is_stone) { + return JS_ThrowTypeError(ctx, "write_fit: cannot write to stone blob"); + } + + int64_t fit; + int32_t length; + if (JS_ToInt64(ctx, &fit, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToInt32(ctx, &length, argv[1]) < 0) return JS_EXCEPTION; + + if (length < 0 || length > 64) { + return JS_ThrowTypeError(ctx, "write_fit: length must be between 0 and 64"); + } + + // Check if fit requires more bits than allowed + if (length < 64) { + int64_t max = (1LL << length) - 1; + if (fit < 0 || fit > max) { + return JS_ThrowTypeError(ctx, "write_fit: fit value requires more bits than allowed by length"); + } + } + + // Write the bits + for (int i = 0; i < length; i++) { + if (js_blob_write_bit_internal(ctx, bd, (fit >> i) & 1) < 0) { + return JS_ThrowTypeError(ctx, "write_fit: out of memory"); + } + } + + return JS_UNDEFINED; +} + +// blob.write_kim(fit) +static JSValue js_blob_write_kim(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "write_kim(fit) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "write_kim: not called on a blob"); + } + + if (bd->is_stone) { + return JS_ThrowTypeError(ctx, "write_kim: cannot write to stone blob"); + } + + // Handle number or single character string + int64_t value; + if (JS_IsString(argv[0])) { + const char *str = JS_ToCString(ctx, argv[0]); + if (!str) return JS_EXCEPTION; + + // Get first UTF-32 character + // For simplicity, assuming ASCII for now + if (strlen(str) == 0) { + JS_FreeCString(ctx, str); + return JS_ThrowTypeError(ctx, "write_kim: empty string"); + } + value = (unsigned char)str[0]; + JS_FreeCString(ctx, str); + } else { + if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION; + } + + if (write_kim_internal(bd, ctx, value) < 0) { + return JS_ThrowTypeError(ctx, "write_kim: out of memory"); + } + + return JS_UNDEFINED; +} + +// blob.write_pad(block_size) +static JSValue js_blob_write_pad(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "write_pad(block_size) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "write_pad: not called on a blob"); + } + + if (bd->is_stone) { + return JS_ThrowTypeError(ctx, "write_pad: cannot write to stone blob"); + } + + int32_t block_size; + if (JS_ToInt32(ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION; + + if (block_size <= 0) { + return JS_ThrowTypeError(ctx, "write_pad: block_size must be positive"); + } + + // Write a 1 bit + if (js_blob_write_bit_internal(ctx, bd, 1) < 0) { + return JS_ThrowTypeError(ctx, "write_pad: out of memory"); + } + + // Calculate how many 0 bits to add + size_t current_len = bd->bit_length; + size_t remainder = current_len % block_size; + if (remainder > 0) { + size_t zeros_needed = block_size - remainder; + for (size_t i = 0; i < zeros_needed; i++) { + if (js_blob_write_bit_internal(ctx, bd, 0) < 0) { + return JS_ThrowTypeError(ctx, "write_pad: out of memory"); + } + } + } + + return JS_UNDEFINED; +} + +// blob.write_text(text) +static JSValue js_blob_write_text(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "write_text(text) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "write_text: not called on a blob"); + } + + if (bd->is_stone) { + return JS_ThrowTypeError(ctx, "write_text: cannot write to stone blob"); + } + + const char *str = JS_ToCString(ctx, argv[0]); + if (!str) return JS_EXCEPTION; + + size_t len = strlen(str); + + // Write kim-encoded length + if (write_kim_internal(bd, ctx, len) < 0) { + JS_FreeCString(ctx, str); + return JS_ThrowTypeError(ctx, "write_text: out of memory"); + } + + // Write each character as kim-encoded UTF-32 + // For simplicity, assuming ASCII + for (size_t i = 0; i < len; i++) { + if (write_kim_internal(bd, ctx, (unsigned char)str[i]) < 0) { + JS_FreeCString(ctx, str); + return JS_ThrowTypeError(ctx, "write_text: out of memory"); + } + } + + JS_FreeCString(ctx, str); + return JS_UNDEFINED; +} + +// blob.read_logical(from) static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 2) { - return JS_ThrowTypeError(ctx, "blob.read_logical(blob, from) requires 2 arguments"); + if (argc < 1) { + return JS_ThrowTypeError(ctx, "read_logical(from) requires 1 argument"); } - JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + blob *bd = js2blob(ctx, this_val); if (!bd) { - return JS_ThrowTypeError(ctx, "blob.read_logical: argument 1 not a blob"); + return JS_ThrowTypeError(ctx, "read_logical: not called on a blob"); } int64_t pos; - if (JS_ToInt64(ctx, &pos, argv[1]) < 0) { + if (JS_ToInt64(ctx, &pos, argv[0]) < 0) { return JS_EXCEPTION; } if (pos < 0) { @@ -334,20 +691,308 @@ static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val, } int bit_val; if (js_blob_read_bit_internal(bd, (size_t)pos, &bit_val) < 0) { - return JS_NULL; // error or out of range + return JS_NULL; // not stone or out of range } return JS_NewBool(ctx, bit_val); } -// blob.stone(blob) +// blob.read_blob(from, to) +static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "read_blob: not called on a blob"); + } + + if (!bd->is_stone) { + return JS_ThrowTypeError(ctx, "read_blob: blob must be stone"); + } + + int64_t from = 0; + int64_t to = bd->bit_length; + + if (argc >= 1) { + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + if (from < 0) from = 0; + } + if (argc >= 2) { + if (JS_ToInt64(ctx, &to, argv[1]) < 0) return JS_EXCEPTION; + if (to > (int64_t)bd->bit_length) to = bd->bit_length; + } + + if (from >= to) { + // Return empty blob + blob *new_bd = calloc(1, sizeof(*new_bd)); + if (!new_bd) return JS_ThrowOutOfMemory(ctx); + return blob2js(ctx, new_bd); + } + + // Create new blob with copy of bits + size_t copy_len = to - from; + blob *new_bd = calloc(1, sizeof(*new_bd)); + if (!new_bd) return JS_ThrowOutOfMemory(ctx); + + new_bd->bit_length = copy_len; + new_bd->bit_capacity = copy_len; + if (new_bd->bit_capacity % 8) { + new_bd->bit_capacity += 8 - (new_bd->bit_capacity % 8); + } + + size_t bytes = new_bd->bit_capacity / 8; + if (bytes) { + new_bd->data = calloc(bytes, 1); + if (!new_bd->data) { + free(new_bd); + return JS_ThrowOutOfMemory(ctx); + } + + // Copy bits + for (size_t i = 0; i < copy_len; i++) { + size_t src_idx = from + i; + size_t src_byte = src_idx / 8; + size_t src_bit = src_idx % 8; + int bit = (bd->data[src_byte] >> src_bit) & 1; + + size_t dst_byte = i / 8; + size_t dst_bit = i % 8; + if (bit) { + new_bd->data[dst_byte] |= (1 << dst_bit); + } + } + } + + return blob2js(ctx, new_bd); +} + +// blob.read_dec64(from) +static JSValue js_blob_read_dec64(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "read_dec64(from) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "read_dec64: not called on a blob"); + } + + if (!bd->is_stone) { + return JS_ThrowTypeError(ctx, "read_dec64: blob must be stone"); + } + + int64_t from; + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + + if (from < 0 || from + 64 > (int64_t)bd->bit_length) { + return JS_ThrowRangeError(ctx, "read_dec64: out of range"); + } + + // Read 64 bits + uint64_t bits = 0; + for (int i = 0; i < 64; i++) { + int bit; + js_blob_read_bit_internal(bd, from + i, &bit); + if (bit) bits |= (1ULL << i); + } + + // Convert to double + double d; + memcpy(&d, &bits, sizeof(d)); + + return JS_NewFloat64(ctx, d); +} + +// blob.read_fit(from, length) +static JSValue js_blob_read_fit(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 2) { + return JS_ThrowTypeError(ctx, "read_fit(from, length) requires 2 arguments"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "read_fit: not called on a blob"); + } + + if (!bd->is_stone) { + return JS_ThrowTypeError(ctx, "read_fit: blob must be stone"); + } + + int64_t from; + int32_t length; + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToInt32(ctx, &length, argv[1]) < 0) return JS_EXCEPTION; + + if (length < 0 || length > 64) { + return JS_ThrowRangeError(ctx, "read_fit: length must be between 0 and 64"); + } + + if (from < 0 || from + length > (int64_t)bd->bit_length) { + return JS_ThrowRangeError(ctx, "read_fit: out of range"); + } + + // Read bits + int64_t value = 0; + for (int i = 0; i < length; i++) { + int bit; + js_blob_read_bit_internal(bd, from + i, &bit); + if (bit) value |= (1LL << i); + } + + return JS_NewInt64(ctx, value); +} + +// blob.read_kim(from) +static JSValue js_blob_read_kim(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "read_kim(from) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "read_kim: not called on a blob"); + } + + if (!bd->is_stone) { + return JS_ThrowTypeError(ctx, "read_kim: blob must be stone"); + } + + int64_t from; + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + + if (from < 0 || from >= (int64_t)bd->bit_length) { + return JS_ThrowRangeError(ctx, "read_kim: out of range"); + } + + int64_t value; + size_t bits_read; + if (read_kim_internal(bd, from, &value, &bits_read) < 0) { + return JS_ThrowRangeError(ctx, "read_kim: invalid kim encoding"); + } + + // Return an object with the value and the number of bits read + JSValue obj = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, obj, "value", JS_NewInt64(ctx, value)); + JS_SetPropertyStr(ctx, obj, "bits_read", JS_NewInt64(ctx, bits_read)); + return obj; +} + +// blob.read_text(from) +static JSValue js_blob_read_text(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "read_text(from) requires 1 argument"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "read_text: not called on a blob"); + } + + if (!bd->is_stone) { + return JS_ThrowTypeError(ctx, "read_text: blob must be stone"); + } + + int64_t from; + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + + if (from < 0 || from >= (int64_t)bd->bit_length) { + return JS_ThrowRangeError(ctx, "read_text: out of range"); + } + + // Read kim-encoded length + int64_t length; + size_t bits_read; + if (read_kim_internal(bd, from, &length, &bits_read) < 0) { + return JS_ThrowRangeError(ctx, "read_text: invalid kim encoding"); + } + + if (length < 0) { + return JS_ThrowRangeError(ctx, "read_text: invalid text length"); + } + + size_t pos = from + bits_read; + + // Read characters + char *str = malloc(length + 1); + if (!str) return JS_ThrowOutOfMemory(ctx); + + for (int64_t i = 0; i < length; i++) { + int64_t ch; + if (read_kim_internal(bd, pos, &ch, &bits_read) < 0) { + free(str); + return JS_ThrowRangeError(ctx, "read_text: invalid character encoding"); + } + // For simplicity, assuming ASCII + str[i] = (char)(ch & 0xFF); + pos += bits_read; + } + str[length] = '\0'; + + JSValue result = JS_NewString(ctx, str); + free(str); + + // Return object with text and total bits read + JSValue obj = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, obj, "text", result); + JS_SetPropertyStr(ctx, obj, "bits_read", JS_NewInt64(ctx, pos - from)); + return obj; +} + +// blob.pad?(from, block_size) +static JSValue js_blob_pad_q(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 2) { + return JS_ThrowTypeError(ctx, "pad?(from, block_size) requires 2 arguments"); + } + blob *bd = js2blob(ctx, this_val); + if (!bd) { + return JS_ThrowTypeError(ctx, "pad?: not called on a blob"); + } + + if (!bd->is_stone) { + 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_ThrowTypeError(ctx, "pad?: block_size must be positive"); + } + + // Check if blob length is multiple of block_size + if (bd->bit_length % block_size != 0) { + return JS_FALSE; + } + + // Check if difference between length and from is <= block_size + int64_t diff = bd->bit_length - from; + if (diff <= 0 || diff > block_size) { + return JS_FALSE; + } + + // Check if bit at from is 1 + int bit; + if (js_blob_read_bit_internal(bd, from, &bit) < 0 || bit != 1) { + return JS_FALSE; + } + + // Check if remaining bits are 0 + for (int64_t i = from + 1; i < bd->bit_length; i++) { + if (js_blob_read_bit_internal(bd, i, &bit) < 0 || bit != 0) { + return JS_FALSE; + } + } + + return JS_TRUE; +} + +// blob.stone() static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "blob.stone(blob) requires 1 argument"); - } - JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + blob *bd = js2blob(ctx, this_val); if (!bd) { - return JS_ThrowTypeError(ctx, "blob.stone: argument not a blob"); + return JS_ThrowTypeError(ctx, "stone: not called on a blob"); } if (!bd->is_stone) { js_blob_make_stone(bd); @@ -355,106 +1000,102 @@ static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val, return JS_UNDEFINED; } -// blob.length(blob) +// blob.length getter // Return number of bits in the blob -static JSValue js_blob_length(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "blob.length(blob) requires 1 argument"); - } - JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); +static JSValue js_blob_get_length(JSContext *ctx, JSValueConst this_val, int magic) { + blob *bd = js2blob(ctx, this_val); if (!bd) { - return JS_ThrowTypeError(ctx, "blob.length: argument not a blob"); + return JS_ThrowTypeError(ctx, "length: not called on a blob"); } return JS_NewInt64(ctx, bd->bit_length); } -// blob.blob?(value) -// Return true if the value is a blob object -static JSValue js_blob_is_blob(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { - if (argc < 1) return JS_FALSE; - JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); - return JS_NewBool(ctx, bd != NULL); -} // ----------------------------------------------------------------------------- // Exports list // ----------------------------------------------------------------------------- static const JSCFunctionListEntry js_blob_funcs[] = { - // The "make" function. - JS_CFUNC_DEF("make", 3, js_blob_make), - // Some example read/write routines - JS_CFUNC_DEF("write_bit", 2, js_blob_write_bit), - JS_CFUNC_DEF("read_logical", 2, js_blob_read_logical), - // Convert blob from antestone -> stone - JS_CFUNC_DEF("stone", 1, js_blob_stone), - // Return the length in bits - JS_CFUNC_DEF("length", 1, js_blob_length), - // Check if a value is a blob - JS_CFUNC_DEF("isblob", 1, js_blob_is_blob), + // Write methods + JS_CFUNC_DEF("write_bit", 1, js_blob_write_bit), + JS_CFUNC_DEF("write_blob", 1, js_blob_write_blob), + JS_CFUNC_DEF("write_dec64", 1, js_blob_write_dec64), + JS_CFUNC_DEF("write_fit", 2, js_blob_write_fit), + JS_CFUNC_DEF("write_kim", 1, js_blob_write_kim), + JS_CFUNC_DEF("write_pad", 1, js_blob_write_pad), + JS_CFUNC_DEF("write_text", 1, js_blob_write_text), + + // Read methods + JS_CFUNC_DEF("read_logical", 1, js_blob_read_logical), + JS_CFUNC_DEF("read_blob", 2, js_blob_read_blob), + JS_CFUNC_DEF("read_dec64", 1, js_blob_read_dec64), + JS_CFUNC_DEF("read_fit", 2, js_blob_read_fit), + JS_CFUNC_DEF("read_kim", 1, js_blob_read_kim), + JS_CFUNC_DEF("read_text", 1, js_blob_read_text), + JS_CFUNC_DEF("pad?", 2, js_blob_pad_q), + + // Other methods + JS_CFUNC_DEF("stone", 0, js_blob_stone), + + // Length property getter + JS_CGETSET_DEF("length", js_blob_get_length, NULL), }; -// ----------------------------------------------------------------------------- -// Class definition for the 'blob' objects -// ----------------------------------------------------------------------------- - -static JSClassDef js_blob_class = { - "BlobClass", - .finalizer = js_blob_finalizer, - .gc_mark = js_blob_mark, -}; - -// Module init function -static int js_blob_init(JSContext *ctx, JSModuleDef *m) { - // Register the class if not already done - if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) { - JS_NewClassID(&js_blob_class_id); - JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class); - } - - // Create a prototype object - JSValue proto = JS_NewObject(ctx); - JS_SetClassProto(ctx, js_blob_class_id, proto); - - // Export our functions as named exports - JS_SetModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0])); - return 0; -} - -// The module entry point -#ifdef JS_SHARED_LIBRARY -#define JS_INIT_MODULE js_init_module -#else -#define JS_INIT_MODULE js_init_module_blob -#endif - -JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { - JSModuleDef *m = JS_NewCModule(ctx, module_name, js_blob_init); - if (!m) return NULL; - JS_AddModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0])); - return m; -} // ----------------------------------------------------------------------------- // js_blob_use(ctx) for easy embedding: returns an object with the blob functions // ----------------------------------------------------------------------------- -JSValue js_blob_use(JSContext *ctx) { - // Ensure class is registered - if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) { - JS_NewClassID(&js_blob_class_id); - JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class); - - // Create a prototype object - JSValue proto = JS_NewObject(ctx); - JS_SetClassProto(ctx, js_blob_class_id, proto); +// Static kim_length function +static JSValue js_blob_kim_length(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "kim_length(value) requires 1 argument"); } - - // Create a plain object (the "exports") and add the funcs - JSValue obj = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, obj, js_blob_funcs, - sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0])); - return obj; + + if (JS_IsBool(argv[0])) { + return JS_NewInt32(ctx, 1); + } + + if (JS_IsNumber(argv[0])) { + int64_t value; + if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewInt32(ctx, kim_length_for_fit(value)); + } + + if (JS_IsString(argv[0])) { + const char *str = JS_ToCString(ctx, argv[0]); + if (!str) return JS_EXCEPTION; + + size_t len = strlen(str); + // Length encoding + each character encoding + int total_bits = kim_length_for_fit(len); + for (size_t i = 0; i < len; i++) { + total_bits += kim_length_for_fit((unsigned char)str[i]); + } + + JS_FreeCString(ctx, str); + return JS_NewInt32(ctx, total_bits); + } + + return JS_NULL; +} + +JSValue js_blob_use(JSContext *js) { + // Register the blob class + QJSCLASSPREP_FUNCS(blob); + + // Create the constructor function + JSValue ctor = JS_NewCFunction2(js, js_blob_constructor, "blob", 3, JS_CFUNC_constructor, 0); + + // Set the prototype on the constructor + JSValue proto = JS_GetClassProto(js, js_blob_id); + JS_SetConstructor(js, ctor, proto); + JS_FreeValue(js, proto); + + // Add static kim_length method to constructor + JS_SetPropertyStr(js, ctor, "kim_length", + JS_NewCFunction(js, js_blob_kim_length, "kim_length", 1)); + + return ctor; } diff --git a/tests/blob.js b/tests/blob.js index 88bf2d7c..a9407852 100644 --- a/tests/blob.js +++ b/tests/blob.js @@ -8,7 +8,11 @@ // // Attempt to "use" the blob module as if it was installed or compiled in. -var blob = use('blob'); +var Blob = use('blob') + +var pp = new Blob() + +console.log(pp.length) // If you're testing in an environment without a 'use' loader, you might do // something like importing the compiled C module or linking it differently. @@ -141,14 +145,14 @@ let tests = [ // 1) Ensure we can create a blank blob { - name: "make() should produce an empty antestone blob of length 0", + name: "new Blob() should produce an empty antestone blob of length 0", run() { - let b = blob.make(); - let isBlob = blob["isblob"](b); - let length = blob.length(b); - let passed = (isBlob === true && length === 0); + let b = new Blob(); + let length = b.length; + let passed = (b instanceof Blob && length === 0); + console.log(`blob len: ${b.length}, is blob? ${b instanceof Blob}`) let messages = []; - if (!isBlob) messages.push("Returned object is not recognized as a blob"); + if (!(b instanceof Blob)) messages.push("Returned object is not recognized as a blob"); if (length !== 0) messages.push(`Expected length 0, got ${length}`); return { passed, messages }; } @@ -156,11 +160,11 @@ let tests = [ // 2) Make a blob with some capacity { - name: "make(16) should create a blob with capacity >=16 bits and length=0", + name: "new Blob(16) should create a blob with capacity >=16 bits and length=0", run() { - let b = blob.make(16); - let isBlob = blob["isblob"](b); - let length = blob.length(b); + let b = new Blob(16); + let isBlob = b instanceof Blob; + let length = b.length; let passed = isBlob && length === 0; let messages = []; if (!isBlob) messages.push("Not recognized as a blob"); @@ -169,21 +173,34 @@ let tests = [ } }, - // 3) Make a blob with (length, logical) + // 3) Make a blob with (length, logical) - but can't read until stone { - name: "make(5, true) should create a blob of length=5 bits, all 1s (antestone)", + name: "new Blob(5, true) should create a blob of length=5 bits, all 1s - needs stone to read", run() { - let b = blob.make(5, true); - let len = blob.length(b); + let b = new Blob(5, true); + let len = b.length; if (len !== 5) { return { passed: false, messages: [`Expected length=5, got ${len}`] }; } - // Check bits + + // Try to read before stone - should return null + let bitVal = b.read_logical(0); + if (bitVal !== null) { + return { + passed: false, + messages: [`Expected null when reading antestone blob, got ${bitVal}`] + }; + } + + // Stone it + b.stone(); + + // Now check bits for (let i = 0; i < 5; i++) { - let bitVal = blob.read_logical(b, i); + let bitVal = b.read_logical(i); if (bitVal !== true) { return { passed: false, @@ -195,26 +212,30 @@ let tests = [ } }, - // 4) Write bits to an empty blob + // 4) Write bits to an empty blob, then stone and read { - name: "write_bit() on an empty blob, then read_logical() to verify bits", + name: "write_bit() on an empty blob, then stone and read_logical() to verify bits", run() { - let b = blob.make(); // starts length=0 + let b = new Blob(); // starts length=0 // write bits: true, false, true - blob.write_bit(b, true); // bit #0 - blob.write_bit(b, false); // bit #1 - blob.write_bit(b, true); // bit #2 - let len = blob.length(b); + b.write_bit(true); // bit #0 + b.write_bit(false); // bit #1 + b.write_bit(true); // bit #2 + let len = b.length; if (len !== 3) { return { passed: false, messages: [`Expected length=3, got ${len}`] }; } + + // Must stone before reading + b.stone(); + let bits = [ - blob.read_logical(b, 0), - blob.read_logical(b, 1), - blob.read_logical(b, 2) + b.read_logical(0), + b.read_logical(1), + b.read_logical(2) ]; let compare = deepCompare([true, false, true], bits); return compare; @@ -225,14 +246,14 @@ let tests = [ { name: "Stoning a blob should prevent further writes", run() { - let b = blob.make(5, false); + let b = new Blob(5, false); // Stone it - blob.stone(b); + b.stone(); // Try to write let passed = true; let messages = []; try { - blob.write_bit(b, true); + b.write_bit(true); passed = false; messages.push("Expected an error or refusal when writing to a stone blob, but none occurred"); } catch (e) { @@ -242,51 +263,344 @@ let tests = [ } }, - // 6) make(blob, from, to) - copying range from an existing blob + // 6) make(blob, from, to) - copying range from an existing blob (copy doesn't need source to be stone) { - name: "make(existing_blob, from, to) can copy partial range", + name: "new Blob(existing_blob, from, to) can copy partial range", run() { // Create a 10-bit blob: pattern T F T F T F T F T F - let original = blob.make(); + let original = new Blob(); for (let i = 0; i < 10; i++) { - blob.write_bit(original, i % 2 === 0); + original.write_bit(i % 2 === 0); } // Copy bits [2..7) // That slice is bits #2..6: T, F, T, F, T // indices: 2: T(1), 3: F(0), 4: T(1), 5: F(0), 6: T(1) // so length=5 - let copy = blob.make(original, 2, 7); - let len = blob.length(copy); + let copy = new Blob(original, 2, 7); + let len = copy.length; if (len !== 5) { return { passed: false, messages: [`Expected length=5, got ${len}`] }; } + + // Stone the copy to read from it + copy.stone(); + let bits = []; for (let i = 0; i < len; i++) { - bits.push(blob.read_logical(copy, i)); + bits.push(copy.read_logical(i)); } let compare = deepCompare([true, false, true, false, true], bits); return compare; } }, - // 7) Checking isblob(something) + // 7) Checking instanceof { - name: "isblob should correctly identify blob vs. non-blob", + name: "instanceof should correctly identify blob vs. non-blob", run() { - let b = blob.make(); - let isB = blob["isblob"](b); - let isNum = blob["isblob"](42); - let isObj = blob["isblob"]({ length: 3 }); + let b = new Blob(); + let isB = b instanceof Blob; + let isNum = 42 instanceof Blob; + let isObj = { length: 3 } instanceof Blob; let passed = (isB === true && isNum === false && isObj === false); let messages = []; if (!passed) { - messages.push(`Expected isblob(b)=true, isblob(42)=false, isblob({})=false; got ${isB}, ${isNum}, ${isObj}`); + messages.push(`Expected (b instanceof Blob)=true, (42 instanceof Blob)=false, ({} instanceof Blob)=false; got ${isB}, ${isNum}, ${isObj}`); } return { passed, messages }; } + }, + + // 8) Test write_blob + { + name: "write_blob() should append one blob to another", + run() { + let b1 = new Blob(); + b1.write_bit(true); + b1.write_bit(false); + + let b2 = new Blob(); + b2.write_bit(true); + b2.write_bit(true); + + b1.write_blob(b2); + + if (b1.length !== 4) { + return { + passed: false, + messages: [`Expected length=4 after write_blob, got ${b1.length}`] + }; + } + + b1.stone(); + let bits = []; + for (let i = 0; i < 4; i++) { + bits.push(b1.read_logical(i)); + } + + return deepCompare([true, false, true, true], bits); + } + }, + + // 9) Test write_fit and read_fit + { + name: "write_fit() and read_fit() should handle fixed-size bit fields", + run() { + let b = new Blob(); + b.write_fit(5, 3); // Write value 5 in 3 bits (101) + b.write_fit(7, 4); // Write value 7 in 4 bits (0111) + + if (b.length !== 7) { + return { + passed: false, + messages: [`Expected length=7, got ${b.length}`] + }; + } + + b.stone(); + + let val1 = b.read_fit(0, 3); + let val2 = b.read_fit(3, 4); + + if (val1 !== 5 || val2 !== 7) { + return { + passed: false, + messages: [`Expected read_fit to return 5 and 7, got ${val1} and ${val2}`] + }; + } + + return { passed: true, messages: [] }; + } + }, + + // 10) Test write_kim and read_kim + { + name: "write_kim() and read_kim() should handle kim encoding", + run() { + let b = new Blob(); + b.write_kim(42); // Small positive number + b.write_kim(-1); // Small negative number + b.write_kim(1000); // Larger number + + b.stone(); + + let result1 = b.read_kim(0); + let result2 = b.read_kim(result1.bits_read); + let result3 = b.read_kim(result1.bits_read + result2.bits_read); + + if (result1.value !== 42 || result2.value !== -1 || result3.value !== 1000) { + return { + passed: false, + messages: [`Expected kim values 42, -1, 1000, got ${result1.value}, ${result2.value}, ${result3.value}`] + }; + } + + return { passed: true, messages: [] }; + } + }, + + // 11) Test write_text and read_text + { + name: "write_text() and read_text() should handle text encoding", + run() { + let b = new Blob(); + b.write_text("Hello"); + + b.stone(); + + let result = b.read_text(0); + + if (result.text !== "Hello") { + return { + passed: false, + messages: [`Expected text "Hello", got "${result.text}"`] + }; + } + + return { passed: true, messages: [] }; + } + }, + + // 12) Test write_dec64 and read_dec64 + { + name: "write_dec64() and read_dec64() should handle decimal encoding", + run() { + let b = new Blob(); + b.write_dec64(3.14159); + b.write_dec64(-42.5); + + b.stone(); + + let val1 = b.read_dec64(0); + let val2 = b.read_dec64(64); + + // Allow small floating point differences + let diff1 = Math.abs(val1 - 3.14159); + let diff2 = Math.abs(val2 - (-42.5)); + + if (diff1 > EPSILON || diff2 > EPSILON) { + return { + passed: false, + messages: [`Expected dec64 values 3.14159 and -42.5, got ${val1} and ${val2}`] + }; + } + + return { passed: true, messages: [] }; + } + }, + + // 13) Test write_pad and pad? + { + name: "write_pad() and pad?() should handle block padding", + run() { + let b = new Blob(); + b.write_bit(true); + b.write_bit(false); + b.write_bit(true); + // Length is now 3 + b.write_pad(8); // Pad to multiple of 8 + + if (b.length !== 8) { + return { + passed: false, + messages: [`Expected length=8 after padding, got ${b.length}`] + }; + } + + b.stone(); + + // Check pad? function + let isPadded = b["pad?"](3, 8); + if (!isPadded) { + return { + passed: false, + messages: [`Expected pad?(3, 8) to return true`] + }; + } + + // Verify padding pattern: original bits, then 1, then 0s + let bits = []; + for (let i = 0; i < 8; i++) { + bits.push(b.read_logical(i)); + } + + return deepCompare([true, false, true, true, false, false, false, false], bits); + } + }, + + // 14) Test Blob.kim_length static function + { + name: "Blob.kim_length() should calculate correct kim encoding lengths", + run() { + let len1 = Blob.kim_length(42); // Should be 8 bits + let len2 = Blob.kim_length(1000); // Should be 16 bits + let len3 = Blob.kim_length("Hello"); // 8 bits for length + 8*5 for chars = 48 + + if (len1 !== 8) { + return { + passed: false, + messages: [`Expected kim_length(42)=8, got ${len1}`] + }; + } + + if (len2 !== 16) { + return { + passed: false, + messages: [`Expected kim_length(1000)=16, got ${len2}`] + }; + } + + if (len3 !== 48) { + return { + passed: false, + messages: [`Expected kim_length("Hello")=48, got ${len3}`] + }; + } + + return { passed: true, messages: [] }; + } + }, + + // 15) Test write_bit with numeric 0 and 1 + { + name: "write_bit() should accept 0, 1, true, false", + run() { + let b = new Blob(); + b.write_bit(1); + b.write_bit(0); + b.write_bit(true); + b.write_bit(false); + + b.stone(); + + let bits = []; + for (let i = 0; i < 4; i++) { + bits.push(b.read_logical(i)); + } + + return deepCompare([true, false, true, false], bits); + } + }, + + // 16) Test read_blob to create copies + { + name: "read_blob() should create partial copies of stone blobs", + run() { + let b = new Blob(); + for (let i = 0; i < 10; i++) { + b.write_bit(i % 3 === 0); // Pattern: T,F,F,T,F,F,T,F,F,T + } + b.stone(); + + let copy = b.read_blob(3, 7); // Extract bits 3-6 + copy.stone(); // Need to stone the copy to read + + if (copy.length !== 4) { + return { + passed: false, + messages: [`Expected copy length=4, got ${copy.length}`] + }; + } + + let bits = []; + for (let i = 0; i < 4; i++) { + bits.push(copy.read_logical(i)); + } + + // Bits 3-6 from original: T,F,F,T + return deepCompare([true, false, false, true], bits); + } + }, + + // 17) Test random blob creation + { + name: "new Blob(length, random_func) should create random blob", + run() { + // Simple random function that alternates + let counter = 0; + let randomFunc = () => counter++; + + let b = new Blob(8, randomFunc); + b.stone(); + + if (b.length !== 8) { + return { + passed: false, + messages: [`Expected length=8, got ${b.length}`] + }; + } + + // Check pattern matches counter LSB: 0,1,0,1,0,1,0,1 + let bits = []; + for (let i = 0; i < 8; i++) { + bits.push(b.read_logical(i)); + } + + return deepCompare([false, true, false, true, false, true, false, true], bits); + } } ];