fix tests

This commit is contained in:
2026-02-24 16:55:07 -06:00
parent 7bd17c6476
commit c2f57d1dae
10 changed files with 1179 additions and 96 deletions

71
boot.ce
View File

@@ -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) {

View File

@@ -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

View File

@@ -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})

View File

@@ -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) {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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
// ============================================================================