add wota
This commit is contained in:
106
benchmarks/wota.js
Normal file
106
benchmarks/wota.js
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// wota_benchmark.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs wota_benchmark.js
|
||||
//
|
||||
// Prerequisite:
|
||||
var wota = use('wota');
|
||||
var os = use('os');
|
||||
// or otherwise ensure `wota` and `os` are available.
|
||||
// Make sure wota_benchmark.js is loaded after wota.js or combined with it.
|
||||
//
|
||||
|
||||
// Helper to run a function repeatedly and measure total time in seconds.
|
||||
// Returns elapsed time in seconds.
|
||||
function measureTime(fn, iterations) {
|
||||
let t1 = os.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
fn();
|
||||
}
|
||||
let t2 = os.now();
|
||||
return t2 - t1;
|
||||
}
|
||||
|
||||
// We'll define a function that does `encode -> decode` for a given value:
|
||||
function roundTripWota(value) {
|
||||
let encoded = wota.encode(value);
|
||||
let decoded = wota.decode(encoded);
|
||||
// Not doing a deep compare here, just measuring performance.
|
||||
// (We trust the test suite to verify correctness.)
|
||||
}
|
||||
|
||||
// A small suite of data we want to benchmark. Each entry includes:
|
||||
// name: label for printing
|
||||
// data: the test value(s) to encode/decode
|
||||
// iterations: how many times to loop
|
||||
//
|
||||
// You can tweak these as you like for heavier or lighter tests.
|
||||
const benchmarks = [
|
||||
{
|
||||
name: "Small Integers",
|
||||
data: [0, 42, -1, 2023],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Strings (short, emoji)",
|
||||
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Small Objects",
|
||||
data: [
|
||||
{ a:1, b:2.2, c:"3", d:false },
|
||||
{ x:42, y:null, z:"test" }
|
||||
],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Nested Arrays",
|
||||
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k numbers)",
|
||||
// A thousand random numbers
|
||||
data: [ Array.from({length:1000}, (_, i) => i * 0.5) ],
|
||||
iterations: 1000
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
// A 256KB ArrayBuffer
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
// Print a header
|
||||
console.log("Wota Encode/Decode Benchmark");
|
||||
console.log("============================\n");
|
||||
|
||||
// We'll run each benchmark scenario in turn.
|
||||
for (let bench of benchmarks) {
|
||||
// We'll measure how long it takes to do 'iterations' *for each test value*
|
||||
// in bench.data. The total loop count is `bench.iterations * bench.data.length`.
|
||||
// Then we compute an overall encode+decode throughput (ops/s).
|
||||
let totalIterations = bench.iterations * bench.data.length;
|
||||
|
||||
// We'll define a function that does a roundTrip for *each* data item in bench.data
|
||||
// to measure in one loop iteration. Then we multiply by bench.iterations.
|
||||
function runAllData() {
|
||||
for (let val of bench.data) {
|
||||
roundTripWota(val);
|
||||
}
|
||||
}
|
||||
|
||||
let elapsedSec = measureTime(runAllData, bench.iterations);
|
||||
let opsPerSec = (totalIterations / elapsedSec).toFixed(1);
|
||||
|
||||
console.log(`${bench.name}:`);
|
||||
console.log(` Iterations: ${bench.iterations} × ${bench.data.length} data items = ${totalIterations}`);
|
||||
console.log(` Elapsed: ${elapsedSec.toFixed(3)} s`);
|
||||
console.log(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
|
||||
}
|
||||
|
||||
// All done
|
||||
console.log("Benchmark completed.\n");
|
||||
182
benchmarks/wota_nota_json.js
Normal file
182
benchmarks/wota_nota_json.js
Normal file
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// benchmark_wota_nota_json.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs benchmark_wota_nota_json.js
|
||||
//
|
||||
// Ensure wota, nota, json, and os are all available, e.g.:
|
||||
var wota = use('wota');
|
||||
var nota = use('nota');
|
||||
var json = use('json');
|
||||
var os = use('os');
|
||||
//
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 1. Setup "libraries" array to easily switch among Wota, Nota, and JSON
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const libraries = [
|
||||
{
|
||||
name: "Wota",
|
||||
encode: wota.encode,
|
||||
decode: wota.decode,
|
||||
// Wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
|
||||
getSize(encoded) {
|
||||
return encoded.byteLength;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Nota",
|
||||
encode: nota.encode,
|
||||
decode: nota.decode,
|
||||
// Nota also produces an ArrayBuffer:
|
||||
getSize(encoded) {
|
||||
return encoded.byteLength;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
encode: json.encode,
|
||||
decode: json.decode,
|
||||
// JSON produces a JS string. We'll measure its UTF-16 code unit length
|
||||
// as a rough "size". Alternatively, you could convert to UTF-8 for
|
||||
// a more accurate byte size. Here we just use `string.length`.
|
||||
getSize(encodedStr) {
|
||||
return encodedStr.length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 2. Test data sets (similar to wota benchmarks).
|
||||
// Each scenario has { name, data, iterations }
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const benchmarks = [
|
||||
{
|
||||
name: "Small Integers",
|
||||
data: [0, 42, -1, 2023],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Floating point",
|
||||
data: [0.1, 1e-50, 3.14159265359],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Strings (short, emoji)",
|
||||
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Small Objects",
|
||||
data: [
|
||||
{ a:1, b:2.2, c:"3", d:false },
|
||||
{ x:42, y:null, z:"test" }
|
||||
],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Nested Arrays",
|
||||
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k integers)",
|
||||
data: [ Array.from({length:1000}, (_, i) => i) ],
|
||||
iterations: 1000
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 3. Utility: measureTime(fn) => how long fn() takes in seconds.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function measureTime(fn) {
|
||||
let start = os.now();
|
||||
fn();
|
||||
let end = os.now();
|
||||
return (end - start); // in seconds
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 4. For each library, we run each benchmark scenario and measure:
|
||||
// - Encoding time (seconds)
|
||||
// - Decoding time (seconds)
|
||||
// - Total encoded size (bytes or code units for JSON)
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function runBenchmarkForLibrary(lib, bench) {
|
||||
// We'll encode and decode each item in `bench.data`.
|
||||
// We do 'bench.iterations' times. Then sum up total time.
|
||||
|
||||
// Pre-store the encoded results for all items so we can measure decode time
|
||||
// in a separate pass. Also measure total size once.
|
||||
let encodedList = [];
|
||||
let totalSize = 0;
|
||||
|
||||
// 1) Measure ENCODING
|
||||
let encodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// For each data item, encode it
|
||||
for (let d of bench.data) {
|
||||
let e = lib.encode(d);
|
||||
// store only in the very first iteration, so we can decode them later
|
||||
// but do not store them every iteration or we blow up memory.
|
||||
if (i === 0) {
|
||||
encodedList.push(e);
|
||||
totalSize += lib.getSize(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Measure DECODING
|
||||
let decodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// decode everything we stored during the first iteration
|
||||
for (let e of encodedList) {
|
||||
let decoded = lib.decode(e);
|
||||
// not verifying correctness here, just measuring speed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { encodeTime, decodeTime, totalSize };
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 5. Main driver: run across all benchmarks, for each library.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
console.log("Benchmark: Wota vs Nota vs JSON");
|
||||
console.log("================================\n");
|
||||
|
||||
for (let bench of benchmarks) {
|
||||
console.log(`SCENARIO: ${bench.name}`);
|
||||
console.log(` Data length: ${bench.data.length} | Iterations: ${bench.iterations}\n`);
|
||||
|
||||
for (let lib of libraries) {
|
||||
let { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
|
||||
|
||||
// We'll compute total operations = bench.iterations * bench.data.length
|
||||
let totalOps = bench.iterations * bench.data.length;
|
||||
let encOpsPerSec = (totalOps / encodeTime).toFixed(1);
|
||||
let decOpsPerSec = (totalOps / decodeTime).toFixed(1);
|
||||
|
||||
console.log(` ${lib.name}:`);
|
||||
console.log(` Encode time: ${encodeTime.toFixed(3)}s => ${encOpsPerSec} encodes/sec`);
|
||||
console.log(` Decode time: ${decodeTime.toFixed(3)}s => ${decOpsPerSec} decodes/sec`);
|
||||
console.log(` Total size: ${totalSize} bytes (or code units for JSON)`);
|
||||
console.log("");
|
||||
}
|
||||
console.log("---------------------------------------------------------\n");
|
||||
}
|
||||
|
||||
console.log("Benchmark complete.\n");
|
||||
@@ -140,7 +140,7 @@ deps += dependency('soloud', static:true)
|
||||
deps += dependency('libqrencode', static: false)
|
||||
|
||||
sources = []
|
||||
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_qr.c']
|
||||
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_qr.c', 'qjs_wota.c']
|
||||
|
||||
# quirc src
|
||||
src += ['thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c','thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c']
|
||||
@@ -236,7 +236,8 @@ tests = [
|
||||
'spawn_actor',
|
||||
'empty',
|
||||
'nota',
|
||||
'enet'
|
||||
'enet',
|
||||
'wota'
|
||||
]
|
||||
|
||||
foreach file : tests
|
||||
|
||||
@@ -7583,6 +7583,7 @@ JSValue js_imgui_use(JSContext *js);
|
||||
|
||||
#include "qjs_dmon.h"
|
||||
#include "qjs_nota.h"
|
||||
#include "qjs_wota.h"
|
||||
#include "qjs_enet.h"
|
||||
#include "qjs_soloud.h"
|
||||
#include "qjs_qr.h"
|
||||
@@ -7612,6 +7613,7 @@ void ffi_load(JSContext *js, int argc, char **argv) {
|
||||
arrput(module_registry, MISTLINE(nota));
|
||||
arrput(module_registry, MISTLINE(enet));
|
||||
arrput(module_registry, MISTLINE(qr));
|
||||
arrput(module_registry, MISTLINE(wota));
|
||||
|
||||
#ifdef TRACY_ENABLE
|
||||
arrput(module_registry, MISTLINE(tracy));
|
||||
|
||||
393
source/qjs_wota.c
Normal file
393
source/qjs_wota.c
Normal file
@@ -0,0 +1,393 @@
|
||||
//
|
||||
// qjs_wota.c
|
||||
//
|
||||
#include "quickjs.h"
|
||||
|
||||
/* We define WOTA_IMPLEMENTATION so wota.h includes its implementation. */
|
||||
#define WOTA_IMPLEMENTATION
|
||||
#include "wota.h"
|
||||
|
||||
typedef struct WotaEncodeContext {
|
||||
JSContext *ctx;
|
||||
JSValue visitedStack; /* array for cycle detection */
|
||||
WotaBuffer wb; /* dynamic buffer for building Wota data */
|
||||
int cycle;
|
||||
} WotaEncodeContext;
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
CYCLE DETECTION (to avoid infinite recursion on circular objects)
|
||||
---------------------------------------------------------------- */
|
||||
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
|
||||
}
|
||||
|
||||
static void wota_stack_pop(WotaEncodeContext *enc)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
|
||||
}
|
||||
|
||||
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
for (int i = 0; i < len; i++) {
|
||||
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
|
||||
if (JS_IsObject(elem) && JS_IsObject(val)) {
|
||||
if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) {
|
||||
JS_FreeValue(ctx, elem);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
JS_FreeValue(ctx, elem);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Forward declaration */
|
||||
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Encode object properties => Wota record
|
||||
---------------------------------------------------------------- */
|
||||
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
|
||||
JSPropertyEnum *ptab;
|
||||
uint32_t plen;
|
||||
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val,
|
||||
JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
|
||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
wota_write_record(&enc->wb, plen);
|
||||
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
/* property name => TEXT */
|
||||
const char *propName = JS_AtomToCString(ctx, ptab[i].atom);
|
||||
wota_write_text(&enc->wb, propName);
|
||||
JS_FreeCString(ctx, propName);
|
||||
|
||||
/* property value => wota_encode_value */
|
||||
JSValue propVal = JS_GetProperty(ctx, val, ptab[i].atom);
|
||||
wota_encode_value(enc, propVal);
|
||||
JS_FreeValue(ctx, propVal);
|
||||
|
||||
JS_FreeAtom(ctx, ptab[i].atom);
|
||||
}
|
||||
js_free(ctx, ptab);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Main dispatcher for any JS value => Wota
|
||||
---------------------------------------------------------------- */
|
||||
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int tag = JS_VALUE_GET_TAG(val);
|
||||
|
||||
switch(tag) {
|
||||
case JS_TAG_INT:
|
||||
{
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, val);
|
||||
wota_write_number(&enc->wb, d);
|
||||
return;
|
||||
}
|
||||
case JS_TAG_FLOAT64:
|
||||
case JS_TAG_BIG_INT:
|
||||
case JS_TAG_BIG_DECIMAL:
|
||||
case JS_TAG_BIG_FLOAT:
|
||||
{
|
||||
/* Convert to double if possible. If it's out of double range,
|
||||
you might need a fallback. QuickJS can handle bigint, etc.
|
||||
But let's just do double. */
|
||||
double d;
|
||||
if (JS_ToFloat64(ctx, &d, val) < 0) {
|
||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||
return;
|
||||
}
|
||||
wota_write_number(&enc->wb, d);
|
||||
return;
|
||||
}
|
||||
case JS_TAG_STRING:
|
||||
{
|
||||
const char *str = JS_ToCString(ctx, val);
|
||||
if (!str) {
|
||||
wota_write_text(&enc->wb, "");
|
||||
return;
|
||||
}
|
||||
wota_write_text(&enc->wb, str);
|
||||
JS_FreeCString(ctx, str);
|
||||
return;
|
||||
}
|
||||
case JS_TAG_BOOL:
|
||||
{
|
||||
if (JS_VALUE_GET_BOOL(val)) {
|
||||
wota_write_sym(&enc->wb, WOTA_TRUE);
|
||||
} else {
|
||||
wota_write_sym(&enc->wb, WOTA_FALSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case JS_TAG_NULL:
|
||||
case JS_TAG_UNDEFINED:
|
||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||
return;
|
||||
|
||||
case JS_TAG_OBJECT:
|
||||
{
|
||||
/* Check if it's an ArrayBuffer => blob */
|
||||
if (JS_IsArrayBuffer(ctx, val)) {
|
||||
size_t bufLen;
|
||||
void *bufData = JS_GetArrayBuffer(ctx, &bufLen, val);
|
||||
wota_write_blob(&enc->wb, (unsigned long long)bufLen * 8,
|
||||
(const char *)bufData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (JS_IsArray(ctx, val)) {
|
||||
if (wota_stack_has(enc, val)) {
|
||||
enc->cycle = 1;
|
||||
return;
|
||||
}
|
||||
wota_stack_push(enc, val);
|
||||
|
||||
int arrLen = JS_ArrayLength(ctx, val);
|
||||
wota_write_array(&enc->wb, arrLen);
|
||||
for (int i = 0; i < arrLen; i++) {
|
||||
JSValue elemVal = JS_GetPropertyUint32(ctx, val, i);
|
||||
wota_encode_value(enc, elemVal);
|
||||
JS_FreeValue(ctx, elemVal);
|
||||
}
|
||||
wota_stack_pop(enc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Otherwise => record */
|
||||
if (wota_stack_has(enc, val)) {
|
||||
enc->cycle = 1;
|
||||
return;
|
||||
}
|
||||
wota_stack_push(enc, val);
|
||||
encode_object_properties(enc, val);
|
||||
wota_stack_pop(enc);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Public JS function: wota.encode(value) => ArrayBuffer
|
||||
---------------------------------------------------------------- */
|
||||
static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv)
|
||||
{
|
||||
if (argc < 1) {
|
||||
return JS_ThrowTypeError(ctx, "wota.encode requires 1 argument");
|
||||
}
|
||||
WotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx);
|
||||
enc->cycle = 0;
|
||||
|
||||
wota_buffer_init(&enc->wb, 16);
|
||||
|
||||
wota_encode_value(enc, argv[0]);
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
wota_buffer_free(&enc->wb);
|
||||
return JS_ThrowTypeError(ctx, "Cannot encode cyclic object with wota");
|
||||
}
|
||||
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
|
||||
/* Prepare the ArrayBuffer result */
|
||||
size_t word_count = enc->wb.size;
|
||||
size_t total_bytes = word_count * sizeof(uint64_t);
|
||||
uint8_t *raw = (uint8_t *)enc->wb.data;
|
||||
|
||||
JSValue ret = JS_NewArrayBufferCopy(ctx, raw, total_bytes);
|
||||
|
||||
wota_buffer_free(&enc->wb);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// A dedicated function that decodes one Wota value from data_ptr,
|
||||
// returns a JSValue, and advances data_ptr.
|
||||
// We return the updated pointer (like wota_read_* functions do).
|
||||
static char* decode_wota_value(JSContext *ctx, char *data_ptr, char *end_ptr, JSValue *outVal) {
|
||||
if ((end_ptr - data_ptr) < 8) {
|
||||
// Not enough data to read; just set undefined
|
||||
*outVal = JS_UNDEFINED;
|
||||
return data_ptr;
|
||||
}
|
||||
uint64_t first_word = *(uint64_t *)data_ptr;
|
||||
int type = (int)(first_word & 0xffU);
|
||||
|
||||
switch (type) {
|
||||
case WOTA_INT: {
|
||||
long long val;
|
||||
data_ptr = wota_read_int(&val, data_ptr);
|
||||
*outVal = JS_NewInt64(ctx, val);
|
||||
break;
|
||||
}
|
||||
case WOTA_FLOAT: {
|
||||
double d;
|
||||
data_ptr = wota_read_float(&d, data_ptr);
|
||||
*outVal = JS_NewFloat64(ctx, d);
|
||||
break;
|
||||
}
|
||||
case WOTA_SYM: {
|
||||
int scode;
|
||||
data_ptr = wota_read_sym(&scode, data_ptr);
|
||||
if (scode == WOTA_NULL) {
|
||||
*outVal = JS_UNDEFINED;
|
||||
} else if (scode == WOTA_FALSE) {
|
||||
*outVal = JS_NewBool(ctx, 0);
|
||||
} else if (scode == WOTA_TRUE) {
|
||||
*outVal = JS_NewBool(ctx, 1);
|
||||
} else {
|
||||
// other symbol codes => undefined or handle them
|
||||
*outVal = JS_UNDEFINED;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WOTA_BLOB: {
|
||||
long long blen;
|
||||
char *bdata = NULL;
|
||||
data_ptr = wota_read_blob(&blen, &bdata, data_ptr);
|
||||
if (bdata) {
|
||||
*outVal = JS_NewArrayBufferCopy(ctx, (uint8_t*)bdata, (size_t)blen);
|
||||
free(bdata);
|
||||
} else {
|
||||
*outVal = JS_NewArrayBufferCopy(ctx, NULL, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WOTA_TEXT: {
|
||||
char *utf8 = NULL;
|
||||
data_ptr = wota_read_text(&utf8, data_ptr);
|
||||
if (utf8) {
|
||||
*outVal = JS_NewString(ctx, utf8);
|
||||
free(utf8);
|
||||
} else {
|
||||
*outVal = JS_NewString(ctx, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WOTA_ARR: {
|
||||
// Recursively decode the array
|
||||
long long c;
|
||||
data_ptr = wota_read_array(&c, data_ptr);
|
||||
JSValue arr = JS_NewArray(ctx);
|
||||
for (long long i = 0; i < c; i++) {
|
||||
JSValue elemVal = JS_UNDEFINED;
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &elemVal);
|
||||
JS_SetPropertyUint32(ctx, arr, i, elemVal);
|
||||
}
|
||||
*outVal = arr;
|
||||
break;
|
||||
}
|
||||
case WOTA_REC: {
|
||||
// Recursively decode the record
|
||||
long long c;
|
||||
data_ptr = wota_read_record(&c, data_ptr);
|
||||
JSValue obj = JS_NewObject(ctx);
|
||||
for (long long i = 0; i < c; i++) {
|
||||
// read the key
|
||||
char *tkey = NULL;
|
||||
data_ptr = wota_read_text(&tkey, data_ptr);
|
||||
// read the value
|
||||
JSValue subVal = JS_UNDEFINED;
|
||||
data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &subVal);
|
||||
|
||||
if (tkey) {
|
||||
JS_SetPropertyStr(ctx, obj, tkey, subVal);
|
||||
free(tkey);
|
||||
} else {
|
||||
JS_FreeValue(ctx, subVal);
|
||||
}
|
||||
}
|
||||
*outVal = obj;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// unknown => skip
|
||||
data_ptr += 8;
|
||||
*outVal = JS_UNDEFINED;
|
||||
break;
|
||||
}
|
||||
|
||||
return data_ptr;
|
||||
}
|
||||
|
||||
static JSValue js_wota_decode(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv)
|
||||
{
|
||||
if (argc < 1) return JS_UNDEFINED;
|
||||
|
||||
size_t len;
|
||||
uint8_t *buf = JS_GetArrayBuffer(ctx, &len, argv[0]);
|
||||
if (!buf) {
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
char *data_ptr = (char *)buf;
|
||||
char *end_ptr = data_ptr + len;
|
||||
|
||||
JSValue result = JS_UNDEFINED;
|
||||
decode_wota_value(ctx, data_ptr, end_ptr, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Expose wota.encode / wota.decode to QuickJS
|
||||
---------------------------------------------------------------- */
|
||||
static const JSCFunctionListEntry js_wota_funcs[] = {
|
||||
JS_CFUNC_DEF("encode", 1, js_wota_encode),
|
||||
JS_CFUNC_DEF("decode", 1, js_wota_decode),
|
||||
};
|
||||
|
||||
/* For module usage */
|
||||
static int js_wota_init(JSContext *ctx, JSModuleDef *m)
|
||||
{
|
||||
JS_SetModuleExportList(ctx, m, js_wota_funcs,
|
||||
sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef JS_SHARED_LIBRARY
|
||||
#define JS_INIT_MODULE js_init_module
|
||||
#else
|
||||
#define JS_INIT_MODULE js_init_module_wota
|
||||
#endif
|
||||
|
||||
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
|
||||
{
|
||||
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_wota_init);
|
||||
if (!m) return NULL;
|
||||
JS_AddModuleExportList(ctx, m, js_wota_funcs,
|
||||
sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
||||
return m;
|
||||
}
|
||||
|
||||
/* An optional helper function if you're using it outside the module system */
|
||||
JSValue js_wota_use(JSContext *ctx)
|
||||
{
|
||||
JSValue exports = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, exports,
|
||||
js_wota_funcs,
|
||||
sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
||||
return exports;
|
||||
}
|
||||
8
source/qjs_wota.h
Normal file
8
source/qjs_wota.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef QJS_WOTA_H
|
||||
#define QJS_WOTA_H
|
||||
|
||||
#include <quickjs.h>
|
||||
|
||||
JSValue js_wota_use(JSContext*);
|
||||
|
||||
#endif
|
||||
727
source/wota.h
Normal file
727
source/wota.h
Normal file
@@ -0,0 +1,727 @@
|
||||
#ifndef WOTA_H
|
||||
#define WOTA_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Wota Type Codes (LSB of a 64-bit word)
|
||||
---------------------------------------------------------------- */
|
||||
#define WOTA_INT 0x00
|
||||
#define WOTA_FLOAT 0x01
|
||||
#define WOTA_ARR 0x02
|
||||
#define WOTA_REC 0x03
|
||||
#define WOTA_BLOB 0x04
|
||||
#define WOTA_TEXT 0x05
|
||||
#define WOTA_SYM 0x07
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Wota Symbol Codes (stored in top 56 bits)
|
||||
e.g. word = ((uint64_t)sym_code << 8) | WOTA_SYM
|
||||
---------------------------------------------------------------- */
|
||||
#define WOTA_NULL 0x00
|
||||
#define WOTA_FALSE 0x02
|
||||
#define WOTA_TRUE 0x03
|
||||
#define WOTA_PRIVATE 0x08
|
||||
#define WOTA_SYSTEM 0x09
|
||||
|
||||
/*
|
||||
We store all data in 64-bit words. The least significant byte
|
||||
is the type code. The top 56 bits are used differently depending
|
||||
on type.
|
||||
|
||||
This version (non-standard) stores floating-point values
|
||||
as *raw 64-bit IEEE 754 doubles* in a second 64-bit word.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Accessor: return the Wota type code from the LSB of a 64-bit word
|
||||
---------------------------------------------------------------- */
|
||||
static inline int wota_type(const uint64_t *w) {
|
||||
return (int)(*w & 0xffU);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Reading function prototypes. Each consumes some number of 64-bit
|
||||
words and returns a pointer to the next Wota data. If you pass
|
||||
a NULL for the output pointer, the function will skip the data.
|
||||
---------------------------------------------------------------- */
|
||||
char *wota_read_blob (long long *byte_len, char **blob, char *wota);
|
||||
char *wota_read_text (char **text_utf8, char *wota);
|
||||
char *wota_read_array (long long *count, char *wota);
|
||||
char *wota_read_record (long long *count, char *wota);
|
||||
char *wota_read_float (double *d, char *wota);
|
||||
char *wota_read_int (long long *n, char *wota);
|
||||
char *wota_read_sym (int *sym_code, char *wota);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
WotaBuffer: dynamic array of 64-bit words for building a Wota
|
||||
message in memory.
|
||||
---------------------------------------------------------------- */
|
||||
typedef struct WotaBuffer {
|
||||
uint64_t *data; /* allocated array of 64-bit words */
|
||||
size_t size; /* how many 64-bit words are used */
|
||||
size_t capacity; /* allocated capacity in 64-bit words */
|
||||
} WotaBuffer;
|
||||
|
||||
/* Buffer management */
|
||||
void wota_buffer_init(WotaBuffer *wb, size_t initial_capacity_in_words);
|
||||
void wota_buffer_free(WotaBuffer *wb);
|
||||
|
||||
/* Writing function prototypes */
|
||||
void wota_write_blob (WotaBuffer *wb, unsigned long long nbits, const char *data);
|
||||
void wota_write_text (WotaBuffer *wb, const char *utf8);
|
||||
void wota_write_array (WotaBuffer *wb, unsigned long long count);
|
||||
void wota_write_record (WotaBuffer *wb, unsigned long long count);
|
||||
/* We'll store numbers as either 56-bit integers or raw double */
|
||||
void wota_write_number (WotaBuffer *wb, double n);
|
||||
/* Symbol codes (WOTA_NULL, WOTA_FALSE, etc.) */
|
||||
void wota_write_sym (WotaBuffer *wb, int sym_code);
|
||||
|
||||
|
||||
#ifdef WOTA_IMPLEMENTATION
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <limits.h>
|
||||
|
||||
/* ================================================================
|
||||
Detect endianness. We'll use this to do 64-bit byte-swaps if needed.
|
||||
If you know you only run on little-endian, you can hard-code that.
|
||||
================================================================ */
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
#define WOTA_BIG_ENDIAN 1
|
||||
#elif defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
||||
#define WOTA_LITTLE_ENDIAN 1
|
||||
#elif defined(_MSC_VER)
|
||||
/* MSVC on x86/x64 is little-endian */
|
||||
#define WOTA_LITTLE_ENDIAN 1
|
||||
#else
|
||||
/* Fallback: assume little-endian if unknown. Adjust if your platform is otherwise. */
|
||||
#define WOTA_LITTLE_ENDIAN 1
|
||||
#endif
|
||||
|
||||
/* 64-bit byte-swap helper (for bit-level blob ordering) */
|
||||
static inline uint64_t wota_bswap64(uint64_t x)
|
||||
{
|
||||
/* Modern compilers often have a built-in. If not, do manually: */
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap64(x);
|
||||
#else
|
||||
/* Portable approach */
|
||||
x = ((x & 0x00000000FFFFFFFFULL) << 32) | ((x >> 32) & 0x00000000FFFFFFFFULL);
|
||||
x = ((x & 0x0000FFFF0000FFFFULL) << 16) | ((x >> 16) & 0x0000FFFF0000FFFFULL);
|
||||
x = ((x & 0x00FF00FF00FF00FFULL) << 8 ) | ((x >> 8 ) & 0x00FF00FF00FF00FFULL);
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
Helper: Grow the buffer to fit 'min_add' more 64-bit words
|
||||
================================================================ */
|
||||
static void wota_buffer_grow(WotaBuffer *wb, size_t min_add)
|
||||
{
|
||||
size_t needed = wb->size + min_add;
|
||||
if (needed <= wb->capacity) return;
|
||||
|
||||
size_t new_cap = (wb->capacity == 0 ? 8 : wb->capacity * 2);
|
||||
while (new_cap < needed) {
|
||||
new_cap *= 2;
|
||||
}
|
||||
uint64_t *new_data = (uint64_t *)realloc(wb->data, new_cap * sizeof(uint64_t));
|
||||
if (!new_data) {
|
||||
fprintf(stderr, "realloc failed in wota_buffer_grow\n");
|
||||
abort();
|
||||
}
|
||||
wb->data = new_data;
|
||||
wb->capacity = new_cap;
|
||||
}
|
||||
|
||||
void wota_buffer_init(WotaBuffer *wb, size_t initial_capacity_in_words)
|
||||
{
|
||||
wb->data = NULL;
|
||||
wb->size = 0;
|
||||
wb->capacity = 0;
|
||||
if (initial_capacity_in_words > 0) {
|
||||
wb->data = (uint64_t *)malloc(initial_capacity_in_words * sizeof(uint64_t));
|
||||
if (!wb->data) {
|
||||
fprintf(stderr, "malloc failed in wota_buffer_init\n");
|
||||
abort();
|
||||
}
|
||||
wb->capacity = initial_capacity_in_words;
|
||||
}
|
||||
}
|
||||
|
||||
void wota_buffer_free(WotaBuffer *wb)
|
||||
{
|
||||
if (wb->data) {
|
||||
free(wb->data);
|
||||
}
|
||||
wb->data = NULL;
|
||||
wb->size = 0;
|
||||
wb->capacity = 0;
|
||||
}
|
||||
|
||||
/* Alloc 'count' 64-bit words in the buffer, return pointer to them */
|
||||
static uint64_t *wota_buffer_alloc(WotaBuffer *wb, size_t count)
|
||||
{
|
||||
wota_buffer_grow(wb, count);
|
||||
uint64_t *p = wb->data + wb->size;
|
||||
wb->size += count;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
READING
|
||||
================================================================ */
|
||||
|
||||
/* We skip 1 word if we do not want to interpret it. */
|
||||
static inline char *wota_skip1(char *wota)
|
||||
{
|
||||
return wota + 8; /* skip one 64-bit word */
|
||||
}
|
||||
|
||||
char *wota_read_int(long long *n, char *wota)
|
||||
{
|
||||
/* WOTA_INT => single 64-bit word: top 56 bits is a signed integer, LSB=0. */
|
||||
if (!n) return wota_skip1(wota);
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_INT) {
|
||||
/* not an int; skip one word */
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
/* sign-extend top 56 bits into a 64-bit signed integer */
|
||||
int64_t val = (int64_t)first;
|
||||
val >>= 8; /* arithmetic shift right 8 bits to keep sign */
|
||||
*n = val;
|
||||
return wota + 8;
|
||||
}
|
||||
|
||||
/*
|
||||
We store a double as:
|
||||
- first 64-bit word => type code (LSB=1), top 56 bits unused
|
||||
- second 64-bit word => raw IEEE 754 bits
|
||||
*/
|
||||
char *wota_read_float(double *out, char *wota)
|
||||
{
|
||||
if (!out) return wota + 16; /* skip 2 words if no pointer */
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_FLOAT) {
|
||||
/* skip if not float */
|
||||
return wota + 8;
|
||||
}
|
||||
/* second word has the raw double bits */
|
||||
uint64_t bits = p[1];
|
||||
union {
|
||||
uint64_t u;
|
||||
double d;
|
||||
} converter;
|
||||
converter.u = bits;
|
||||
*out = converter.d;
|
||||
return (char *)(p + 2);
|
||||
}
|
||||
|
||||
char *wota_read_sym(int *sym_code, char *wota)
|
||||
{
|
||||
if (!sym_code) return wota_skip1(wota);
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_SYM) {
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
uint64_t top56 = (first >> 8); /* symbol code in top 56 bits */
|
||||
*sym_code = (int)top56;
|
||||
return wota + 8;
|
||||
}
|
||||
|
||||
char *wota_read_array(long long *count, char *wota)
|
||||
{
|
||||
if (!count) return wota_skip1(wota);
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_ARR) {
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
uint64_t c = (first >> 8);
|
||||
*count = (long long)c;
|
||||
return wota + 8;
|
||||
}
|
||||
|
||||
char *wota_read_record(long long *count, char *wota)
|
||||
{
|
||||
if (!count) return wota_skip1(wota);
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_REC) {
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
uint64_t c = (first >> 8);
|
||||
*count = (long long)c;
|
||||
return wota + 8;
|
||||
}
|
||||
|
||||
/*
|
||||
BLOB:
|
||||
preamble => top 56 bits = #bits, LSB=0x04
|
||||
then floor((nbits + 63)/64) 64-bit words of data
|
||||
The first bit is the MSB of the first data word.
|
||||
|
||||
Faster approach:
|
||||
- If nbits is a multiple of 8, do fast 8-byte copying (with endianness fix).
|
||||
- If partial bits remain, handle them with old bit-by-bit logic.
|
||||
*/
|
||||
char *wota_read_blob(long long *byte_len, char **blob, char *wota)
|
||||
{
|
||||
if (!byte_len || !blob) {
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_BLOB) {
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
|
||||
uint64_t nbits = (first >> 8);
|
||||
long long nwords = (long long)((nbits + 63ULL) >> 6); /* # of 64-bit blocks */
|
||||
|
||||
*byte_len = (long long)((nbits + 7ULL) >> 3);
|
||||
*blob = (char *)malloc((size_t)(*byte_len));
|
||||
if (!(*blob)) {
|
||||
fprintf(stderr, "malloc failed in wota_read_blob\n");
|
||||
abort();
|
||||
}
|
||||
memset(*blob, 0, (size_t)(*byte_len));
|
||||
|
||||
uint64_t *data_words = p + 1;
|
||||
long long bits_remaining = (long long)nbits;
|
||||
size_t byte_i = 0;
|
||||
int bit_in_byte = 0;
|
||||
|
||||
/* If nbits is multiple of 8, we can do a bulk copy in 64-bit chunks, then do leftover if any. */
|
||||
long long full_bytes = (long long)(nbits / 8ULL); /* how many full bytes total */
|
||||
long long leftover_bits = (long long)(nbits % 8ULL);
|
||||
|
||||
/* We'll process 8 bytes at a time from each 64-bit block in big-endian format. */
|
||||
long long full_64_chunks = full_bytes / 8; /* # of full 8-byte chunks we can copy. */
|
||||
long long remainder_bytes = full_bytes % 8; /* leftover bytes after those chunks. */
|
||||
|
||||
size_t chunk_index = 0;
|
||||
|
||||
/* Bulk 64-bit copy for each full 8-byte chunk. */
|
||||
for (long long i = 0; i < full_64_chunks; i++) {
|
||||
uint64_t block = data_words[i];
|
||||
/* If we are on a little-endian system, we must swap to get the "first bit in MSB" ordering. */
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
block = wota_bswap64(block);
|
||||
#endif
|
||||
/* Copy the 8 bytes from `block` into the output. */
|
||||
memcpy((*blob) + (i * 8), &block, 8);
|
||||
chunk_index = i + 1;
|
||||
}
|
||||
|
||||
/* Now we handle leftover bytes (0..7) in the next block, if any. */
|
||||
if (remainder_bytes > 0) {
|
||||
uint64_t block = data_words[chunk_index];
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
block = wota_bswap64(block);
|
||||
#endif
|
||||
memcpy((*blob) + (chunk_index * 8), &block, (size_t)remainder_bytes);
|
||||
/* The chunk_index used up one block if there's leftover bytes. */
|
||||
chunk_index++;
|
||||
}
|
||||
|
||||
/* If leftover_bits != 0, we still have some partial bits at the end to decode. */
|
||||
if (leftover_bits != 0) {
|
||||
/* We'll handle that partial chunk bit-by-bit (since we only have up to 7 bits). */
|
||||
/* The next block is data_words[chunk_index-1] if remainder_bytes > 0,
|
||||
or data_words[chunk_index] if remainder_bytes == 0, depending on how we count. */
|
||||
long long block_idx;
|
||||
if (remainder_bytes > 0) {
|
||||
/* We partially used data_words[chunk_index-1]. So let's re-read it bit by bit. */
|
||||
block_idx = (long long)(chunk_index - 1);
|
||||
} else {
|
||||
/* We haven't used data_words[chunk_index] yet. */
|
||||
block_idx = (long long)chunk_index;
|
||||
}
|
||||
|
||||
uint64_t partial_block = data_words[block_idx];
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
partial_block = wota_bswap64(partial_block);
|
||||
#endif
|
||||
/* We used up remainder_bytes * 8 bits from this partial_block if remainder_bytes>0. */
|
||||
int start_bit = 63 - (int)(remainder_bytes * 8);
|
||||
if (start_bit < 0) start_bit = 63; /* if we used the entire block, clamp it */
|
||||
|
||||
/* Now decode leftover_bits from partial_block, from MSB to LSB. */
|
||||
for (int b = start_bit; b >= 0 && leftover_bits > 0; b--) {
|
||||
int bitval = (int)((partial_block >> b) & 1ULL);
|
||||
(*blob)[full_bytes] |= (char)(bitval << bit_in_byte);
|
||||
bit_in_byte++;
|
||||
leftover_bits--;
|
||||
if (bit_in_byte == 8) {
|
||||
bit_in_byte = 0;
|
||||
full_bytes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If the total # of blocks was more than chunk_index, skip them if necessary. */
|
||||
return (char *)(data_words + nwords);
|
||||
}
|
||||
|
||||
/*
|
||||
TEXT:
|
||||
preamble => top 56 bits = #characters, LSB=0x05
|
||||
then floor((nchars+1)/2) 64-bit words
|
||||
each word has 2 UTF-32 codepoints: top 32 bits = codepoint1,
|
||||
low 32 bits = codepoint2
|
||||
*/
|
||||
char *wota_read_text(char **text_utf8, char *wota)
|
||||
{
|
||||
if (!text_utf8) return wota_skip1(wota);
|
||||
|
||||
uint64_t *p = (uint64_t *)wota;
|
||||
uint64_t first = p[0];
|
||||
int type = (int)(first & 0xffU);
|
||||
if (type != WOTA_TEXT) {
|
||||
return wota_skip1(wota);
|
||||
}
|
||||
|
||||
uint64_t nchars = (first >> 8);
|
||||
long long nwords = (long long)((nchars + 1ULL) >> 1);
|
||||
|
||||
uint64_t *data_words = p + 1;
|
||||
/*
|
||||
We'll convert them to a UTF-8 string. Each codepoint can
|
||||
become up to 4 bytes. So we need up to 4*nchars + 1.
|
||||
*/
|
||||
size_t max_utf8 = (size_t)(4 * nchars + 1);
|
||||
char *out = (char *)malloc(max_utf8);
|
||||
if (!out) {
|
||||
fprintf(stderr, "malloc failed in wota_read_text\n");
|
||||
abort();
|
||||
}
|
||||
size_t out_len = 0;
|
||||
|
||||
for (long long i = 0; i < nwords; i++) {
|
||||
uint64_t wval = data_words[i];
|
||||
uint32_t c1 = (uint32_t)(wval >> 32);
|
||||
uint32_t c2 = (uint32_t)(wval & 0xffffffffULL);
|
||||
|
||||
// If we haven't exceeded nchars, convert c1 -> UTF-8
|
||||
if ((i * 2) + 0 < (long long)nchars) {
|
||||
uint32_t c = c1;
|
||||
if (c < 0x80) {
|
||||
out[out_len++] = (char)c;
|
||||
} else if (c < 0x800) {
|
||||
out[out_len++] = (char)(0xC0 | (c >> 6));
|
||||
out[out_len++] = (char)(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
out[out_len++] = (char)(0xE0 | (c >> 12));
|
||||
out[out_len++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
out[out_len++] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
out[out_len++] = (char)(0xF0 | (c >> 18));
|
||||
out[out_len++] = (char)(0x80 | ((c >> 12) & 0x3F));
|
||||
out[out_len++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
out[out_len++] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
// Similarly for c2:
|
||||
if ((i * 2) + 1 < (long long)nchars) {
|
||||
uint32_t c = c2;
|
||||
if (c < 0x80) {
|
||||
out[out_len++] = (char)c;
|
||||
} else if (c < 0x800) {
|
||||
out[out_len++] = (char)(0xC0 | (c >> 6));
|
||||
out[out_len++] = (char)(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
out[out_len++] = (char)(0xE0 | (c >> 12));
|
||||
out[out_len++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
out[out_len++] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
out[out_len++] = (char)(0xF0 | (c >> 18));
|
||||
out[out_len++] = (char)(0x80 | ((c >> 12) & 0x3F));
|
||||
out[out_len++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
out[out_len++] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out[out_len] = '\0';
|
||||
*text_utf8 = out;
|
||||
|
||||
return (char *)(data_words + nwords);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
WRITING
|
||||
================================================================ */
|
||||
|
||||
/*
|
||||
Helper to see if double is integral and can fit in 56 bits signed.
|
||||
Range: -2^55 <= x <= 2^55 - 1
|
||||
*/
|
||||
static int fits_in_56_bits(long long x)
|
||||
{
|
||||
const long long min_val = -(1LL << 55);
|
||||
const long long max_val = (1LL << 55) - 1;
|
||||
return (x >= min_val && x <= max_val);
|
||||
}
|
||||
|
||||
/*
|
||||
Write a WOTA_INT (single 64-bit word):
|
||||
top 56 bits = signed integer (arithmetic shift), LSB=0x00
|
||||
*/
|
||||
static void wota_write_int_word(WotaBuffer *wb, long long val)
|
||||
{
|
||||
/* shift 'val' left by 8 bits into the top 56,
|
||||
then OR the type code in the bottom byte. */
|
||||
uint64_t u = (uint64_t)((int64_t)val) << 8;
|
||||
u |= WOTA_INT;
|
||||
uint64_t *p = wota_buffer_alloc(wb, 1);
|
||||
p[0] = u;
|
||||
}
|
||||
|
||||
/*
|
||||
Write a WOTA_FLOAT (2 words):
|
||||
first word => type=0x01 in LSB, top 56 bits=0
|
||||
second word => raw IEEE 754 double bits
|
||||
*/
|
||||
static void wota_write_float_word(WotaBuffer *wb, double val)
|
||||
{
|
||||
uint64_t *p = wota_buffer_alloc(wb, 2);
|
||||
p[0] = (uint64_t)WOTA_FLOAT; /* top 56 bits=0, LSB=0x01 */
|
||||
|
||||
union {
|
||||
double d;
|
||||
uint64_t u;
|
||||
} converter;
|
||||
converter.d = val;
|
||||
|
||||
p[1] = converter.u;
|
||||
}
|
||||
|
||||
void wota_write_sym(WotaBuffer *wb, int sym_code)
|
||||
{
|
||||
/* single word => top 56 bits = sym_code, LSB=0x07 */
|
||||
uint64_t w = ((uint64_t)(sym_code) << 8) | WOTA_SYM;
|
||||
uint64_t *p = wota_buffer_alloc(wb, 1);
|
||||
p[0] = w;
|
||||
}
|
||||
|
||||
/*
|
||||
BLOB:
|
||||
preamble word => top 56 bits= nbits, LSB=0x04
|
||||
then floor((nbits + 63)/64) 64-bit words
|
||||
If nbits is multiple of 8, we do a fast copy in 64-bit chunks
|
||||
(with a byte-swap if on little-endian) to place the first bit
|
||||
in the MSB of the first word.
|
||||
If partial bits remain, we handle them bit-by-bit at the end.
|
||||
*/
|
||||
void wota_write_blob(WotaBuffer *wb, unsigned long long nbits, const char *data)
|
||||
{
|
||||
/* preamble word => top 56 bits= nbits, LSB=0x04 */
|
||||
uint64_t preamble = ((uint64_t)nbits << 8) | (uint64_t)WOTA_BLOB;
|
||||
uint64_t *p = wota_buffer_alloc(wb, 1);
|
||||
p[0] = preamble;
|
||||
|
||||
unsigned long long nwords = (nbits + 63ULL) >> 6; /* # of 64-bit blocks */
|
||||
if (nwords == 0) {
|
||||
return; /* empty blob => done */
|
||||
}
|
||||
uint64_t *blocks = wota_buffer_alloc(wb, (size_t)nwords);
|
||||
memset(blocks, 0, (size_t)(nwords * sizeof(uint64_t)));
|
||||
|
||||
/* If exactly byte-aligned, do a fast copy first. */
|
||||
unsigned long long full_bytes = (nbits / 8ULL); /* total full bytes */
|
||||
unsigned long long leftover_bits = (nbits % 8ULL);/* leftover bits if not multiple of 8 */
|
||||
|
||||
size_t block_index = 0;
|
||||
unsigned long long num_full_64_chunks = full_bytes / 8ULL; /* how many full 8-byte chunks */
|
||||
unsigned long long remainder_bytes = full_bytes % 8ULL;
|
||||
|
||||
/* 1) Bulk copy each 8-byte chunk */
|
||||
for (unsigned long long i = 0; i < num_full_64_chunks; i++) {
|
||||
/* read 8 bytes from data, build a 64-bit. */
|
||||
uint64_t tmp = 0;
|
||||
memcpy(&tmp, data + (i * 8), 8);
|
||||
/* We must store it so that the first bit is in the MSB. On a little-endian CPU, that means bswap. */
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
tmp = wota_bswap64(tmp);
|
||||
#endif
|
||||
blocks[i] = tmp;
|
||||
block_index = (size_t)(i + 1);
|
||||
}
|
||||
|
||||
/* 2) If there's remainder_bytes in the next block, handle them. */
|
||||
if (remainder_bytes > 0) {
|
||||
uint64_t tmp = 0;
|
||||
memcpy(&tmp, data + (block_index * 8), (size_t)remainder_bytes);
|
||||
/* swap if needed */
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
tmp = wota_bswap64(tmp);
|
||||
#endif
|
||||
blocks[block_index] = tmp;
|
||||
block_index++;
|
||||
}
|
||||
|
||||
/* 3) If leftover_bits != 0, handle the final partial bits bit-by-bit. */
|
||||
if (leftover_bits != 0) {
|
||||
/* We have leftover_bits up to 7. We'll write them starting from the MSB. */
|
||||
/* We'll write them from data[full_bytes]. */
|
||||
/* The partial block is blocks[block_index - 1] if remainder_bytes>0, else blocks[block_index]. */
|
||||
size_t partial_idx;
|
||||
if (remainder_bytes > 0) {
|
||||
partial_idx = block_index - 1;
|
||||
} else {
|
||||
partial_idx = block_index;
|
||||
}
|
||||
|
||||
uint64_t outword = blocks[partial_idx];
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
/* We want to unify our approach: the block is currently in "MSB=first bit" form. Actually, let's do direct approach: re-swap? */
|
||||
/* For safety, let's swap back, set bits, then swap again. Another approach is to set bits from the top down. */
|
||||
outword = wota_bswap64(outword);
|
||||
#endif
|
||||
unsigned long long bits_used = remainder_bytes * 8ULL; /* how many bits we've used in this block so far if remainder_bytes>0 */
|
||||
int bitpos = 63 - (int)bits_used; /* start from MSB downwards */
|
||||
|
||||
for (unsigned long long b = 0; b < leftover_bits; b++) {
|
||||
int bitval = ( (unsigned char)data[full_bytes] >> b ) & 1;
|
||||
outword |= ((uint64_t)bitval << (bitpos));
|
||||
bitpos--;
|
||||
}
|
||||
|
||||
#if defined(WOTA_LITTLE_ENDIAN)
|
||||
outword = wota_bswap64(outword);
|
||||
#endif
|
||||
blocks[partial_idx] = outword;
|
||||
}
|
||||
}
|
||||
|
||||
void wota_write_text(WotaBuffer *wb, const char *utf8)
|
||||
{
|
||||
if (!utf8) utf8 = "";
|
||||
|
||||
/* Convert the utf8 string to an array of UTF-32 codepoints. */
|
||||
size_t len = strlen(utf8);
|
||||
const unsigned char *uc = (const unsigned char *)utf8;
|
||||
/* In worst case, every single byte might form a codepoint, so we allocate enough: */
|
||||
uint32_t *codepoints = (uint32_t *)malloc(sizeof(uint32_t)*(len+1));
|
||||
if (!codepoints) {
|
||||
fprintf(stderr, "malloc failed in wota_write_text\n");
|
||||
abort();
|
||||
}
|
||||
size_t ccount = 0;
|
||||
|
||||
while (*uc) {
|
||||
uint32_t c;
|
||||
if ((uc[0] & 0x80) == 0) {
|
||||
c = uc[0];
|
||||
uc += 1;
|
||||
} else if ((uc[0] & 0xe0) == 0xc0 && (uc[1] != 0)) {
|
||||
c = ((uc[0] & 0x1f) << 6) | (uc[1] & 0x3f);
|
||||
uc += 2;
|
||||
} else if ((uc[0] & 0xf0) == 0xe0 && (uc[1] != 0) && (uc[2] != 0)) {
|
||||
c = ((uc[0] & 0x0f) << 12) | ((uc[1] & 0x3f) << 6) | (uc[2] & 0x3f);
|
||||
uc += 3;
|
||||
} else if ((uc[0] & 0xf8) == 0xf0 && (uc[1] != 0) && (uc[2] != 0) && (uc[3] != 0)) {
|
||||
c = ((uc[0] & 0x07) << 18) | ((uc[1] & 0x3f) << 12)
|
||||
| ((uc[2] & 0x3f) << 6) | (uc[3] & 0x3f);
|
||||
uc += 4;
|
||||
} else {
|
||||
/* invalid sequence => skip 1 byte */
|
||||
c = uc[0];
|
||||
uc++;
|
||||
}
|
||||
codepoints[ccount++] = c;
|
||||
}
|
||||
|
||||
/* preamble => top 56 bits = ccount, LSB=0x05 */
|
||||
uint64_t preamble = ((uint64_t)ccount << 8) | (uint64_t)WOTA_TEXT;
|
||||
uint64_t *pw = wota_buffer_alloc(wb, 1);
|
||||
pw[0] = preamble;
|
||||
|
||||
/* store pairs of 32-bit codepoints in 64-bit words */
|
||||
size_t nwords = (ccount + 1) / 2;
|
||||
if (nwords == 0) {
|
||||
free(codepoints);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t *blocks = wota_buffer_alloc(wb, nwords);
|
||||
size_t idx = 0;
|
||||
for (size_t i = 0; i < nwords; i++) {
|
||||
uint64_t hi = 0, lo = 0;
|
||||
if (idx < ccount) {
|
||||
hi = codepoints[idx++];
|
||||
}
|
||||
if (idx < ccount) {
|
||||
lo = codepoints[idx++];
|
||||
}
|
||||
blocks[i] = ((hi & 0xffffffffULL) << 32) | (lo & 0xffffffffULL);
|
||||
}
|
||||
|
||||
free(codepoints);
|
||||
}
|
||||
|
||||
void wota_write_array(WotaBuffer *wb, unsigned long long count)
|
||||
{
|
||||
/* single 64-bit word => top 56 bits = count, LSB=0x02 */
|
||||
uint64_t w = ((uint64_t)count << 8) | (uint64_t)WOTA_ARR;
|
||||
uint64_t *p = wota_buffer_alloc(wb, 1);
|
||||
p[0] = w;
|
||||
}
|
||||
|
||||
void wota_write_record(WotaBuffer *wb, unsigned long long count)
|
||||
{
|
||||
/* single 64-bit word => top 56 bits = count, LSB=0x03 */
|
||||
uint64_t w = ((uint64_t)count << 8) | (uint64_t)WOTA_REC;
|
||||
uint64_t *p = wota_buffer_alloc(wb, 1);
|
||||
p[0] = w;
|
||||
}
|
||||
|
||||
/*
|
||||
wota_write_number:
|
||||
If n is an integer (within 2^53 range) you might store as int,
|
||||
or specifically check if it fits in 56 bits. If it does, store
|
||||
as WOTA_INT. Otherwise store as WOTA_FLOAT (raw double).
|
||||
*/
|
||||
void wota_write_number(WotaBuffer *wb, double n)
|
||||
{
|
||||
/* Is it integral within 2^53? Quick check: */
|
||||
double ip;
|
||||
double frac = modf(n, &ip);
|
||||
if (frac == 0.0) {
|
||||
/* candidate integer */
|
||||
long long i = (long long)ip;
|
||||
if ((double)i == ip && fits_in_56_bits(i)) {
|
||||
/* store as a 56-bit integer */
|
||||
wota_write_int_word(wb, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* fallback: store as double */
|
||||
wota_write_float_word(wb, n);
|
||||
}
|
||||
|
||||
#endif /* WOTA_IMPLEMENTATION */
|
||||
|
||||
#endif /* WOTA_H */
|
||||
226
tests/wota.js
Normal file
226
tests/wota.js
Normal file
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// wota.js
|
||||
//
|
||||
// A test script that exercises wota.encode() / wota.decode() in QuickJS.
|
||||
//
|
||||
|
||||
// Load the Wota module. If you compiled qjs_wota.c as a module named "wota.so",
|
||||
// then in QuickJS you might do:
|
||||
var wota = use('wota');
|
||||
var os = use('os');
|
||||
|
||||
// A small epsilon for floating comparisons
|
||||
var EPSILON = 1e-12;
|
||||
|
||||
/* Deep comparison function (for JS values).
|
||||
Compares numbers within EPSILON, compares arrays and objects recursively, etc. */
|
||||
function deepCompare(expected, actual, path = '') {
|
||||
// Shortcut: if strictly equal, fine
|
||||
if (expected === actual) {
|
||||
return { passed: true, messages: [] };
|
||||
}
|
||||
|
||||
// Compare numbers with tolerance
|
||||
if (typeof expected === 'number' && typeof actual === 'number') {
|
||||
// Handle NaN:
|
||||
if (isNaN(expected) && isNaN(actual)) {
|
||||
return { passed: true, messages: [] };
|
||||
}
|
||||
const diff = Math.abs(expected - actual);
|
||||
if (diff <= EPSILON) {
|
||||
return { passed: true, messages: [] };
|
||||
}
|
||||
return {
|
||||
passed: false,
|
||||
messages: [
|
||||
`Number mismatch at ${path}: expected ${expected}, got ${actual}.`,
|
||||
`Difference ${diff} > epsilon ${EPSILON}.`
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Compare ArrayBuffers by contents
|
||||
if (expected instanceof ArrayBuffer && actual instanceof ArrayBuffer) {
|
||||
const eArr = new Uint8Array(expected);
|
||||
const aArr = new Uint8Array(actual);
|
||||
return deepCompare([...eArr], [...aArr], path);
|
||||
}
|
||||
|
||||
// If one is an ArrayBuffer, convert for array comparison
|
||||
if (actual instanceof ArrayBuffer) {
|
||||
actual = [...new Uint8Array(actual)];
|
||||
}
|
||||
|
||||
// Compare arrays
|
||||
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 subMessages = [];
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
let r = deepCompare(expected[i], actual[i], `${path}[${i}]`);
|
||||
if (!r.passed) subMessages.push(...r.messages);
|
||||
}
|
||||
return { passed: subMessages.length === 0, messages: subMessages };
|
||||
}
|
||||
|
||||
// Compare objects
|
||||
if (expected && typeof expected === 'object' &&
|
||||
actual && typeof actual === 'object') {
|
||||
let expKeys = Object.keys(expected).sort();
|
||||
let actKeys = Object.keys(actual).sort();
|
||||
// Quick JSON-based check on key sets
|
||||
if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) {
|
||||
return {
|
||||
passed: false,
|
||||
messages: [
|
||||
`Object key mismatch at ${path}:\n expected keys: ${expKeys}\n actual keys: ${actKeys}`
|
||||
]
|
||||
};
|
||||
}
|
||||
let subMessages = [];
|
||||
for (let k of expKeys) {
|
||||
let r = deepCompare(expected[k], actual[k], path ? `${path}.${k}` : k);
|
||||
if (!r.passed) subMessages.push(...r.messages);
|
||||
}
|
||||
return { passed: subMessages.length === 0, messages: subMessages };
|
||||
}
|
||||
|
||||
// Fallback: primitive mismatch
|
||||
return {
|
||||
passed: false,
|
||||
messages: [
|
||||
`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
let testCases = [
|
||||
// Basic numbers
|
||||
0,
|
||||
1,
|
||||
-1,
|
||||
2023,
|
||||
1e46,
|
||||
2.5e120,
|
||||
2e120,
|
||||
4.3e-7,
|
||||
42,
|
||||
1.5,
|
||||
-0.123456,
|
||||
|
||||
// Integer boundaries (56-bit limit in Wota spec)
|
||||
// Wota can store -2^55 through 2^55 - 1 as an INT
|
||||
-(2**55),
|
||||
(2**55) - 1,
|
||||
|
||||
// Larger than 56-bit => stored as float
|
||||
-(2**55) - 1,
|
||||
(2**55),
|
||||
|
||||
// Infinity and NaN
|
||||
Infinity,
|
||||
-Infinity,
|
||||
NaN,
|
||||
|
||||
// Booleans
|
||||
true,
|
||||
false,
|
||||
|
||||
// undefined (WOTA_NULL)
|
||||
undefined,
|
||||
|
||||
// A couple strings
|
||||
"Hello, Wota!",
|
||||
"",
|
||||
"Emoji test: \u{1f600}\u{1f64f}", // 😀🙏
|
||||
|
||||
// An ArrayBuffer (binary blob)
|
||||
new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer,
|
||||
// empty blob
|
||||
new Uint8Array([]).buffer,
|
||||
|
||||
// Arrays
|
||||
[],
|
||||
[1,2,3],
|
||||
[true, false, undefined, "test", -999],
|
||||
// Nested array
|
||||
[[[]]],
|
||||
|
||||
// Objects / Records
|
||||
{},
|
||||
{a:1, b:2.2, c:"3", d:false},
|
||||
{ nested: { arr: [1, { x: "y" }] } },
|
||||
// Symbol-like keys in JS (just unusual keys)
|
||||
{ "": 123, "foo": "bar" },
|
||||
|
||||
// Larger array length test (not extreme, just a demonstration)
|
||||
Array.from({length: 10}, (_, i) => i),
|
||||
// Some deeper nesting
|
||||
{
|
||||
deep: {
|
||||
deeper: {
|
||||
arr: [ { level: "three" }, [ "and four?" ] ]
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
/* We’ll just do a round-trip test:
|
||||
decoded = wota.decode( wota.encode(input) )
|
||||
and compare decoded vs. original.
|
||||
*/
|
||||
let results = [];
|
||||
let passCount = 0;
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
let input = testCases[i];
|
||||
let testName = `Test ${i+1}: ${JSON.stringify(input)}`;
|
||||
let passed = true;
|
||||
let messages = [];
|
||||
|
||||
try {
|
||||
let encoded = wota.encode(input);
|
||||
if (!(encoded instanceof ArrayBuffer)) {
|
||||
passed = false;
|
||||
messages.push("wota.encode did not return an ArrayBuffer!");
|
||||
} else {
|
||||
let decoded = wota.decode(encoded);
|
||||
|
||||
let compareResult = deepCompare(input, decoded, "");
|
||||
if (!compareResult.passed) {
|
||||
passed = false;
|
||||
messages.push(`Roundtrip mismatch for input=${JSON.stringify(input)}`);
|
||||
messages.push(...compareResult.messages);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
passed = false;
|
||||
messages.push(`Exception thrown: ${e}`);
|
||||
}
|
||||
|
||||
results.push({ testName, passed, messages });
|
||||
if (passed) passCount++;
|
||||
}
|
||||
|
||||
// Print a summary
|
||||
for (let r of results) {
|
||||
console.log(`${r.testName}: ${r.passed ? "PASS" : "FAIL"}`);
|
||||
if (!r.passed && r.messages.length > 0) {
|
||||
for (let m of r.messages) {
|
||||
console.log(" ", m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nOverall: ${passCount}/${results.length} tests passed.`);
|
||||
if (passCount < results.length) {
|
||||
os.exit(1);
|
||||
} else {
|
||||
os.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user