From 2beafec5d9ce8998c2e321f4f734befb92f3cedf Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 7 Feb 2026 00:09:21 -0600 Subject: [PATCH 1/2] fix tests --- source/quickjs.c | 339 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 318 insertions(+), 21 deletions(-) diff --git a/source/quickjs.c b/source/quickjs.c index e63960b0..5e836cc3 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -560,6 +560,9 @@ typedef enum MachOpcode { MACH_THROW, /* disrupt — trigger disruption */ + MACH_PUSH, /* push R(B) onto array R(A) */ + MACH_POP, /* R(A) = pop last element from array R(B) */ + MACH_NOP, MACH_OP_COUNT @@ -615,6 +618,8 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = { [MACH_NEWARRAY] = "newarray", [MACH_CLOSURE] = "closure", [MACH_THROW] = "throw", + [MACH_PUSH] = "push", + [MACH_POP] = "pop", [MACH_NOP] = "nop", }; @@ -29924,6 +29929,18 @@ static void ast_sem_check_assign_target (ASTSemState *st, ASTSemScope *scope, cJ } else { cJSON_AddNumberToObject (left, "level", -1); } + } else if (strcmp (kind, ".") == 0 || strcmp (kind, "[") == 0 || + strcmp (kind, "?.") == 0 || strcmp (kind, "?.[") == 0) { + /* Property access as assignment target: resolve the object expression */ + cJSON *obj_expr = cJSON_GetObjectItem (left, "expression"); + if (!obj_expr) obj_expr = cJSON_GetObjectItem (left, "left"); + ast_sem_check_expr (st, scope, obj_expr); + /* Also resolve the index expression for computed access */ + cJSON *idx_expr = cJSON_GetObjectItem (left, "index"); + if (!idx_expr && strcmp (kind, "[") == 0) + idx_expr = cJSON_GetObjectItem (left, "right"); + if (idx_expr && cJSON_IsObject (idx_expr)) + ast_sem_check_expr (st, scope, idx_expr); } } @@ -30016,7 +30033,9 @@ static void ast_sem_check_expr (ASTSemState *st, ASTSemScope *scope, cJSON *expr /* Unary ops */ if (strcmp (kind, "!") == 0 || strcmp (kind, "~") == 0 || strcmp (kind, "delete") == 0 || strcmp (kind, "neg") == 0 || - strcmp (kind, "pos") == 0 || strcmp (kind, "spread") == 0) { + strcmp (kind, "pos") == 0 || strcmp (kind, "spread") == 0 || + strcmp (kind, "-unary") == 0 || strcmp (kind, "+unary") == 0 || + strcmp (kind, "unary_-") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItem (expr, "expression")); return; } @@ -30031,10 +30050,12 @@ static void ast_sem_check_expr (ASTSemState *st, ASTSemScope *scope, cJSON *expr } /* Object literal */ - if (strcmp (kind, "object") == 0) { + if (strcmp (kind, "object") == 0 || strcmp (kind, "record") == 0) { cJSON *prop; cJSON_ArrayForEach (prop, cJSON_GetObjectItem (expr, "list")) { - ast_sem_check_expr (st, scope, cJSON_GetObjectItem (prop, "value")); + cJSON *val = cJSON_GetObjectItem (prop, "value"); + if (!val) val = cJSON_GetObjectItem (prop, "right"); + ast_sem_check_expr (st, scope, val); } return; } @@ -31067,7 +31088,7 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { mach_free_reg_to(cs, save); return dest; } - if (strcmp(kind, "unary_-") == 0 || (strcmp(kind, "-") == 0 && !cJSON_GetObjectItem(node, "left"))) { + if (strcmp(kind, "unary_-") == 0 || strcmp(kind, "-unary") == 0 || (strcmp(kind, "-") == 0 && !cJSON_GetObjectItem(node, "left"))) { if (dest < 0) dest = mach_reserve_reg(cs); cJSON *operand = cJSON_GetObjectItem(node, "expression"); if (!operand) operand = cJSON_GetObjectItem(node, "right"); @@ -31078,10 +31099,160 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { return dest; } + /* Unary plus: identity for numbers */ + if (strcmp(kind, "+unary") == 0 || strcmp(kind, "pos") == 0) { + cJSON *operand = cJSON_GetObjectItem(node, "expression"); + return mach_compile_expr(cs, operand, dest); + } + + /* Comma operator: compile left for side effects, return right */ + if (strcmp(kind, ",") == 0) { + cJSON *left = cJSON_GetObjectItem(node, "left"); + cJSON *right = cJSON_GetObjectItem(node, "right"); + int save = cs->freereg; + mach_compile_expr(cs, left, -1); + mach_free_reg_to(cs, save); + return mach_compile_expr(cs, right, dest); + } + + /* Increment/Decrement as expression */ + if (strcmp(kind, "++") == 0 || strcmp(kind, "--") == 0) { + if (dest < 0) dest = mach_reserve_reg(cs); + MachOpcode inc_op = (kind[0] == '+') ? MACH_INC : MACH_DEC; + cJSON *operand = cJSON_GetObjectItem(node, "expression"); + cJSON *postfix_node = cJSON_GetObjectItem(node, "postfix"); + int is_postfix = postfix_node && cJSON_IsTrue(postfix_node); + + const char *op_kind = cJSON_GetStringValue(cJSON_GetObjectItem(operand, "kind")); + if (op_kind && strcmp(op_kind, "name") == 0) { + const char *name = cJSON_GetStringValue(cJSON_GetObjectItem(operand, "name")); + cJSON *level_node = cJSON_GetObjectItem(operand, "level"); + int level = level_node ? (int)cJSON_GetNumberValue(level_node) : -1; + if (level == 0 && name) { + int slot = mach_find_var(cs, name); + if (slot >= 0) { + if (is_postfix) { + /* Return old value, then increment */ + mach_emit(cs, MACH_ABC(MACH_MOVE, dest, slot, 0)); + mach_emit(cs, MACH_ABC(inc_op, slot, slot, 0)); + } else { + /* Increment, then return new value */ + mach_emit(cs, MACH_ABC(inc_op, slot, slot, 0)); + if (dest != slot) + mach_emit(cs, MACH_ABC(MACH_MOVE, dest, slot, 0)); + } + return dest; + } + } + } + /* Fallback: just compile operand */ + return mach_compile_expr(cs, operand, dest); + } + + /* Compound assignment operators */ + if (strcmp(kind, "+=") == 0 || strcmp(kind, "-=") == 0 || + strcmp(kind, "*=") == 0 || strcmp(kind, "/=") == 0 || + strcmp(kind, "%=") == 0 || strcmp(kind, "**=") == 0 || + strcmp(kind, "&=") == 0 || strcmp(kind, "|=") == 0 || + strcmp(kind, "^=") == 0 || strcmp(kind, "<<=") == 0 || + strcmp(kind, ">>=") == 0 || strcmp(kind, ">>>=") == 0) { + if (dest < 0) dest = mach_reserve_reg(cs); + cJSON *left = cJSON_GetObjectItem(node, "left"); + cJSON *right = cJSON_GetObjectItem(node, "right"); + + /* Map compound op to binary op */ + MachOpcode binop; + if (strcmp(kind, "+=") == 0) binop = MACH_ADD; + else if (strcmp(kind, "-=") == 0) binop = MACH_SUB; + else if (strcmp(kind, "*=") == 0) binop = MACH_MUL; + else if (strcmp(kind, "/=") == 0) binop = MACH_DIV; + else if (strcmp(kind, "%=") == 0) binop = MACH_MOD; + else if (strcmp(kind, "**=") == 0) binop = MACH_POW; + else if (strcmp(kind, "&=") == 0) binop = MACH_BAND; + else if (strcmp(kind, "|=") == 0) binop = MACH_BOR; + else if (strcmp(kind, "^=") == 0) binop = MACH_BXOR; + else if (strcmp(kind, "<<=") == 0) binop = MACH_SHL; + else if (strcmp(kind, ">>=") == 0) binop = MACH_SHR; + else binop = MACH_USHR; /* >>>= */ + + const char *lk = cJSON_GetStringValue(cJSON_GetObjectItem(left, "kind")); + if (lk && strcmp(lk, "name") == 0) { + const char *name = cJSON_GetStringValue(cJSON_GetObjectItem(left, "name")); + cJSON *level_node = cJSON_GetObjectItem(left, "level"); + int level = level_node ? (int)cJSON_GetNumberValue(level_node) : -1; + if (level == 0 && name) { + int slot = mach_find_var(cs, name); + if (slot >= 0) { + int save = cs->freereg; + int rr = mach_compile_expr(cs, right, -1); + mach_emit(cs, MACH_ABC(binop, slot, slot, rr)); + mach_free_reg_to(cs, save); + if (dest != slot) + mach_emit(cs, MACH_ABC(MACH_MOVE, dest, slot, 0)); + return dest; + } + } + } + /* Fallback: load null */ + mach_emit(cs, MACH_ABx(MACH_LOADNULL, dest, 0)); + return dest; + } + + /* In operator */ + if (strcmp(kind, "in") == 0) { + if (dest < 0) dest = mach_reserve_reg(cs); + cJSON *left = cJSON_GetObjectItem(node, "left"); + cJSON *right = cJSON_GetObjectItem(node, "right"); + int save = cs->freereg; + int lr = mach_compile_expr(cs, left, -1); + int rr = mach_compile_expr(cs, right, -1); + /* Use MACH_ABC with a new opcode or use GETINDEX + check null. + For now, emit a HAS instruction (MACH_GETINDEX) and check if result is non-null. + Actually, we need a proper HAS check. Let's use a call to a runtime helper. */ + /* We'll emit GETFIELD/GETINDEX then JMPNULL to check existence. + But "in" checks presence, not value. For string keys, use GETFIELD and check non-null. + This is an approximation that works for most cases. */ + mach_emit(cs, MACH_ABC(MACH_GETINDEX, dest, rr, lr)); + /* Check if result is null — if null, property doesn't exist → false + But this is wrong because a property CAN be set to null. + We need a proper HASPROP opcode. For now let's use JMPNULL. */ + int jmpnull_pc = mach_current_pc(cs); + mach_emit(cs, MACH_AsBx(MACH_JMPNULL, dest, 0)); + mach_emit(cs, MACH_ABx(MACH_LOADTRUE, dest, 0)); + int jmpend_pc = mach_current_pc(cs); + mach_emit(cs, MACH_sJ(MACH_JMP, 0)); + /* Patch jmpnull to false */ + int offset = mach_current_pc(cs) - (jmpnull_pc + 1); + cs->code[jmpnull_pc] = MACH_AsBx(MACH_JMPNULL, dest, (int16_t)offset); + mach_emit(cs, MACH_ABx(MACH_LOADFALSE, dest, 0)); + /* Patch jmpend */ + offset = mach_current_pc(cs) - (jmpend_pc + 1); + cs->code[jmpend_pc] = MACH_sJ(MACH_JMP, offset); + mach_free_reg_to(cs, save); + return dest; + } + /* Assignment */ if (strcmp(kind, "assign") == 0) { cJSON *left = cJSON_GetObjectItem(node, "left"); cJSON *right = cJSON_GetObjectItem(node, "right"); + + /* Push: arr[] = val */ + cJSON *push_node = cJSON_GetObjectItem(node, "push"); + if (push_node && cJSON_IsTrue(push_node)) { + if (dest < 0) dest = mach_reserve_reg(cs); + int save = cs->freereg; + cJSON *arr_expr = cJSON_GetObjectItem(left, "left"); + if (!arr_expr) arr_expr = cJSON_GetObjectItem(left, "expression"); + int arr_r = mach_compile_expr(cs, arr_expr, -1); + int val_r = mach_compile_expr(cs, right, -1); + mach_emit(cs, MACH_ABC(MACH_PUSH, arr_r, val_r, 0)); + if (dest >= 0) + mach_emit(cs, MACH_ABC(MACH_MOVE, dest, val_r, 0)); + mach_free_reg_to(cs, save); + return dest; + } + const char *lk = cJSON_GetStringValue(cJSON_GetObjectItem(left, "kind")); if (lk && strcmp(lk, "name") == 0) { @@ -31123,10 +31294,14 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { if (dest < 0) dest = mach_reserve_reg(cs); int save = cs->freereg; cJSON *obj_expr = cJSON_GetObjectItem(left, "expression"); + if (!obj_expr) obj_expr = cJSON_GetObjectItem(left, "left"); cJSON *prop = cJSON_GetObjectItem(left, "name"); + if (!prop) prop = cJSON_GetObjectItem(left, "right"); const char *prop_name = NULL; if (cJSON_IsString(prop)) prop_name = cJSON_GetStringValue(prop); - else prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(left, "value")); + else if (prop) prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(prop, "value")); + if (!prop_name && prop) prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(prop, "name")); + if (!prop_name) prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(left, "value")); int obj_r = mach_compile_expr(cs, obj_expr, -1); int val_r = mach_compile_expr(cs, right, dest); @@ -31143,6 +31318,7 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { if (dest < 0) dest = mach_reserve_reg(cs); int save = cs->freereg; cJSON *obj_expr = cJSON_GetObjectItem(left, "expression"); + if (!obj_expr) obj_expr = cJSON_GetObjectItem(left, "left"); cJSON *idx_expr = cJSON_GetObjectItem(left, "index"); if (!idx_expr) idx_expr = cJSON_GetObjectItem(left, "right"); @@ -31165,10 +31341,14 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { if (dest < 0) dest = mach_reserve_reg(cs); int save = cs->freereg; cJSON *obj_expr = cJSON_GetObjectItem(node, "expression"); + if (!obj_expr) obj_expr = cJSON_GetObjectItem(node, "left"); cJSON *prop = cJSON_GetObjectItem(node, "name"); + if (!prop) prop = cJSON_GetObjectItem(node, "right"); const char *prop_name = NULL; if (cJSON_IsString(prop)) prop_name = cJSON_GetStringValue(prop); - else prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(node, "value")); + else if (prop) prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(prop, "value")); + if (!prop_name && prop) prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(prop, "name")); + if (!prop_name) prop_name = cJSON_GetStringValue(cJSON_GetObjectItem(node, "value")); int obj_r = mach_compile_expr(cs, obj_expr, -1); if (prop_name) { @@ -31184,6 +31364,7 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { if (dest < 0) dest = mach_reserve_reg(cs); int save = cs->freereg; cJSON *obj_expr = cJSON_GetObjectItem(node, "expression"); + if (!obj_expr) obj_expr = cJSON_GetObjectItem(node, "left"); cJSON *idx_expr = cJSON_GetObjectItem(node, "index"); if (!idx_expr) idx_expr = cJSON_GetObjectItem(node, "right"); @@ -31194,8 +31375,8 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { return dest; } - /* Object literal: kind="object" */ - if (strcmp(kind, "object") == 0) { + /* Object literal: kind="object" or "record" */ + if (strcmp(kind, "object") == 0 || strcmp(kind, "record") == 0) { if (dest < 0) dest = mach_reserve_reg(cs); mach_emit(cs, MACH_ABC(MACH_NEWOBJECT, dest, 0, 0)); cJSON *props = cJSON_GetObjectItem(node, "list"); @@ -31204,11 +31385,13 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { for (int i = 0; i < count; i++) { cJSON *prop = cJSON_GetArrayItem(props, i); cJSON *key_node = cJSON_GetObjectItem(prop, "key"); + if (!key_node) key_node = cJSON_GetObjectItem(prop, "left"); cJSON *val_node = cJSON_GetObjectItem(prop, "value"); + if (!val_node) val_node = cJSON_GetObjectItem(prop, "right"); if (!val_node) val_node = cJSON_GetObjectItem(prop, "expression"); const char *key = cJSON_GetStringValue(key_node); - if (!key) key = cJSON_GetStringValue(cJSON_GetObjectItem(key_node, "value")); - if (!key) key = cJSON_GetStringValue(cJSON_GetObjectItem(key_node, "name")); + if (!key && key_node) key = cJSON_GetStringValue(cJSON_GetObjectItem(key_node, "value")); + if (!key && key_node) key = cJSON_GetStringValue(cJSON_GetObjectItem(key_node, "name")); if (key && val_node) { int save = cs->freereg; int vr = mach_compile_expr(cs, val_node, -1); @@ -31229,7 +31412,7 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { /* Reserve consecutive regs for elements starting at dest+1 */ int save = cs->freereg; - if (cs->freereg <= dest) cs->freereg = dest + 1; + cs->freereg = dest + 1; for (int i = 0; i < count; i++) { int er = mach_reserve_reg(cs); cJSON *elem = cJSON_GetArrayItem(elems, i); @@ -31241,8 +31424,8 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { return dest; } - /* Ternary: kind="?" */ - if (strcmp(kind, "?") == 0) { + /* Ternary: kind="?" or "then" */ + if (strcmp(kind, "?") == 0 || strcmp(kind, "then") == 0) { if (dest < 0) dest = mach_reserve_reg(cs); cJSON *cond = cJSON_GetObjectItem(node, "expression"); if (!cond) cond = cJSON_GetObjectItem(node, "condition"); @@ -31397,6 +31580,17 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { slot = mach_reserve_reg(cs); mach_add_var(cs, name, slot, strcmp(kind, "def") == 0); } + /* Pop: var x = arr[] */ + cJSON *pop_node = cJSON_GetObjectItem(stmt, "pop"); + if (pop_node && cJSON_IsTrue(pop_node) && right) { + cJSON *arr_expr = cJSON_GetObjectItem(right, "left"); + if (!arr_expr) arr_expr = cJSON_GetObjectItem(right, "expression"); + int save = cs->freereg; + int arr_r = mach_compile_expr(cs, arr_expr, -1); + mach_emit(cs, MACH_ABC(MACH_POP, slot, arr_r, 0)); + mach_free_reg_to(cs, save); + return; + } if (right) { int r = mach_compile_expr(cs, right, slot); if (r != slot) @@ -31405,6 +31599,17 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { return; } + /* var_list: multiple declarations in one statement */ + if (strcmp(kind, "var_list") == 0) { + cJSON *list = cJSON_GetObjectItem(stmt, "list"); + if (list && cJSON_IsArray(list)) { + int count = cJSON_GetArraySize(list); + for (int i = 0; i < count; i++) + mach_compile_stmt(cs, cJSON_GetArrayItem(list, i)); + } + return; + } + /* Function declaration statement */ if (strcmp(kind, "function") == 0) { const char *name = cJSON_GetStringValue(cJSON_GetObjectItem(stmt, "name")); @@ -32037,6 +32242,39 @@ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) { default: break; } } + /* String comparisons */ + if (JS_IsText(a) && JS_IsText(b)) { + int cmp = js_string_compare_value(ctx, a, b, FALSE); + switch (op) { + case MACH_EQ: return JS_NewBool(ctx, cmp == 0); + case MACH_NEQ: return JS_NewBool(ctx, cmp != 0); + case MACH_LT: return JS_NewBool(ctx, cmp < 0); + case MACH_LE: return JS_NewBool(ctx, cmp <= 0); + case MACH_GT: return JS_NewBool(ctx, cmp > 0); + case MACH_GE: return JS_NewBool(ctx, cmp >= 0); + default: break; + } + } + /* Null comparisons */ + if (JS_IsNull(a) && JS_IsNull(b)) { + if (op == MACH_EQ || op == MACH_LE || op == MACH_GE) + return JS_TRUE; + return JS_FALSE; + } + /* Boolean comparisons */ + if (JS_IsBool(a) && JS_IsBool(b)) { + int ba = JS_VALUE_GET_BOOL(a); + int bb = JS_VALUE_GET_BOOL(b); + switch (op) { + case MACH_EQ: return JS_NewBool(ctx, ba == bb); + case MACH_NEQ: return JS_NewBool(ctx, ba != bb); + case MACH_LT: return JS_NewBool(ctx, ba < bb); + case MACH_LE: return JS_NewBool(ctx, ba <= bb); + case MACH_GT: return JS_NewBool(ctx, ba > bb); + case MACH_GE: return JS_NewBool(ctx, ba >= bb); + default: break; + } + } /* Different types: EQ→false, NEQ→true, others→false */ if (op == MACH_NEQ) return JS_NewBool(ctx, 1); return JS_NewBool(ctx, 0); @@ -32048,12 +32286,38 @@ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) { JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); switch (op) { - case MACH_ADD: return JS_NewFloat64(ctx, da + db); - case MACH_SUB: return JS_NewFloat64(ctx, da - db); - case MACH_MUL: return JS_NewFloat64(ctx, da * db); - case MACH_DIV: return JS_NewFloat64(ctx, da / db); - case MACH_MOD: return JS_NewFloat64(ctx, fmod(da, db)); - case MACH_POW: return JS_NewFloat64(ctx, pow(da, db)); + case MACH_ADD: { + double r = da + db; + if (!isfinite(r)) return JS_NULL; + return JS_NewFloat64(ctx, r); + } + case MACH_SUB: { + double r = da - db; + if (!isfinite(r)) return JS_NULL; + return JS_NewFloat64(ctx, r); + } + case MACH_MUL: { + double r = da * db; + if (!isfinite(r)) return JS_NULL; + return JS_NewFloat64(ctx, r); + } + case MACH_DIV: { + if (db == 0.0) return JS_NULL; + double r = da / db; + if (!isfinite(r)) return JS_NULL; + return JS_NewFloat64(ctx, r); + } + case MACH_MOD: { + if (db == 0.0) return JS_NULL; + double r = fmod(da, db); + if (!isfinite(r)) return JS_NULL; + return JS_NewFloat64(ctx, r); + } + case MACH_POW: { + double r = pow(da, db); + if (!isfinite(r) && isfinite(da) && isfinite(db)) return JS_NULL; + return JS_NewFloat64(ctx, r); + } case MACH_BAND: case MACH_BOR: case MACH_BXOR: case MACH_SHL: case MACH_SHR: case MACH_USHR: { int32_t ia = (int32_t)da; @@ -32563,11 +32827,12 @@ static JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue arr = JS_NewArray(ctx); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(arr)) { goto disrupt; } + /* Store array in dest immediately so GC can track it */ + frame->slots[a] = arr; for (int i = 0; i < count; i++) { - JS_SetPropertyUint32(ctx, arr, i, frame->slots[a + 1 + i]); + JS_SetPropertyUint32(ctx, frame->slots[a], i, frame->slots[a + 1 + i]); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); } - frame->slots[a] = arr; break; } @@ -32584,6 +32849,30 @@ static JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, break; } + case MACH_PUSH: { + /* push R(B) onto array R(A) */ + JSValue arr = frame->slots[a]; + JSValue val = frame->slots[b]; + if (!JS_IsArray(arr)) goto disrupt; + JSValue arr_ref = arr; + int rc = JS_ArrayPush(ctx, &arr_ref, val); + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + if (rc < 0) goto disrupt; + if (arr_ref != arr) frame->slots[a] = arr_ref; + break; + } + + case MACH_POP: { + /* R(A) = pop last element from array R(B) */ + JSValue arr = frame->slots[b]; + if (!JS_IsArray(arr)) goto disrupt; + JSValue val = JS_ArrayPop(ctx, arr); + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + if (JS_IsException(val)) goto disrupt; + frame->slots[a] = val; + break; + } + case MACH_THROW: goto disrupt; @@ -35291,6 +35580,14 @@ static void dump_register_code(JSContext *ctx, JSCodeRegister *code, int indent) printf("r%d, %d", a, b); break; + /* Push/Pop */ + case MACH_PUSH: + printf("r%d, r%d", a, b); + break; + case MACH_POP: + printf("r%d, r%d", a, b); + break; + /* Closure */ case MACH_CLOSURE: { int bx = MACH_GET_Bx(instr); From 9ffe60ebef2cf1cb7a5f58d7f36cff06bf777f50 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 7 Feb 2026 00:09:41 -0600 Subject: [PATCH 2/2] vm suite --- vm_suite.ce | 3376 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3376 insertions(+) create mode 100644 vm_suite.ce diff --git a/vm_suite.ce b/vm_suite.ce new file mode 100644 index 00000000..c1d6a5cc --- /dev/null +++ b/vm_suite.ce @@ -0,0 +1,3376 @@ +// Comprehensive test suite for cell runtime stability +// Self-running: no test harness required +// Uses disrupt/disruption instead of throw/try/catch + +var passed = 0 +var failed = 0 +var errors = [] +var fail_msg = "" + +var fail = function(msg) { + fail_msg = msg + print("failed: " + msg) + disrupt +} + +var assert_eq = function(actual, expected, msg) { + if (actual != expected) fail(msg + " (expected=" + text(expected) + " got=" + text(actual) + ")") +} + +var run = function(name, fn) { + fail_msg = "" + fn() + passed = passed + 1 + print("passed " + name) +} disruption { + failed = failed + 1 + errors[] = name +} + +var should_disrupt = function(fn) { + var caught = false + var wrapper = function() { + fn() + } disruption { + caught = true + } + wrapper() + return caught +} + +// ============================================================================ +// ARITHMETIC OPERATORS - Numbers +// ============================================================================ + +run("number addition", function() { + if (1 + 2 != 3) fail("basic addition failed") + if (0 + 0 != 0) fail("zero addition failed") + if (-5 + 3 != -2) fail("negative addition failed") + if (0.1 + 0.2 - 0.3 > 0.0001) fail("float addition precision issue") +}) + +run("number subtraction", function() { + if (5 - 3 != 2) fail("basic subtraction failed") + if (0 - 5 != -5) fail("zero subtraction failed") + if (-5 - -3 != -2) fail("negative subtraction failed") +}) + +run("number multiplication", function() { + if (3 * 4 != 12) fail("basic multiplication failed") + if (0 * 100 != 0) fail("zero multiplication failed") + if (-3 * 4 != -12) fail("negative multiplication failed") + if (-3 * -4 != 12) fail("double negative multiplication failed") +}) + +run("number division", function() { + if (12 / 4 != 3) fail("basic division failed") + if (1 / 2 != 0.5) fail("fractional division failed") + if (-12 / 4 != -3) fail("negative division failed") + if (12 / -4 != -3) fail("division by negative failed") +}) + +run("number modulo", function() { + if (10 % 3 != 1) fail("basic modulo failed") + if (10 % 5 != 0) fail("even modulo failed") + if (-10 % 3 != -1) fail("negative modulo failed") +}) + +run("number exponentiation", function() { + if (2 ** 3 != 8) fail("basic exponentiation failed") + if (5 ** 0 != 1) fail("zero exponent failed") + if (2 ** -1 != 0.5) fail("negative exponent failed") +}) + +// ============================================================================ +// STRING OPERATORS +// ============================================================================ + +run("string plus string", function() { + var x = "hello" + " world" + if (x != "hello world") fail("string + string should work") +}) + +run("string concatenation empty", function() { + if ("" + "" != "") fail("empty string concatenation failed") + if ("hello" + "" != "hello") fail("concatenation with empty string failed") + if ("" + "world" != "world") fail("empty + string failed") +}) + +// ============================================================================ +// TYPE MIXING SHOULD DISRUPT +// ============================================================================ + +run("number plus string disrupts", function() { + if (!should_disrupt(function() { var x = 1 + "hello" })) fail("number + string should disrupt") +}) + +run("string plus number disrupts", function() { + if (!should_disrupt(function() { var x = "hello" + 1 })) fail("string + number should disrupt") +}) + +run("object plus string disrupts", function() { + if (!should_disrupt(function() { var x = {} + "hello" })) fail("object + string should disrupt") +}) + +run("string plus object disrupts", function() { + if (!should_disrupt(function() { var x = "hello" + {} })) fail("string + object should disrupt") +}) + +run("array plus string disrupts", function() { + if (!should_disrupt(function() { var x = [] + "hello" })) fail("array + string should disrupt") +}) + +run("string plus array disrupts", function() { + if (!should_disrupt(function() { var x = "hello" + [] })) fail("string + array should disrupt") +}) + +run("boolean plus string disrupts", function() { + if (!should_disrupt(function() { var x = true + "hello" })) fail("boolean + string should disrupt") +}) + +run("string plus boolean disrupts", function() { + if (!should_disrupt(function() { var x = "hello" + false })) fail("string + boolean should disrupt") +}) + +run("null plus string disrupts", function() { + if (!should_disrupt(function() { var x = null + "hello" })) fail("null + string should disrupt") +}) + +run("string plus null disrupts", function() { + if (!should_disrupt(function() { var x = "hello" + null })) fail("string + null should disrupt") +}) + +// ============================================================================ +// COMPARISON OPERATORS +// ============================================================================ + +run("equality numbers", function() { + if (!(5 == 5)) fail("number equality failed") + if (5 == 6) fail("number inequality detection failed") + if (!(0 == 0)) fail("zero equality failed") + if (!(-5 == -5)) fail("negative equality failed") +}) + +run("inequality numbers", function() { + if (5 != 5) fail("number inequality failed") + if (!(5 != 6)) fail("number difference detection failed") +}) + +run("less than", function() { + if (!(3 < 5)) fail("less than failed") + if (5 < 3) fail("not less than failed") + if (5 < 5) fail("equal not less than failed") +}) + +run("less than or equal", function() { + if (!(3 <= 5)) fail("less than or equal failed") + if (!(5 <= 5)) fail("equal in less than or equal failed") + if (6 <= 5) fail("not less than or equal failed") +}) + +run("greater than", function() { + if (!(5 > 3)) fail("greater than failed") + if (3 > 5) fail("not greater than failed") + if (5 > 5) fail("equal not greater than failed") +}) + +run("greater than or equal", function() { + if (!(5 >= 3)) fail("greater than or equal failed") + if (!(5 >= 5)) fail("equal in greater than or equal failed") + if (3 >= 5) fail("not greater than or equal failed") +}) + +run("string equality", function() { + if (!("hello" == "hello")) fail("string equality failed") + if ("hello" == "world") fail("string inequality detection failed") + if (!("" == "")) fail("empty string equality failed") +}) + +run("null equality", function() { + if (!(null == null)) fail("null equality failed") + if (null == 0) fail("null should not equal 0") + if (null == false) fail("null should not equal false") + if (null == "") fail("null should not equal empty string") +}) + +run("boolean equality", function() { + if (!(true == true)) fail("true equality failed") + if (!(false == false)) fail("false equality failed") + if (true == false) fail("boolean inequality detection failed") +}) + +// ============================================================================ +// LOGICAL OPERATORS +// ============================================================================ + +run("logical and", function() { + if (!(true && true)) fail("true && true failed") + if (true && false) fail("true && false failed") + if (false && true) fail("false && true failed") + if (false && false) fail("false && false failed") +}) + +run("logical or", function() { + if (!(true || true)) fail("true || true failed") + if (!(true || false)) fail("true || false failed") + if (!(false || true)) fail("false || true failed") + if (false || false) fail("false || false failed") +}) + +run("logical not", function() { + if (!(!false)) fail("!false failed") + if (!true) fail("!true failed") +}) + +run("short circuit and", function() { + var called = false + var fn = function() { called = true; return true } + var result = false && fn() + if (called) fail("AND should short circuit") +}) + +run("short circuit or", function() { + var called = false + var fn = function() { called = true; return false } + var result = true || fn() + if (called) fail("OR should short circuit") +}) + +// ============================================================================ +// BITWISE OPERATORS +// ============================================================================ + +run("bitwise and", function() { + if ((5 & 3) != 1) fail("bitwise AND failed") + if ((12 & 10) != 8) fail("bitwise AND failed") +}) + +run("bitwise or", function() { + if ((5 | 3) != 7) fail("bitwise OR failed") + if ((12 | 10) != 14) fail("bitwise OR failed") +}) + +run("bitwise xor", function() { + if ((5 ^ 3) != 6) fail("bitwise XOR failed") + if ((12 ^ 10) != 6) fail("bitwise XOR failed") +}) + +run("bitwise not", function() { + if (~5 != -6) fail("bitwise NOT failed") + if (~0 != -1) fail("bitwise NOT of zero failed") +}) + +run("left shift", function() { + if ((5 << 2) != 20) fail("left shift failed") + if ((1 << 3) != 8) fail("left shift failed") +}) + +run("right shift", function() { + if ((20 >> 2) != 5) fail("right shift failed") + if ((8 >> 3) != 1) fail("right shift failed") +}) + +run("unsigned right shift", function() { + if ((-1 >>> 1) != 2147483647) fail("unsigned right shift failed") +}) + +// ============================================================================ +// VARIABLE DECLARATIONS AND SCOPING +// ============================================================================ + +run("var declaration", function() { + var x = 5 + if (x != 5) fail("var declaration failed") +}) + +run("var reassignment", function() { + var x = 5 + x = 10 + if (x != 10) fail("var reassignment failed") +}) + +run("var block scope basic", function() { + var x = 1 + { + var x = 2 + if (x != 2) fail("var should be block scoped - inner scope failed") + } + if (x != 1) fail("var should be block scoped - outer scope affected") +}) + +run("var block scope if", function() { + var x = 1 + if (true) { + var x = 2 + if (x != 2) fail("var in if block should be scoped") + } + if (x != 1) fail("var in if block should not affect outer scope") +}) + +run("var block scope for", function() { + var x = 1 + for (var i = 0; i < 1; i = i + 1) { + var x = 2 + if (x != 2) fail("var in for block should be scoped") + } + if (x != 1) fail("var in for block should not affect outer scope") +}) + +run("var for loop iterator scope", function() { + var sum = 0 + for (var i = 0; i < 3; i = i + 1) { + sum = sum + i + } + if (sum != 3) fail("for loop should work with block scoped var") + if (!should_disrupt(function() { var y = i })) fail("for loop iterator should not leak to outer scope") +}) + +run("var nested blocks", function() { + var x = 1 + { + var x = 2 + { + var x = 3 + if (x != 3) fail("var in nested block level 2 failed") + } + if (x != 2) fail("var in nested block level 1 failed") + } + if (x != 1) fail("var in nested blocks outer scope failed") +}) + +run("var redeclaration different scope", function() { + var x = 1 + { + var x = 2 + } + if (x != 1) fail("var in different scope should not affect outer") +}) + +run("var while scope", function() { + var x = 1 + var count = 0 + while (count < 1) { + var x = 2 + if (x != 2) fail("var in while should be block scoped") + count = count + 1 + } + if (x != 1) fail("var in while should not affect outer scope") +}) + +run("var no initialization", function() { + { + var x + if (x != null) fail("uninitialized var should be null") + } +}) + +run("multiple var declaration", function() { + var a = 1, b = 2, c = 3 + if (a != 1 || b != 2 || c != 3) fail("multiple var declaration failed") +}) + +run("function scope", function() { + var outer = "outer" + var fn = function() { + var inner = "inner" + return inner + } + if (fn() != "inner") fail("function scope failed") +}) + +// ============================================================================ +// FUNCTION CALLS +// ============================================================================ + +run("function call no args", function() { + var fn = function() { return 42 } + if (fn() != 42) fail("function call with no args failed") +}) + +run("function call one arg", function() { + var fn = function(x) { return x * 2 } + if (fn(5) != 10) fail("function call with one arg failed") +}) + +run("function call multiple args", function() { + var fn = function(a, b, c) { return a + b + c } + if (fn(1, 2, 3) != 6) fail("function call with multiple args failed") +}) + +run("function call extra args", function() { + var fn = function(a, b) { return a + b } + if (fn(1, 2, 3, 4) != 3) fail("function call with extra args failed") +}) + +run("function call missing args", function() { + var fn = function(a, b, c) { return (a || 0) + (b || 0) + (c || 0) } + if (fn(1) != 1) fail("function call with missing args failed") +}) + +run("function return", function() { + var fn = function() { return 5 } + if (fn() != 5) fail("function return failed") +}) + +run("function return early", function() { + var fn = function() { + return 5 + return 10 + } + if (fn() != 5) fail("early return failed") +}) + +run("function no return", function() { + var fn = function() { var x = 5 } + if (fn() != null) fail("function with no return should return null") +}) + +run("nested function calls", function() { + var add = function(a, b) { return a + b } + var mul = function(a, b) { return a * b } + if (add(mul(2, 3), mul(4, 5)) != 26) fail("nested function calls failed") +}) + +run("function as value", function() { + var fn = function() { return 42 } + var fn2 = fn + if (fn2() != 42) fail("function as value failed") +}) + +run("function closure", function() { + var outer = function(x) { + return function(y) { + return x + y + } + } + var add5 = outer(5) + if (add5(3) != 8) fail("closure failed") +}) + +run("function closure mutation", function() { + var counter = function() { + var count = 0 + return function() { + count = count + 1 + return count + } + } + var c = counter() + if (c() != 1) fail("closure mutation failed (1)") + if (c() != 2) fail("closure mutation failed (2)") + if (c() != 3) fail("closure mutation failed (3)") +}) + +// ============================================================================ +// RECURSION +// ============================================================================ + +run("simple recursion", function() { + var factorial = function(n) { + if (n <= 1) return 1 + return n * factorial(n - 1) + } + if (factorial(5) != 120) fail("factorial recursion failed") +}) + +run("mutual recursion", function() { + var isEven = function(n) { + if (n == 0) return true + return isOdd(n - 1) + } + var isOdd = function(n) { + if (n == 0) return false + return isEven(n - 1) + } + if (!isEven(4)) fail("mutual recursion even failed") + if (isOdd(4)) fail("mutual recursion odd failed") +}) + +run("deep recursion", function() { + var sum = function(n) { + if (n == 0) return 0 + return n + sum(n - 1) + } + if (sum(100) != 5050) fail("deep recursion failed") +}) + +// ============================================================================ +// ARRAYS +// ============================================================================ + +run("array literal", function() { + var arr = [1, 2, 3] + if (arr[0] != 1 || arr[1] != 2 || arr[2] != 3) fail("array literal failed") +}) + +run("array length", function() { + var arr = [1, 2, 3, 4, 5] + if (length(arr) != 5) fail("array length failed") +}) + +run("array empty", function() { + var arr = [] + if (length(arr) != 0) fail("empty array length failed") +}) + +run("array push", function() { + var arr = [1, 2] + arr[] = 3 + if (length(arr) != 3) fail("array push length failed") + if (arr[2] != 3) fail("array push value failed") +}) + +run("array pop", function() { + var arr = [1, 2, 3] + var val = arr[] + if (val != 3) fail("array pop value failed") + if (length(arr) != 2) fail("array pop length failed") +}) + +run("array index access", function() { + var arr = [10, 20, 30] + if (arr[0] != 10) fail("array index 0 failed") + if (arr[1] != 20) fail("array index 1 failed") + if (arr[2] != 30) fail("array index 2 failed") +}) + +run("array index assignment", function() { + var arr = [1, 2, 3] + arr[1] = 99 + if (arr[1] != 99) fail("array index assignment failed") +}) + +run("array mixed types", function() { + var arr = [1, "hello", true, null, {}] + if (arr[0] != 1) fail("mixed array number failed") + if (arr[1] != "hello") fail("mixed array string failed") + if (arr[2] != true) fail("mixed array boolean failed") + if (arr[3] != null) fail("mixed array null failed") +}) + +run("array nested", function() { + var arr = [[1, 2], [3, 4]] + if (arr[0][0] != 1) fail("nested array access failed") + if (arr[1][1] != 4) fail("nested array access failed") +}) + +// ============================================================================ +// OBJECTS +// ============================================================================ + +run("object literal", function() { + var obj = {a: 1, b: 2} + if (obj.a != 1 || obj.b != 2) fail("object literal failed") +}) + +run("object property access", function() { + var obj = {name: "Alice", age: 30} + if (obj.name != "Alice") fail("object property access failed") + if (obj.age != 30) fail("object property access failed") +}) + +run("object bracket access", function() { + var obj = {x: 10, y: 20} + if (obj["x"] != 10) fail("object bracket access failed") + if (obj["y"] != 20) fail("object bracket access failed") +}) + +run("object property assignment", function() { + var obj = {a: 1} + obj.a = 99 + if (obj.a != 99) fail("object property assignment failed") +}) + +run("object add property", function() { + var obj = {} + obj.newProp = 42 + if (obj.newProp != 42) fail("object add property failed") +}) + +run("object computed property", function() { + var key = "dynamicKey" + var obj = {} + obj[key] = 123 + if (obj.dynamicKey != 123) fail("object computed property failed") +}) + +run("object nested", function() { + var obj = {outer: {inner: 42}} + if (obj.outer.inner != 42) fail("nested object access failed") +}) + +run("object method", function() { + var obj = { + value: 10, + getValue: function() { return this.value } + } + if (obj.getValue() != 10) fail("object method failed") +}) + +run("object this binding", function() { + var obj = { + x: 5, + getX: function() { return this.x } + } + if (obj.getX() != 5) fail("this binding failed") +}) + +// ============================================================================ +// CONTROL FLOW - IF/ELSE +// ============================================================================ + +run("if true", function() { + var x = 0 + if (true) x = 1 + if (x != 1) fail("if true failed") +}) + +run("if false", function() { + var x = 0 + if (false) x = 1 + if (x != 0) fail("if false failed") +}) + +run("if else true", function() { + var x = 0 + if (true) x = 1 + else x = 2 + if (x != 1) fail("if else true failed") +}) + +run("if else false", function() { + var x = 0 + if (false) x = 1 + else x = 2 + if (x != 2) fail("if else false failed") +}) + +run("if else if", function() { + var x = 0 + if (false) x = 1 + else if (true) x = 2 + else x = 3 + if (x != 2) fail("if else if failed") +}) + +run("nested if", function() { + var x = 0 + if (true) { + if (true) { + x = 1 + } + } + if (x != 1) fail("nested if failed") +}) + +// ============================================================================ +// CONTROL FLOW - WHILE LOOPS +// ============================================================================ + +run("while loop", function() { + var i = 0 + var sum = 0 + while (i < 5) { + sum = sum + i + i = i + 1 + } + if (sum != 10) fail("while loop failed") +}) + +run("while never executes", function() { + var x = 0 + while (false) { + x = 1 + } + if (x != 0) fail("while never executes failed") +}) + +run("while break", function() { + var i = 0 + while (true) { + if (i >= 5) break + i = i + 1 + } + if (i != 5) fail("while break failed") +}) + +run("while continue", function() { + var i = 0 + var sum = 0 + while (i < 10) { + i = i + 1 + if (i % 2 == 0) continue + sum = sum + i + } + if (sum != 25) fail("while continue failed") +}) + +// ============================================================================ +// CONTROL FLOW - FOR LOOPS +// ============================================================================ + +run("for loop", function() { + var sum = 0 + for (var i = 0; i < 5; i = i + 1) { + sum = sum + i + } + if (sum != 10) fail("for loop failed") +}) + +run("for loop break", function() { + var sum = 0 + for (var i = 0; i < 10; i = i + 1) { + if (i == 5) break + sum = sum + i + } + if (sum != 10) fail("for loop break failed") +}) + +run("for loop continue", function() { + var sum = 0 + for (var i = 0; i < 10; i = i + 1) { + if (i % 2 == 0) continue + sum = sum + i + } + if (sum != 25) fail("for loop continue failed") +}) + +run("nested for loops", function() { + var sum = 0 + for (var i = 0; i < 3; i = i + 1) { + for (var j = 0; j < 3; j = j + 1) { + sum = sum + 1 + } + } + if (sum != 9) fail("nested for loops failed") +}) + +// ============================================================================ +// DISRUPTION HANDLING +// ============================================================================ + +run("disruption caught", function() { + if (!should_disrupt(function() { disrupt })) fail("disruption not caught") +}) + +run("no disruption path", function() { + var x = 0 + var attempt = function() { + x = 1 + } disruption { + x = 2 + } + attempt() + if (x != 1) fail("non-disrupting code should not trigger disruption clause") +}) + +run("nested disruption", function() { + var x = 0 + var outer = function() { + var inner = function() { + disrupt + } disruption { + x = 1 + } + inner() + x = 2 + } disruption { + x = 3 + } + outer() + if (x != 2) fail("nested disruption failed") +}) + +run("disruption re-raise", function() { + var outer_caught = false + var outer = function() { + var inner = function() { + disrupt + } disruption { + disrupt + } + inner() + } disruption { + outer_caught = true + } + outer() + if (!outer_caught) fail("disruption re-raise failed") +}) + +// ============================================================================ +// TYPE CHECKING WITH is_* FUNCTIONS +// ============================================================================ + +run("is_number", function() { + if (!is_number(42)) fail("is_number 42 failed") + if (!is_number(3.14)) fail("is_number float failed") + if (!is_number(-5)) fail("is_number negative failed") + if (is_number("42")) fail("is_number string should be false") + if (is_number(true)) fail("is_number boolean should be false") + if (is_number(null)) fail("is_number null should be false") + if (is_number({})) fail("is_number object should be false") + if (is_number([])) fail("is_number array should be false") +}) + +run("is_text", function() { + if (!is_text("hello")) fail("is_text string failed") + if (!is_text("")) fail("is_text empty string failed") + if (is_text(42)) fail("is_text number should be false") + if (is_text(true)) fail("is_text boolean should be false") + if (is_text(null)) fail("is_text null should be false") + if (is_text({})) fail("is_text object should be false") + if (is_text([])) fail("is_text array should be false") +}) + +run("is_logical", function() { + if (!is_logical(true)) fail("is_logical true failed") + if (!is_logical(false)) fail("is_logical false failed") + if (is_logical(1)) fail("is_logical number should be false") + if (is_logical("true")) fail("is_logical string should be false") + if (is_logical(null)) fail("is_logical null should be false") + if (is_logical({})) fail("is_logical object should be false") + if (is_logical([])) fail("is_logical array should be false") +}) + +run("is_object", function() { + if (!is_object({})) fail("is_object empty object failed") + if (!is_object({a: 1})) fail("is_object object failed") + if (is_object([])) fail("is_object array should be false") + if (is_object(null)) fail("is_object null should be false") + if (is_object(42)) fail("is_object number should be false") + if (is_object("hello")) fail("is_object string should be false") + if (is_object(true)) fail("is_object boolean should be false") +}) + +run("is_array", function() { + if (!is_array([])) fail("is_array empty array failed") + if (!is_array([1, 2, 3])) fail("is_array array failed") + if (is_array({})) fail("is_array object should be false") + if (is_array(null)) fail("is_array null should be false") + if (is_array(42)) fail("is_array number should be false") + if (is_array("hello")) fail("is_array string should be false") + if (is_array(true)) fail("is_array boolean should be false") +}) + +run("is_function", function() { + if (!is_function(function(){})) fail("is_function function failed") + var fn = function(x) { return x * 2 } + if (!is_function(fn)) fail("is_function named function failed") + if (is_function({})) fail("is_function object should be false") + if (is_function([])) fail("is_function array should be false") + if (is_function(null)) fail("is_function null should be false") + if (is_function(42)) fail("is_function number should be false") + if (is_function("hello")) fail("is_function string should be false") + if (is_function(true)) fail("is_function boolean should be false") +}) + +run("is_null", function() { + if (!is_null(null)) fail("is_null null failed") + if (is_null(0)) fail("is_null zero should be false") + if (is_null(false)) fail("is_null false should be false") + if (is_null("")) fail("is_null empty string should be false") + if (is_null({})) fail("is_null object should be false") + if (is_null([])) fail("is_null array should be false") + var x + if (!is_null(x)) fail("is_null undefined variable should be true") +}) + +run("is_blob", function() { + if (is_blob(null)) fail("is_blob null should be false") + if (is_blob(42)) fail("is_blob number should be false") + if (is_blob("hello")) fail("is_blob string should be false") + if (is_blob(true)) fail("is_blob boolean should be false") + if (is_blob({})) fail("is_blob object should be false") + if (is_blob([])) fail("is_blob array should be false") + if (is_blob(function(){})) fail("is_blob function should be false") +}) + +run("is_proto", function() { + var a = {} + var b = meme(a) + if (!is_proto(b, a)) fail("is_proto failed on meme") +}) + +// ============================================================================ +// GLOBAL FUNCTIONS - LENGTH +// ============================================================================ + +run("length string", function() { + if (length("hello") != 5) fail("length string failed") + if (length("") != 0) fail("length empty string failed") +}) + +run("length array", function() { + if (length([1,2,3]) != 3) fail("length array failed") + if (length([]) != 0) fail("length empty array failed") +}) + +run("length null", function() { + if (length(null) != null) fail("length null failed") +}) + +run("length number", function() { + if (length(123) != null) fail("length number should return null") +}) + +// ============================================================================ +// GLOBAL FUNCTIONS - REVERSE +// ============================================================================ + +run("reverse array", function() { + var arr = [1, 2, 3, 4, 5] + var rev = reverse(arr) + if (rev[0] != 5) fail("reverse array first failed") + if (rev[4] != 1) fail("reverse array last failed") + if (length(rev) != 5) fail("reverse array length failed") +}) + +run("reverse empty array", function() { + var rev = reverse([]) + if (length(rev) != 0) fail("reverse empty array failed") +}) + +run("reverse preserves original", function() { + var arr = [1, 2, 3] + var rev = reverse(arr) + if (arr[0] != 1) fail("reverse should not mutate original") +}) + +// ============================================================================ +// GLOBAL FUNCTIONS - MEME (PROTOTYPAL INHERITANCE) +// ============================================================================ + +run("meme basic", function() { + var parent = {x: 10} + var child = meme(parent) + if (child.x != 10) fail("meme basic inheritance failed") +}) + +run("meme with mixins", function() { + var parent = {x: 10} + var mixin = {y: 20} + var child = meme(parent, mixin) + if (child.x != 10) fail("meme with mixin parent prop failed") + if (child.y != 20) fail("meme with mixin own prop failed") +}) + +run("meme override", function() { + var parent = {x: 10} + var child = meme(parent) + child.x = 20 + if (child.x != 20) fail("meme override failed") + if (parent.x != 10) fail("meme should not mutate parent") +}) + +run("meme multiple mixins", function() { + var parent = {a: 1} + var mixin1 = {b: 2} + var mixin2 = {c: 3} + var child = meme(parent, [mixin1, mixin2]) + if (child.a != 1 || child.b != 2 || child.c != 3) fail("meme multiple mixins failed") +}) + +// ============================================================================ +// GLOBAL FUNCTIONS - PROTO +// ============================================================================ + +run("proto basic", function() { + var parent = {x: 10} + var child = meme(parent) + var p = proto(child) + if (p != parent) fail("proto basic failed") +}) + +run("proto object literal", function() { + var obj = {x: 10} + var p = proto(obj) + if (p != null) fail("proto of object literal should be null") +}) + +run("proto non object", function() { + if (proto(42) != null) fail("proto of number should return null") + if (proto("hello") != null) fail("proto of string should return null") +}) + +// ============================================================================ +// GLOBAL FUNCTIONS - STONE (FREEZE) +// ============================================================================ + +run("stone object", function() { + var obj = {x: 10} + stone(obj) + if (!should_disrupt(function() { obj.x = 20 })) fail("stone object should prevent modification") +}) + +run("is_stone frozen", function() { + var obj = {x: 10} + if (is_stone(obj)) fail("stone.p should return false before freezing") + stone(obj) + if (!is_stone(obj)) fail("stone.p should return true after freezing") +}) + +run("stone array", function() { + var arr = [1, 2, 3] + stone(arr) + if (!should_disrupt(function() { arr[0] = 99 })) fail("stone array should prevent modification") +}) + +// ============================================================================ +// TERNARY OPERATOR +// ============================================================================ + +run("ternary true", function() { + var x = true ? 1 : 2 + if (x != 1) fail("ternary true failed") +}) + +run("ternary false", function() { + var x = false ? 1 : 2 + if (x != 2) fail("ternary false failed") +}) + +run("ternary nested", function() { + var x = true ? (false ? 1 : 2) : 3 + if (x != 2) fail("ternary nested failed") +}) + +run("ternary with expressions", function() { + var a = 5 + var b = 10 + var max = (a > b) ? a : b + if (max != 10) fail("ternary with expressions failed") +}) + +// ============================================================================ +// UNARY OPERATORS +// ============================================================================ + +run("unary plus", function() { + if (+5 != 5) fail("unary plus positive failed") + if (+-5 != -5) fail("unary plus negative failed") +}) + +run("unary minus", function() { + if (-5 != -5) fail("unary minus failed") + if (-(-5) != 5) fail("double unary minus failed") +}) + +run("increment postfix", function() { + var x = 5 + var y = x++ + if (y != 5) fail("postfix increment return value failed") + if (x != 6) fail("postfix increment side effect failed") +}) + +run("increment prefix", function() { + var x = 5 + var y = ++x + if (y != 6) fail("prefix increment return value failed") + if (x != 6) fail("prefix increment side effect failed") +}) + +run("decrement postfix", function() { + var x = 5 + var y = x-- + if (y != 5) fail("postfix decrement return value failed") + if (x != 4) fail("postfix decrement side effect failed") +}) + +run("decrement prefix", function() { + var x = 5 + var y = --x + if (y != 4) fail("prefix decrement return value failed") + if (x != 4) fail("prefix decrement side effect failed") +}) + +// ============================================================================ +// COMPOUND ASSIGNMENT OPERATORS +// ============================================================================ + +run("plus equals", function() { + var x = 5 + x += 3 + if (x != 8) fail("plus equals failed") +}) + +run("minus equals", function() { + var x = 10 + x -= 3 + if (x != 7) fail("minus equals failed") +}) + +run("times equals", function() { + var x = 4 + x *= 3 + if (x != 12) fail("times equals failed") +}) + +run("divide equals", function() { + var x = 12 + x /= 3 + if (x != 4) fail("divide equals failed") +}) + +run("modulo equals", function() { + var x = 10 + x %= 3 + if (x != 1) fail("modulo equals failed") +}) + +// ============================================================================ +// EDGE CASES AND SPECIAL VALUES +// ============================================================================ + +run("division by zero is null", function() { + var inf = 1 / 0 + if (inf != null) fail("division by zero should be null") + var ninf = -1 / 0 + if (ninf != null) fail("negative division by zero should be null") +}) + +run("zero div zero is null", function() { + var nan = 0 / 0 + if (nan != null) fail("0/0 should be null") +}) + +run("max safe integer", function() { + var max = 9007199254740991 + if (max + 1 - 1 != max) fail("max safe integer precision lost") +}) + +run("min safe integer", function() { + var min = -9007199254740991 + if (min - 1 + 1 != min) fail("min safe integer precision lost") +}) + +run("empty string falsy", function() { + if ("") fail("empty string should be falsy") +}) + +run("zero falsy", function() { + if (0) fail("zero should be falsy") +}) + +run("null falsy", function() { + if (null) fail("null should be falsy") +}) + +run("false falsy", function() { + if (false) fail("false should be falsy") +}) + +run("nonempty string truthy", function() { + if (!"hello") fail("non-empty string should be truthy") +}) + +run("nonzero number truthy", function() { + if (!42) fail("non-zero number should be truthy") +}) + +run("object truthy", function() { + if (!{}) fail("empty object should be truthy") +}) + +run("array truthy", function() { + if (![]) fail("empty array should be truthy") +}) + +// ============================================================================ +// OPERATOR PRECEDENCE +// ============================================================================ + +run("precedence multiply add", function() { + if (2 + 3 * 4 != 14) fail("multiply before add precedence failed") +}) + +run("precedence parentheses", function() { + if ((2 + 3) * 4 != 20) fail("parentheses precedence failed") +}) + +run("precedence comparison logical", function() { + if (!(1 < 2 && 3 < 4)) fail("comparison before logical precedence failed") +}) + +run("precedence equality logical", function() { + if (!(1 == 1 || 2 == 3)) fail("equality before logical precedence failed") +}) + +run("precedence unary multiplication", function() { + if (-2 * 3 != -6) fail("unary before multiplication precedence failed") +}) + +// ============================================================================ +// COMMA OPERATOR +// ============================================================================ + +run("comma operator", function() { + var x = (1, 2, 3) + if (x != 3) fail("comma operator failed") +}) + +run("comma operator with side effects", function() { + var a = 0 + var x = (a = 1, a = 2, a + 1) + if (x != 3) fail("comma operator with side effects failed") + if (a != 2) fail("comma operator side effects failed") +}) + +// ============================================================================ +// VARIABLE SHADOWING +// ============================================================================ + +run("variable shadowing function", function() { + var x = 10 + var fn = function() { + var x = 20 + return x + } + if (fn() != 20) fail("function shadowing failed") + if (x != 10) fail("outer variable after shadowing failed") +}) + +run("variable shadowing nested", function() { + var x = 10 + var fn1 = function() { + var x = 20 + var fn2 = function() { + var x = 30 + return x + } + return fn2() + x + } + if (fn1() != 50) fail("nested shadowing failed") +}) + +// ============================================================================ +// FUNCTION ARITY +// ============================================================================ + +run("function length property", function() { + var fn0 = function() {} + var fn1 = function(a) {} + var fn2 = function(a, b) {} + if (length(fn0) != 0) fail("function length 0 failed") + if (length(fn1) != 1) fail("function length 1 failed") + if (length(fn2) != 2) fail("function length 2 failed") +}) + +// ============================================================================ +// NULL AND UNDEFINED BEHAVIOR +// ============================================================================ + +run("undefined variable is null", function() { + var x + if (x != null) fail("undefined variable should be null") +}) + +// ============================================================================ +// NUMBERS - SPECIAL OPERATIONS +// ============================================================================ + +run("number plus empty string disrupts", function() { + if (!should_disrupt(function() { var n = 42; var result = n + "" })) fail("number + string should disrupt") +}) + +run("number division by zero", function() { + var result = 1 / 0 + if (result != null) fail("division by zero should give null") +}) + +run("number negative division by zero", function() { + var result = -1 / 0 + if (result != null) fail("negative division by zero should give null") +}) + +run("zero division by zero", function() { + var result = 0 / 0 + if (result != null) fail("0/0 should give null") +}) + +run("negative zero normalized", function() { + var nz = -0 + if (nz != 0) fail("-0 should equal 0") + var mul_nz = 0 * -1 + if (mul_nz != 0) fail("0 * -1 should be 0") + var neg_zero = -(0) + if (neg_zero != 0) fail("-(0) should be 0") +}) + +run("overflow is null", function() { + var result = 1e38 * 1e38 + if (result != null) fail("overflow should give null") +}) + +run("modulo by zero is null", function() { + var result = 5 % 0 + if (result != null) fail("modulo by zero should give null") +}) + +// ============================================================================ +// OBJECT PROPERTY EXISTENCE +// ============================================================================ + +run("in operator", function() { + var obj = {a: 1, b: 2} + if (!("a" in obj)) fail("in operator for existing property failed") + if ("c" in obj) fail("in operator for non-existing property failed") +}) + +run("in operator prototype", function() { + var parent = {x: 10} + var child = meme(parent) + if (!("x" in child)) fail("in operator should find inherited property") +}) + +// ============================================================================ +// GLOBAL FUNCTIONS - LOGICAL +// ============================================================================ + +run("logical function numbers", function() { + if (logical(0) != false) fail("logical(0) should be false") + if (logical(1) != true) fail("logical(1) should be true") +}) + +run("logical function strings", function() { + if (logical("false") != false) fail("logical('false') should be false") + if (logical("true") != true) fail("logical('true') should be true") +}) + +run("logical function booleans", function() { + if (logical(false) != false) fail("logical(false) should be false") + if (logical(true) != true) fail("logical(true) should be true") +}) + +run("logical function null", function() { + if (logical(null) != false) fail("logical(null) should be false") +}) + +run("logical function invalid", function() { + if (logical("invalid") != null) fail("logical(invalid) should return null") + if (logical(42) != null) fail("logical(42) should return null") +}) + +// ============================================================================ +// ARRAY METHODS +// ============================================================================ + +run("array concat", function() { + var arr1 = [1, 2] + var arr2 = [3, 4] + var combined = array(arr1, arr2) + if (length(combined) != 4) fail("array concat length failed") + if (combined[2] != 3) fail("array concat values failed") +}) + +run("array join", function() { + var arr = ["a", "b", "c"] + var str = text(arr, ",") + if (str != "a,b,c") fail("array join with text() failed") +}) + +run("text array join numbers disrupt", function() { + if (!should_disrupt(function() { text([1, 2, 3], ",") })) fail("text([numbers], sep) should disrupt") +}) + +run("text array join numbers explicit", function() { + var arr = array([1, 2, 3], x => text(x)) + if (text(arr, ",") != "1,2,3") fail("explicit numeric join failed") +}) + +// ============================================================================ +// STRING METHODS +// ============================================================================ + +run("string substring", function() { + var str = "hello" + if (text(str, 1, 4) != "ell") fail("string substring failed") +}) + +run("string substring first", function() { + var str = "hello" + if (text(str, 1) != "ello") fail("string substring first failed") +}) + +run("string substring to neg", function() { + var str = "hello" + if (text(str, 1, -2) != "el") fail("string substring to negative failed") +}) + +run("string slice", function() { + var str = "hello" + if (text(str, 1, 4) != "ell") fail("string slice failed") + if (text(str, -2) != "lo") fail("string slice negative failed") +}) + +run("string indexOf", function() { + var str = "hello world" + if (search(str, "world") != 6) fail("string search failed") + if (search(str, "xyz") != null) fail("string search not found failed") +}) + +run("string toLowerCase", function() { + var str = "HELLO" + if (lower(str) != "hello") fail("string toLowerCase failed") +}) + +run("string toUpperCase", function() { + var str = "hello" + if (upper(str) != "HELLO") fail("string toUpperCase failed") +}) + +run("string split", function() { + var str = "a,b,c" + var parts = array(str, ",") + if (length(parts) != 3) fail("string split length failed") + if (parts[1] != "b") fail("string split values failed") +}) + +run("null access", function() { + var val = {} + var nn = val.a + if (nn != null) fail("val.a should return null") +}) + +// ============================================================================ +// OBJECT-AS-KEY (Private Property Access) +// ============================================================================ + +run("object key basic", function() { + var k1 = {} + var k2 = {} + var o = {} + o[k1] = 123 + o[k2] = 456 + if (o[k1] != 123) fail("object key k1 failed") + if (o[k2] != 456) fail("object key k2 failed") +}) + +run("object key new object different key", function() { + var k1 = {} + var o = {} + o[k1] = 123 + if (o[{}] != null) fail("new object should be different key") +}) + +run("object key in operator", function() { + var k1 = {} + var o = {} + o[k1] = 123 + if (!(k1 in o)) fail("in operator should find object key") +}) + +run("object key delete", function() { + var k1 = {} + var o = {} + o[k1] = 123 + delete o[k1] + if ((k1 in o)) fail("delete should remove object key") +}) + +run("object key no string collision", function() { + var a = {} + var b = {} + var o = {} + o[a] = 1 + o[b] = 2 + if (o[a] != 1) fail("object key a should be 1") + if (o[b] != 2) fail("object key b should be 2") +}) + +run("object key same object same key", function() { + var k = {} + var o = {} + o[k] = 100 + o[k] = 200 + if (o[k] != 200) fail("same object should be same key") +}) + +run("object key computed property", function() { + var k = {} + var o = {} + o[k] = function() { return 42 } + if (o[k]() != 42) fail("object key with function value failed") +}) + +run("object key multiple objects multiple keys", function() { + var k1 = {} + var k2 = {} + var k3 = {} + var o = {} + o[k1] = "one" + o[k2] = "two" + o[k3] = "three" + if (o[k1] != "one") fail("multiple keys k1 failed") + if (o[k2] != "two") fail("multiple keys k2 failed") + if (o[k3] != "three") fail("multiple keys k3 failed") +}) + +run("object key with string keys", function() { + var k = {} + var o = {name: "test"} + o[k] = "private" + if (o.name != "test") fail("string key should still work") + if (o[k] != "private") fail("object key should work with string keys") +}) + +run("object key overwrite", function() { + var k = {} + var o = {} + o[k] = 1 + o[k] = 2 + o[k] = 3 + if (o[k] != 3) fail("object key overwrite failed") +}) + +run("object key nested objects", function() { + var k1 = {} + var k2 = {} + var inner = {} + inner[k2] = "nested" + var outer = {} + outer[k1] = inner + if (outer[k1][k2] != "nested") fail("nested object keys failed") +}) + +run("array for", function() { + var a = [1,2,3] + arrfor(a, (x,i) => { + if (x-1 != i) fail("array for failed") + }) +}) + +// ============================================================================ +// INVALID KEY TYPES DISRUPT ON SET +// ============================================================================ + +run("array string key disrupts", function() { + if (!should_disrupt(function() { var a = []; a["a"] = 1 })) fail("array should not use string as key") +}) + +run("array object key disrupts", function() { + if (!should_disrupt(function() { var a = []; var b = {}; a[b] = 1 })) fail("array should not use object as key") +}) + +run("array boolean key disrupts", function() { + if (!should_disrupt(function() { var a = []; a[true] = 1 })) fail("array should not use boolean as key") +}) + +run("array null key disrupts", function() { + if (!should_disrupt(function() { var a = []; a[null] = 1 })) fail("array should not use null as key") +}) + +run("array array key disrupts", function() { + if (!should_disrupt(function() { var a = []; var c = []; a[c] = 1 })) fail("array should not use array as key") +}) + +run("obj number key disrupts", function() { + if (!should_disrupt(function() { var a = {}; a[1] = 1 })) fail("object should not use number as key") +}) + +run("obj array key disrupts", function() { + if (!should_disrupt(function() { var a = {}; var c = []; a[c] = 1 })) fail("object should not use array as key") +}) + +run("obj boolean key disrupts", function() { + if (!should_disrupt(function() { var a = {}; a[true] = 1 })) fail("object should not use boolean as key") +}) + +run("obj null key disrupts", function() { + if (!should_disrupt(function() { var a = {}; a[null] = 1 })) fail("object should not use null as key") +}) + +// ============================================================================ +// RETRIEVAL WITH INVALID KEY RETURNS NULL (not disrupt) +// ============================================================================ + +run("array get string key returns null", function() { + var a = [1, 2, 3] + if (a["x"] != null) fail("array get with string key should return null") +}) + +run("array get negative index returns null", function() { + var a = [1, 2, 3] + if (a[-1] != null) fail("array get with negative index should return null") +}) + +run("array get object key returns null", function() { + var a = [1, 2, 3] + var k = {} + if (a[k] != null) fail("array get with object key should return null") +}) + +run("array get array key returns null", function() { + var a = [1, 2, 3] + if (a[[1, 2]] != null) fail("array get with array key should return null") +}) + +run("array get boolean key returns null", function() { + var a = [1, 2, 3] + if (a[true] != null) fail("array get with boolean key should return null") +}) + +run("array get null key returns null", function() { + var a = [1, 2, 3] + if (a[null] != null) fail("array get with null key should return null") +}) + +run("obj get number key returns null", function() { + var o = {a: 1} + if (o[5] != null) fail("object get with number key should return null") +}) + +run("obj get array key returns null", function() { + var o = {a: 1} + if (o[[1, 2]] != null) fail("object get with array key should return null") +}) + +run("obj get boolean key returns null", function() { + var o = {a: 1} + if (o[true] != null) fail("object get with boolean key should return null") +}) + +run("obj get null key returns null", function() { + var o = {a: 1} + if (o[null] != null) fail("object get with null key should return null") +}) + +// ============================================================================ +// FUNCTION AS VALUE - functions should not have properties +// ============================================================================ + +run("function property get arity", function() { + var fn = function(a, b) { return a + b } + var arity = length(fn) + if (arity != 2) fail("length of function should return its arity") +}) + +run("function property set disrupts", function() { + if (!should_disrupt(function() { var fn = function() {}; fn.foo = 123 })) fail("setting property on function should disrupt") +}) + +run("function bracket access disrupts", function() { + if (!should_disrupt(function() { var fn = function() {}; var x = fn["length"]() })) fail("bracket access on function should disrupt") +}) + +run("length returns function arity", function() { + var fn0 = function() { return 1 } + var fn1 = function(a) { return a } + var fn2 = function(a, b) { return a + b } + var fn3 = function(a, b, c) { return a + b + c } + if (length(fn0) != 0) fail("length(fn0) should be 0") + if (length(fn1) != 1) fail("length(fn1) should be 1") + if (length(fn2) != 2) fail("length(fn2) should be 2") + if (length(fn3) != 3) fail("length(fn3) should be 3") +}) + +run("call invokes function", function() { + var fn = function(a, b) { return a + b } + var result = call(fn, null, [3, 4]) + if (result != 7) fail("call(fn, null, [3, 4]) should return 7") +}) + +run("call with this binding", function() { + var obj = { value: 10 } + var fn = function(x) { return this.value + x } + var result = call(fn, obj, [5]) + if (result != 15) fail("call(fn, obj, [5]) should return 15") +}) + +run("call no args", function() { + var fn = function() { return 42 } + var result = call(fn, null) + if (result != 42) fail("call(fn, null) should return 42") +}) + +run("builtin function properties still work", function() { + var min_result = min(5, 3) + if (min_result != 3) fail("min should work") +}) + +// ============================================================================ +// FUNCTION PROXY - Method call sugar for bytecode functions +// ============================================================================ + +run("function proxy basic", function() { + var proxy = function(name, args) { + return `called:${name}:${length(args)}` + } + var result = proxy.foo() + if (result != "called:foo:0") fail("basic proxy call failed") +}) + +run("function proxy with one arg", function() { + var proxy = function(name, args) { + return `${name}-${args[0]}` + } + var result = proxy.test("value") + if (result != "test-value") fail("proxy with one arg failed") +}) + +run("function proxy with multiple args", function() { + var proxy = function(name, args) { + var sum = 0 + for (var i = 0; i < length(args); i++) { + sum = sum + args[i] + } + return `${name}:${sum}` + } + var result = proxy.add(1, 2, 3, 4) + if (result != "add:10") fail("proxy with multiple args failed") +}) + +run("function proxy bracket notation", function() { + var proxy = function(name, args) { + return `bracket:${name}` + } + var result = proxy["myMethod"]() + if (result != "bracket:myMethod") fail("proxy bracket notation failed") +}) + +run("function proxy dynamic method name", function() { + var proxy = function(name, args) { + return name + } + var methodName = "dynamic" + var result = proxy[methodName]() + if (result != "dynamic") fail("proxy dynamic method name failed") +}) + +run("function proxy dispatch to record", function() { + var my_record = { + greet: function(name) { + return `Hello, ${name}` + }, + add: function(a, b) { + return a + b + } + } + var proxy = function(name, args) { + if (is_function(my_record[name])) { + return apply(my_record[name], args) + } + disrupt + } + if (proxy.greet("World") != "Hello, World") fail("proxy dispatch greet failed") + if (proxy.add(3, 4) != 7) fail("proxy dispatch add failed") +}) + +run("function proxy unknown method disrupts", function() { + var proxy = function(name, args) { + disrupt + } + if (!should_disrupt(function() { proxy.nonexistent() })) fail("proxy should disrupt for unknown method") +}) + +run("function proxy is function", function() { + var proxy = function(name, args) { + return name + } + if (!is_function(proxy)) fail("proxy should be a function") +}) + +run("function proxy length is 2", function() { + var proxy = function(name, args) { + return name + } + if (length(proxy) != 2) fail("proxy function should have length 2") +}) + +run("function proxy property read disrupts", function() { + if (!should_disrupt(function() { var fn = function() { return 1 }; var x = fn.someProp })) fail("reading property from non-proxy function should disrupt") +}) + +run("function proxy nested calls", function() { + var outer = function(name, args) { + if (name == "inner") { + return args[0].double(5) + } + return "outer:" + name + } + var inner = function(name, args) { + if (name == "double") { + return args[0] * 2 + } + return "inner:" + name + } + var result = outer.inner(inner) + if (result != 10) fail("nested proxy calls failed") +}) + +run("function proxy returns null", function() { + var proxy = function(name, args) { + return null + } + var result = proxy.anything() + if (result != null) fail("proxy returning null failed") +}) + +run("function proxy returns object", function() { + var proxy = function(name, args) { + return {method: name, argCount: length(args)} + } + var result = proxy.test(1, 2, 3) + if (result.method != "test") fail("proxy returning object method failed") + if (result.argCount != 3) fail("proxy returning object argCount failed") +}) + +run("function proxy returns function", function() { + var proxy = function(name, args) { + return function() { return name } + } + var result = proxy.getFn() + if (result() != "getFn") fail("proxy returning function failed") +}) + +run("function proxy args array is real array", function() { + var proxy = function(name, args) { + if (!is_array(args)) disrupt + args[] = 4 + return length(args) + } + var result = proxy.test(1, 2, 3) + if (result != 4) fail("proxy args should be modifiable array") +}) + +run("function proxy no this binding", function() { + var proxy = function(name, args) { + return this + } + var result = proxy.test() + if (result != null) fail("proxy should have null this") +}) + +run("function proxy integer bracket key disrupts", function() { + var proxy = function(name, args) { + return `key:${name}` + } + if (!should_disrupt(function() { var result = proxy[42]() })) fail("proxy with integer bracket key should disrupt") +}) + +// ============================================================================ +// REDUCE FUNCTION +// ============================================================================ + +run("reduce sum", function() { + var arr = [1, 2, 3, 4, 5] + var result = reduce(arr, (a, b) => a + b) + if (result != 15) fail("reduce sum failed") +}) + +run("reduce product", function() { + var arr = [1, 2, 3, 4, 5] + var result = reduce(arr, (a, b) => a * b) + if (result != 120) fail("reduce product failed") +}) + +run("reduce with initial", function() { + var arr = [1, 2, 3] + var result = reduce(arr, (a, b) => a + b, 10) + if (result != 16) fail("reduce with initial failed") +}) + +run("reduce with initial zero", function() { + var arr = [1, 2, 3] + var result = reduce(arr, (a, b) => a + b, 0) + if (result != 6) fail("reduce with initial zero failed") +}) + +run("reduce empty array no initial", function() { + var arr = [] + var result = reduce(arr, (a, b) => a + b) + if (result != null) fail("reduce empty array without initial should return null") +}) + +run("reduce empty array with initial", function() { + var arr = [] + var result = reduce(arr, (a, b) => a + b, 42) + if (result != 42) fail("reduce empty array with initial should return initial") +}) + +run("reduce single element no initial", function() { + var arr = [42] + var result = reduce(arr, (a, b) => a + b) + if (result != 42) fail("reduce single element without initial failed") +}) + +run("reduce single element with initial", function() { + var arr = [5] + var result = reduce(arr, (a, b) => a + b, 10) + if (result != 15) fail("reduce single element with initial failed") +}) + +run("reduce reverse", function() { + var arr = [1, 2, 3, 4] + var result = reduce(arr, (a, b) => a - b, 0, true) + if (result != -10) fail("reduce reverse failed") +}) + +run("reduce string concat", function() { + var arr = ["a", "b", "c"] + var result = reduce(arr, (a, b) => a + b) + if (result != "abc") fail("reduce string concat failed") +}) + +// ============================================================================ +// SORT FUNCTION +// ============================================================================ + +run("sort numbers", function() { + var arr = [3, 1, 4, 1, 5, 9, 2, 6] + var sorted = sort(arr) + if (sorted[0] != 1 || sorted[1] != 1 || sorted[2] != 2) fail("sort numbers failed") + if (sorted[7] != 9) fail("sort numbers last element failed") +}) + +run("sort strings", function() { + var arr = ["banana", "apple", "cherry"] + var sorted = sort(arr) + if (sorted[0] != "apple") fail("sort strings failed") + if (sorted[2] != "cherry") fail("sort strings last failed") +}) + +run("sort preserves original", function() { + var arr = [3, 1, 2] + var sorted = sort(arr) + if (arr[0] != 3) fail("sort should not mutate original") +}) + +run("sort empty array", function() { + var arr = [] + var sorted = sort(arr) + if (length(sorted) != 0) fail("sort empty array failed") +}) + +run("sort single element", function() { + var arr = [42] + var sorted = sort(arr) + if (sorted[0] != 42) fail("sort single element failed") +}) + +run("sort by field", function() { + var arr = [ + {name: "Charlie", age: 30}, + {name: "Alice", age: 25}, + {name: "Bob", age: 35} + ] + var sorted = sort(arr, "name") + if (sorted[0].name != "Alice") fail("sort by field failed") + if (sorted[2].name != "Charlie") fail("sort by field last failed") +}) + +run("sort by index", function() { + var arr = [[3, "c"], [1, "a"], [2, "b"]] + var sorted = sort(arr, 0) + if (sorted[0][1] != "a") fail("sort by index failed") +}) + +run("sort stable", function() { + var arr = [ + {name: "A", order: 1}, + {name: "B", order: 1}, + {name: "C", order: 1} + ] + var sorted = sort(arr, "order") + if (sorted[0].name != "A" || sorted[1].name != "B" || sorted[2].name != "C") { + fail("sort should be stable") + } +}) + +run("sort negative numbers", function() { + var arr = [-5, 3, -1, 0, 2] + var sorted = sort(arr) + if (sorted[0] != -5 || sorted[4] != 3) fail("sort negative numbers failed") +}) + +// ============================================================================ +// FILTER FUNCTION +// ============================================================================ + +run("filter basic", function() { + var arr = [1, 2, 3, 4, 5, 6] + var evens = filter(arr, x => x % 2 == 0) + if (length(evens) != 3) fail("filter basic length failed") + if (evens[0] != 2 || evens[1] != 4 || evens[2] != 6) fail("filter basic values failed") +}) + +run("filter all pass", function() { + var arr = [2, 4, 6] + var result = filter(arr, x => x % 2 == 0) + if (length(result) != 3) fail("filter all pass failed") +}) + +run("filter none pass", function() { + var arr = [1, 3, 5] + var result = filter(arr, x => x % 2 == 0) + if (length(result) != 0) fail("filter none pass failed") +}) + +run("filter empty array", function() { + var arr = [] + var result = filter(arr, x => true) + if (length(result) != 0) fail("filter empty array failed") +}) + +run("filter with index", function() { + var arr = ["a", "b", "c", "d"] + var result = filter(arr, (x, i) => i % 2 == 0) + if (length(result) != 2) fail("filter with index length failed") + if (result[0] != "a" || result[1] != "c") fail("filter with index values failed") +}) + +run("filter preserves original", function() { + var arr = [1, 2, 3] + var result = filter(arr, x => x > 1) + if (length(arr) != 3) fail("filter should not mutate original") +}) + +run("filter objects", function() { + var arr = [{active: true}, {active: false}, {active: true}] + var result = filter(arr, x => x.active) + if (length(result) != 2) fail("filter objects failed") +}) + +// ============================================================================ +// FIND FUNCTION +// ============================================================================ + +run("find basic", function() { + var arr = [1, 2, 3, 4, 5] + var idx = find(arr, x => x > 3) + if (idx != 3) fail("find basic failed") +}) + +run("find first element", function() { + var arr = [10, 2, 3] + var idx = find(arr, x => x > 5) + if (idx != 0) fail("find first element failed") +}) + +run("find last element", function() { + var arr = [1, 2, 10] + var idx = find(arr, x => x > 5) + if (idx != 2) fail("find last element failed") +}) + +run("find not found", function() { + var arr = [1, 2, 3] + var idx = find(arr, x => x > 10) + if (idx != null) fail("find not found should return null") +}) + +run("find empty array", function() { + var arr = [] + var idx = find(arr, x => true) + if (idx != null) fail("find in empty array should return null") +}) + +run("find by value", function() { + var arr = [10, 20, 30, 20] + var idx = find(arr, 20) + if (idx != 1) fail("find by value failed") +}) + +run("find reverse", function() { + var arr = [10, 20, 30, 20] + var idx = find(arr, 20, true) + if (idx != 3) fail("find reverse failed") +}) + +run("find with from", function() { + var arr = [10, 20, 30, 20] + var idx = find(arr, 20, false, 2) + if (idx != 3) fail("find with from failed") +}) + +run("find with index callback", function() { + var arr = ["a", "b", "c"] + var idx = find(arr, (x, i) => i == 1) + if (idx != 1) fail("find with index callback failed") +}) + +// ============================================================================ +// ABS FUNCTION +// ============================================================================ + +run("abs positive", function() { + if (abs(5) != 5) fail("abs positive failed") +}) + +run("abs negative", function() { + if (abs(-5) != 5) fail("abs negative failed") +}) + +run("abs zero", function() { + if (abs(0) != 0) fail("abs zero failed") +}) + +run("abs float", function() { + if (abs(-3.14) != 3.14) fail("abs float failed") +}) + +run("abs non number", function() { + if (abs("5") != null) fail("abs non-number should return null") + if (abs(null) != null) fail("abs null should return null") +}) + +// ============================================================================ +// FLOOR FUNCTION +// ============================================================================ + +run("floor positive", function() { + if (floor(3.7) != 3) fail("floor positive failed") +}) + +run("floor negative", function() { + if (floor(-3.7) != -4) fail("floor negative failed") +}) + +run("floor integer", function() { + if (floor(5) != 5) fail("floor integer failed") +}) + +run("floor zero", function() { + if (floor(0) != 0) fail("floor zero failed") +}) + +run("floor with place", function() { + if (floor(12.3775, -2) != 12.37) fail("floor with place failed") +}) + +run("floor negative with place", function() { + if (floor(-12.3775, -2) != -12.38) fail("floor negative with place failed") +}) + +// ============================================================================ +// CEILING FUNCTION +// ============================================================================ + +run("ceiling positive", function() { + if (ceiling(3.2) != 4) fail("ceiling positive failed") +}) + +run("ceiling negative", function() { + if (ceiling(-3.7) != -3) fail("ceiling negative failed") +}) + +run("ceiling integer", function() { + if (ceiling(5) != 5) fail("ceiling integer failed") +}) + +run("ceiling zero", function() { + if (ceiling(0) != 0) fail("ceiling zero failed") +}) + +run("ceiling with place", function() { + if (ceiling(12.3775, -2) != 12.38) fail("ceiling with place failed") +}) + +run("ceiling negative with place", function() { + if (ceiling(-12.3775, -2) != -12.37) fail("ceiling negative with place failed") +}) + +// ============================================================================ +// ROUND FUNCTION +// ============================================================================ + +run("round down", function() { + if (round(3.4) != 3) fail("round down failed") +}) + +run("round up", function() { + if (round(3.6) != 4) fail("round up failed") +}) + +run("round half", function() { + if (round(3.5) != 4) fail("round half failed") +}) + +run("round negative", function() { + if (round(-3.5) != -3 && round(-3.5) != -4) fail("round negative failed") +}) + +run("round integer", function() { + if (round(5) != 5) fail("round integer failed") +}) + +run("round with places", function() { + if (round(12.3775, -2) != 12.38) fail("round with places failed") +}) + +run("round to tens", function() { + if (round(12.3775, 1) != 10) fail("round to tens failed") +}) + +// ============================================================================ +// TRUNC FUNCTION +// ============================================================================ + +run("trunc positive", function() { + if (trunc(3.7) != 3) fail("trunc positive failed") +}) + +run("trunc negative", function() { + if (trunc(-3.7) != -3) fail("trunc negative failed") +}) + +run("trunc integer", function() { + if (trunc(5) != 5) fail("trunc integer failed") +}) + +run("trunc zero", function() { + if (trunc(0) != 0) fail("trunc zero failed") +}) + +run("trunc with places", function() { + if (trunc(12.3775, -2) != 12.37) fail("trunc with places failed") +}) + +run("trunc negative with places", function() { + if (trunc(-12.3775, -2) != -12.37) fail("trunc negative with places failed") +}) + +// ============================================================================ +// SIGN FUNCTION +// ============================================================================ + +run("sign positive", function() { + if (sign(5) != 1) fail("sign positive failed") +}) + +run("sign negative", function() { + if (sign(-5) != -1) fail("sign negative failed") +}) + +run("sign zero", function() { + if (sign(0) != 0) fail("sign zero failed") +}) + +run("sign float", function() { + if (sign(0.001) != 1) fail("sign positive float failed") + if (sign(-0.001) != -1) fail("sign negative float failed") +}) + +run("sign non number", function() { + if (sign("5") != null) fail("sign non-number should return null") +}) + +// ============================================================================ +// WHOLE AND FRACTION FUNCTIONS +// ============================================================================ + +run("whole positive", function() { + if (whole(3.7) != 3) fail("whole positive failed") +}) + +run("whole negative", function() { + if (whole(-3.7) != -3) fail("whole negative failed") +}) + +run("whole integer", function() { + if (whole(5) != 5) fail("whole integer failed") +}) + +run("whole non number", function() { + if (whole("5") != null) fail("whole non-number should return null") +}) + +run("fraction positive", function() { + var f = fraction(3.75) + if (f < 0.74 || f > 0.76) fail("fraction positive failed") +}) + +run("fraction negative", function() { + var f = fraction(-3.75) + if (f > -0.74 || f < -0.76) fail("fraction negative failed") +}) + +run("fraction integer", function() { + if (fraction(5) != 0) fail("fraction integer failed") +}) + +run("fraction non number", function() { + if (fraction("5") != null) fail("fraction non-number should return null") +}) + +// ============================================================================ +// NEG FUNCTION +// ============================================================================ + +run("neg positive", function() { + if (neg(5) != -5) fail("neg positive failed") +}) + +run("neg negative", function() { + if (neg(-5) != 5) fail("neg negative failed") +}) + +run("neg zero", function() { + if (neg(0) != 0) fail("neg zero failed") +}) + +run("neg float", function() { + if (neg(3.14) != -3.14) fail("neg float failed") +}) + +run("neg non number", function() { + if (neg("5") != null) fail("neg non-number should return null") +}) + +// ============================================================================ +// MODULO FUNCTION +// ============================================================================ + +run("modulo positive", function() { + if (modulo(10, 3) != 1) fail("modulo positive failed") +}) + +run("modulo negative dividend", function() { + var result = modulo(-10, 3) + if (result != 2) fail("modulo negative dividend failed") +}) + +run("modulo negative divisor", function() { + var result = modulo(10, -3) + if (result != -2) fail("modulo negative divisor failed") +}) + +run("modulo both negative", function() { + var result = modulo(-10, -3) + if (result != -1) fail("modulo both negative failed") +}) + +run("modulo zero dividend", function() { + if (modulo(0, 5) != 0) fail("modulo zero dividend failed") +}) + +run("modulo zero divisor", function() { + if (modulo(10, 0) != null) fail("modulo zero divisor should return null") +}) + +run("modulo floats", function() { + var result = modulo(5.5, 2) + if (result < 1.4 || result > 1.6) fail("modulo floats failed") +}) + +// ============================================================================ +// MIN AND MAX FUNCTIONS +// ============================================================================ + +run("min basic", function() { + if (min(3, 5) != 3) fail("min basic failed") +}) + +run("min equal", function() { + if (min(5, 5) != 5) fail("min equal failed") +}) + +run("min negative", function() { + if (min(-3, -5) != -5) fail("min negative failed") +}) + +run("min mixed", function() { + if (min(-3, 5) != -3) fail("min mixed failed") +}) + +run("min float", function() { + if (min(3.14, 2.71) != 2.71) fail("min float failed") +}) + +run("min non number", function() { + if (min(3, "5") != null) fail("min non-number should return null") + if (min("3", 5) != null) fail("min first non-number should return null") +}) + +run("max basic", function() { + if (max(3, 5) != 5) fail("max basic failed") +}) + +run("max equal", function() { + if (max(5, 5) != 5) fail("max equal failed") +}) + +run("max negative", function() { + if (max(-3, -5) != -3) fail("max negative failed") +}) + +run("max mixed", function() { + if (max(-3, 5) != 5) fail("max mixed failed") +}) + +run("max float", function() { + if (max(3.14, 2.71) != 3.14) fail("max float failed") +}) + +run("max non number", function() { + if (max(3, "5") != null) fail("max non-number should return null") +}) + +run("min max constrain", function() { + var val = 8 + var constrained = min(max(val, 0), 10) + if (constrained != 8) fail("min max constrain in range failed") + constrained = min(max(-5, 0), 10) + if (constrained != 0) fail("min max constrain below failed") + constrained = min(max(15, 0), 10) + if (constrained != 10) fail("min max constrain above failed") +}) + +// ============================================================================ +// CODEPOINT FUNCTION +// ============================================================================ + +run("codepoint letter", function() { + if (codepoint("A") != 65) fail("codepoint A failed") + if (codepoint("a") != 97) fail("codepoint a failed") +}) + +run("codepoint digit", function() { + if (codepoint("0") != 48) fail("codepoint 0 failed") +}) + +run("codepoint unicode", function() { + if (codepoint("\u00E9") != 233) fail("codepoint unicode failed") +}) + +run("codepoint first char", function() { + if (codepoint("ABC") != 65) fail("codepoint should return first char") +}) + +run("codepoint empty", function() { + if (codepoint("") != null) fail("codepoint empty should return null") +}) + +run("codepoint non text", function() { + if (codepoint(65) != null) fail("codepoint non-text should return null") +}) + +// ============================================================================ +// CHARACTER FUNCTION +// ============================================================================ + +run("character letter", function() { + if (character(65) != "A") fail("character 65 failed") + if (character(97) != "a") fail("character 97 failed") +}) + +run("character digit", function() { + if (character(48) != "0") fail("character 48 failed") +}) + +run("character unicode", function() { + if (character(233) != "\u00E9") fail("character unicode failed") +}) + +run("character from text", function() { + if (character("hello") != "h") fail("character from text failed") +}) + +run("character invalid", function() { + if (character(-1) != "") fail("character negative should return empty") +}) + +// ============================================================================ +// SEARCH FUNCTION +// ============================================================================ + +run("search found", function() { + if (search("hello world", "world") != 6) fail("search found failed") +}) + +run("search not found", function() { + if (search("hello world", "xyz") != null) fail("search not found should return null") +}) + +run("search beginning", function() { + if (search("hello world", "hello") != 0) fail("search beginning failed") +}) + +run("search single char", function() { + if (search("hello", "l") != 2) fail("search single char failed") +}) + +run("search with from", function() { + if (search("hello hello", "hello", 1) != 6) fail("search with from failed") +}) + +run("search empty pattern", function() { + if (search("hello", "") != 0) fail("search empty pattern failed") +}) + +run("search negative from", function() { + var result = search("hello world", "world", -5) + if (result != 6) fail("search negative from failed") +}) + +// ============================================================================ +// REPLACE FUNCTION +// ============================================================================ + +run("replace basic", function() { + var result = replace("hello world", "world", "universe") + if (result != "hello universe") fail("replace basic failed") +}) + +run("replace not found", function() { + var result = replace("hello world", "xyz", "abc") + if (result != "hello world") fail("replace not found should return original") +}) + +run("replace multiple", function() { + var result = replace("banana", "a", "o") + if (result != "bonono") fail("replace multiple failed") +}) + +run("replace with limit", function() { + var result = replace("banana", "a", "o", 1) + if (result != "bonana") fail("replace with limit failed") +}) + +run("replace empty target", function() { + var result = replace("abc", "", "-") + if (result != "-a-b-c-") fail("replace empty target failed") +}) + +run("replace to empty", function() { + var result = replace("hello", "l", "") + if (result != "heo") fail("replace to empty failed") +}) + +run("replace with function", function() { + var result = replace("hello", "l", (match, pos) => `[${pos}]`) + if (result != "he[2][3]o") fail("replace with function failed") +}) + +run("replace with function limit", function() { + var result = replace("banana", "a", (match, pos) => `[${pos}]`, 2) + if (result != "b[1]n[3]na") fail("replace with function limit failed") +}) + +run("replace with regex", function() { + var result = replace("banana", /a/, "o") + if (result != "bonono") fail("replace with regex failed") +}) + +run("replace with regex limit", function() { + var result = replace("banana", /a/, "o", 2) + if (result != "bonona") fail("replace with regex limit failed") +}) + +run("replace with regex function", function() { + var result = replace("hello", /l/, (match, pos) => `[${pos}]`) + if (result != "he[2][3]o") fail("replace with regex function failed") +}) + +// ============================================================================ +// TEXT FUNCTION (Conversion and Slicing) +// ============================================================================ + +run("text number basic", function() { + if (text(123) != "123") fail("text number basic failed") +}) + +run("text number negative", function() { + if (text(-456) != "-456") fail("text number negative failed") +}) + +run("text number float", function() { + var result = text(3.14) + if (search(result, "3.14") != 0) fail("text number float failed") +}) + +run("text array join empty sep", function() { + var result = text(["a", "b", "c"], "") + if (result != "abc") fail("text array join empty sep failed") +}) + +run("text slice basic", function() { + if (text("hello", 1, 4) != "ell") fail("text slice basic failed") +}) + +run("text slice from only", function() { + if (text("hello", 2) != "llo") fail("text slice from only failed") +}) + +run("text slice negative from", function() { + if (text("hello", -2) != "lo") fail("text slice negative from failed") +}) + +run("text slice negative to", function() { + if (text("hello", 0, -2) != "hel") fail("text slice negative to failed") +}) + +run("text boolean", function() { + if (text(true) != "true") fail("text true failed") + if (text(false) != "false") fail("text false failed") +}) + +run("text null", function() { + if (text(null) != "null") fail("text null failed") +}) + +// ============================================================================ +// NUMBER FUNCTION (Conversion) +// ============================================================================ + +run("number from string", function() { + if (number("123") != 123) fail("number from string failed") +}) + +run("number from negative string", function() { + if (number("-456") != -456) fail("number from negative string failed") +}) + +run("number from float string", function() { + if (number("3.14") != 3.14) fail("number from float string failed") +}) + +run("number invalid string", function() { + if (number("abc") != null) fail("number invalid string should return null") +}) + +run("number from boolean", function() { + if (number(true) != 1) fail("number from true failed") + if (number(false) != 0) fail("number from false failed") +}) + +run("number from number", function() { + if (number(42) != 42) fail("number from number failed") +}) + +run("number with radix", function() { + if (number("FF", 16) != 255) fail("number hex failed") + if (number("1010", 2) != 10) fail("number binary failed") +}) + +run("number leading zeros", function() { + if (number("007") != 7) fail("number leading zeros failed") +}) + +// ============================================================================ +// ARRAY FUNCTION (Creator and Slicing) +// ============================================================================ + +run("array create with length", function() { + var arr = array(5) + if (length(arr) != 5) fail("array create length failed") + if (arr[0] != null) fail("array create should init to null") +}) + +run("array create with initial", function() { + var arr = array(3, 42) + if (arr[0] != 42 || arr[1] != 42 || arr[2] != 42) fail("array create with initial failed") +}) + +run("array create with function", function() { + var arr = array(3, i => i * 2) + if (arr[0] != 0 || arr[1] != 2 || arr[2] != 4) fail("array create with function failed") +}) + +run("array copy", function() { + var orig = [1, 2, 3] + var copy = array(orig) + copy[0] = 99 + if (orig[0] != 1) fail("array copy should not affect original") +}) + +run("array slice basic", function() { + var arr = [1, 2, 3, 4, 5] + var sliced = array(arr, 1, 3) + if (length(sliced) != 2) fail("array slice length failed") + if (sliced[0] != 2 || sliced[1] != 3) fail("array slice values failed") +}) + +run("array slice negative", function() { + var arr = [1, 2, 3, 4, 5] + var sliced = array(arr, -3) + if (length(sliced) != 3) fail("array slice negative failed") + if (sliced[0] != 3) fail("array slice negative value failed") +}) + +run("array from object keys", function() { + var obj = {a: 1, b: 2, c: 3} + var keys = array(obj) + if (length(keys) != 3) fail("array from object keys length failed") +}) + +run("array from text", function() { + var arr = array("abc") + if (length(arr) != 3) fail("array from text length failed") + if (arr[0] != "a" || arr[1] != "b" || arr[2] != "c") fail("array from text values failed") +}) + +run("array split text", function() { + var arr = array("a,b,c", ",") + if (length(arr) != 3) fail("array split text length failed") + if (arr[1] != "b") fail("array split text value failed") +}) + +// ============================================================================ +// TRIM FUNCTION +// ============================================================================ + +run("trim spaces", function() { + if (trim(" hello ") != "hello") fail("trim spaces failed") +}) + +run("trim tabs", function() { + if (trim("\thello\t") != "hello") fail("trim tabs failed") +}) + +run("trim mixed", function() { + if (trim(" \t hello \n ") != "hello") fail("trim mixed failed") +}) + +run("trim no whitespace", function() { + if (trim("hello") != "hello") fail("trim no whitespace failed") +}) + +run("trim empty", function() { + if (trim("") != "") fail("trim empty failed") +}) + +run("trim all whitespace", function() { + if (trim(" ") != "") fail("trim all whitespace failed") +}) + +// ============================================================================ +// LOWER AND UPPER FUNCTIONS +// ============================================================================ + +run("lower basic", function() { + if (lower("HELLO") != "hello") fail("lower basic failed") +}) + +run("lower mixed", function() { + if (lower("HeLLo WoRLD") != "hello world") fail("lower mixed failed") +}) + +run("lower already lower", function() { + if (lower("hello") != "hello") fail("lower already lower failed") +}) + +run("lower with numbers", function() { + if (lower("ABC123") != "abc123") fail("lower with numbers failed") +}) + +run("upper basic", function() { + if (upper("hello") != "HELLO") fail("upper basic failed") +}) + +run("upper mixed", function() { + if (upper("HeLLo WoRLD") != "HELLO WORLD") fail("upper mixed failed") +}) + +run("upper already upper", function() { + if (upper("HELLO") != "HELLO") fail("upper already upper failed") +}) + +run("upper with numbers", function() { + if (upper("abc123") != "ABC123") fail("upper with numbers failed") +}) + +// ============================================================================ +// APPLY FUNCTION +// ============================================================================ + +run("apply basic", function() { + var fn = function(a, b) { return a + b } + var result = apply(fn, [3, 4]) + if (result != 7) fail("apply basic failed") +}) + +run("apply no args", function() { + var fn = function() { return 42 } + var result = apply(fn, []) + if (result != 42) fail("apply no args failed") +}) + +run("apply single arg", function() { + var fn = function(x) { return x * 2 } + var result = apply(fn, [5]) + if (result != 10) fail("apply single arg failed") +}) + +run("apply many args", function() { + var fn = function(a, b, c, d) { return a + b + c + d } + var result = apply(fn, [1, 2, 3, 4]) + if (result != 10) fail("apply many args failed") +}) + +run("apply non function", function() { + var result = apply(42, [1, 2]) + if (result != 42) fail("apply non-function should return first arg") +}) + +// ============================================================================ +// CALL FUNCTION (Additional Tests) +// ============================================================================ + +run("call many args", function() { + var fn = function(a, b, c, d) { return a * b + c * d } + var result = call(fn, null, [2, 3, 4, 5]) + if (result != 26) fail("call many args failed") +}) + +run("call method style", function() { + var obj = { + value: 10, + multiply: function(x) { return this.value * x } + } + var result = call(obj.multiply, obj, [5]) + if (result != 50) fail("call method style failed") +}) + +run("call change this", function() { + var obj1 = { value: 10 } + var obj2 = { value: 20 } + var fn = function() { return this.value } + if (call(fn, obj1) != 10) fail("call this obj1 failed") + if (call(fn, obj2) != 20) fail("call this obj2 failed") +}) + +// ============================================================================ +// ARRFOR FUNCTION (Array For-Each) +// ============================================================================ + +run("arrfor basic", function() { + var arr = [1, 2, 3] + var sum = 0 + arrfor(arr, x => { sum = sum + x }) + if (sum != 6) fail("arrfor basic failed") +}) + +run("arrfor with index", function() { + var arr = ["a", "b", "c"] + var indices = [] + arrfor(arr, (x, i) => { indices[] = i }) + if (indices[0] != 0 || indices[2] != 2) fail("arrfor with index failed") +}) + +run("arrfor empty", function() { + var called = false + arrfor([], x => { called = true }) + if (called) fail("arrfor empty should not call function") +}) + +run("arrfor mutation", function() { + var arr = [1, 2, 3] + var results = [] + arrfor(arr, x => { results[] = x * 2 }) + if (results[0] != 2 || results[1] != 4 || results[2] != 6) fail("arrfor mutation failed") +}) + +// ============================================================================ +// STONE FUNCTION (Additional Tests) +// ============================================================================ + +run("stone returns value", function() { + var obj = {x: 1} + var result = stone(obj) + if (result != obj) fail("stone should return the value") +}) + +run("stone idempotent", function() { + var obj = {x: 1} + stone(obj) + stone(obj) + if (!is_stone(obj)) fail("stone should be idempotent") +}) + +// ============================================================================ +// PROTO FUNCTION (Additional Tests) +// ============================================================================ + +run("proto chain", function() { + var grandparent = {a: 1} + var parent = meme(grandparent) + var child = meme(parent) + if (proto(child) != parent) fail("proto chain child->parent failed") + if (proto(parent) != grandparent) fail("proto chain parent->grandparent failed") +}) + +run("proto array disrupts", function() { + if (!should_disrupt(function() { proto([1, 2, 3]) })) fail("proto of array should disrupt") +}) + +// ============================================================================ +// MEME FUNCTION (Additional Tests) +// ============================================================================ + +run("meme method inheritance", function() { + var parent = { + greet: function() { return "hello" } + } + var child = meme(parent) + if (child.greet() != "hello") fail("meme method inheritance failed") +}) + +run("meme this in inherited method", function() { + var parent = { + getValue: function() { return this.value } + } + var child = meme(parent) + child.value = 42 + if (child.getValue() != 42) fail("meme this in inherited method failed") +}) + +run("meme deep chain", function() { + var a = {x: 1} + var b = meme(a) + var c = meme(b) + var d = meme(c) + if (d.x != 1) fail("meme deep chain failed") +}) + +// ============================================================================ +// DELETE OPERATOR +// ============================================================================ + +run("delete property", function() { + var obj = {a: 1, b: 2} + delete obj.a + if ("a" in obj) fail("delete property failed") + if (obj.b != 2) fail("delete should not affect other properties") +}) + +run("delete array element disrupts", function() { + if (!should_disrupt(function() { var arr = [1, 2, 3]; delete arr[1] })) fail("delete on array element should disrupt") +}) + +run("delete nonexistent", function() { + var obj = {a: 1} + delete obj.b + if (obj.a != 1) fail("delete nonexistent should not affect object") +}) + +// ============================================================================ +// TYPEOF-LIKE BEHAVIOR +// ============================================================================ + +run("is integer", function() { + if (!is_number(5) || 5 % 1 != 0) fail("is_integer positive failed") + if (!is_number(-5) || -5 % 1 != 0) fail("is_integer negative failed") + if (is_number(5.5) && 5.5 % 1 == 0) fail("is_integer float should not be integer") +}) + +// ============================================================================ +// ARRAY MAP-LIKE WITH ARRAY FUNCTION +// ============================================================================ + +run("array map basic", function() { + var arr = [1, 2, 3] + var doubled = array(arr, x => x * 2) + if (doubled[0] != 2 || doubled[1] != 4 || doubled[2] != 6) fail("array map basic failed") +}) + +run("array map with index", function() { + var arr = ["a", "b", "c"] + var result = array(arr, (x, i) => `${x}${i}`) + if (result[0] != "a0" || result[1] != "b1") fail("array map with index failed") +}) + +run("array map reverse", function() { + var arr = [1, 2, 3] + var result = array(arr, x => x * 2, true) + if (result[0] != 6 || result[2] != 2) fail("array map reverse failed") +}) + +run("array map with exit", function() { + var arr = [1, 2, 3, 4, 5] + var result = array(arr, x => { + if (x > 3) return null + return x * 2 + }, false, null) + if (length(result) != 5) fail("array map with exit length unexpected") +}) + +// ============================================================================ +// ERROR OBJECTS +// ============================================================================ + +run("error creation", function() { + var e = Error("test message") + if (e.message != "test message") fail("Error creation failed") +}) + +// ============================================================================ +// STRING METHOD EDGE CASES +// ============================================================================ + +run("string startsWith", function() { + if (!starts_with("hello", "hel")) fail("startsWith match failed") + if (starts_with("hello", "ell")) fail("startsWith no match failed") + if (!starts_with("hello", "")) fail("startsWith empty should match") +}) + +run("string endsWith", function() { + if (!ends_with("hello", "llo")) fail("endsWith match failed") + if (ends_with("hello", "ell")) fail("endsWith no match failed") + if (!ends_with("hello", "")) fail("endsWith empty should match") +}) + +run("string includes", function() { + if (search("hello world", "world") == null) fail("includes match failed") + if (search("hello", "xyz") != null) fail("includes no match failed") + if (search("hello", "") == null) fail("includes empty should match") +}) + +// ============================================================================ +// ARRAY METHOD EDGE CASES +// ============================================================================ + +run("array includes", function() { + var arr = [1, 2, 3] + if (find(arr, 2) == null) fail("array includes match failed") + if (find(arr, 5) != null) fail("array includes no match failed") +}) + +run("array every", function() { + var arr = [2, 4, 6] + if (!every(arr, x => x % 2 == 0)) fail("array every all pass failed") + arr = [2, 3, 6] + if (every(arr, x => x % 2 == 0)) fail("array every not all pass failed") +}) + +run("array some", function() { + var arr = [1, 2, 3] + if (!some(arr, x => x > 2)) fail("array some match failed") + if (some(arr, x => x > 5)) fail("array some no match failed") +}) + +// ============================================================================ +// ADDITIONAL EDGE CASES +// ============================================================================ + +run("nested array access", function() { + var arr = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + if (arr[0][1][0] != 3) fail("nested array access failed") + if (arr[1][1][1] != 8) fail("nested array access deep failed") +}) + +run("nested object access", function() { + var obj = {a: {b: {c: {d: 42}}}} + if (obj.a.b.c.d != 42) fail("nested object access failed") +}) + +run("mixed nested access", function() { + var data = {users: [{name: "Alice"}, {name: "Bob"}]} + if (data.users[1].name != "Bob") fail("mixed nested access failed") +}) + +run("object with null value", function() { + var obj = {a: null, b: 2} + if (obj.a != null) fail("object null value failed") + if (!("a" in obj)) fail("object with null should have key") +}) + +run("array with null values", function() { + var arr = [1, null, 3] + if (arr[1] != null) fail("array null value failed") + if (length(arr) != 3) fail("array with null length failed") +}) + +run("function returning function", function() { + var outer = function(x) { + return function(y) { + return function(z) { + return x + y + z + } + } + } + if (outer(1)(2)(3) != 6) fail("function returning function failed") +}) + +run("immediately invoked function", function() { + var result = (function(x) { return x * 2 })(21) + if (result != 42) fail("immediately invoked function failed") +}) + +run("text split text", function() { + var t = "hello world" + var result = array(t, " ") + if (length(result) != 2) fail("text split failed") + if (result[0] != "hello") fail("text split first failed") + if (result[1] != "world") fail("text split second failed") +}) + +run("text split regex", function() { + var t = "hello world" + var result = array(t, /\s+/) + if (length(result) != 2) fail("text split regex failed") + if (result[0] != "hello") fail("text split regex first failed") + if (result[1] != "world") fail("text split regex second failed") +}) + +run("text search text", function() { + var t = "hello world" + var result = search(t, "world") + if (result != 6) fail("text search failed") +}) + +run("text search regex", function() { + var t = "hello world" + var result = search(t, /world/) + if (result != 6) fail("text search regex failed") +}) + +run("extract basic text", function() { + var t = "hello world" + var result = extract(t, "world") + if (result[0] != "world") fail("extract basic text failed") +}) + +run("extract text not found", function() { + var t = "hello world" + var result = extract(t, "xyz") + if (result != null) fail("extract not found should return null") +}) + +run("extract regex basic", function() { + var t = "hello world" + var result = extract(t, /world/) + if (result[0] != "world") fail("extract regex basic failed") +}) + +run("extract regex with capture group", function() { + var t = "hello world" + var result = extract(t, /(\w+) (\w+)/) + if (result[0] != "hello world") fail("extract regex full match failed") + if (result[1] != "hello") fail("extract regex capture group 1 failed") + if (result[2] != "world") fail("extract regex capture group 2 failed") +}) + +run("extract regex digits", function() { + var t = "abc123def456" + var result = extract(t, /(\d+)/) + if (result[0] != "123") fail("extract regex digits failed") + if (result[1] != "123") fail("extract regex digits capture failed") +}) + +run("extract with from", function() { + var t = "hello hello world" + var result = extract(t, "hello", 1) + if (result[0] != "hello") fail("extract with from failed") +}) + +run("extract with from to", function() { + var t = "hello world hello" + var result = extract(t, "hello", 0, 10) + if (result[0] != "hello") fail("extract with from to failed") +}) + +run("extract regex case insensitive", function() { + var t = "Hello World" + var result = extract(t, /hello/i) + if (result[0] != "Hello") fail("extract regex case insensitive failed") +}) + +// ============================================================================ +// GC PATHOLOGICAL CASES +// ============================================================================ + +run("gc cycle object self", function() { + var obj = {name: "root"} + obj.self = obj + if (obj.self != obj) fail("self cycle failed") +}) + +run("gc cycle array self", function() { + var arr = [] + for (var i = 0; i < 10; i++) { + arr[] = arr + } + if (arr[0] != arr) fail("array self cycle failed") +}) + +run("gc cycle object array pair", function() { + var obj = {kind: "node"} + var arr = [obj] + obj.arr = arr + if (obj.arr[0] != obj) fail("object/array cycle failed") +}) + +run("gc shared references", function() { + var shared = {value: 42} + var a = {ref: shared} + var b = {ref: shared} + if (a.ref != shared || b.ref != shared) fail("shared reference failed") +}) + +run("gc object key cycle", function() { + var k = {} + var v = {label: "value"} + var o = {} + o[k] = v + v.back = o + if (o[k].back != o) fail("object key cycle failed") +}) + +run("gc object text key mix", function() { + var obj = {} + var key = "alpha" + var inner = {token: "x"} + obj[key] = inner + obj["beta"] = [inner, obj] + if (obj.alpha.token != "x") fail("text key value failed") + if (obj.beta[1] != obj) fail("text key cycle failed") +}) + +// ============================================================================ +// OBJECT INTRINSIC TESTS +// ============================================================================ + +run("object shallow copy", function() { + var orig = {a: 1, b: 2, c: 3} + var copy = object(orig) + if (copy.a != 1) fail("object copy a failed") + if (copy.b != 2) fail("object copy b failed") + if (copy.c != 3) fail("object copy c failed") + copy.a = 99 + if (orig.a != 1) fail("object copy should not mutate original") +}) + +run("object combine", function() { + var obj1 = {a: 1, b: 2} + var obj2 = {c: 3, d: 4} + var combined = object(obj1, obj2) + if (combined.a != 1) fail("object combine a failed") + if (combined.b != 2) fail("object combine b failed") + if (combined.c != 3) fail("object combine c failed") + if (combined.d != 4) fail("object combine d failed") +}) + +run("object combine override", function() { + var obj1 = {a: 1, b: 2} + var obj2 = {b: 99, c: 3} + var combined = object(obj1, obj2) + if (combined.a != 1) fail("object combine override a failed") + if (combined.b != 99) fail("object combine should override with second arg") + if (combined.c != 3) fail("object combine override c failed") +}) + +run("object select keys", function() { + var orig = {a: 1, b: 2, c: 3, d: 4} + var selected = object(orig, ["a", "c"]) + if (selected.a != 1) fail("object select a failed") + if (selected.c != 3) fail("object select c failed") + if (selected.b != null) fail("object select should not include b") + if (selected.d != null) fail("object select should not include d") +}) + +run("object from keys true", function() { + var keys = ["x", "y", "z"] + var obj = object(keys) + if (obj.x != true) fail("object from keys x failed") + if (obj.y != true) fail("object from keys y failed") + if (obj.z != true) fail("object from keys z failed") +}) + +run("object from keys function", function() { + var keys = ["a", "b", "c"] + var obj = object(keys, function(k) { return k + "_val" }) + if (obj.a != "a_val") fail("object from keys func a failed") + if (obj.b != "b_val") fail("object from keys func b failed") + if (obj.c != "c_val") fail("object from keys func c failed") +}) + +// ============================================================================ +// SPLAT INTRINSIC TESTS +// ============================================================================ + +run("splat prototype flattening", function() { + var proto = {x: 10, y: 20} + var obj = meme(proto, {z: 30}) + var flat = splat(obj) + if (flat.x != 10) fail("splat x failed") + if (flat.y != 20) fail("splat y failed") + if (flat.z != 30) fail("splat z failed") +}) + +// ============================================================================ +// REVERSE INTRINSIC TESTS (detailed) +// ============================================================================ + +run("reverse array detailed", function() { + var arr = [1, 2, 3, 4, 5] + var rev = reverse(arr) + if (rev[0] != 5) fail("reverse[0] failed") + if (rev[1] != 4) fail("reverse[1] failed") + if (rev[2] != 3) fail("reverse[2] failed") + if (rev[3] != 2) fail("reverse[3] failed") + if (rev[4] != 1) fail("reverse[4] failed") + if (arr[0] != 1) fail("reverse should not mutate original") +}) + +// ============================================================================ +// APPLY INTRINSIC TESTS +// ============================================================================ + +run("fn.apply with array args", function() { + def sum = function(a, b, c) { return a + b + c } + var result = fn.apply(sum, [1, 2, 3]) + if (result != 6) fail("apply with array args failed") +}) + +run("fn.apply with no args", function() { + def ret42 = function() { return 42 } + var result = fn.apply(ret42) + if (result != 42) fail("apply with no args failed") +}) + +run("fn.apply with single value", function() { + def double = function(x) { return x * 2 } + var result = fn.apply(double, 10) + if (result != 20) fail("apply with single value failed") +}) + +// ============================================================================ +// GC STRESS TESTS FOR FIXED INTRINSICS +// ============================================================================ + +run("gc reverse under pressure", function() { + var arrays = [] + for (var i = 0; i < 100; i = i + 1) { + arrays[i] = [i, i+1, i+2, i+3, i+4] + } + for (var i = 0; i < 100; i = i + 1) { + var rev = reverse(arrays[i]) + if (rev[0] != i+4) fail("gc reverse stress failed") + } +}) + +run("gc object select under pressure", function() { + var objs = [] + for (var i = 0; i < 100; i = i + 1) { + objs[i] = {a: i, b: i+1, c: i+2, d: i+3} + } + for (var i = 0; i < 100; i = i + 1) { + var selected = object(objs[i], ["a", "c"]) + if (selected.a != i) fail("gc object select stress failed") + if (selected.c != i+2) fail("gc object select stress c failed") + } +}) + +run("gc object from keys function under pressure", function() { + var keysets = [] + for (var i = 0; i < 50; i = i + 1) { + keysets[i] = [`k${i}`, `j${i}`, `m${i}`] + } + for (var i = 0; i < 50; i = i + 1) { + var obj = object(keysets[i], function(k) { return k + "_value" }) + var expected = `k${i}_value` + if (obj[`k${i}`] != expected) fail("gc object from keys func stress failed") + } +}) + +// ============================================================================ +// SUMMARY +// ============================================================================ + +print(`\nResults: ${passed} passed, ${failed} failed out of ${passed + failed}`) +if (failed > 0) { + print("Failed tests:") + arrfor(errors, function(name) { + print(` - ${name}`) + }) +}