From 7ea79c8ced7693d298fc55e89a45d44dc7c6fb92 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 23 Feb 2025 16:06:40 -0600 Subject: [PATCH] Fix nota implementation; add nota test suite --- meson.build | 3 +- scripts/modules/nota.js | 20 +++ source/kim.h | 4 +- source/nota.h | 333 +++++++++++++++++++++++++++++++--------- source/qjs_nota.c | 52 +++++-- source/script.c | 1 + tests/nota.js | 224 ++++++++++++++++++++------- 7 files changed, 494 insertions(+), 143 deletions(-) create mode 100644 scripts/modules/nota.js diff --git a/meson.build b/meson.build index 16861951..2ba808f9 100644 --- a/meson.build +++ b/meson.build @@ -234,7 +234,8 @@ copy_tests = custom_target( tests = [ 'spawn_actor', - 'empty' + 'empty', + 'nota' ] foreach file : tests diff --git a/scripts/modules/nota.js b/scripts/modules/nota.js new file mode 100644 index 00000000..277d30c4 --- /dev/null +++ b/scripts/modules/nota.js @@ -0,0 +1,20 @@ +var nota = this + +nota.encode[prosperon.DOC] = `Convert a JavaScript value into a NOTA-encoded ArrayBuffer. + +This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value. + +:param value: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer). +:return: An ArrayBuffer containing the NOTA-encoded data. +:throws: An error if no argument is provided. +` + +nota.decode[prosperon.DOC] = `Decode a NOTA-encoded ArrayBuffer into a JavaScript value. + +This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined. + +:param buffer: An ArrayBuffer containing NOTA-encoded data to decode. +:return: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided. +` + +return nota diff --git a/source/kim.h b/source/kim.h index 216650e8..738f4f6b 100755 --- a/source/kim.h +++ b/source/kim.h @@ -85,7 +85,7 @@ static inline void encode_kim(char **s, int rune) while (bits > 7) { bits -= 7; - **s = KIM_CONT | KIM_DATA & (rune >> bits); + **s = KIM_CONT | (KIM_DATA & (rune >> bits)); (*s)++; } **s = KIM_DATA & rune; @@ -107,7 +107,7 @@ int decode_kim(char **s) void utf8_to_kim(const char **utf, char **kim) { - char * str = *utf; + const char * str = *utf; while (*str) encode_kim(kim, decode_utf8(&str)); } diff --git a/source/nota.h b/source/nota.h index c6feb59c..9d235ec2 100755 --- a/source/nota.h +++ b/source/nota.h @@ -2,16 +2,16 @@ #define NOTA_H #define NOTA_BLOB 0x00 // C 0 0 0 -#define NOTA_TEXT 0x10 // -#define NOTA_ARR 0x20 // 0 1 0 +#define NOTA_TEXT 0x10 // C 0 0 1 +#define NOTA_ARR 0x20 // C 0 1 0 #define NOTA_REC 0x30 // C 0 1 1 #define NOTA_FLOAT 0x40 // C 1 0 #define NOTA_INT 0x60 // C 1 1 0 #define NOTA_SYM 0x70 // C 1 1 1 -#define NOTA_FALSE 0x00 -#define NOTA_TRUE 0x01 -#define NOTA_NULL 0x02 +#define NOTA_NULL 0x00 +#define NOTA_FALSE 0x02 +#define NOTA_TRUE 0x03 #define NOTA_INF 0x03 #define NOTA_PRIVATE 0x08 #define NOTA_SYSTEM 0x09 @@ -23,7 +23,7 @@ int nota_type(char *nota); // Pass NULL into the read in variable to skip over it -char *nota_read_blob(long long *len, char *nota); +char *nota_read_blob(long long *len, char **blob, char *nota); // ALLOCATES! Uses strdup to return it via the text pointer char *nota_read_text(char **text, char *nota); char *nota_read_array(long long *len, char *nota); @@ -32,7 +32,7 @@ char *nota_read_float(double *d, char *nota); char *nota_read_int(long long *l, char *nota); char *nota_read_sym(int *sym, char *nota); -char *nota_write_blob(unsigned long long n, char *nota); +char *nota_write_blob(unsigned long long n, char *data, char *nota); char *nota_write_text(const char *s, char *nota); char *nota_write_array(unsigned long long n, char *nota); char *nota_write_record(unsigned long long n, char *nota); @@ -84,7 +84,7 @@ char *nota_read_num(long long *n, char *nota) *n |= (*nota) & NOTA_HEAD_DATA; while (CONTINUE(*(nota++))) - *n = (*n<<7) | (*nota) & NOTA_DATA; + *n = (((*n<<7) | *nota) & NOTA_DATA); return nota; } @@ -170,99 +170,276 @@ char *nota_write_int(long long n, char *nota) #include #include -void extract_mantissa_coefficient(double num, long *mantissa, long* coefficient) { +void extract_mantissa_coefficient(double num, long *coefficient, long *exponent) +{ + if (num == 0.0) { + *coefficient = 0; + *exponent = 0; + return; + } + + // Optional: handle sign separately if you want 'coefficient' always positive. + // For simplicity, let's just let atol(...) parse the sign if it's there. + + // 1) Slightly round the number to avoid too many FP trailing digits: + // Example: Round to 12 decimal places. + double rounded = floor(fabs(num) * 1e12 + 0.5) / 1e12; + if (num < 0) { + rounded = -rounded; + } + + // 2) Convert to string with fewer digits of precision so we do NOT get + // the long binary-fraction expansions (like 98.599999999999994). char buf[64]; - char *p, *dec_point; - int exp = 0, coeff = 0; + snprintf(buf, sizeof(buf), "%.14g", rounded); - // Convert double to string with maximum precision - snprintf(buf, sizeof(buf), "%.17g", num); - - // Find if 'e' or 'E' is present (scientific notation) - p = strchr(buf, 'e'); - if (!p) p = strchr(buf, 'E'); - if (p) { - // There is an exponent part - exp = atol(p + 1); - *p = '\0'; // Remove exponent part from the string + // 3) Look for scientific notation + char *exp_pos = strpbrk(buf, "eE"); + long exp_from_sci = 0; + if (exp_pos) { + exp_from_sci = atol(exp_pos + 1); + *exp_pos = '\0'; // Truncate the exponent part from the string } - // Find decimal point - dec_point = strchr(buf, '.'); + // 4) Find decimal point + char *dec_point = strchr(buf, '.'); + int digits_after_decimal = 0; + if (dec_point) { - // Count number of digits after decimal point - int digits_after_point = strlen(dec_point + 1); - coeff = digits_after_point; - // Remove decimal point by shifting characters + digits_after_decimal = (int)strlen(dec_point + 1); + // Remove the '.' by shifting the remainder left memmove(dec_point, dec_point + 1, strlen(dec_point)); - } else - coeff = 0; + } - // Adjust coefficient with exponent from scientific notation - coeff -= exp; + // 5) Now the string is just an integer (possibly signed), so parse it + long long coeff_ll = atoll(buf); // support up to 64-bit range + *coefficient = (long)coeff_ll; - // Copy the mantissa - *mantissa = atol(buf); + // 6) The final decimal exponent is whatever came from 'e/E' + // minus however many digits we removed by removing the decimal point. + *exponent = exp_from_sci - digits_after_decimal; +} - // Set coefficient - *coefficient = coeff; +char *nota_write_decimal_str(const char *decimal, char *nota) +{ + // Handle negative sign + int neg = (decimal[0] == '-'); + if (neg) decimal++; // Skip the '-' if present + + // Parse integer part + long coef = 0; + long exp = 0; + int decimal_point_seen = 0; + const char *ptr = decimal; + int has_exponent = 0; + + // First pass: calculate coefficient up to 'e' or 'E' + while (*ptr && *ptr != 'e' && *ptr != 'E') { + if (*ptr == '.') { + decimal_point_seen = 1; + ptr++; + continue; + } + + if (*ptr >= '0' && *ptr <= '9') { + coef = coef * 10 + (*ptr - '0'); + if (decimal_point_seen) { + exp--; // Each digit after decimal point decreases exponent + } + } + ptr++; + } + + // Parse exponent part if present + if (*ptr == 'e' || *ptr == 'E') { + has_exponent = 1; + ptr++; // Skip 'e' or 'E' + + int exp_sign = 1; + if (*ptr == '-') { + exp_sign = -1; + ptr++; + } else if (*ptr == '+') { + ptr++; + } + + long explicit_exp = 0; + while (*ptr >= '0' && *ptr <= '9') { + explicit_exp = explicit_exp * 10 + (*ptr - '0'); + ptr++; + } + exp += exp_sign * explicit_exp; + } + + // If no decimal point and no exponent, treat as integer + if (!decimal_point_seen && !has_exponent) { + return nota_write_int(coef * (neg ? -1 : 1), nota); + } + + // Remove trailing zeros from coefficient + while (coef > 0 && coef % 10 == 0 && exp < 0) { + coef /= 10; + exp++; + } + + // Handle zero case + if (coef == 0) { + return nota_write_int(0, nota); + } + + // Set up the notation format similar to nota_write_float + int expsign = exp < 0 ? ~0 : 0; + exp = llabs(exp); + + nota[0] = NOTA_FLOAT; + nota[0] |= (expsign & 1) << 4; // Exponent sign bit + nota[0] |= (neg & 1) << 3; // Number sign bit + + char *c = nota_continue_num(exp, nota, 3); + return nota_continue_num(coef, c, 7); } char *nota_write_float(double n, char *nota) { - int neg = n < 0; - long digits; + int neg = (n < 0); long coef; - extract_mantissa_coefficient(n, &digits, &coef); + long exp; + extract_mantissa_coefficient(n, &coef, &exp); - printf("Values of %g are %ld e %ld\n", n, digits, coef); - if (coef == 0) + // Store integer if exponent is zero + if (exp == 0) return nota_write_int(coef * (neg ? -1 : 1), nota); - int expsign = coef < 0 ? ~0 : 0; - coef = llabs(coef); + int expsign = exp < 0 ? ~0 : 0; + exp = labs(exp); nota[0] = NOTA_FLOAT; - nota[0] |= 0x10 & expsign; - nota[0] |= 0x08 & neg; + nota[0] |= (expsign & 1) << 4; + nota[0] |= (neg & 1) << 3; - char *c = nota_continue_num(coef, nota, 3); + char *c = nota_continue_num(exp, nota, 3); + return nota_continue_num(labs(coef), c, 7); +} - return nota_continue_num(digits, c, 7); +char *nota_read_float_str(char **d, char *nota) +{ + // Extract sign bits from the first byte + int neg = NOTA_SIG_SIGN(*nota); // bit 3 => mantissa sign + int esign = NOTA_EXP_SIGN(*nota); // bit 4 => exponent sign + + // Read the exponent’s lower 3 bits from the first byte + long long e = (*nota) & NOTA_INT_DATA; // NOTA_INT_DATA = 0x07 + + // Count exponent bytes and accumulate value + int e_bytes = 1; + while (CONTINUE(*nota)) { + nota++; + e = (e << 7) | ((*nota) & NOTA_DATA); // NOTA_DATA = 0x7F + e_bytes++; + } + + // Move past the last exponent byte + nota++; + + // Read the mantissa + long long sig = (*nota) & NOTA_DATA; + int sig_bytes = 1; + while (CONTINUE(*nota)) { + nota++; + sig = (sig << 7) | ((*nota) & NOTA_DATA); + sig_bytes++; + } + + // Move past the last mantissa byte + nota++; + + // Apply sign bits + if (neg) sig = -sig; + if (esign) e = -e; + + // Calculate digits in mantissa (sig) and exponent (e) + int sig_digits = (sig == 0) ? 1 : (int)log10(llabs(sig)) + 1; + int e_digits = (e == 0) ? 1 : (int)log10(llabs(e)) + 1; + + // Calculate total string size: + // - Mantissa: sign (1), digits, decimal (1), 2 decimal places + // - Exponent: 'e', sign (1), digits + // - Null terminator (1) + int size = 1 + sig_digits + 1 + 2 + 1 + 1 + e_digits + 1; + if (neg) size++; // Extra space for negative mantissa + if (esign) size++; // Extra space for negative exponent + + // Allocate the string + char *result = (char *)malloc(size); + if (!result) { + *d = NULL; + return nota; // Return current position even on failure + } + + // Format the string as "xey" (e.g., "1.23e4") + double value = (double)sig * pow(10.0, (double)e); + snprintf(result, size, "%.*fe%lld", 2, value/pow(10.0, (double)e), e); + + // Set the output pointer and return + *d = result; + return nota; } char *nota_read_float(double *d, char *nota) { - long long sig = 0; - long long e = 0; + // If the caller passed NULL for d, just skip over the float encoding + if (!d) { + return nota_skip(nota); + } - char *c = nota; - e = (*c) & NOTA_INT_DATA; /* first three bits */ + // Extract sign bits from the first byte + int neg = NOTA_SIG_SIGN(*nota); // bit 3 => mantissa sign + int esign = NOTA_EXP_SIGN(*nota); // bit 4 => exponent sign - while (CONTINUE(*c)) { - e = (e<<7) | (*c) & NOTA_DATA; - c++; - } - - c++; + // Read the exponent’s lower 3 bits from the first byte + long long e = (*nota) & NOTA_INT_DATA; // NOTA_INT_DATA = 0x07 - do - sig = (sig<<7) | *c & NOTA_DATA; - while (CONTINUE(*(c++))); + // While the continuation bit is set, advance and accumulate exponent + while (CONTINUE(*nota)) { + nota++; + e = (e << 7) | ((*nota) & NOTA_DATA); // NOTA_DATA = 0x7F + } - if (NOTA_SIG_SIGN(*nota)) sig *= -1; - if (NOTA_EXP_SIGN(*nota)) e *= -1; + // Move past the last exponent byte + nota++; - *d = (double)sig * pow(10.0, e); - return c; + // Now read the mantissa in the same variable-length style + long long sig = (*nota) & NOTA_DATA; + while (CONTINUE(*nota)) { + nota++; + sig = (sig << 7) | ((*nota) & NOTA_DATA); + } + + // Move past the last mantissa byte + nota++; + + // Apply sign bits + if (neg) sig = -sig; + if (esign) e = -e; + + // Finally compute the double value: mantissa * 10^exponent + *d = (double)sig * pow(10.0, (double)e); + + // Return the pointer to wherever we ended + return nota; } char *nota_write_number(double n, char *nota) { if (n < (double)INT64_MIN || n > (double)INT64_MAX) return nota_write_float(n, nota); - if (floor(n) == n) - return nota_write_int(n, nota); - return nota_write_float(n, nota); + + double int_part; + double frac = modf(n, &int_part); + + if (fabs(frac) < 1e-14) + return nota_write_int((long long)int_part, nota); + else + return nota_write_float(n, nota); } char *nota_read_int(long long *n, char *nota) @@ -274,7 +451,7 @@ char *nota_read_int(long long *n, char *nota) char *c = nota; *n |= (*c) & NOTA_INT_DATA; /* first three bits */ while (CONTINUE(*(c++))) - *n = (*n<<7) | (*c) & NOTA_DATA; + *n = (*n<<7) | (*c & NOTA_DATA); if (NOTA_INT_SIGN(*nota)) *n *= -1; @@ -282,10 +459,15 @@ char *nota_read_int(long long *n, char *nota) } /* n is the number of bits */ -char *nota_write_blob(unsigned long long n, char *nota) +char *nota_write_blob(unsigned long long n, char *data, char *nota) { nota[0] = NOTA_BLOB; - return nota_continue_num(n, nota, 4); + nota = nota_continue_num(n, nota, 4); + int bytes = floor((n+7)/8); + for (int i = 0; i < bytes; i++) + nota[i] = data[i]; + + return nota+bytes; } char *nota_write_array(unsigned long long n, char *nota) @@ -306,10 +488,17 @@ char *nota_read_record(long long *len, char *nota) return nota_read_num(len, nota); } -char *nota_read_blob(long long *len, char *nota) +char *nota_read_blob(long long *len, char **blob, char *nota) { if (!len) return nota; - return nota_read_num(len, nota); + nota = nota_read_num(len,nota); + int bytes = floor((*len+7)/8); + *len = bytes; + + *blob = malloc(bytes); + memcpy(*blob,nota,bytes); + + return nota+bytes; } char *nota_write_record(unsigned long long n, char *nota) @@ -326,7 +515,7 @@ char *nota_write_sym(int sym, char *nota) char *nota_read_sym(int *sym, char *nota) { - if (*sym) *sym = (*nota) & 0x0f; + if (sym) *sym = (*nota) & 0x0f; return nota+1; } diff --git a/source/qjs_nota.c b/source/qjs_nota.c index c236840b..a71c3621 100755 --- a/source/qjs_nota.c +++ b/source/qjs_nota.c @@ -4,6 +4,8 @@ #define NOTA_IMPLEMENTATION #include "nota.h" +JSValue number; + char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota) { int type = nota_type(nota); @@ -12,9 +14,13 @@ char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota) double d; int b; char *str; + uint8_t *blob; switch(type) { case NOTA_BLOB: + nota = nota_read_blob(&n, &blob, nota); + *tmp = JS_NewArrayBufferCopy(js, blob, n); + free(blob); break; case NOTA_TEXT: nota = nota_read_text(&str, nota); @@ -46,9 +52,17 @@ char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota) break; case NOTA_SYM: nota = nota_read_sym(&b, nota); - if (b == NOTA_NULL) *tmp = JS_UNDEFINED; - else - *tmp = JS_NewBool(js,b); + switch(b) { + case NOTA_NULL: + *tmp = JS_UNDEFINED; + break; + case NOTA_FALSE: + *tmp = JS_NewBool(js,0); + break; + case NOTA_TRUE: + *tmp = JS_NewBool(js,1); + break; + } break; default: case NOTA_FLOAT: @@ -67,27 +81,41 @@ char *js_do_nota_encode(JSContext *js, JSValue v, char *nota) const char *str = NULL; JSPropertyEnum *ptab; uint32_t plen; - int n; double nval; JSValue val; + void *blob; + size_t bloblen; switch(tag) { case JS_TAG_FLOAT64: case JS_TAG_INT: + case JS_TAG_BIG_DECIMAL: + case JS_TAG_BIG_INT: + case JS_TAG_BIG_FLOAT: JS_ToFloat64(js, &nval, v); return nota_write_number(nval, nota); +/* str = JS_ToCString(js,v); + nota = nota_write_decimal_str(str, nota); + JS_FreeCString(js,str); + return nota; +*/ case JS_TAG_STRING: str = JS_ToCString(js, v); nota = nota_write_text(str, nota); JS_FreeCString(js, str); return nota; case JS_TAG_BOOL: - return nota_write_sym(JS_VALUE_GET_BOOL(v), nota); + if (JS_VALUE_GET_BOOL(v)) return nota_write_sym(NOTA_TRUE, nota); + else + return nota_write_sym(NOTA_FALSE, nota); case JS_TAG_UNDEFINED: - return nota_write_sym(NOTA_NULL, nota); case JS_TAG_NULL: return nota_write_sym(NOTA_NULL, nota); case JS_TAG_OBJECT: + blob = JS_GetArrayBuffer(js,&bloblen, v); + if (blob) + return nota_write_blob(bloblen*8, blob, nota); + if (JS_IsArray(js, v)) { int n; JS_ToInt32(js, &n, JS_GetPropertyStr(js, v, "length")); @@ -96,7 +124,7 @@ char *js_do_nota_encode(JSContext *js, JSValue v, char *nota) nota = js_do_nota_encode(js, JS_GetPropertyUint32(js, v, i), nota); return nota; } - n = JS_GetOwnPropertyNames(js, &ptab, &plen, v, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK); + JS_GetOwnPropertyNames(js, &ptab, &plen, v, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK); nota = nota_write_record(plen, nota); for (int i = 0; i < plen; i++) { @@ -140,18 +168,9 @@ JSValue js_nota_decode(JSContext *js, JSValue self, int argc, JSValue *argv) return ret; } -JSValue js_nota_hex(JSContext *js, JSValue self, int argc, JSValue *argv) -{ - size_t len; - unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]); - print_nota_hex(nota); - return JS_UNDEFINED; -} - static const JSCFunctionListEntry js_nota_funcs[] = { JS_CFUNC_DEF("encode", 1, js_nota_encode), JS_CFUNC_DEF("decode", 1, js_nota_decode), - JS_CFUNC_DEF("hex", 1, js_nota_hex), }; static int js_nota_init(JSContext *ctx, JSModuleDef *m) { @@ -163,6 +182,7 @@ JSValue js_nota_use(JSContext *js) { JSValue export = JS_NewObject(js); JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); + number = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "Number"); return export; } diff --git a/source/script.c b/source/script.c index bab86fe1..4399f665 100644 --- a/source/script.c +++ b/source/script.c @@ -157,6 +157,7 @@ void script_startup(int argc, char **argv) { JS_AddIntrinsicBigFloat(js); JS_AddIntrinsicBigDecimal(js); JS_AddIntrinsicOperators(js); + JS_EnableBignumExt(js, 1); on_exception = JS_UNDEFINED; diff --git a/tests/nota.js b/tests/nota.js index b0cd68cb..a403995e 100644 --- a/tests/nota.js +++ b/tests/nota.js @@ -5,7 +5,7 @@ var os = use('os'); function hexToBuffer(hex) { let bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { - bytes[i/2] = parseInt(hex.substr(i, 2), 16); + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); } return bytes.buffer; } @@ -18,46 +18,161 @@ function bufferToHex(buffer) { .toLowerCase(); } -// Test cases +var EPSILON = 1e-12 + +// Deep comparison function for objects and arrays +function deepCompare(expected, actual, path = '') { + if (expected === actual) return { passed: true, messages: [] }; + + // If both are numbers, compare with tolerance + if (typeof expected === 'number' && typeof actual === 'number') { + // e.g. handle NaN specially if you like: + if (isNaN(expected) && isNaN(actual)) { + return { passed: true, messages: [] }; + } + + const diff = Math.abs(expected - actual); + // Pass the test if difference is within EPSILON + if (diff <= EPSILON) { + return { passed: true, messages: [] }; + } + + return { + passed: false, + messages: [ + `Value mismatch at ${path}: expected ${expected}, got ${actual}`, + `Difference of ${diff} is larger than tolerance ${EPSILON}` + ] + }; + } + + if (expected instanceof ArrayBuffer && actual instanceof ArrayBuffer) { + const expArray = Array.from(new Uint8Array(expected)); + const actArray = Array.from(new Uint8Array(actual)); + return deepCompare(expArray, actArray, path); + } + + if (actual instanceof ArrayBuffer) { + actual = Array.from(new Uint8Array(actual)); + } + + if (Array.isArray(expected) && Array.isArray(actual)) { + if (expected.length !== actual.length) { + return { + passed: false, + messages: [`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`] + }; + } + let messages = []; + for (let i = 0; i < expected.length; i++) { + const result = deepCompare(expected[i], actual[i], `${path}[${i}]`); + if (!result.passed) messages.push(...result.messages); + } + return { passed: messages.length === 0, messages }; + } + + if (typeof expected === 'object' && expected !== null && + typeof actual === 'object' && actual !== null) { + const expKeys = Object.keys(expected).sort(); + const actKeys = Object.keys(actual).sort(); + if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) { + return { + passed: false, + messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`] + }; + } + let messages = []; + for (let key of expKeys) { + const result = deepCompare(expected[key], actual[key], `${path}.${key}`); + if (!result.passed) messages.push(...result.messages); + } + return { passed: messages.length === 0, messages }; + } + + return { + passed: false, + messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`] + }; +} + +// Extended test cases covering all Nota types from documentation var testCases = [ + // Integer tests { input: 0, expectedHex: "60" }, { input: 2023, expectedHex: "e08f67" }, { input: -1, expectedHex: "69" }, - { input: true, expectedHex: "71" }, + { input: 7, expectedHex: "67" }, + { input: -7, expectedHex: "6f" }, + { input: 1023, expectedHex: "e07f" }, + { input: -1023, expectedHex: "ef7f" }, + + // Symbol tests + { input: undefined, expectedHex: "70" }, { input: false, expectedHex: "72" }, - { input: null, expectedHex: "73" }, + { input: true, expectedHex: "73" }, + // Note: private (78) and system (79) require following records, tested below + + // Floating Point tests { input: -1.01, expectedHex: "5a65" }, { input: 98.6, expectedHex: "51875a" }, + { input: -0.5772156649, expectedHex: "d80a95c0b0bd69" }, + { input: -1.00000000000001, expectedHex: "d80e96deb183e98001" }, + { input: -10000000000000, expectedHex: "c80d01" }, + + // Text tests { input: "", expectedHex: "10" }, { input: "cat", expectedHex: "13636174" }, + { input: "U+1F4A9 「うんち絵文字」 «💩»", + expectedHex: "9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b" }, + + // Blob tests + { input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" }, + { input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer, + expectedHex: "8019f0e32080" }, // 25 bits example padded to 32 bits + + // Array tests { input: [], expectedHex: "20" }, { input: [1, 2, 3], expectedHex: "23616263" }, + { input: [-1, 0, 1.5], expectedHex: "2369605043" }, + + // Record tests { input: {}, expectedHex: "30" }, - { input: {a: 1, b: 2}, expectedHex: "32116161116262" }, - { input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "020280ffaa" }, + { input: { a: 1, b: 2 }, expectedHex: "32116161116262" }, + + // Complex nested structures { input: { num: 42, arr: [1, -1, 2.5], str: "test", - obj: {x: true} - }, - expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" - } + obj: { x: true } + }, + expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" }, + + // Private prefix test (requires record) + { input: { private: { address: "test" } }, + expectedHex: "317821616464726573731474657374" }, + + // System prefix test (requires record) + { input: { system: { msg: "hello" } }, + expectedHex: "3179216d73671568656c6c6f" }, + + // Additional edge cases + { input: new Uint8Array([]).buffer, expectedHex: "00" }, // Empty blob + { input: [[]], expectedHex: "2120" }, // Nested empty array + { input: { "": "" }, expectedHex: "311010" }, // Empty string key and value + { input: 1e-10, expectedHex: "d00a01" }, // Small floating point ]; // Run tests and collect results -let testCount = 0; -let failedCount = 0; let results = []; +let testCount = 0; for (let test of testCases) { testCount++; let testName = `Test ${testCount}: ${JSON.stringify(test.input)}`; let passed = true; let messages = []; - - console.log(`Running ${testName}`); - + // Test encoding let encoded = nota.encode(test.input); if (!(encoded instanceof ArrayBuffer)) { @@ -66,61 +181,66 @@ for (let test of testCases) { } else { let encodedHex = bufferToHex(encoded); if (encodedHex !== test.expectedHex.toLowerCase()) { - passed = false; messages.push( - `Encoding failed\n` + - `Expected: ${test.expectedHex}\n` + - `Got: ${encodedHex}` + `Hex encoding differs (informational): + Expected: ${test.expectedHex} + Got: ${encodedHex}` ); } - + // Test decoding let decoded = nota.decode(encoded); - let inputStr = JSON.stringify(test.input instanceof ArrayBuffer ? - Array.from(new Uint8Array(test.input)) : test.input); - let decodedStr = JSON.stringify(decoded); - - if (inputStr !== decodedStr) { + let expected = test.input; + + // Normalize ArrayBuffer and special cases for comparison + if (expected instanceof ArrayBuffer) { + expected = Array.from(new Uint8Array(expected)); + } + if (decoded instanceof ArrayBuffer) { + decoded = Array.from(new Uint8Array(decoded)); + } + // Handle private/system prefix objects + if (expected && (expected.private || expected.system)) { + const key = expected.private ? 'private' : 'system'; + expected = { [key]: expected[key] }; + } + + const compareResult = deepCompare(expected, decoded); + if (!compareResult.passed) { passed = false; - messages.push( - `Decoding failed\n` + - `Expected: ${inputStr}\n` + - `Got: ${decodedStr}` - ); + messages.push("Decoding failed:"); + messages.push(...compareResult.messages); } } - + // Record result - let status = passed ? "PASSED" : "FAILED"; - results.push({ testName, status, messages }); - if (!passed) failedCount++; - - // Print immediate feedback - console.log(`${testName} - ${status}`); + results.push({ testName, passed, messages }); + + // Print detailed results on first failure if (!passed) { + console.log(`\nDetailed Failure Report for ${testName}:`); + console.log(`Input: ${JSON.stringify(test.input)}`); console.log(messages.join("\n")); + console.log(""); } - console.log(""); // Empty line between tests } // Summary -console.log("Test Summary:"); -console.log(`Total tests: ${testCount}`); -console.log(`Passed: ${testCount - failedCount}`); -console.log(`Failed: ${failedCount}`); -console.log("\nDetailed Results:"); +console.log("\nTest Summary:"); results.forEach(result => { - console.log(`${result.testName} - ${result.status}`); - if (result.messages.length > 0) { - console.log(result.messages.join("\n")); - console.log(""); - } + console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`); + if (!result.passed) + console.log(result.messages) }); -if (failedCount > 0) { - console.log("Overall result: FAILED"); +let passedCount = results.filter(r => r.passed).length; +console.log(`\nResult: ${passedCount}/${testCount} tests passed`); + +if (passedCount < testCount) { + console.log("Overall: FAILED"); os.exit(1); } else { - console.log("Overall result: PASSED"); + console.log("Overall: PASSED"); os.exit(0); -} \ No newline at end of file +} +