diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..19c99a62 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.mach binary merge=ours diff --git a/internal/engine.mach b/internal/engine.mach index 245e07eb..f5841e61 100644 Binary files a/internal/engine.mach and b/internal/engine.mach differ diff --git a/parse.cm b/parse.cm index 7e490459..77262d1f 100644 --- a/parse.cm +++ b/parse.cm @@ -391,7 +391,7 @@ var parse = function(tokens, src, filename, tokenizer) { ast_node_end(param) if (tok.kind == "=" || tok.kind == "|") { advance() - param.expression = parse_expr() + param.expression = parse_assign_expr() } push(params, param) } else { @@ -426,7 +426,12 @@ var parse = function(tokens, src, filename, tokenizer) { function_nr = function_nr + 1 ast_node_end(fn) pair.right = fn - } else if (!(is_ident && (tok.kind == "," || tok.kind == "}"))) { + } else if (is_ident && (tok.kind == "," || tok.kind == "}")) { + right = ast_node("name", pair.left) + right.name = pair.left.name + ast_node_end(right) + pair.right = right + } else { parse_error(tok, "expected ':' after property name") } push(list, pair) diff --git a/parse.mach b/parse.mach index 76d89a78..d5dce8b2 100644 Binary files a/parse.mach and b/parse.mach differ diff --git a/source/mach.c b/source/mach.c index e71336e2..606c1afc 100644 --- a/source/mach.c +++ b/source/mach.c @@ -93,6 +93,16 @@ typedef struct MachCompState { int loop_break; /* instruction index to patch, or -1 */ int loop_continue; /* instruction index to patch, or -1 */ + /* Named label stack for labeled break/continue */ +#define MACH_MAX_LABELS 8 + struct { + const char *name; + int break_chain; + int continue_chain; + } labels[MACH_MAX_LABELS]; + int label_count; + int pending_label; /* label index associated with next loop, or -1 */ + /* Parent for nested function compilation */ struct MachCompState *parent; @@ -1160,6 +1170,7 @@ static int mach_compile_expr(MachCompState *cs, cJSON *node, int dest) { child.scopes = cs->scopes; child.filename = cs->filename; child.freereg = 1; /* slot 0 = this */ + child.pending_label = -1; /* Read function_nr from AST node */ cJSON *fn_nr_node = cJSON_GetObjectItemCaseSensitive(node, "function_nr"); @@ -1513,6 +1524,9 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { cJSON *cond = cJSON_GetObjectItemCaseSensitive(stmt, "expression"); if (!cond) cond = cJSON_GetObjectItemCaseSensitive(stmt, "condition"); + int my_label = cs->pending_label; + cs->pending_label = -1; + int old_break = cs->loop_break; int old_continue = cs->loop_continue; cs->loop_break = -1; @@ -1550,6 +1564,17 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { cs->code[cp] = MACH_sJ(MACH_JMP, off); cp = prev; } + /* Also patch labeled continue chain */ + if (my_label >= 0) { + cp = cs->labels[my_label].continue_chain; + while (cp >= 0) { + int prev = MACH_GET_sJ(cs->code[cp]); + int off = loop_top - (cp + 1); + cs->code[cp] = MACH_sJ(MACH_JMP, off); + cp = prev; + } + cs->labels[my_label].continue_chain = -1; + } } /* Jump back to loop top */ @@ -1573,6 +1598,87 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { return; } + /* Do-while loop */ + if (strcmp(kind, "do") == 0) { + cJSON *cond = cJSON_GetObjectItemCaseSensitive(stmt, "expression"); + if (!cond) cond = cJSON_GetObjectItemCaseSensitive(stmt, "condition"); + + int my_label = cs->pending_label; + cs->pending_label = -1; + + int old_break = cs->loop_break; + int old_continue = cs->loop_continue; + cs->loop_break = -1; + cs->loop_continue = -1; + + int body_top = mach_current_pc(cs); + + /* Compile body first (always executes at least once) */ + { + cJSON *body = cJSON_GetObjectItemCaseSensitive(stmt, "block"); + if (!body) body = cJSON_GetObjectItemCaseSensitive(stmt, "body"); + cJSON *stmts = body ? cJSON_GetObjectItemCaseSensitive(body, "statements") : NULL; + if (!stmts) stmts = cJSON_GetObjectItemCaseSensitive(stmt, "statements"); + if (stmts && cJSON_IsArray(stmts)) { + int count = cJSON_GetArraySize(stmts); + for (int i = 0; i < count; i++) + mach_compile_stmt(cs, cJSON_GetArrayItem(stmts, i)); + } else if (body) { + mach_compile_stmt(cs, body); + } + } + + /* Patch continue chain to condition check */ + int cond_pc = mach_current_pc(cs); + { + int cp = cs->loop_continue; + while (cp >= 0) { + int prev = MACH_GET_sJ(cs->code[cp]); + int off = cond_pc - (cp + 1); + cs->code[cp] = MACH_sJ(MACH_JMP, off); + cp = prev; + } + /* Also patch labeled continue chain */ + if (my_label >= 0) { + cp = cs->labels[my_label].continue_chain; + while (cp >= 0) { + int prev = MACH_GET_sJ(cs->code[cp]); + int off = cond_pc - (cp + 1); + cs->code[cp] = MACH_sJ(MACH_JMP, off); + cp = prev; + } + cs->labels[my_label].continue_chain = -1; + } + } + + /* Condition check — jump back to body if true */ + int save = cs->freereg; + int cr = mach_compile_expr(cs, cond, -1); + int jmpfalse_pc = mach_current_pc(cs); + mach_emit(cs, MACH_AsBx(MACH_JMPFALSE, cr, 0)); + mach_free_reg_to(cs, save); + + /* Jump back to body top */ + int offset = body_top - (mach_current_pc(cs) + 1); + mach_emit(cs, MACH_sJ(MACH_JMP, offset)); + + /* Patch jmpfalse to after loop */ + offset = mach_current_pc(cs) - (jmpfalse_pc + 1); + cs->code[jmpfalse_pc] = MACH_AsBx(MACH_JMPFALSE, cr, (int16_t)offset); + + /* Patch break chain */ + int bp = cs->loop_break; + while (bp >= 0) { + int prev = MACH_GET_sJ(cs->code[bp]); + offset = mach_current_pc(cs) - (bp + 1); + cs->code[bp] = MACH_sJ(MACH_JMP, offset); + bp = prev; + } + cs->loop_break = old_break; + cs->loop_continue = old_continue; + return; + } + /* For loop */ if (strcmp(kind, "for") == 0) { cJSON *init = cJSON_GetObjectItemCaseSensitive(stmt, "init"); @@ -1581,6 +1687,9 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { cJSON *body = cJSON_GetObjectItemCaseSensitive(stmt, "block"); if (!body) body = cJSON_GetObjectItemCaseSensitive(stmt, "body"); + int my_label = cs->pending_label; + cs->pending_label = -1; + int old_break = cs->loop_break; int old_continue = cs->loop_continue; cs->loop_break = -1; @@ -1624,6 +1733,17 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { cs->code[cp] = MACH_sJ(MACH_JMP, off); cp = prev; } + /* Also patch labeled continue chain */ + if (my_label >= 0) { + cp = cs->labels[my_label].continue_chain; + while (cp >= 0) { + int prev = MACH_GET_sJ(cs->code[cp]); + int off = continue_target - (cp + 1); + cs->code[cp] = MACH_sJ(MACH_JMP, off); + cp = prev; + } + cs->labels[my_label].continue_chain = -1; + } } /* Update — assignment expressions must be compiled as statements */ @@ -1656,8 +1776,46 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { return; } + /* Label statement */ + if (strcmp(kind, "label") == 0) { + const char *name = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(stmt, "name")); + cJSON *inner = cJSON_GetObjectItemCaseSensitive(stmt, "statement"); + if (inner && cs->label_count < MACH_MAX_LABELS) { + int li = cs->label_count++; + cs->labels[li].name = name; + cs->labels[li].break_chain = -1; + cs->labels[li].continue_chain = -1; + cs->pending_label = li; + mach_compile_stmt(cs, inner); + /* Patch labeled break chain to after the statement */ + int bp = cs->labels[li].break_chain; + while (bp >= 0) { + int prev = MACH_GET_sJ(cs->code[bp]); + int offset = mach_current_pc(cs) - (bp + 1); + cs->code[bp] = MACH_sJ(MACH_JMP, offset); + bp = prev; + } + cs->label_count--; + } else if (inner) { + mach_compile_stmt(cs, inner); + } + return; + } + /* Break */ if (strcmp(kind, "break") == 0) { + const char *name = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(stmt, "name")); + if (name) { + /* Labeled break — find the label */ + for (int i = cs->label_count - 1; i >= 0; i--) { + if (cs->labels[i].name && strcmp(cs->labels[i].name, name) == 0) { + int pc = mach_current_pc(cs); + mach_emit(cs, MACH_sJ(MACH_JMP, cs->labels[i].break_chain)); + cs->labels[i].break_chain = pc; + return; + } + } + } int pc = mach_current_pc(cs); mach_emit(cs, MACH_sJ(MACH_JMP, cs->loop_break)); cs->loop_break = pc; @@ -1666,6 +1824,18 @@ static void mach_compile_stmt(MachCompState *cs, cJSON *stmt) { /* Continue */ if (strcmp(kind, "continue") == 0) { + const char *name = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(stmt, "name")); + if (name) { + /* Labeled continue — find the label */ + for (int i = cs->label_count - 1; i >= 0; i--) { + if (cs->labels[i].name && strcmp(cs->labels[i].name, name) == 0) { + int pc = mach_current_pc(cs); + mach_emit(cs, MACH_sJ(MACH_JMP, cs->labels[i].continue_chain)); + cs->labels[i].continue_chain = pc; + return; + } + } + } int pc = mach_current_pc(cs); mach_emit(cs, MACH_sJ(MACH_JMP, cs->loop_continue)); cs->loop_continue = pc; @@ -1797,6 +1967,7 @@ MachCode *JS_CompileMachTree(cJSON *ast) { MachCompState cs = {0}; cs.freereg = 1; /* slot 0 = this */ cs.maxreg = 1; + cs.pending_label = -1; MachCode *code = mach_compile_program(&cs, ast); diff --git a/vm_suite.ce b/vm_suite.ce index 7bbeb573..9a9a2eeb 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -3671,6 +3671,597 @@ run("functino >>>! unsigned shift right", function() { assert_eq(>>>!(-1, 28), 15, ">>>! unsigned shift right") }) +// ============================================================================ +// FUNCTION DEFAULT PARAMETERS +// ============================================================================ + +run("default param basic", function() { + var fn = function(a = 3, b = 5) { return a + b } + assert_eq(fn(), 8, "both defaults") + assert_eq(fn(10), 15, "first provided") + assert_eq(fn(10, 20), 30, "both provided") +}) + +run("default param null triggers default", function() { + var fn = function(a = 99) { return a } + assert_eq(fn(null), 99, "null should trigger default") + assert_eq(fn(), 99, "missing should trigger default") + assert_eq(fn(0), 0, "zero should not trigger default") + assert_eq(fn(false), false, "false should not trigger default") + assert_eq(fn(""), "", "empty string should not trigger default") +}) + +run("default param pipe syntax", function() { + var fn = function(a | 3, b | 5) { return a + b } + assert_eq(fn(), 8, "pipe both defaults") + assert_eq(fn(10), 15, "pipe first provided") + assert_eq(fn(10, 20), 30, "pipe both provided") +}) + +run("default param object literal", function() { + var fn = function(a = {x: 1, y: 2}) { return a.x + a.y } + assert_eq(fn(), 3, "object default") + assert_eq(fn({x: 10, y: 20}), 30, "object provided") +}) + +run("default param array literal", function() { + var fn = function(a = [10, 20, 30]) { return a[1] } + assert_eq(fn(), 20, "array default") + assert_eq(fn([99]), null, "array provided") +}) + +run("default param nested object", function() { + var fn = function(a = {inner: {val: 42}}) { return a.inner.val } + assert_eq(fn(), 42, "nested object default") +}) + +run("default param expression", function() { + var fn = function(a = 1 + 2, b = 3 * 4) { return a + b } + assert_eq(fn(), 15, "expression defaults") +}) + +run("default param negative number", function() { + var fn = function(a = -1) { return a } + assert_eq(fn(), -1, "negative default") +}) + +run("default param function call", function() { + var double = function(x) { return x * 2 } + var fn = function(a = double(21)) { return a } + assert_eq(fn(), 42, "function call default") +}) + +run("default param function value", function() { + var fn = function(f = function() { return 42 }) { return f() } + assert_eq(fn(), 42, "function as default") +}) + +run("default param arrow function", function() { + var fn = function(f = (x) => x * 2) { return f(5) } + assert_eq(fn(), 10, "arrow function as default") +}) + +run("default param ternary in default", function() { + var fn = function(a = true ? 10 : 20) { return a } + assert_eq(fn(), 10, "ternary default") +}) + +run("default param string with comma", function() { + var fn = function(a = "hello, world") { return a } + assert_eq(fn(), "hello, world", "string comma default") +}) + +run("default param empty object", function() { + var fn = function(a = {}) { return is_object(a) } + assert_eq(fn(), true, "empty object default") +}) + +run("default param empty array", function() { + var fn = function(a = []) { return is_array(a) } + assert_eq(fn(), true, "empty array default") +}) + +run("default param multiple complex", function() { + var fn = function(a = {k: 1}, b = [4, 5], c = "hi") { + return text(a.k) + text(b[0]) + c + } + assert_eq(fn(), "14hi", "multiple complex defaults") + assert_eq(fn({k: 9}), "94hi", "first overridden") + assert_eq(fn({k: 9}, [7]), "97hi", "first two overridden") + assert_eq(fn({k: 9}, [7], "yo"), "97yo", "all overridden") +}) + +run("default param independent defaults", function() { + var fn = function(a = 1, b = 2) { return a + b } + assert_eq(fn(), 3, "both defaults") + assert_eq(fn(10), 12, "first provided") +}) + +run("default param arrow function syntax", function() { + var fn = (a = 7, b = 8) => a + b + assert_eq(fn(), 15, "arrow defaults") + assert_eq(fn(100), 108, "arrow first provided") + assert_eq(fn(100, 200), 300, "arrow both provided") +}) + +run("default param single arrow", function() { + var fn = (a = 42) => a + assert_eq(fn(), 42, "single arrow default") + assert_eq(fn(0), 0, "single arrow zero") +}) + +run("default param method shorthand", function() { + var obj = { + calc(a = 10, b = 20) { + return a + b + } + } + assert_eq(obj.calc(), 30, "method defaults") + assert_eq(obj.calc(1), 21, "method first provided") + assert_eq(obj.calc(1, 2), 3, "method both provided") +}) + +run("default param each fresh object", function() { + var fn = function(a = {count: 0}) { + a.count = a.count + 1 + return a.count + } + assert_eq(fn(), 1, "first call fresh object") + assert_eq(fn(), 1, "second call fresh object") +}) + +run("default param each fresh array", function() { + var fn = function(a = []) { + a[] = 1 + return length(a) + } + assert_eq(fn(), 1, "first call fresh array") + assert_eq(fn(), 1, "second call fresh array") +}) + +run("default param nested parens", function() { + var fn = function(a = (1 + 2) * 3) { return a } + assert_eq(fn(), 9, "nested parens default") +}) + +run("default param closure over outer", function() { + var base = 100 + var fn = function(a = base) { return a } + assert_eq(fn(), 100, "closure over outer") + base = 200 + assert_eq(fn(), 200, "closure sees updated outer") +}) + +// ============================================================================ +// DO-WHILE LOOPS +// ============================================================================ + +run("do-while basic", function() { + var i = 0 + do { + i = i + 1 + } while (i < 3) + assert_eq(i, 3, "do-while counted to 3") +}) + +run("do-while executes body once when false", function() { + var count = 0 + do { + count = count + 1 + } while (false) + assert_eq(count, 1, "body ran once") +}) + +run("do-while break", function() { + var i = 0 + do { + if (i == 2) break + i = i + 1 + } while (i < 10) + assert_eq(i, 2, "break at 2") +}) + +run("do-while continue", function() { + var sum = 0 + var j = 0 + do { + j = j + 1 + if (j == 3) continue + sum = sum + j + } while (j < 5) + assert_eq(sum, 12, "skip 3: 1+2+4+5") +}) + +run("do-while nested", function() { + var result = 0 + var i = 0 + var j = 0 + do { + j = 0 + do { + result = result + 1 + j = j + 1 + } while (j < 2) + i = i + 1 + } while (i < 3) + assert_eq(result, 6, "3 outer * 2 inner") +}) + +// ============================================================================ +// LABELED BREAK AND CONTINUE +// ============================================================================ + +run("labeled break exits outer for loop", function() { + var result = 0 + var j = 0 + var k = 0 + outer: for (j = 0; j < 3; j = j + 1) { + for (k = 0; k < 3; k = k + 1) { + if (k == 1) break outer + result = result + 1 + } + } + assert_eq(result, 1, "only one iteration before break outer") + assert_eq(j, 0, "outer loop did not advance") +}) + +run("labeled continue skips to outer iteration", function() { + var result = 0 + var a = 0 + var b = 0 + outer: for (a = 0; a < 3; a = a + 1) { + for (b = 0; b < 3; b = b + 1) { + if (b == 1) continue outer + result = result + 1 + } + } + assert_eq(result, 3, "3 outer iters, each runs b=0 only") +}) + +run("labeled break from while", function() { + var x = 0 + top: while (true) { + x = x + 1 + if (x == 5) break top + } + assert_eq(x, 5, "broke out at 5") +}) + +run("labeled break triple nested", function() { + var r = 0 + var c = 0 + var d = 0 + var e = 0 + outer: for (c = 0; c < 3; c = c + 1) { + for (d = 0; d < 3; d = d + 1) { + for (e = 0; e < 3; e = e + 1) { + if (e == 1) break outer + r = r + 1 + } + } + } + assert_eq(r, 1, "broke out from 3 levels") +}) + +run("labeled continue with do-while", function() { + var total = 0 + var i = 0 + var j = 0 + outer: for (i = 0; i < 3; i = i + 1) { + j = 0 + do { + if (j == 1) continue outer + total = total + 1 + j = j + 1 + } while (j < 3) + } + assert_eq(total, 3, "3 outer iters, each does j=0 only") +}) + +// ============================================================================ +// SHORTHAND PROPERTY SYNTAX +// ============================================================================ + +run("shorthand property basic", function() { + var name = "Alice" + var age = 30 + var obj = {name, age} + assert_eq(obj.name, "Alice", "name shorthand") + assert_eq(obj.age, 30, "age shorthand") +}) + +run("shorthand property mixed with regular", function() { + var x = 10 + var obj = {x, y: 20} + assert_eq(obj.x, 10, "shorthand x") + assert_eq(obj.y, 20, "regular y") +}) + +run("shorthand property with function value", function() { + var greet = function() { return "hi" } + var obj = {greet} + assert_eq(obj.greet(), "hi", "shorthand fn") +}) + +run("shorthand property single", function() { + var val = 42 + var obj = {val} + assert_eq(obj.val, 42, "single shorthand") +}) + +run("shorthand property with null", function() { + var x = null + var obj = {x} + assert_eq(obj.x, null, "null shorthand") +}) + +run("shorthand property many", function() { + var a = 1 + var b = 2 + var c = 3 + var d = 4 + var e = 5 + var f = 6 + var g = 7 + var h = 8 + var i = 9 + var j = 10 + var obj = {a, b, c, d, e, f, g, h, i, j} + assert_eq(obj.a, 1, "a") + assert_eq(obj.e, 5, "e") + assert_eq(obj.j, 10, "j") +}) + +run("shorthand property with method", function() { + var z = 300 + var obj = {z, double(n) { return n * 2 }} + assert_eq(obj.z, 300, "shorthand z") + assert_eq(obj.double(5), 10, "method double") +}) + +run("shorthand property is a snapshot", function() { + var val = "original" + var obj = {val} + val = "changed" + assert_eq(obj.val, "original", "object keeps original") +}) + +// ============================================================================ +// SHARED CLOSURES +// ============================================================================ + +run("closures share captured variable", function() { + var x = 0 + var inc = function() { x = x + 1 } + var get = function() { return x } + inc() + assert_eq(get(), 1, "after one inc") + inc() + assert_eq(get(), 2, "after two incs") +}) + +run("closure factory returns shared state", function() { + var make = function() { + var count = 0 + return { + inc: function() { count = count + 1 }, + get: function() { return count } + } + } + var c = make() + c.inc() + c.inc() + c.inc() + assert_eq(c.get(), 3, "factory counter") +}) + +run("closure set and get", function() { + var make = function() { + var val = null + return { + set: function(v) { val = v }, + get: function() { return val } + } + } + var o = make() + o.set("hello") + assert_eq(o.get(), "hello", "set then get") + o.set(42) + assert_eq(o.get(), 42, "overwrite") +}) + +// ============================================================================ +// STRING COMPARISON OPERATORS +// ============================================================================ + +run("string less than", function() { + assert_eq("apple" < "banana", true, "apple < banana") + assert_eq("banana" < "apple", false, "banana < apple") + assert_eq("abc" < "abd", true, "abc < abd") +}) + +run("string greater than", function() { + assert_eq("banana" > "apple", true, "banana > apple") + assert_eq("apple" > "banana", false, "apple > banana") +}) + +run("string less than or equal", function() { + assert_eq("abc" <= "abc", true, "equal strings") + assert_eq("abc" <= "abd", true, "abc <= abd") + assert_eq("abd" <= "abc", false, "abd <= abc") +}) + +run("string greater than or equal", function() { + assert_eq("abc" >= "abc", true, "equal strings") + assert_eq("abd" >= "abc", true, "abd >= abc") +}) + +// ============================================================================ +// CROSS-TYPE STRICT EQUALITY +// ============================================================================ + +run("strict equality different types", function() { + assert_eq(5 == "5", false, "number != string") + assert_eq(true == 1, false, "bool != number") + assert_eq(false == 0, false, "false != zero") + assert_eq(null == false, false, "null != false") + assert_eq("" == false, false, "empty string != false") + assert_eq(0 == null, false, "zero != null") +}) + +// ============================================================================ +// COMPOUND ASSIGNMENT OPERATORS (&&= and ||=) +// ============================================================================ + +run("logical and assign", function() { + var x = true + x &&= false + assert_eq(x, false, "true &&= false") + var y = false + y &&= true + assert_eq(y, false, "false &&= true") +}) + +run("logical or assign", function() { + var x = false + x ||= true + assert_eq(x, true, "false ||= true") + var y = true + y ||= false + assert_eq(y, true, "true ||= false") +}) + +// ============================================================================ +// EVERY/SOME ON EMPTY ARRAYS +// ============================================================================ + +run("every on empty array", function() { + var result = every([], function(x) { return false }) + assert_eq(result, true, "vacuous truth") +}) + +run("some on empty array", function() { + var result = some([], function(x) { return true }) + assert_eq(result, false, "no elements match") +}) + +// ============================================================================ +// CHAINED METHOD CALLS +// ============================================================================ + +run("chained method calls", function() { + var make_chain = function() { + var obj = { + val: 0, + add(n) { + obj.val = obj.val + n + return obj + } + } + return obj + } + var c = make_chain() + var result = c.add(1).add(2).add(3).val + assert_eq(result, 6, "1+2+3 chained") +}) + +// ============================================================================ +// TEMPLATE LITERALS WITH EXPRESSIONS +// ============================================================================ + +run("template literal arithmetic", function() { + var a = 3 + var b = 4 + assert_eq(`${a + b}`, "7", "addition in template") +}) + +run("template literal nested calls", function() { + var double = function(x) { return x * 2 } + assert_eq(`result: ${double(5)}`, "result: 10", "fn call in template") +}) + +// ============================================================================ +// COMMA-SEPARATED DECLARATIONS +// ============================================================================ + +run("comma separated var declarations", function() { + var a = 1, b = 2, c = 3 + assert_eq(a, 1, "first") + assert_eq(b, 2, "second") + assert_eq(c, 3, "third") +}) + +run("comma separated def declarations", function() { + def x = 10, y = 20 + assert_eq(x, 10, "def first") + assert_eq(y, 20, "def second") +}) + +// ============================================================================ +// DEF CONSTANTS +// ============================================================================ + +run("def prevents reassignment", function() { + var caught = false + def x = 42 + assert_eq(x, 42, "def value") + // reassignment should disrupt +}) + +// ============================================================================ +// RECURSIVE CLOSURES +// ============================================================================ + +run("recursive closure fibonacci", function() { + var fib = function(n) { + if (n <= 1) return n + return fib(n - 1) + fib(n - 2) + } + assert_eq(fib(0), 0, "fib(0)") + assert_eq(fib(1), 1, "fib(1)") + assert_eq(fib(6), 8, "fib(6)") + assert_eq(fib(10), 55, "fib(10)") +}) + +// ============================================================================ +// KEYWORD PROPERTY NAMES +// ============================================================================ + +run("keyword property names", function() { + var obj = {if: 1, while: 2, return: 3} + assert_eq(obj.if, 1, "if prop") + assert_eq(obj.while, 2, "while prop") + assert_eq(obj.return, 3, "return prop") +}) + +// ============================================================================ +// NESTED RETURN FROM IF/ELSE +// ============================================================================ + +run("nested return in if-else chain", function() { + var classify = function(n) { + if (n < 0) { + return "negative" + } else if (n == 0) { + return "zero" + } else { + return "positive" + } + } + assert_eq(classify(-5), "negative", "negative") + assert_eq(classify(0), "zero", "zero") + assert_eq(classify(10), "positive", "positive") +}) + +// ============================================================================ +// IIFE (IMMEDIATELY INVOKED FUNCTION EXPRESSION) +// ============================================================================ + +run("IIFE with arguments", function() { + var result = (function(a, b) { return a + b })(10, 20) + assert_eq(result, 30, "IIFE sum") +}) + // ============================================================================ // SUMMARY // ============================================================================