diff --git a/boot.ce b/boot.ce index 3bafb4b7..64ae9a9f 100644 --- a/boot.ce +++ b/boot.ce @@ -8,6 +8,8 @@ var shop = use('internal/shop') var fd = use('fd') +var pkg_tools = use('package') +var build = use('build') var is_native = false var target_prog = null @@ -49,52 +51,7 @@ if (args && length(args) > 0) { // Discover all transitive module dependencies for a file function discover_deps(file_path) { - var visited = {} - var scripts = [] - var c_packages = {} - - function trace(fp) { - if (visited[fp]) return - visited[fp] = true - - var fi = shop.file_info(fp) - var file_pkg = fi.package - var idx = null - var j = 0 - var imp = null - var mod_path = null - var rinfo = null - - // record this script (skip the root program itself) - if (ends_with(fp, '.cm')) { - scripts[] = {path: fp, package: file_pkg} - } - - var _trace = function() { - idx = shop.index_file(fp) - if (!idx || !idx.imports) return - - j = 0 - while (j < length(idx.imports)) { - imp = idx.imports[j] - mod_path = imp.module_path - rinfo = shop.resolve_import_info(mod_path, file_pkg) - - if (rinfo) { - if (rinfo.type == 'script' && rinfo.resolved_path) { - trace(rinfo.resolved_path) - } else if (rinfo.type == 'native' && rinfo.package) { - c_packages[rinfo.package] = true - } - } - j = j + 1 - } - } disruption {} - _trace() - } - - trace(file_path) - return {scripts: scripts, c_packages: array(c_packages)} + return shop.trace_deps(file_path) } // Filter out already-cached modules @@ -118,11 +75,21 @@ function filter_uncached(deps) { j = j + 1 } - // C packages always included — build_dynamic handles its own caching + // Expand C packages into individual files for parallel compilation + var target = build.detect_host_target() + var pkg = null + var c_files = null + var k = 0 j = 0 while (j < length(deps.c_packages)) { - if (deps.c_packages[j] != 'core') { - uncached[] = {type: 'c_package', package: deps.c_packages[j]} + pkg = deps.c_packages[j] + if (pkg != 'core') { + c_files = pkg_tools.get_c_files(pkg, target, true) + k = 0 + while (k < length(c_files)) { + uncached[] = {type: 'c_file', package: pkg, file: c_files[k]} + k = k + 1 + } } j = j + 1 } @@ -132,6 +99,7 @@ function filter_uncached(deps) { function item_name(item) { if (item.path) return item.path + if (item.file) return item.package + '/' + item.file return item.package } @@ -147,7 +115,8 @@ function make_compile_requestor(item) { send(event.actor, { type: item.type, path: item.path, - package: item.package + package: item.package, + file: item.file }) } if (event.type == 'stop') { @@ -205,7 +174,7 @@ run_boot = function() { } // Compile uncached modules in parallel using worker actors - log.console('boot: compiling ' + text(length(uncached)) + ' modules...') + log.console('boot: ' + text(length(uncached)) + ' modules to compile') requestors = array(uncached, make_compile_requestor) parallel(requestors)(function(results, reason) { if (reason) { diff --git a/build.cm b/build.cm index d79119e1..86b84f3a 100644 --- a/build.cm +++ b/build.cm @@ -714,6 +714,28 @@ Build.build_module_dylib = function(pkg, file, target, opts) { return dylib_path } +// Compile a single C module file for a package (support objects + one dylib). +// Used by parallel boot workers. No manifest writing — the runtime handles that. +Build.compile_c_module = function(pkg, file, target, opts) { + var _target = target || Build.detect_host_target() + var _opts = opts || {} + var _buildtype = _opts.buildtype || 'release' + var pkg_dir = shop.get_package_dir(pkg) + var cached_cflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'CFLAGS', _target), pkg_dir) + + // Compile support sources to cached objects (content-addressed, safe for concurrent workers) + var sources = pkg_tools.get_sources(pkg) + var support_objects = [] + if (pkg != 'core') { + arrfor(sources, function(src_file) { + var obj = Build.compile_file(pkg, src_file, _target, {buildtype: _buildtype, cflags: cached_cflags}) + if (obj != null) push(support_objects, obj) + }) + } + + return Build.build_module_dylib(pkg, file, _target, {buildtype: _buildtype, extra_objects: support_objects, cflags: cached_cflags}) +} + // Build a dynamic library for a package (one dylib per C file) // Returns array of {file, symbol, dylib} for each module // Also writes a manifest mapping symbols to dylib paths diff --git a/compile_worker.ce b/compile_worker.ce index c3918cfb..4549b3b3 100644 --- a/compile_worker.ce +++ b/compile_worker.ce @@ -4,6 +4,7 @@ // {type: 'script', path, package} — bytecode compile // {type: 'native_script', path, package} — native compile // {type: 'c_package', package} — C package build +// {type: 'c_file', package, file} — single C module build // // Replies with {ok: true/false, path} and stops. @@ -11,7 +12,7 @@ var shop = use('internal/shop') var build = use('build') $receiver(function(msg) { - var name = msg.path || msg.package + var name = msg.path || (msg.file ? msg.package + '/' + msg.file : msg.package) var _work = function() { if (msg.type == 'script') { log.console('compile_worker: compiling ' + name) @@ -22,6 +23,9 @@ $receiver(function(msg) { } else if (msg.type == 'c_package') { log.console('compile_worker: building package ' + name) build.build_dynamic(msg.package, null, null, null) + } else if (msg.type == 'c_file') { + log.console('compile_worker: building ' + name) + build.compile_c_module(msg.package, msg.file) } log.console('compile_worker: done ' + name) send(msg, {ok: true, path: name}) diff --git a/internal/engine.cm b/internal/engine.cm index 69d49de3..58a5c0d1 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -922,6 +922,7 @@ function load_log_config() { type: "console", format: "pretty", channels: ["*"], + exclude: ["system", "shop", "build"], stack: ["error"] } } @@ -1845,11 +1846,44 @@ $_.clock(_ => { // --- Auto-boot: pre-compile uncached deps before running --- // Only auto-boot for the root program (not child actors, not boot itself). - // Delegates all discovery + compilation to boot.ce (separate actor/memory). + // Quick-check deps inline; only spawn boot actor if something needs compiling. var _is_root_actor = !_cell.args.overling_id var _skip_boot = !_is_root_actor || prog == 'boot' || prog == 'compile_worker' - if (_skip_boot) { + var _needs_boot = false + var _boot_check = function() { + var _deps = shop.trace_deps(prog_path) + var _bi = 0 + var _bs = null + while (_bi < length(_deps.scripts)) { + _bs = _deps.scripts[_bi] + if (native_mode) { + if (!shop.is_native_cached(_bs.path, _bs.package)) { + _needs_boot = true + return + } + } else { + if (!shop.is_cached(_bs.path)) { + _needs_boot = true + return + } + } + _bi = _bi + 1 + } + _bi = 0 + while (_bi < length(_deps.c_packages)) { + if (_deps.c_packages[_bi] != 'core' && !shop.has_c_manifest(_deps.c_packages[_bi])) { + _needs_boot = true + return + } + _bi = _bi + 1 + } + } disruption { + _needs_boot = true + } + if (!_skip_boot) _boot_check() + + if (_skip_boot || !_needs_boot) { run_program() } else { $_.start(function(event) { diff --git a/internal/shop.cm b/internal/shop.cm index 7bd58feb..6aa9d62a 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -2288,6 +2288,52 @@ Shop.load_as_dylib = function(path, pkg) { return os.native_module_load_named(result._handle, result._sym, env) } +// Trace all transitive module dependencies for a file. +// Returns {scripts: [{path, package}], c_packages: [string]} +Shop.trace_deps = function(file_path) { + var visited = {} + var scripts = [] + var c_packages = {} + + function trace(fp) { + if (visited[fp]) return + visited[fp] = true + var fi = Shop.file_info(fp) + var file_pkg = fi.package + var idx = null + var j = 0 + var imp = null + var rinfo = null + if (ends_with(fp, '.cm')) + scripts[] = {path: fp, package: file_pkg} + var _trace = function() { + idx = Shop.index_file(fp) + if (!idx || !idx.imports) return + j = 0 + while (j < length(idx.imports)) { + imp = idx.imports[j] + rinfo = Shop.resolve_import_info(imp.module_path, file_pkg) + if (rinfo) { + if (rinfo.type == 'script' && rinfo.resolved_path) + trace(rinfo.resolved_path) + else if (rinfo.type == 'native' && rinfo.package) + c_packages[rinfo.package] = true + } + j = j + 1 + } + } disruption {} + _trace() + } + + trace(file_path) + return {scripts: scripts, c_packages: array(c_packages)} +} + +// Check if a C package has a build manifest (was previously built) +Shop.has_c_manifest = function(pkg) { + return fd.is_file(dylib_manifest_path(pkg)) +} + // Check if a .cm file has a cached bytecode artifact (mach or mcode) Shop.is_cached = function(path) { if (!fd.is_file(path)) return false diff --git a/mcode.cm b/mcode.cm index f6354253..cc8a55a4 100644 --- a/mcode.cm +++ b/mcode.cm @@ -1207,6 +1207,8 @@ var mcode = function(ast) { var f = alloc_slot() var val = alloc_slot() var skip = gen_label("filter_skip") + var bail = gen_label("filter_bail") + var bool_check = alloc_slot() var ctx = {fn: fn_slot, fn_arity: fn_arity, result: val, null_s: null_s, frame: f, zero: zero, one: one, az: az, ao: ao, prefix: "filter"} var L = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one, @@ -1219,12 +1221,19 @@ var mcode = function(ast) { emit_2("length", fn_arity, fn_slot) emit_forward_loop(L, function(L) { emit_arity_call(ctx, [L.item, L.i], 2) - emit_jump_cond("wary_false", val, skip) + emit_2("is_bool", bool_check, val) + emit_jump_cond("jump_false", bool_check, bail) + emit_jump_cond("jump_false", val, skip) emit_2("push", result, L.item) emit_label(skip) return null }) emit_2("move", dest, result) + var end = gen_label("filter_end") + emit_jump(end) + emit_label(bail) + emit_1("null", dest) + emit_label(end) return dest } diff --git a/source/mach.c b/source/mach.c index 87bc80b8..58f445b4 100644 --- a/source/mach.c +++ b/source/mach.c @@ -246,7 +246,7 @@ typedef enum MachOpcode { MACH_LENGTH, /* R(A) = length(R(B)) — array/text/blob length */ MACH_IS_PROXY, /* R(A) = is_function(R(B)) && R(B).length == 2 */ MACH_IS_BLOB, /* R(A) = is_blob(R(B)) */ - MACH_IS_DATA, /* R(A) = is_data(R(B)) — plain record, not array/func/blob */ + MACH_IS_DATA, /* R(A) = is_data(R(B)) — not function or null */ MACH_IS_TRUE, /* R(A) = (R(B) === true) */ MACH_IS_FALSE, /* R(A) = (R(B) === false) */ MACH_IS_FIT, /* R(A) = is_fit(R(B)) — safe integer */ @@ -2398,10 +2398,8 @@ vm_dispatch: VM_BREAK(); VM_CASE(MACH_IS_DATA): { JSValue v = frame->slots[b]; - int result = 0; - if (mist_is_gc_object(v) && !mist_is_array(v) - && !mist_is_function(v) && !mist_is_blob(v)) - result = 1; + /* data is text, number, logical, array, blob, or record — not function, null */ + int result = (v != JS_NULL && !mist_is_function(v)); frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } diff --git a/source/runtime.c b/source/runtime.c index 48585008..432b64fe 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -8375,7 +8375,15 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu /* Map - GC-safe: root result throughout, use rooted refs for func and array */ int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (arg1_ref.val))->length; - int reverse = argc > 2 && JS_ToBool (ctx, argv[2]); + int reverse = 0; + if (argc > 2 && !JS_IsNull (argv[2])) { + if (!JS_IsBool (argv[2])) { + JS_PopGCRef (ctx, &arg1_ref); + JS_PopGCRef (ctx, &arg0_ref); + return JS_RaiseDisrupt (ctx, "array: reverse must be a logical"); + } + reverse = JS_VALUE_GET_BOOL (argv[2]); + } JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; JSGCRef result_ref; @@ -8505,7 +8513,7 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu if (!JS_IsInteger (argv[1])) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); - return JS_NULL; + return JS_RaiseDisrupt (ctx, "array slice: from must be an integer"); } int from = JS_VALUE_GET_INT (argv[1]); int to; @@ -8513,7 +8521,7 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu if (!JS_IsNumber (argv[2]) || !JS_IsInteger (argv[2])) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); - return JS_NULL; + return JS_RaiseDisrupt (ctx, "array slice: to must be an integer"); } to = JS_VALUE_GET_INT (argv[2]); } else { @@ -8550,7 +8558,7 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); - return JS_NULL; + return JS_RaiseDisrupt (ctx, "array: invalid argument combination"); } /* array(object) - keys */ @@ -8822,7 +8830,12 @@ static JSValue js_cell_array_reduce (JSContext *ctx, JSValue this_val, int argc, word_t len = arr->len; JSValue fn = argv[1]; - int reverse = argc > 3 && JS_ToBool (ctx, argv[3]); + int reverse = 0; + if (argc > 3 && !JS_IsNull (argv[3])) { + if (!JS_IsBool (argv[3])) + return JS_RaiseDisrupt (ctx, "reduce: reverse must be a logical"); + reverse = JS_VALUE_GET_BOOL (argv[3]); + } JSGCRef acc_ref; JSValue acc; @@ -8898,7 +8911,12 @@ static JSValue js_cell_array_for (JSContext *ctx, JSValue this_val, int argc, JS word_t len = arr->len; if (len == 0) return JS_NULL; - int reverse = argc > 2 && JS_ToBool (ctx, argv[2]); + int reverse = 0; + if (argc > 2 && !JS_IsNull (argv[2])) { + if (!JS_IsBool (argv[2])) + return JS_RaiseDisrupt (ctx, "arrfor: reverse must be a logical"); + reverse = JS_VALUE_GET_BOOL (argv[2]); + } JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; if (reverse) { @@ -8938,7 +8956,12 @@ static JSValue js_cell_array_find (JSContext *ctx, JSValue this_val, int argc, J JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; - int reverse = argc > 2 && JS_ToBool (ctx, argv[2]); + int reverse = 0; + if (argc > 2 && !JS_IsNull (argv[2])) { + if (!JS_IsBool (argv[2])) + return JS_RaiseDisrupt (ctx, "find: reverse must be a logical"); + reverse = JS_VALUE_GET_BOOL (argv[2]); + } int32_t from; if (argc > 3 && !JS_IsNull (argv[3])) { if (JS_ToInt32 (ctx, &from, argv[3])) return JS_NULL; @@ -9416,7 +9439,7 @@ static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSVal /* Use text directly as key */ JSValue prop_key = js_key_from_string (ctx, key); JSValue val; - if (argc < 2 || JS_IsNull (func_ref.val)) { + if (argc < 2) { val = JS_TRUE; } else if (is_func) { JSValue arg_key = key; @@ -11336,11 +11359,9 @@ static JSValue js_cell_is_blob (JSContext *ctx, JSValue this_val, int argc, JSVa static JSValue js_cell_is_data (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; - if (!mist_is_gc_object (val)) return JS_FALSE; - if (JS_IsArray (val)) return JS_FALSE; + /* data is text, number, logical, array, blob, or record — not function, null */ + if (JS_IsNull (val)) return JS_FALSE; if (JS_IsFunction (val)) return JS_FALSE; - if (mist_is_blob (val)) return JS_FALSE; - /* Check if it's a plain object (prototype is Object.prototype or null) */ return JS_TRUE; } diff --git a/source/suite.c b/source/suite.c index 57551026..ea4eff8c 100644 --- a/source/suite.c +++ b/source/suite.c @@ -1323,6 +1323,10 @@ TEST(cell_not) { CELL CORE FUNCTION TESTS ============================================================================ */ +static JSValue cfunc_returns_99(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + return JS_NewInt32(ctx, 99); +} + TEST(cell_length_array) { JSGCRef arr_ref; JS_PushGCRef(ctx, &arr_ref); @@ -1343,6 +1347,81 @@ TEST(cell_length_string) { return 1; } +TEST(cell_length_blob) { + uint8_t data[] = {0xAA, 0xBB, 0xCC}; + JSValue blob = js_new_blob_stoned_copy(ctx, data, 3); + JSValue result = JS_CellLength(ctx, blob); + /* blob length is in bits: 3 bytes = 24 bits */ + ASSERT_INT(result, 24); + return 1; +} + +TEST(cell_length_null) { + JSValue result = JS_CellLength(ctx, JS_NULL); + ASSERT(JS_IsNull(result)); + return 1; +} + +TEST(cell_length_logical) { + JSValue result_t = JS_CellLength(ctx, JS_TRUE); + JSValue result_f = JS_CellLength(ctx, JS_FALSE); + ASSERT(JS_IsNull(result_t)); + ASSERT(JS_IsNull(result_f)); + return 1; +} + +TEST(cell_length_number) { + JSValue result = JS_CellLength(ctx, JS_NewInt32(ctx, 42)); + ASSERT(JS_IsNull(result)); + return 1; +} + +TEST(cell_length_function_arity) { + JSGCRef func_ref; + JS_PushGCRef(ctx, &func_ref); + func_ref.val = JS_NewCFunction(ctx, cfunc_add, "add", 2); + JSValue result = JS_CellLength(ctx, func_ref.val); + JS_PopGCRef(ctx, &func_ref); + ASSERT_INT(result, 2); + return 1; +} + +TEST(cell_length_object_no_length) { + JSGCRef obj_ref; + JS_PushGCRef(ctx, &obj_ref); + obj_ref.val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, obj_ref.val, "x", JS_NewInt32(ctx, 10)); + JSValue result = JS_CellLength(ctx, obj_ref.val); + JS_PopGCRef(ctx, &obj_ref); + ASSERT(JS_IsNull(result)); + return 1; +} + +TEST(cell_length_object_number) { + JSGCRef obj_ref; + JS_PushGCRef(ctx, &obj_ref); + obj_ref.val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, obj_ref.val, "length", JS_NewInt32(ctx, 7)); + JSValue result = JS_CellLength(ctx, obj_ref.val); + JS_PopGCRef(ctx, &obj_ref); + ASSERT_INT(result, 7); + return 1; +} + +TEST(cell_length_object_function) { + JSGCRef obj_ref, func_ref; + JS_PushGCRef(ctx, &obj_ref); + JS_PushGCRef(ctx, &func_ref); + obj_ref.val = JS_NewObject(ctx); + func_ref.val = JS_NewCFunction(ctx, cfunc_returns_99, "length", 0); + JS_SetPropertyStr(ctx, obj_ref.val, "length", func_ref.val); + JSValue result = JS_CellLength(ctx, obj_ref.val); + JS_PopGCRef(ctx, &func_ref); + JS_PopGCRef(ctx, &obj_ref); + ASSERT_INT(result, 99); + return 1; +} + TEST(cell_reverse_array) { JSGCRef arr_ref; JS_PushGCRef(ctx, &arr_ref); @@ -2245,6 +2324,14 @@ int run_c_test_suite(JSContext *ctx) printf("\nCell Core Functions:\n"); RUN_TEST(cell_length_array); RUN_TEST(cell_length_string); + RUN_TEST(cell_length_blob); + RUN_TEST(cell_length_null); + RUN_TEST(cell_length_logical); + RUN_TEST(cell_length_number); + RUN_TEST(cell_length_function_arity); + RUN_TEST(cell_length_object_no_length); + RUN_TEST(cell_length_object_number); + RUN_TEST(cell_length_object_function); RUN_TEST(cell_reverse_array); RUN_TEST(cell_reverse_string); RUN_TEST(cell_meme_shallow); diff --git a/vm_suite.ce b/vm_suite.ce index 7909ee23..1a44957f 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -984,11 +984,11 @@ run("is_proto", function() { run("is_data", function() { if (!is_data({})) fail("is_data {} should be true") - if (is_data([])) fail("is_data [] should be false") - if (is_data(42)) fail("is_data number should be false") - if (is_data("hello")) fail("is_data string should be false") + if (!is_data([])) fail("is_data [] should be true") + if (!is_data(42)) fail("is_data number should be true") + if (!is_data("hello")) fail("is_data string should be true") if (is_data(null)) fail("is_data null should be false") - if (is_data(true)) fail("is_data bool should be false") + if (!is_data(true)) fail("is_data bool should be true") if (is_data(function(){})) fail("is_data function should be false") }) @@ -1097,6 +1097,26 @@ run("length number", function() { if (length(123) != null) fail("length number should return null") }) +run("length logical", function() { + if (length(true) != null) fail("length true should return null") + if (length(false) != null) fail("length false should return null") +}) + +run("length object no length", function() { + var obj = {x: 1, y: 2} + if (length(obj) != null) fail("length of object without length should return null") +}) + +run("length object number", function() { + var obj = {length: 7} + if (length(obj) != 7) fail("length of object with number length should return 7") +}) + +run("length object function", function() { + var obj = {length: function() { return 42 }} + if (length(obj) != 42) fail("length of object with function length should call it") +}) + // ============================================================================ // GLOBAL FUNCTIONS - REVERSE // ============================================================================ @@ -3530,10 +3550,10 @@ run("inline map intrinsic is_data", function() { var items = [{}, [], "hello", 42, true] var result = array(items, is_data) if (result[0] != true) fail("is_data {} should be true") - if (result[1] != false) fail("is_data [] should be false") - if (result[2] != false) fail("is_data string should be false") - if (result[3] != false) fail("is_data number should be false") - if (result[4] != false) fail("is_data bool should be false") + if (result[1] != true) fail("is_data [] should be true") + if (result[2] != true) fail("is_data string should be true") + if (result[3] != true) fail("is_data number should be true") + if (result[4] != true) fail("is_data bool should be true") if (length(result) != 5) fail("result length should be 5") }) @@ -5577,9 +5597,9 @@ run("blob w16 w32 wf", function() { if (length(b) != 80) fail("expected 80 bits, got " + text(length(b))) }) -run("blob is_data false for blob", function() { +run("blob is_data true for blob", function() { var b = blob() - if (is_data(b)) fail("blob should not be is_data") + if (!is_data(b)) fail("blob should be is_data") }) run("blob text hex format", function() { @@ -7232,30 +7252,36 @@ run("reverse does not mutate", function() { // Tests that inlined loops correctly handle truthy/falsy for all types. // ============================================================================ -run("filter inline - string truthiness", function() { +run("filter inline - non-empty strings", function() { var items = ["hello", "", "world", "", "ok"] - var result = filter(items, function(x) { return x }) - assert_eq(length(result), 3, "filter truthy strings count") - assert_eq(result[0], "hello", "filter truthy [0]") - assert_eq(result[1], "world", "filter truthy [1]") - assert_eq(result[2], "ok", "filter truthy [2]") + var result = filter(items, function(x) { return length(x) > 0 }) + assert_eq(length(result), 3, "filter non-empty strings count") + assert_eq(result[0], "hello", "filter non-empty [0]") + assert_eq(result[1], "world", "filter non-empty [1]") + assert_eq(result[2], "ok", "filter non-empty [2]") }) -run("filter inline - number truthiness", function() { +run("filter inline - nonzero numbers", function() { var items = [0, 1, 0, 2, 0, 3] - var result = filter(items, function(x) { return x }) - assert_eq(length(result), 3, "filter truthy numbers count") - assert_eq(result[0], 1, "filter truthy num [0]") - assert_eq(result[1], 2, "filter truthy num [1]") - assert_eq(result[2], 3, "filter truthy num [2]") + var result = filter(items, function(x) { return x != 0 }) + assert_eq(length(result), 3, "filter nonzero numbers count") + assert_eq(result[0], 1, "filter nonzero [0]") + assert_eq(result[1], 2, "filter nonzero [1]") + assert_eq(result[2], 3, "filter nonzero [2]") }) -run("filter inline - null truthiness", function() { +run("filter inline - non-null values", function() { var items = [1, null, 2, null, 3] - var result = filter(items, function(x) { return x }) + var result = filter(items, function(x) { return !is_null(x) }) assert_eq(length(result), 3, "filter non-null count") }) +run("filter non-boolean callback returns null", function() { + // Callbacks that return non-boolean values should cause filter to return null + var result = filter([1, 2, 3], function(x) { return x }) + assert_eq(result, null, "filter truthy-return is null") +}) + run("filter inline - negated predicate", function() { var items = ["hello", "", "world", ""] var result = filter(items, function(x) { return !x }) @@ -7717,6 +7743,873 @@ run("function explicit return before trailing expr", function() { assert_eq(fn(), 7, "explicit return should take precedence") }) +// ============================================================================ +// COMPREHENSIVE EDGE CASE TESTS +// ============================================================================ + +// --- array() edge cases --- + +run("array(number) negative disrupts", function() { + var result = array(-1) + assert_eq(result, null, "array(-1) returns null") +}) + +run("array(float) disrupts", function() { + if (!should_disrupt(function() { array(3.5) })) fail("array(3.5) should disrupt") +}) + +run("array(number, value) all initialized", function() { + var a = array(4, "x") + assert_eq(a[0], "x", "init [0]") + assert_eq(a[3], "x", "init [3]") + assert_eq(length(a), 4, "init length") +}) + +run("array(number, null) all null", function() { + var a = array(3, null) + assert_eq(a[0], null, "null init [0]") + assert_eq(a[2], null, "null init [2]") +}) + +run("array(number, function) index passed", function() { + var a = array(4, function(i) { return i * i }) + assert_eq(a[0], 0, "fn [0]") + assert_eq(a[1], 1, "fn [1]") + assert_eq(a[2], 4, "fn [2]") + assert_eq(a[3], 9, "fn [3]") +}) + +run("array(array, function) basic map", function() { + var result = array([1, 2, 3], function(x) { return x + 10 }) + assert_eq(result[0], 11, "map [0]") + assert_eq(result[2], 13, "map [2]") +}) + +run("array(array, function, true) reverse map", function() { + var order = [] + var result = array([10, 20, 30], function(x) { + order[] = x + return x + }, true) + assert_eq(order[0], 30, "reverse order [0]") + assert_eq(order[1], 20, "reverse order [1]") + assert_eq(order[2], 10, "reverse order [2]") + assert_eq(length(result), 3, "reverse map length") +}) + +run("array(array, function, false) forward map", function() { + var order = [] + array([1, 2, 3], function(x) { + order[] = x + return x + }, false) + assert_eq(order[0], 1, "forward order [0]") + assert_eq(order[2], 3, "forward order [2]") +}) + +run("array(array, function, reverse) non-boolean reverse disrupts", function() { + if (!should_disrupt(function() { array([1, 2, 3], function(x) { return x }, 42) })) + fail("array map with integer reverse should disrupt") +}) + +run("array(array, function, reverse) string reverse disrupts", function() { + if (!should_disrupt(function() { array([1, 2, 3], function(x) { return x }, "yes") })) + fail("array map with string reverse should disrupt") +}) + +run("array(array, function, reverse) null reverse ok", function() { + // null is treated as absent — forward iteration + var order = [] + array([1, 2, 3], function(x) { + order[] = x + return x + }, null) + assert_eq(order[0], 1, "null reverse forward") +}) + +run("array(array, function, true, exit) early exit", function() { + var result = array([1, 2, 3, 4, 5], function(x) { + if (x == 4) return "stop" + return x * 10 + }, false, "stop") + assert_eq(length(result), 3, "exit length") + assert_eq(result[0], 10, "exit [0]") + assert_eq(result[2], 30, "exit [2]") +}) + +run("array(array, function, true, exit) reverse early exit", function() { + var result = array([1, 2, 3, 4, 5], function(x) { + if (x == 2) return "stop" + return x * 10 + }, true, "stop") + // reverse: processes 5,4,3,2 -> stops at 2 + assert_eq(result[0], 50, "rev exit [0]") + assert_eq(result[1], 40, "rev exit [1]") + assert_eq(result[2], 30, "rev exit [2]") + assert_eq(length(result), 3, "rev exit length") +}) + +run("array(array, from, to) wrong type for to disrupts", function() { + if (!should_disrupt(function() { array([1, 2, 3, 4, 5], 1, true) })) + fail("array slice with boolean to should disrupt") +}) + +run("array(array, from, to) wrong type for from disrupts", function() { + if (!should_disrupt(function() { array([1, 2, 3, 4, 5], true) })) + fail("array with boolean from should disrupt") +}) + +run("array(array, from, to) float from disrupts", function() { + if (!should_disrupt(function() { array([1, 2, 3, 4, 5], 1.5, 3) })) + fail("array slice with float from should disrupt") +}) + +run("array(array, from, to) from > to returns null", function() { + var result = array([1, 2, 3, 4, 5], 3, 1) + assert_eq(result, null, "slice from > to returns null") +}) + +run("array(array, from, to) from == to empty", function() { + var result = array([1, 2, 3], 2, 2) + assert_eq(length(result), 0, "slice from==to empty") +}) + +run("array(array, from, to) full range", function() { + var result = array([10, 20, 30], 0, 3) + assert_eq(length(result), 3, "full range length") + assert_eq(result[0], 10, "full range [0]") + assert_eq(result[2], 30, "full range [2]") +}) + +run("array(array, from) to defaults to end", function() { + var result = array([10, 20, 30, 40], 2) + assert_eq(length(result), 2, "default to length") + assert_eq(result[0], 30, "default to [0]") + assert_eq(result[1], 40, "default to [1]") +}) + +run("array(array, array) concat both empty", function() { + var result = array([], []) + assert_eq(length(result), 0, "concat both empty") +}) + +run("array copy empty", function() { + var copy = array([]) + assert_eq(length(copy), 0, "copy empty length") +}) + +run("array from empty record", function() { + var keys = array({}) + assert_eq(length(keys), 0, "empty record keys") +}) + +run("array from text single char", function() { + var chars = array("x") + assert_eq(length(chars), 1, "single char length") + assert_eq(chars[0], "x", "single char value") +}) + +run("array(text, length) dice exact", function() { + var result = array("abcdef", 3) + assert_eq(length(result), 2, "dice exact length") + assert_eq(result[0], "abc", "dice exact [0]") + assert_eq(result[1], "def", "dice exact [1]") +}) + +// --- text() edge cases --- + +run("text(text, from, to) from > to returns null", function() { + assert_eq(text("hello", 3, 1), null, "text from>to null") +}) + +run("text(text, from, to) empty range", function() { + assert_eq(text("hello", 2, 2), "", "text from==to empty") +}) + +run("text(text, from) from at length", function() { + assert_eq(text("hello", 5), "", "text from==len empty") +}) + +run("text(text, from) from beyond length", function() { + var result = text("hello", 6) + // from > len should clamp + assert_eq(result, "", "text from>len") +}) + +run("text(text, from, to) full range", function() { + assert_eq(text("hello", 0, 5), "hello", "text full range") +}) + +run("text(text, negative from, negative to)", function() { + assert_eq(text("hello", -3, -1), "ll", "text neg from neg to") +}) + +run("text from number format string", function() { + assert_eq(text(255, "h"), "FF", "text hex format") + assert_eq(text(10, "b"), "1010", "text binary format") + assert_eq(text(8, "o"), "10", "text octal format") +}) + +run("text from array disrupts on non-text element", function() { + if (!should_disrupt(function() { text([1, 2, 3]) })) + fail("text(array of numbers) should disrupt") +}) + +run("text from array disrupts on mixed", function() { + if (!should_disrupt(function() { text(["a", 1, "b"]) })) + fail("text(array with number) should disrupt") +}) + +run("text from logical", function() { + assert_eq(text(true), "true", "text(true)") + assert_eq(text(false), "false", "text(false)") +}) + +run("text from null", function() { + assert_eq(text(null), "null", "text(null)") +}) + +// --- number() edge cases --- + +run("number from invalid text returns null", function() { + assert_eq(number("abc"), null, "number('abc')") + assert_eq(number(""), null, "number('')") +}) + +run("number from null returns null", function() { + assert_eq(number(null), null, "number(null)") +}) + +run("number radix boundaries", function() { + assert_eq(number("10", 2), 2, "number base 2") + assert_eq(number("10", 36), 36, "number base 36") + assert_eq(number("z", 36), 35, "number z base 36") +}) + +run("number invalid for radix returns null", function() { + assert_eq(number("g", 16), null, "number('g',16)") + assert_eq(number("2", 2), null, "number('2',2)") +}) + +// --- object() edge cases --- + +run("object copy empty", function() { + var copy = object({}) + assert_eq(length(array(copy)), 0, "object copy empty") +}) + +run("object merge empty into filled", function() { + var result = object({a: 1}, {}) + assert_eq(result.a, 1, "merge empty keeps a") +}) + +run("object merge filled into empty", function() { + var result = object({}, {b: 2}) + assert_eq(result.b, 2, "merge into empty gets b") +}) + +run("object select missing key", function() { + var result = object({a: 1, b: 2}, ["a", "z"]) + assert_eq(result.a, 1, "select present key") + assert_eq(result.z, null, "select missing key null") +}) + +run("object from empty keys", function() { + var r = object([]) + assert_eq(length(array(r)), 0, "empty keys") +}) + +run("object from keys with null value", function() { + var r = object(["a", "b"], null) + assert_eq(r.a, null, "null value a") + assert_eq(r.b, null, "null value b") +}) + +// --- filter() edge cases --- + +run("filter non-boolean return returns null", function() { + var result = filter([1, 2, 3], function(x) { return x }) + assert_eq(result, null, "filter non-boolean returns null") +}) + +// filter with non-function: statically rejected by compiler (invoking int) + +run("filter on number disrupts", function() { + if (!should_disrupt(function() { filter(42, function(x) { return true }) })) + fail("filter on number should disrupt") +}) + +run("filter with index callback", function() { + var result = filter([10, 20, 30, 40], function(el, i) { return i < 2 }) + assert_eq(length(result), 2, "filter index length") + assert_eq(result[0], 10, "filter index [0]") + assert_eq(result[1], 20, "filter index [1]") +}) + +// --- find() edge cases --- + +run("find value search exact match", function() { + assert_eq(find([10, 20, 30], 20), 1, "find value 20") +}) + +run("find value not present", function() { + assert_eq(find([10, 20, 30], 99), null, "find value missing") +}) + +run("find reverse value", function() { + assert_eq(find([1, 2, 1, 2], 1, true), 2, "find reverse value") +}) + +run("find with from index", function() { + assert_eq(find([1, 2, 3, 2, 1], 2, false, 2), 3, "find from index") +}) + +run("find predicate with index", function() { + assert_eq(find(["a", "bb", "ccc"], function(el, i) { return length(el) == 2 }), 1, "find predicate idx") +}) + +run("find empty array", function() { + assert_eq(find([], 1), null, "find empty") + assert_eq(find([], function(x) { return true }), null, "find empty fn") +}) + +run("find on number disrupts", function() { + if (!should_disrupt(function() { find(42, 4) })) + fail("find on number should disrupt") +}) + +// --- reduce() edge cases --- + +run("reduce empty no initial returns null", function() { + assert_eq(reduce([], function(a, b) { return a + b }), null, "reduce empty") +}) + +run("reduce empty with initial returns initial", function() { + assert_eq(reduce([], function(a, b) { return a + b }, 99), 99, "reduce empty initial") +}) + +run("reduce single no initial returns element", function() { + assert_eq(reduce([42], function(a, b) { return a + b }), 42, "reduce single") +}) + +run("reduce single with initial", function() { + assert_eq(reduce([5], function(a, b) { return a + b }, 10), 15, "reduce single initial") +}) + +run("reduce reverse", function() { + var result = reduce([1, 2, 3], function(a, b) { return a - b }, 0, true) + // reverse: 0 - 3 = -3, -3 - 2 = -5, -5 - 1 = -6 + assert_eq(result, -6, "reduce reverse") +}) + +// reduce with non-function: statically rejected by compiler (invoking int) + +run("reduce on number disrupts", function() { + if (!should_disrupt(function() { reduce(42, function(a, b) { return a + b }) })) + fail("reduce on number should disrupt") +}) + +// --- sort() edge cases --- + +run("sort empty array", function() { + var result = sort([]) + assert_eq(length(result), 0, "sort empty") +}) + +run("sort single element", function() { + var result = sort([42]) + assert_eq(result[0], 42, "sort single") +}) + +run("sort already sorted", function() { + var result = sort([1, 2, 3]) + assert_eq(result[0], 1, "sort sorted [0]") + assert_eq(result[2], 3, "sort sorted [2]") +}) + +run("sort descending input", function() { + var result = sort([5, 4, 3, 2, 1]) + assert_eq(result[0], 1, "sort desc [0]") + assert_eq(result[4], 5, "sort desc [4]") +}) + +run("sort strings", function() { + var result = sort(["cherry", "apple", "banana"]) + assert_eq(result[0], "apple", "sort str [0]") + assert_eq(result[2], "cherry", "sort str [2]") +}) + +run("sort by field name", function() { + var items = [{n: "c", v: 3}, {n: "a", v: 1}, {n: "b", v: 2}] + var result = sort(items, "n") + assert_eq(result[0].n, "a", "sort field [0]") + assert_eq(result[2].n, "c", "sort field [2]") +}) + +run("sort by array index", function() { + var items = [[3, "c"], [1, "a"], [2, "b"]] + var result = sort(items, 0) + assert_eq(result[0][0], 1, "sort idx [0]") + assert_eq(result[2][0], 3, "sort idx [2]") +}) + +run("sort does not mutate original", function() { + var arr = [3, 1, 2] + var result = sort(arr) + assert_eq(arr[0], 3, "sort no mutate") +}) + +run("sort negative numbers", function() { + var result = sort([-5, 3, -1, 0, 2]) + assert_eq(result[0], -5, "sort neg [0]") + assert_eq(result[4], 3, "sort neg [4]") +}) + +run("sort stable", function() { + var items = [{k: 1, v: "a"}, {k: 2, v: "b"}, {k: 1, v: "c"}] + var result = sort(items, "k") + assert_eq(result[0].v, "a", "sort stable [0]") + assert_eq(result[1].v, "c", "sort stable [1]") + assert_eq(result[2].v, "b", "sort stable [2]") +}) + +// --- reverse() edge cases --- + +run("reverse single element", function() { + var result = reverse([42]) + assert_eq(result[0], 42, "reverse single") + assert_eq(length(result), 1, "reverse single length") +}) + +run("reverse does not mutate", function() { + var arr = [1, 2, 3] + var rev = reverse(arr) + assert_eq(arr[0], 1, "reverse no mutate") + assert_eq(rev[0], 3, "reverse result [0]") +}) + +run("reverse text", function() { + var result = reverse("abc") + assert_eq(result, "cba", "reverse text") +}) + +run("reverse empty text", function() { + var result = reverse("") + assert_eq(result, "", "reverse empty text") +}) + +// --- every() / some() edge cases --- + +run("every empty returns true", function() { + assert_eq(every([], function(x) { return false }), true, "every empty") +}) + +run("every all pass", function() { + assert_eq(every([2, 4, 6], function(x) { return x % 2 == 0 }), true, "every all") +}) + +run("every one fails", function() { + assert_eq(every([2, 3, 6], function(x) { return x % 2 == 0 }), false, "every one fail") +}) + +run("some empty returns false", function() { + assert_eq(some([], function(x) { return true }), false, "some empty") +}) + +run("some one match", function() { + assert_eq(some([1, 3, 5], function(x) { return x == 3 }), true, "some one match") +}) + +run("some no match", function() { + assert_eq(some([1, 3, 5], function(x) { return x > 10 }), false, "some no match") +}) + +// --- apply() edge cases --- + +run("apply basic", function() { + var fn = function(a, b) { return a + b } + assert_eq(apply(fn, [3, 4]), 7, "apply basic") +}) + +run("apply no args", function() { + var fn = function() { return 42 } + assert_eq(apply(fn, []), 42, "apply no args") +}) + +run("apply non-function returns value", function() { + assert_eq(apply(42, [1, 2]), 42, "apply non-fn") +}) + +run("apply single arg", function() { + var fn = function(x) { return x * 2 } + assert_eq(apply(fn, [5]), 10, "apply single") +}) + +run("apply too many args disrupts", function() { + var fn = function(a) { return a } + if (!should_disrupt(function() { apply(fn, [1, 2, 3]) })) + fail("apply with too many args should disrupt") +}) + +// --- abs() edge cases --- + +run("abs non-number returns null", function() { + assert_eq(abs("5"), null, "abs string") + assert_eq(abs(null), null, "abs null") + assert_eq(abs(true), null, "abs bool") +}) + +run("abs zero", function() { + assert_eq(abs(0), 0, "abs zero") +}) + +// --- sign() edge cases --- + +run("sign non-number returns null", function() { + assert_eq(sign("5"), null, "sign string") + assert_eq(sign(null), null, "sign null") +}) + +// --- neg() edge cases --- + +run("neg non-number returns null", function() { + assert_eq(neg("5"), null, "neg string") + assert_eq(neg(null), null, "neg null") +}) + +// --- not() edge cases --- + +run("not non-logical returns null", function() { + assert_eq(not(0), null, "not 0") + assert_eq(not(1), null, "not 1") + assert_eq(not("true"), null, "not string") + assert_eq(not(null), null, "not null") +}) + +// --- min/max edge cases --- + +run("min non-number returns null", function() { + assert_eq(min("3", 5), null, "min string") + assert_eq(min(3, "5"), null, "min string 2") + assert_eq(min(null, 5), null, "min null") +}) + +run("max non-number returns null", function() { + assert_eq(max("3", 5), null, "max string") + assert_eq(max(3, null), null, "max null") +}) + +// --- modulo() edge cases --- + +run("modulo zero divisor returns null", function() { + assert_eq(modulo(10, 0), null, "modulo div 0") +}) + +run("modulo non-number returns null", function() { + assert_eq(modulo("10", 3), null, "modulo string") + assert_eq(modulo(10, "3"), null, "modulo string 2") +}) + +run("modulo zero dividend", function() { + assert_eq(modulo(0, 5), 0, "modulo 0 dividend") +}) + +// --- remainder() edge cases --- + +run("remainder basic", function() { + assert_eq(remainder(7, 3), 1, "remainder 7,3") + assert_eq(remainder(-7, 3), -1, "remainder -7,3") +}) + +// --- floor/ceiling/round/trunc edge cases --- + +run("floor integer passthrough", function() { + assert_eq(floor(5), 5, "floor int") +}) + +run("floor with place", function() { + assert_eq(floor(12.3775, -2), 12.37, "floor place -2") + assert_eq(floor(-12.3775, -2), -12.38, "floor neg place -2") +}) + +run("ceiling integer passthrough", function() { + assert_eq(ceiling(5), 5, "ceiling int") +}) + +run("ceiling with place", function() { + assert_eq(ceiling(12.3775, -2), 12.38, "ceiling place -2") + assert_eq(ceiling(-12.3775, -2), -12.37, "ceiling neg place -2") +}) + +run("round with place", function() { + assert_eq(round(12.3775, -2), 12.38, "round place -2") +}) + +run("trunc toward zero", function() { + assert_eq(trunc(3.7), 3, "trunc pos") + assert_eq(trunc(-3.7), -3, "trunc neg") +}) + +run("trunc with place", function() { + assert_eq(trunc(12.3775, -2), 12.37, "trunc place -2") + assert_eq(trunc(-12.3775, -2), -12.37, "trunc neg place -2") +}) + +// --- whole/fraction edge cases --- + +run("whole non-number returns null", function() { + assert_eq(whole("5"), null, "whole string") + assert_eq(whole(null), null, "whole null") +}) + +run("fraction non-number returns null", function() { + assert_eq(fraction("5"), null, "fraction string") + assert_eq(fraction(null), null, "fraction null") +}) + +run("whole integer", function() { + assert_eq(whole(5), 5, "whole int") + assert_eq(whole(-5), -5, "whole neg int") +}) + +run("fraction integer returns zero", function() { + assert_eq(fraction(5), 0, "fraction int") +}) + +// --- lower/upper edge cases --- + +run("lower empty string", function() { + assert_eq(lower(""), "", "lower empty") +}) + +run("upper empty string", function() { + assert_eq(upper(""), "", "upper empty") +}) + +// --- trim edge cases --- + +run("trim with custom reject", function() { + assert_eq(trim("xxhelloxx", "x"), "hello", "trim custom reject") +}) + +// --- search() edge cases --- + +run("search not found", function() { + assert_eq(search("hello", "xyz"), null, "search not found") +}) + +run("search empty text in empty text", function() { + assert_eq(search("", ""), 0, "search empty in empty") +}) + +run("search from negative index", function() { + assert_eq(search("hello world", "world", -5), 6, "search neg from") +}) + +// --- replace() edge cases --- + +run("replace not found", function() { + assert_eq(replace("hello", "xyz", "abc"), "hello", "replace not found") +}) + +run("replace with limit 0", function() { + assert_eq(replace("aaa", "a", "b", 0), "aaa", "replace limit 0") +}) + +run("replace to empty", function() { + assert_eq(replace("hello", "l", ""), "heo", "replace to empty") +}) + +run("replace with function", function() { + var result = replace("abc", "b", function(match, pos) { return text(pos) }) + assert_eq(result, "a1c", "replace fn") +}) + +// --- starts_with / ends_with edge cases --- + +run("starts_with full match", function() { + assert_eq(starts_with("hello", "hello"), true, "starts_with full") +}) + +run("starts_with empty", function() { + assert_eq(starts_with("hello", ""), true, "starts_with empty") +}) + +run("ends_with full match", function() { + assert_eq(ends_with("hello", "hello"), true, "ends_with full") +}) + +run("ends_with empty", function() { + assert_eq(ends_with("hello", ""), true, "ends_with empty") +}) + +// --- stone() edge cases --- + +run("stone array prevents modification", function() { + var arr = [1, 2, 3] + stone(arr) + if (!should_disrupt(function() { arr[0] = 99 })) + fail("modifying stone array should disrupt") +}) + +run("stone returns the value", function() { + var arr = [1, 2, 3] + var result = stone(arr) + assert_eq(result[0], 1, "stone returns value") + assert_eq(is_stone(result), true, "stone result is stone") +}) + +run("stone idempotent", function() { + var arr = [1, 2, 3] + stone(arr) + stone(arr) + assert_eq(is_stone(arr), true, "double stone ok") +}) + +run("stone primitives already stone", function() { + assert_eq(is_stone(42), true, "number is stone") + assert_eq(is_stone("hello"), true, "text is stone") + assert_eq(is_stone(true), true, "bool is stone") + assert_eq(is_stone(null), true, "null is stone") +}) + +// --- codepoint / character edge cases --- + +run("codepoint non-text returns null", function() { + assert_eq(codepoint(65), null, "codepoint number") + assert_eq(codepoint(null), null, "codepoint null") +}) + +run("codepoint empty returns null", function() { + assert_eq(codepoint(""), null, "codepoint empty") +}) + +run("character negative returns empty", function() { + assert_eq(character(-1), "", "character -1") +}) + +run("character text returns first char", function() { + assert_eq(character("hello"), "h", "character text") +}) + +run("character roundtrip", function() { + assert_eq(codepoint(character(65)), 65, "roundtrip 65") + assert_eq(character(codepoint("A")), "A", "roundtrip A") +}) + +// --- sensory function edge cases --- + +run("is_integer floats that are whole", function() { + assert_eq(is_integer(3.0), true, "is_integer 3.0") + assert_eq(is_integer(-5.0), true, "is_integer -5.0") +}) + +run("is_integer actual floats", function() { + assert_eq(is_integer(3.5), false, "is_integer 3.5") + assert_eq(is_integer(0.1), false, "is_integer 0.1") +}) + +run("is_integer non-numbers", function() { + assert_eq(is_integer("3"), false, "is_integer string") + assert_eq(is_integer(null), false, "is_integer null") + assert_eq(is_integer(true), false, "is_integer bool") +}) + +run("is_character edge cases", function() { + assert_eq(is_character(""), false, "is_character empty") + assert_eq(is_character("ab"), false, "is_character multi") + assert_eq(is_character("x"), true, "is_character single") + assert_eq(is_character(65), false, "is_character number") +}) + +run("is_digit edge cases", 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(""), false, "is_digit empty") + assert_eq(is_digit("10"), false, "is_digit multi") +}) + +run("is_letter edge cases", function() { + assert_eq(is_letter("a"), true, "is_letter a") + assert_eq(is_letter("Z"), true, "is_letter Z") + assert_eq(is_letter("0"), false, "is_letter 0") + assert_eq(is_letter(""), false, "is_letter empty") + assert_eq(is_letter("ab"), false, "is_letter multi") +}) + +run("is_lower edge cases", function() { + assert_eq(is_lower("a"), true, "is_lower a") + assert_eq(is_lower("A"), false, "is_lower A") + assert_eq(is_lower("1"), false, "is_lower 1") + assert_eq(is_lower(""), false, "is_lower empty") +}) + +run("is_upper edge cases", function() { + assert_eq(is_upper("A"), true, "is_upper A") + assert_eq(is_upper("a"), false, "is_upper a") + assert_eq(is_upper("1"), false, "is_upper 1") + assert_eq(is_upper(""), false, "is_upper empty") +}) + +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(""), false, "is_whitespace empty") + assert_eq(is_whitespace("a"), false, "is_whitespace letter") + assert_eq(is_whitespace(32), false, "is_whitespace number") +}) + +run("is_data edge cases", function() { + // is_data: true for text, number, logical, array, blob, record + // false for function and null + assert_eq(is_data({}), true, "is_data record") + assert_eq(is_data("hello"), true, "is_data text") + assert_eq(is_data(42), true, "is_data number") + assert_eq(is_data(true), true, "is_data bool") + assert_eq(is_data([]), true, "is_data array") + assert_eq(is_data(null), false, "is_data null") + assert_eq(is_data(function() {}), false, "is_data function") +}) + +// --- format() edge cases --- + +run("format basic array", function() { + var result = format("{0} in {1}!", ["Malmborg", "Plano"]) + assert_eq(result, "Malmborg in Plano!", "format basic") +}) + +run("format record", function() { + var result = format("{name} is {age}", {name: "Alice", age: "30"}) + assert_eq(result, "Alice is 30", "format record") +}) + +run("format missing key unchanged", function() { + var result = format("{0} and {1}", ["hello"]) + assert_eq(starts_with(result, "hello"), true, "format missing keeps prefix") +}) + +// --- arrfor edge cases --- + +run("arrfor reverse with exit", function() { + var visited = [] + var result = arrfor([10, 20, 30], function(x) { + visited[] = x + if (x == 20) return true + return null + }, true, true) + assert_eq(result, true, "arrfor rev exit returns exit value") + assert_eq(visited[0], 30, "arrfor rev exit started from end") +}) + +run("arrfor with index", function() { + var indices = [] + arrfor([10, 20, 30], function(x, i) { indices[] = i }) + assert_eq(indices[0], 0, "arrfor idx [0]") + assert_eq(indices[2], 2, "arrfor idx [2]") +}) + // ============================================================================ // SUMMARY // ============================================================================