From 33d901340919faf5db6ce1c4e1e1d175f650c7fa Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 24 Feb 2026 17:41:18 -0600 Subject: [PATCH] fix tests; add comprehensive tests for functions and fix bugs in the mach VM regarding them. --- mcode.cm | 18 ++ source/cell.h | 21 ++ source/mach.c | 34 +++- source/runtime.c | 136 ++++++++++++- source/suite.c | 488 +++++++++++++++++++++++++++++++++++++++++++++++ vm_suite.ce | 326 ++++++++++++++++++++++++++++++- 6 files changed, 1009 insertions(+), 14 deletions(-) diff --git a/mcode.cm b/mcode.cm index cc8a55a4..d824ce06 100644 --- a/mcode.cm +++ b/mcode.cm @@ -951,6 +951,19 @@ var mcode = function(ast) { return null } + // --- Helper: guard that reverse param is logical (or null) --- + // Disrupts if rev_slot is not null and not a boolean. + var emit_reverse_guard = function(rev_slot, msg) { + var ok_label = gen_label("rev_ok") + var g = alloc_slot() + emit_jump_cond("jump_null", rev_slot, ok_label) + emit_2("is_bool", g, rev_slot) + emit_jump_cond("jump_true", g, ok_label) + emit_log_error(msg) + emit_0("disrupt") + emit_label(ok_label) + } + // --- Helper: forward loop scaffolding --- // L = {arr, len, i, check, item, one, loop_label, done_label} // body_fn(L) — called between element load and increment @@ -1068,6 +1081,7 @@ var mcode = function(ast) { if (nargs <= 2) { emit_forward_loop(fwd_L, body_fn) } else { + emit_reverse_guard(args.rev, "arrfor: reverse must be a logical") emit_jump_cond("wary_true", args.rev, rev_label) emit_forward_loop(fwd_L, body_fn) emit_jump(final_label) @@ -1289,6 +1303,9 @@ var mcode = function(ast) { emit_2("int", zero, 0) emit_2("int", one, 1) emit_1("null", null_s) + if (nargs > 2) { + emit_reverse_guard(args.rev, "find: reverse must be a logical") + } emit_2("is_func", is_fn, target) emit_jump_cond("jump_true", is_fn, fn_mode_label) // === Value mode === @@ -1523,6 +1540,7 @@ var mcode = function(ast) { d2 = gen_label("reduce_d2") d3 = gen_label("reduce_d3") d4 = gen_label("reduce_d4") + emit_reverse_guard(rev_slot, "reduce: reverse must be a logical") emit_2("is_null", check, init_slot) emit_jump_cond("jump_false", check, has_init) // No initial diff --git a/source/cell.h b/source/cell.h index 96b7258a..4b847137 100644 --- a/source/cell.h +++ b/source/cell.h @@ -267,6 +267,19 @@ JS_BOOL JS_IsBlob(JSValue v); JS_BOOL JS_IsText(JSValue v); JS_BOOL JS_IsStone(JSValue v); +/* Sensory function wrappers (require JSContext for string inspection) */ +JS_BOOL JS_IsDigit(JSContext *ctx, JSValue val); +JS_BOOL JS_IsLetter(JSContext *ctx, JSValue val); +JS_BOOL JS_IsLower(JSContext *ctx, JSValue val); +JS_BOOL JS_IsUpper(JSContext *ctx, JSValue val); +JS_BOOL JS_IsWhitespace(JSContext *ctx, JSValue val); +JS_BOOL JS_IsCharacter(JSContext *ctx, JSValue val); +JS_BOOL JS_IsFit(JSContext *ctx, JSValue val); +JS_BOOL JS_IsData(JSValue val); +static inline JS_BOOL JS_IsTrue(JSValue v) { return v == JS_TRUE; } +static inline JS_BOOL JS_IsFalse(JSValue v) { return v == JS_FALSE; } +JS_BOOL JS_IsActor(JSContext *ctx, JSValue val); + /* ============================================================ GC References — no-ops with copying GC ============================================================ */ @@ -559,6 +572,14 @@ JSValue JS_CellObject (JSContext *ctx, JSValue proto, JSValue props); /* Format */ JSValue JS_CellFormat (JSContext *ctx, JSValue text, JSValue collection, JSValue transformer); +/* Additional cell functions */ +JSValue JS_CellLogical (JSContext *ctx, JSValue val); +JSValue JS_CellEvery (JSContext *ctx, JSValue arr, JSValue pred); +JSValue JS_CellSome (JSContext *ctx, JSValue arr, JSValue pred); +JSValue JS_CellStartsWith (JSContext *ctx, JSValue text, JSValue prefix); +JSValue JS_CellEndsWith (JSContext *ctx, JSValue text, JSValue suffix); +JSValue JS_CellNormalize (JSContext *ctx, JSValue text); + /* Output helpers */ void JS_PrintText (JSContext *ctx, JSValue val); void JS_PrintTextLn (JSContext *ctx, JSValue val); diff --git a/source/mach.c b/source/mach.c index 58f445b4..6091a329 100644 --- a/source/mach.c +++ b/source/mach.c @@ -2486,14 +2486,32 @@ vm_dispatch: VM_CASE(MACH_IS_WS): { JSValue v = frame->slots[b]; int result = 0; - if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) { - int ch = MIST_GetImmediateASCIIChar(v, 0); - result = (ch == ' ' || ch == '\t' || ch == '\n' - || ch == '\r' || ch == '\f' || ch == '\v'); - } else if (mist_is_text(v) && js_string_value_len(v) == 1) { - uint32_t ch = js_string_value_get(v, 0); - result = (ch == ' ' || ch == '\t' || ch == '\n' - || ch == '\r' || ch == '\f' || ch == '\v'); + if (MIST_IsImmediateASCII(v)) { + int len = MIST_GetImmediateASCIILen(v); + if (len > 0) { + result = 1; + for (int i = 0; i < len; i++) { + int ch = MIST_GetImmediateASCIIChar(v, i); + if (!(ch == ' ' || ch == '\t' || ch == '\n' + || ch == '\r' || ch == '\f' || ch == '\v')) { + result = 0; + break; + } + } + } + } else if (mist_is_text(v)) { + int len = js_string_value_len(v); + if (len > 0) { + result = 1; + for (int i = 0; i < len; i++) { + uint32_t ch = js_string_value_get(v, i); + if (!(ch == ' ' || ch == '\t' || ch == '\n' + || ch == '\r' || ch == '\f' || ch == '\v')) { + result = 0; + break; + } + } + } } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); diff --git a/source/runtime.c b/source/runtime.c index 432b64fe..c1cb3c2f 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -11503,15 +11503,20 @@ static JSValue js_cell_is_upper (JSContext *ctx, JSValue this_val, int argc, JSV return JS_NewBool (ctx, c >= 'A' && c <= 'Z'); } -/* is_whitespace(val) - check if value is a single whitespace character */ +/* is_whitespace(val) - check if all characters are whitespace (non-empty) */ static JSValue js_cell_is_whitespace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; - if (js_string_value_len (val) != 1) return JS_FALSE; - uint32_t c = js_string_value_get (val, 0); - return JS_NewBool (ctx, c == ' ' || c == '\t' || c == '\n' - || c == '\r' || c == '\f' || c == '\v'); + int len = js_string_value_len (val); + if (len == 0) return JS_FALSE; + for (int i = 0; i < len; i++) { + uint32_t c = js_string_value_get (val, i); + if (!(c == ' ' || c == '\t' || c == '\n' + || c == '\r' || c == '\f' || c == '\v')) + return JS_FALSE; + } + return JS_TRUE; } /* is_proto(val, master) - check if val has master in prototype chain */ @@ -11571,6 +11576,47 @@ static JSValue js_cell_logical(JSContext *ctx, JSValue this_val, int argc, JSVal return JS_NULL; } +/* realloc wrapper for unicode_normalize */ +static void *normalize_realloc(void *opaque, void *ptr, size_t size) { + (void)opaque; + if (size == 0) { + pjs_free(ptr); + return NULL; + } + return pjs_realloc(ptr, size); +} + +/* normalize(text) — NFC normalization */ +static JSValue js_cell_normalize(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + if (argc < 1) return JS_NULL; + JSValue val = argv[0]; + if (!JS_IsText(val)) return JS_NULL; + int len = js_string_value_len(val); + if (len == 0) return JS_NewString(ctx, ""); + uint32_t *src = pjs_malloc(len * sizeof(uint32_t)); + if (!src) return JS_EXCEPTION; + for (int i = 0; i < len; i++) + src[i] = js_string_value_get(val, i); + uint32_t *dst = NULL; + int dst_len = unicode_normalize(&dst, src, len, UNICODE_NFC, NULL, + normalize_realloc); + pjs_free(src); + if (dst_len < 0) { + pjs_free(dst); + return JS_NULL; + } + JSText *str = js_alloc_string(ctx, dst_len); + if (!str) { + pjs_free(dst); + return JS_EXCEPTION; + } + for (int i = 0; i < dst_len; i++) + string_put(str, i, dst[i]); + str->length = dst_len; + pjs_free(dst); + return pretext_end(ctx, str); +} + /* starts_with(str, prefix) — search(str, prefix) == 0 */ static JSValue js_cell_starts_with(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; @@ -11616,6 +11662,85 @@ static JSValue js_cell_some(JSContext *ctx, JSValue this_val, int argc, JSValue return JS_FALSE; } +/* C API: Type-check wrappers for sensory functions */ +JS_BOOL JS_IsDigit (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_digit (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsLetter (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_letter (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsLower (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_lower (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsUpper (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_upper (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsWhitespace (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_whitespace (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsCharacter (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_character (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsFit (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_fit (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +JS_BOOL JS_IsData (JSValue val) { + return !JS_IsNull (val) && !JS_IsFunction (val); +} + +JS_BOOL JS_IsActor (JSContext *ctx, JSValue val) { + JSValue r = js_cell_is_actor (ctx, JS_NULL, 1, &val); + return JS_VALUE_GET_BOOL (r); +} + +/* C API: logical(val) */ +JSValue JS_CellLogical (JSContext *ctx, JSValue val) { + return js_cell_logical (ctx, JS_NULL, 1, &val); +} + +/* C API: every(arr, pred) */ +JSValue JS_CellEvery (JSContext *ctx, JSValue arr, JSValue pred) { + JSValue argv[2] = { arr, pred }; + return js_cell_every (ctx, JS_NULL, 2, argv); +} + +/* C API: some(arr, pred) */ +JSValue JS_CellSome (JSContext *ctx, JSValue arr, JSValue pred) { + JSValue argv[2] = { arr, pred }; + return js_cell_some (ctx, JS_NULL, 2, argv); +} + +/* C API: starts_with(text, prefix) */ +JSValue JS_CellStartsWith (JSContext *ctx, JSValue text, JSValue prefix) { + JSValue argv[2] = { text, prefix }; + return js_cell_starts_with (ctx, JS_NULL, 2, argv); +} + +/* C API: ends_with(text, suffix) */ +JSValue JS_CellEndsWith (JSContext *ctx, JSValue text, JSValue suffix) { + JSValue argv[2] = { text, suffix }; + return js_cell_ends_with (ctx, JS_NULL, 2, argv); +} + +/* C API: normalize(text) */ +JSValue JS_CellNormalize (JSContext *ctx, JSValue text) { + return js_cell_normalize (ctx, JS_NULL, 1, &text); +} + static void js_set_global_cfunc(JSContext *ctx, const char *name, JSCFunction *func, int length) { JSGCRef ref; JS_PushGCRef(ctx, &ref); @@ -11740,6 +11865,7 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) { js_set_global_cfunc(ctx, "ends_with", js_cell_ends_with, 2); js_set_global_cfunc(ctx, "every", js_cell_every, 2); js_set_global_cfunc(ctx, "some", js_cell_some, 2); + js_set_global_cfunc(ctx, "normalize", js_cell_normalize, 1); /* fn record with apply property */ { diff --git a/source/suite.c b/source/suite.c index ea4eff8c..fced5dc0 100644 --- a/source/suite.c +++ b/source/suite.c @@ -2187,6 +2187,424 @@ TEST(wota_encode_blob) { return 1; } +/* ============================================================================ + Sensory Function C API Tests + ============================================================================ */ + +TEST(is_true_true) { + ASSERT(JS_IsTrue(JS_TRUE)); + return 1; +} + +TEST(is_true_false) { + ASSERT(!JS_IsTrue(JS_FALSE)); + ASSERT(!JS_IsTrue(JS_NewInt32(ctx, 1))); + ASSERT(!JS_IsTrue(JS_NULL)); + ASSERT(!JS_IsTrue(JS_NewString(ctx, "true"))); + return 1; +} + +TEST(is_false_false) { + ASSERT(JS_IsFalse(JS_FALSE)); + return 1; +} + +TEST(is_false_other) { + ASSERT(!JS_IsFalse(JS_TRUE)); + ASSERT(!JS_IsFalse(JS_NewInt32(ctx, 0))); + ASSERT(!JS_IsFalse(JS_NULL)); + return 1; +} + +TEST(is_data_values) { + ASSERT(JS_IsData(JS_NewInt32(ctx, 42))); + ASSERT(JS_IsData(JS_NewString(ctx, "hello"))); + ASSERT(JS_IsData(JS_TRUE)); + ASSERT(JS_IsData(JS_NewArray(ctx))); + ASSERT(JS_IsData(JS_NewObject(ctx))); + return 1; +} + +TEST(is_data_non_data) { + ASSERT(!JS_IsData(JS_NULL)); + return 1; +} + +TEST(is_fit_integers) { + ASSERT(JS_IsFit(ctx, JS_NewInt32(ctx, 0))); + ASSERT(JS_IsFit(ctx, JS_NewInt32(ctx, 42))); + ASSERT(JS_IsFit(ctx, JS_NewInt32(ctx, -100))); + ASSERT(JS_IsFit(ctx, JS_NewFloat64(ctx, 3.0))); + return 1; +} + +TEST(is_fit_non_fit) { + ASSERT(!JS_IsFit(ctx, JS_NewFloat64(ctx, 3.5))); + ASSERT(!JS_IsFit(ctx, JS_NewString(ctx, "3"))); + ASSERT(!JS_IsFit(ctx, JS_NULL)); + ASSERT(!JS_IsFit(ctx, JS_TRUE)); + return 1; +} + +TEST(is_character_single) { + ASSERT(JS_IsCharacter(ctx, JS_NewString(ctx, "a"))); + ASSERT(JS_IsCharacter(ctx, JS_NewString(ctx, "Z"))); + ASSERT(JS_IsCharacter(ctx, JS_NewString(ctx, "5"))); + return 1; +} + +TEST(is_character_non_char) { + ASSERT(!JS_IsCharacter(ctx, JS_NewString(ctx, "ab"))); + ASSERT(!JS_IsCharacter(ctx, JS_NewString(ctx, ""))); + ASSERT(!JS_IsCharacter(ctx, JS_NewInt32(ctx, 65))); + ASSERT(!JS_IsCharacter(ctx, JS_NULL)); + return 1; +} + +TEST(is_digit_digits) { + ASSERT(JS_IsDigit(ctx, JS_NewString(ctx, "0"))); + ASSERT(JS_IsDigit(ctx, JS_NewString(ctx, "9"))); + return 1; +} + +TEST(is_digit_non_digit) { + ASSERT(!JS_IsDigit(ctx, JS_NewString(ctx, "a"))); + ASSERT(!JS_IsDigit(ctx, JS_NewString(ctx, "55"))); + ASSERT(!JS_IsDigit(ctx, JS_NewInt32(ctx, 5))); + ASSERT(!JS_IsDigit(ctx, JS_NULL)); + return 1; +} + +TEST(is_letter_letters) { + ASSERT(JS_IsLetter(ctx, JS_NewString(ctx, "a"))); + ASSERT(JS_IsLetter(ctx, JS_NewString(ctx, "Z"))); + return 1; +} + +TEST(is_letter_non_letter) { + ASSERT(!JS_IsLetter(ctx, JS_NewString(ctx, "5"))); + ASSERT(!JS_IsLetter(ctx, JS_NewString(ctx, "ab"))); + ASSERT(!JS_IsLetter(ctx, JS_NewInt32(ctx, 65))); + return 1; +} + +TEST(is_lower_lowercase) { + ASSERT(JS_IsLower(ctx, JS_NewString(ctx, "a"))); + ASSERT(JS_IsLower(ctx, JS_NewString(ctx, "z"))); + return 1; +} + +TEST(is_lower_non_lower) { + ASSERT(!JS_IsLower(ctx, JS_NewString(ctx, "A"))); + ASSERT(!JS_IsLower(ctx, JS_NewString(ctx, "5"))); + ASSERT(!JS_IsLower(ctx, JS_NewString(ctx, "ab"))); + return 1; +} + +TEST(is_upper_uppercase) { + ASSERT(JS_IsUpper(ctx, JS_NewString(ctx, "A"))); + ASSERT(JS_IsUpper(ctx, JS_NewString(ctx, "Z"))); + return 1; +} + +TEST(is_upper_non_upper) { + ASSERT(!JS_IsUpper(ctx, JS_NewString(ctx, "a"))); + ASSERT(!JS_IsUpper(ctx, JS_NewString(ctx, "5"))); + ASSERT(!JS_IsUpper(ctx, JS_NewString(ctx, "AB"))); + return 1; +} + +TEST(is_whitespace_single) { + ASSERT(JS_IsWhitespace(ctx, JS_NewString(ctx, " "))); + ASSERT(JS_IsWhitespace(ctx, JS_NewString(ctx, "\t"))); + ASSERT(JS_IsWhitespace(ctx, JS_NewString(ctx, "\n"))); + return 1; +} + +TEST(is_whitespace_multi) { + ASSERT(JS_IsWhitespace(ctx, JS_NewString(ctx, " "))); + ASSERT(JS_IsWhitespace(ctx, JS_NewString(ctx, "\r\n"))); + ASSERT(JS_IsWhitespace(ctx, JS_NewString(ctx, " \t\n"))); + return 1; +} + +TEST(is_whitespace_non_ws) { + ASSERT(!JS_IsWhitespace(ctx, JS_NewString(ctx, "a"))); + ASSERT(!JS_IsWhitespace(ctx, JS_NewString(ctx, ""))); + ASSERT(!JS_IsWhitespace(ctx, JS_NewInt32(ctx, 32))); + return 1; +} + +TEST(is_actor_non_actor) { + ASSERT(!JS_IsActor(ctx, JS_NewObject(ctx))); + ASSERT(!JS_IsActor(ctx, JS_NULL)); + ASSERT(!JS_IsActor(ctx, JS_NewInt32(ctx, 42))); + return 1; +} + +/* ============================================================================ + Logical, Whole, Fraction C API Tests + ============================================================================ */ + +TEST(logical_zero) { + JSValue r = JS_CellLogical(ctx, JS_NewInt32(ctx, 0)); + ASSERT_FALSE(r); + return 1; +} + +TEST(logical_one) { + JSValue r = JS_CellLogical(ctx, JS_NewInt32(ctx, 1)); + ASSERT_TRUE(r); + return 1; +} + +TEST(logical_false) { + JSValue r = JS_CellLogical(ctx, JS_FALSE); + ASSERT_FALSE(r); + return 1; +} + +TEST(logical_true) { + JSValue r = JS_CellLogical(ctx, JS_TRUE); + ASSERT_TRUE(r); + return 1; +} + +TEST(logical_string_false) { + JSValue r = JS_CellLogical(ctx, JS_NewString(ctx, "false")); + ASSERT_FALSE(r); + return 1; +} + +TEST(logical_string_true) { + JSValue r = JS_CellLogical(ctx, JS_NewString(ctx, "true")); + ASSERT_TRUE(r); + return 1; +} + +TEST(logical_null) { + JSValue r = JS_CellLogical(ctx, JS_NULL); + ASSERT_FALSE(r); + return 1; +} + +TEST(logical_invalid) { + JSValue r = JS_CellLogical(ctx, JS_NewInt32(ctx, 42)); + ASSERT_NULL(r); + r = JS_CellLogical(ctx, JS_NewString(ctx, "maybe")); + ASSERT_NULL(r); + return 1; +} + +TEST(whole_positive) { + JSValue r = JS_CellWhole(ctx, JS_NewFloat64(ctx, 3.7)); + ASSERT_FLOAT(r, 3.0, 0.001); + return 1; +} + +TEST(whole_negative) { + JSValue r = JS_CellWhole(ctx, JS_NewFloat64(ctx, -3.7)); + ASSERT_FLOAT(r, -3.0, 0.001); + return 1; +} + +TEST(whole_integer) { + JSValue r = JS_CellWhole(ctx, JS_NewInt32(ctx, 42)); + ASSERT_FLOAT(r, 42.0, 0.001); + return 1; +} + +TEST(whole_non_number) { + JSValue r = JS_CellWhole(ctx, JS_NewString(ctx, "hi")); + ASSERT_NULL(r); + return 1; +} + +TEST(fraction_positive) { + JSValue r = JS_CellFraction(ctx, JS_NewFloat64(ctx, 3.7)); + ASSERT_FLOAT(r, 0.7, 0.001); + return 1; +} + +TEST(fraction_negative) { + JSValue r = JS_CellFraction(ctx, JS_NewFloat64(ctx, -3.7)); + ASSERT_FLOAT(r, -0.7, 0.001); + return 1; +} + +TEST(fraction_integer) { + JSValue r = JS_CellFraction(ctx, JS_NewInt32(ctx, 42)); + ASSERT_FLOAT(r, 0.0, 0.001); + return 1; +} + +TEST(fraction_non_number) { + JSValue r = JS_CellFraction(ctx, JS_NewString(ctx, "hi")); + ASSERT_NULL(r); + return 1; +} + +/* ============================================================================ + StartsWith / EndsWith C API Tests + ============================================================================ */ + +TEST(starts_with_match) { + JSValue r = JS_CellStartsWith(ctx, JS_NewString(ctx, "hello world"), JS_NewString(ctx, "hello")); + ASSERT_TRUE(r); + return 1; +} + +TEST(starts_with_no_match) { + JSValue r = JS_CellStartsWith(ctx, JS_NewString(ctx, "hello world"), JS_NewString(ctx, "world")); + ASSERT_FALSE(r); + return 1; +} + +TEST(starts_with_empty) { + JSValue r = JS_CellStartsWith(ctx, JS_NewString(ctx, "hello"), JS_NewString(ctx, "")); + ASSERT_TRUE(r); + return 1; +} + +TEST(ends_with_match) { + JSValue r = JS_CellEndsWith(ctx, JS_NewString(ctx, "hello world"), JS_NewString(ctx, "world")); + ASSERT_TRUE(r); + return 1; +} + +TEST(ends_with_no_match) { + JSValue r = JS_CellEndsWith(ctx, JS_NewString(ctx, "hello world"), JS_NewString(ctx, "hello")); + ASSERT_FALSE(r); + return 1; +} + +TEST(ends_with_full_match) { + JSValue r = JS_CellEndsWith(ctx, JS_NewString(ctx, "test"), JS_NewString(ctx, "test")); + ASSERT_TRUE(r); + return 1; +} + +/* ============================================================================ + Normalize C API Tests + ============================================================================ */ + +TEST(normalize_ascii) { + JSValue r = JS_CellNormalize(ctx, JS_NewString(ctx, "hello")); + ASSERT_STR(r, "hello"); + return 1; +} + +TEST(normalize_non_text) { + JSValue r = JS_CellNormalize(ctx, JS_NewInt32(ctx, 42)); + ASSERT_NULL(r); + return 1; +} + +TEST(normalize_null) { + JSValue r = JS_CellNormalize(ctx, JS_NULL); + ASSERT_NULL(r); + return 1; +} + +TEST(normalize_empty) { + JSValue r = JS_CellNormalize(ctx, JS_NewString(ctx, "")); + ASSERT_STR(r, ""); + return 1; +} + +/* ============================================================================ + Format C API Tests + ============================================================================ */ + +TEST(format_array_placeholder) { + JS_FRAME(ctx); + JS_ROOT(arr, JS_NewArray(ctx)); + JSValue a = JS_NewString(ctx, "world"); + JS_SetPropertyNumber(ctx, arr.val, 0, a); + JSValue r = JS_CellFormat(ctx, JS_NewString(ctx, "hello {0}"), arr.val, JS_NULL); + ASSERT_STR(r, "hello world"); + JS_RETURN(JS_NewInt32(ctx, 1)); +} + +TEST(format_record_placeholder) { + JS_FRAME(ctx); + JS_ROOT(rec, JS_NewObject(ctx)); + JSValue name = JS_NewString(ctx, "world"); + JS_SetPropertyStr(ctx, rec.val, "name", name); + JSValue r = JS_CellFormat(ctx, JS_NewString(ctx, "hello {name}"), rec.val, JS_NULL); + ASSERT_STR(r, "hello world"); + JS_RETURN(JS_NewInt32(ctx, 1)); +} + +TEST(format_missing_key) { + JS_FRAME(ctx); + JS_ROOT(rec, JS_NewObject(ctx)); + JSValue r = JS_CellFormat(ctx, JS_NewString(ctx, "hello {name}"), rec.val, JS_NULL); + ASSERT_STR(r, "hello null"); + JS_RETURN(JS_NewInt32(ctx, 1)); +} + +/* ============================================================================ + Object C API Tests + ============================================================================ */ + +TEST(cell_object_null_null) { + JSValue r = JS_CellObject(ctx, JS_NULL, JS_NULL); + ASSERT_NULL(r); + return 1; +} + +TEST(cell_object_copy) { + JS_FRAME(ctx); + JS_ROOT(orig, JS_NewObject(ctx)); + JS_SetPropertyStr(ctx, orig.val, "x", JS_NewInt32(ctx, 1)); + JSValue copy = JS_CellObject(ctx, orig.val, JS_NULL); + ASSERT(JS_IsRecord(copy)); + JSValue x = JS_GetPropertyStr(ctx, copy, "x"); + ASSERT_INT(x, 1); + JS_RETURN(JS_NewInt32(ctx, 1)); +} + +TEST(cell_object_merge) { + JS_FRAME(ctx); + JS_ROOT(a, JS_NewObject(ctx)); + JS_SetPropertyStr(ctx, a.val, "x", JS_NewInt32(ctx, 1)); + JS_ROOT(b, JS_NewObject(ctx)); + JS_SetPropertyStr(ctx, b.val, "y", JS_NewInt32(ctx, 2)); + JSValue merged = JS_CellObject(ctx, a.val, b.val); + ASSERT(JS_IsRecord(merged)); + JSValue x = JS_GetPropertyStr(ctx, merged, "x"); + JSValue y = JS_GetPropertyStr(ctx, merged, "y"); + ASSERT_INT(x, 1); + ASSERT_INT(y, 2); + JS_RETURN(JS_NewInt32(ctx, 1)); +} + +/* ============================================================================ + Splat C API Tests + ============================================================================ */ + +TEST(cell_splat_non_object) { + JSValue r = JS_CellSplat(ctx, JS_NewInt32(ctx, 42)); + ASSERT_NULL(r); + return 1; +} + +TEST(cell_splat_null) { + JSValue r = JS_CellSplat(ctx, JS_NULL); + ASSERT_NULL(r); + return 1; +} + +TEST(cell_splat_record) { + JS_FRAME(ctx); + JS_ROOT(obj, JS_NewObject(ctx)); + JS_SetPropertyStr(ctx, obj.val, "x", JS_NewInt32(ctx, 1)); + JSValue r = JS_CellSplat(ctx, obj.val); + ASSERT(JS_IsRecord(r)); + JS_RETURN(JS_NewInt32(ctx, 1)); +} + /* ============================================================================ MAIN TEST RUNNER ============================================================================ */ @@ -2408,6 +2826,76 @@ int run_c_test_suite(JSContext *ctx) RUN_TEST(wota_encode_nested_array); RUN_TEST(wota_encode_blob); + printf("\nSensory Function C API:\n"); + RUN_TEST(is_true_true); + RUN_TEST(is_true_false); + RUN_TEST(is_false_false); + RUN_TEST(is_false_other); + RUN_TEST(is_data_values); + RUN_TEST(is_data_non_data); + RUN_TEST(is_fit_integers); + RUN_TEST(is_fit_non_fit); + RUN_TEST(is_character_single); + RUN_TEST(is_character_non_char); + RUN_TEST(is_digit_digits); + RUN_TEST(is_digit_non_digit); + RUN_TEST(is_letter_letters); + RUN_TEST(is_letter_non_letter); + RUN_TEST(is_lower_lowercase); + RUN_TEST(is_lower_non_lower); + RUN_TEST(is_upper_uppercase); + RUN_TEST(is_upper_non_upper); + RUN_TEST(is_whitespace_single); + RUN_TEST(is_whitespace_multi); + RUN_TEST(is_whitespace_non_ws); + RUN_TEST(is_actor_non_actor); + + printf("\nLogical/Whole/Fraction:\n"); + RUN_TEST(logical_zero); + RUN_TEST(logical_one); + RUN_TEST(logical_false); + RUN_TEST(logical_true); + RUN_TEST(logical_string_false); + RUN_TEST(logical_string_true); + RUN_TEST(logical_null); + RUN_TEST(logical_invalid); + RUN_TEST(whole_positive); + RUN_TEST(whole_negative); + RUN_TEST(whole_integer); + RUN_TEST(whole_non_number); + RUN_TEST(fraction_positive); + RUN_TEST(fraction_negative); + RUN_TEST(fraction_integer); + RUN_TEST(fraction_non_number); + + printf("\nStartsWith/EndsWith:\n"); + RUN_TEST(starts_with_match); + RUN_TEST(starts_with_no_match); + RUN_TEST(starts_with_empty); + RUN_TEST(ends_with_match); + RUN_TEST(ends_with_no_match); + RUN_TEST(ends_with_full_match); + + printf("\nNormalize:\n"); + RUN_TEST(normalize_ascii); + RUN_TEST(normalize_non_text); + RUN_TEST(normalize_null); + RUN_TEST(normalize_empty); + + printf("\nFormat:\n"); + RUN_TEST(format_array_placeholder); + RUN_TEST(format_record_placeholder); + RUN_TEST(format_missing_key); + + printf("\nObject:\n"); + RUN_TEST(cell_object_null_null); + RUN_TEST(cell_object_copy); + RUN_TEST(cell_object_merge); + + printf("\nSplat:\n"); + RUN_TEST(cell_splat_non_object); + RUN_TEST(cell_splat_null); + RUN_TEST(cell_splat_record); printf("\n=================================\n"); printf("Results: %d passed, %d failed\n", tests_passed, tests_failed); diff --git a/vm_suite.ce b/vm_suite.ce index 1a44957f..6cfce18c 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -1070,7 +1070,10 @@ run("is_whitespace", function() { if (!is_whitespace("\t")) fail("is_whitespace tab should be true") if (!is_whitespace("\n")) fail("is_whitespace newline should be true") if (is_whitespace("a")) fail("is_whitespace a should be false") - if (is_whitespace(" ")) fail("is_whitespace two spaces should be false") + if (!is_whitespace(" ")) fail("is_whitespace two spaces should be true") + if (!is_whitespace("\r\n")) fail("is_whitespace cr-lf should be true") + if (!is_whitespace(" \t\n")) fail("is_whitespace mixed ws should be true") + if (is_whitespace("")) fail("is_whitespace empty should be false") if (is_whitespace(42)) fail("is_whitespace number should be false") if (is_whitespace(null)) fail("is_whitespace null should be false") }) @@ -8556,9 +8559,13 @@ run("is_whitespace edge cases", function() { assert_eq(is_whitespace(" "), true, "is_whitespace space") assert_eq(is_whitespace("\t"), true, "is_whitespace tab") assert_eq(is_whitespace("\n"), true, "is_whitespace newline") + assert_eq(is_whitespace(" "), true, "is_whitespace multi space") + assert_eq(is_whitespace("\r\n"), true, "is_whitespace crlf") + assert_eq(is_whitespace(" \t\n"), true, "is_whitespace mixed") assert_eq(is_whitespace(""), false, "is_whitespace empty") assert_eq(is_whitespace("a"), false, "is_whitespace letter") assert_eq(is_whitespace(32), false, "is_whitespace number") + assert_eq(is_whitespace(" a"), false, "is_whitespace space-letter") }) run("is_data edge cases", function() { @@ -8610,6 +8617,323 @@ run("arrfor with index", function() { assert_eq(indices[2], 2, "arrfor idx [2]") }) +// ============================================================================ +// SENSORY FUNCTION COMPLETENESS +// ============================================================================ + +run("is_true completeness", function() { + assert_eq(is_true(true), true, "is_true true") + assert_eq(is_true(false), false, "is_true false") + assert_eq(is_true(1), false, "is_true 1") + assert_eq(is_true(null), false, "is_true null") + assert_eq(is_true("true"), false, "is_true text") +}) + +run("is_false completeness", function() { + assert_eq(is_false(false), true, "is_false false") + assert_eq(is_false(true), false, "is_false true") + assert_eq(is_false(0), false, "is_false 0") + assert_eq(is_false(null), false, "is_false null") +}) + +run("is_fit completeness", function() { + assert_eq(is_fit(0), true, "is_fit 0") + assert_eq(is_fit(42), true, "is_fit 42") + assert_eq(is_fit(-100), true, "is_fit -100") + assert_eq(is_fit(3.0), true, "is_fit 3.0") + assert_eq(is_fit(3.5), false, "is_fit 3.5") + assert_eq(is_fit("3"), false, "is_fit text") + assert_eq(is_fit(null), false, "is_fit null") + assert_eq(is_fit(true), false, "is_fit bool") +}) + +run("is_character completeness", function() { + assert_eq(is_character("a"), true, "is_character a") + assert_eq(is_character("Z"), true, "is_character Z") + assert_eq(is_character("ab"), false, "is_character ab") + assert_eq(is_character(""), false, "is_character empty") + assert_eq(is_character(65), false, "is_character number") + assert_eq(is_character(null), false, "is_character null") +}) + +run("is_digit completeness", function() { + assert_eq(is_digit("0"), true, "is_digit 0") + assert_eq(is_digit("9"), true, "is_digit 9") + assert_eq(is_digit("a"), false, "is_digit a") + assert_eq(is_digit("55"), false, "is_digit 55") + assert_eq(is_digit(5), false, "is_digit num") + assert_eq(is_digit(null), false, "is_digit null") +}) + +run("is_letter completeness", function() { + assert_eq(is_letter("a"), true, "is_letter a") + assert_eq(is_letter("Z"), true, "is_letter Z") + assert_eq(is_letter("5"), false, "is_letter 5") + assert_eq(is_letter("ab"), false, "is_letter ab") + assert_eq(is_letter(65), false, "is_letter number") +}) + +run("is_lower completeness", function() { + assert_eq(is_lower("a"), true, "is_lower a") + assert_eq(is_lower("z"), true, "is_lower z") + assert_eq(is_lower("A"), false, "is_lower A") + assert_eq(is_lower("5"), false, "is_lower 5") + assert_eq(is_lower("ab"), false, "is_lower ab") +}) + +run("is_upper completeness", function() { + assert_eq(is_upper("A"), true, "is_upper A") + assert_eq(is_upper("Z"), true, "is_upper Z") + assert_eq(is_upper("a"), false, "is_upper a") + assert_eq(is_upper("5"), false, "is_upper 5") + assert_eq(is_upper("AB"), false, "is_upper AB") +}) + +// ============================================================================ +// CREATOR FUNCTION WRONG-ARGUMENT DISRUPTIONS +// ============================================================================ + +run("array float index disrupts", function() { + assert_eq(should_disrupt(function() { array([1,2,3], 1.5, 3) }), true, "array float from") + assert_eq(should_disrupt(function() { array([1,2,3], 0, 2.5) }), true, "array float to") +}) + +run("number invalid radix", function() { + assert_eq(number("ff", 1), null, "radix 1 too low") + assert_eq(number("ff", 38), null, "radix 38 too high") +}) + +run("number invalid text", function() { + assert_eq(number("abc"), null, "number abc") + assert_eq(number(""), null, "number empty") +}) + +run("object invalid input", function() { + assert_eq(object(42), null, "object number") + assert_eq(object(null), null, "object null") +}) + +// ============================================================================ +// STANDARD FUNCTION EDGE CASES +// ============================================================================ + +run("abs/sign/floor/ceiling with non-number", function() { + assert_eq(abs("hello"), null, "abs text") + assert_eq(sign("hello"), null, "sign text") + assert_eq(floor("hello"), null, "floor text") + assert_eq(ceiling("hello"), null, "ceiling text") + assert_eq(round("hello"), null, "round text") + assert_eq(trunc("hello"), null, "trunc text") +}) + +run("min/max with non-number", function() { + assert_eq(min(1, "b"), null, "min text arg") + assert_eq(max(null, 5), null, "max null arg") +}) + +run("not with non-logical", function() { + assert_eq(not(42), null, "not number") + assert_eq(not("true"), null, "not text") +}) + +run("whole and fraction", function() { + assert_eq(whole(3.7), 3, "whole 3.7") + assert_eq(whole(-3.7), -3, "whole -3.7") + assert_eq(whole(42), 42, "whole 42") + assert_eq(whole("hi"), null, "whole text") + var f1 = fraction(3.7) + if (f1 < 0.69 || f1 > 0.71) fail("fraction 3.7 should be ~0.7") + var f2 = fraction(-3.7) + if (f2 > -0.69 || f2 < -0.71) fail("fraction -3.7 should be ~-0.7") + assert_eq(fraction(42), 0, "fraction 42") + assert_eq(fraction("hi"), null, "fraction text") +}) + +run("logical comprehensive", function() { + assert_eq(logical(0), false, "logical 0") + assert_eq(logical(1), true, "logical 1") + assert_eq(logical(false), false, "logical false") + assert_eq(logical(true), true, "logical true") + assert_eq(logical("false"), false, "logical text false") + assert_eq(logical("true"), true, "logical text true") + assert_eq(logical(null), false, "logical null") + assert_eq(logical(42), null, "logical 42") + assert_eq(logical("maybe"), null, "logical maybe") +}) + +run("normalize basic", function() { + assert_eq(normalize("hello"), "hello", "normalize ascii") + assert_eq(normalize(42), null, "normalize non-text") + assert_eq(normalize(null), null, "normalize null") + assert_eq(normalize(""), "", "normalize empty") +}) + +run("starts_with / ends_with comprehensive", function() { + assert_eq(starts_with("hello world", "hello"), true, "starts_with match") + assert_eq(starts_with("hello world", "world"), false, "starts_with no match") + assert_eq(starts_with("hello", ""), true, "starts_with empty prefix") + assert_eq(starts_with("hello", "hello"), true, "starts_with full match") + assert_eq(ends_with("hello world", "world"), true, "ends_with match") + assert_eq(ends_with("hello world", "hello"), false, "ends_with no match") + assert_eq(ends_with("test", "test"), true, "ends_with full match") +}) + +run("every and some", function() { + var gt0 = function(x) { return x > 0 } + assert_eq(every([1, 2, 3], gt0), true, "every all pass") + assert_eq(every([1, -1, 3], gt0), false, "every one fail") + assert_eq(every([], gt0), true, "every empty") + assert_eq(some([1, 2, 3], gt0), true, "some one match") + assert_eq(some([-1, -2, -3], gt0), false, "some no match") + assert_eq(some([], gt0), false, "some empty") +}) + +run("reduce edge cases", function() { + assert_eq(reduce([], function(a, b) { return a + b }), null, "reduce empty") + assert_eq(reduce([42], function(a, b) { return a + b }), 42, "reduce single") + assert_eq(reduce([1, 2, 3], function(a, b) { return a + b }), 6, "reduce sum") +}) + +run("filter on non-array disrupts", function() { + assert_eq(should_disrupt(function() { + filter(42, function(x) { return true }) + }), true, "filter non-array disrupts") +}) + +run("find edge cases", function() { + assert_eq(find([1, 2, 3], function(x) { return x == 2 }), 1, "find match index") + assert_eq(find([1, 2, 3], function(x) { return x == 5 }), null, "find no match") + assert_eq(find([], function(x) { return true }), null, "find empty") + assert_eq(find([10, 20, 30], 20), 1, "find value mode index") +}) + +run("format edge cases", function() { + var r1 = format("{0} and {1}", ["hello", "world"]) + assert_eq(r1, "hello and world", "format array basic") + var r2 = format("{name}", {name: "Alice"}) + assert_eq(r2, "Alice", "format record basic") + var r3 = format("{missing}", {}) + assert_eq(r3, "null", "format missing key") +}) + +// ============================================================================ +// DUAL-PATH TESTING (MCODE vs C FALLBACK) +// ============================================================================ + +run("is_digit dual path", function() { + // Direct call tests mcode-lowered opcode + assert_eq(is_digit("5"), true, "is_digit direct true") + assert_eq(is_digit("a"), false, "is_digit direct false") + // As callback tests C fallback + var items = ["5", "a", "3"] + var result = filter(items, is_digit) + assert_eq(length(result), 2, "is_digit filter count") + assert_eq(result[0], "5", "is_digit filter [0]") + assert_eq(result[1], "3", "is_digit filter [1]") +}) + +run("is_letter dual path", function() { + assert_eq(is_letter("a"), true, "is_letter direct true") + assert_eq(is_letter("5"), false, "is_letter direct false") + var items = ["a", "5", "Z"] + var result = filter(items, is_letter) + assert_eq(length(result), 2, "is_letter filter count") +}) + +run("is_upper dual path", function() { + assert_eq(is_upper("A"), true, "is_upper direct true") + assert_eq(is_upper("a"), false, "is_upper direct false") + var items = ["A", "a", "Z"] + var result = filter(items, is_upper) + assert_eq(length(result), 2, "is_upper filter count") +}) + +run("is_lower dual path", function() { + assert_eq(is_lower("a"), true, "is_lower direct true") + assert_eq(is_lower("A"), false, "is_lower direct false") + var items = ["a", "A", "z"] + var result = filter(items, is_lower) + assert_eq(length(result), 2, "is_lower filter count") +}) + +run("is_whitespace dual path", function() { + assert_eq(is_whitespace(" "), true, "is_whitespace direct true") + assert_eq(is_whitespace("a"), false, "is_whitespace direct false") + var items = [" ", "a", "\t", "b"] + var result = filter(items, is_whitespace) + assert_eq(length(result), 2, "is_whitespace filter count") +}) + +run("is_number dual path", function() { + assert_eq(is_number(42), true, "is_number direct true") + assert_eq(is_number("42"), false, "is_number direct false") + var items = [42, "hello", 3.14, null] + var result = filter(items, is_number) + assert_eq(length(result), 2, "is_number filter count") +}) + +run("is_text dual path", function() { + assert_eq(is_text("hello"), true, "is_text direct true") + assert_eq(is_text(42), false, "is_text direct false") + var items = ["a", 42, "b", null] + var result = filter(items, is_text) + assert_eq(length(result), 2, "is_text filter count") +}) + +run("is_null dual path", function() { + assert_eq(is_null(null), true, "is_null direct true") + assert_eq(is_null(0), false, "is_null direct false") + var items = [null, 1, null, "x"] + var result = filter(items, is_null) + assert_eq(length(result), 2, "is_null filter count") +}) + +// ============================================================================ +// ARRFOR DISRUPTION TESTS +// ============================================================================ + +run("arrfor reverse non-boolean disrupts", function() { + assert_eq(should_disrupt(function() { + arrfor([1, 2], function(x) {}, 1) + }), true, "arrfor reverse must be logical") +}) + +run("find reverse non-boolean disrupts", function() { + assert_eq(should_disrupt(function() { + find([1, 2], function(x) { return true }, 1) + }), true, "find reverse must be logical") +}) + +run("reduce reverse non-boolean disrupts", function() { + assert_eq(should_disrupt(function() { + reduce([1, 2, 3], function(a, b) { return a + b }, null, 1) + }), true, "reduce reverse must be logical") +}) + +// ============================================================================ +// IS_STONE BEHAVIOR +// ============================================================================ + +run("is_stone primitives", function() { + assert_eq(is_stone(42), true, "is_stone number") + assert_eq(is_stone("hello"), true, "is_stone text") + assert_eq(is_stone(null), true, "is_stone null") + assert_eq(is_stone(true), true, "is_stone bool") + assert_eq(is_stone(false), true, "is_stone false") +}) + +run("is_stone mutable objects", function() { + assert_eq(is_stone([1, 2]), false, "is_stone array") + assert_eq(is_stone({x: 1}), false, "is_stone record") +}) + +run("is_stone frozen objects", function() { + var a = stone([1, 2]) + assert_eq(is_stone(a), true, "is_stone stoned array") + var r = stone({x: 1}) + assert_eq(is_stone(r), true, "is_stone stoned record") +}) + // ============================================================================ // SUMMARY // ============================================================================