diff --git a/src/ant.c b/src/ant.c index 368745a..ff30c90 100644 --- a/src/ant.c +++ b/src/ant.c @@ -2948,11 +2948,7 @@ jsval_t js_setprop(struct js *js, jsval_t obj, jsval_t k, jsval_t v) { no_descriptor: if (existing <= 0) goto create_new; - - if (is_const_prop(js, existing)) { - if (js->flags & F_STRICT) return js_mkerr(js, "assignment to constant"); - return mkval(T_PROP, existing); - } + if (is_const_prop(js, existing)) return js_mkerr(js, "assignment to constant"); saveval(js, existing + sizeof(jsoff_t) * 2, v); if (vtype(obj) != T_ARR || klen == 0 || key[0] < '0' || key[0] > '9') goto done_update; @@ -5003,20 +4999,15 @@ static jsval_t assign(struct js *js, jsval_t lhs, jsval_t val) { if (vtype(lhs) != T_PROP) { if (js->flags & F_STRICT) { return js_mkerr_typed(js, JS_ERR_SYNTAX, "Invalid left-hand side in assignment"); - } - return val; + } return val; } jsoff_t propoff = (jsoff_t) vdata(lhs); - jsoff_t koff = loadoff(js, propoff + sizeof(jsoff_t)); jsoff_t klen = offtolen(loadoff(js, koff)); - const char *key = (char *)&js->mem[koff + sizeof(jsoff_t)]; - if (is_const_prop(js, propoff)) { - if (js->flags & F_STRICT) return js_mkerr(js, "assignment to constant"); - return mkval(T_PROP, propoff); - } + const char *key = (char *)&js->mem[koff + sizeof(jsoff_t)]; + if (is_const_prop(js, propoff)) return js_mkerr(js, "assignment to constant"); if ((klen == 9 && memcmp(key, "undefined", 9) == 0) || (klen == 3 && memcmp(key, "NaN", 3) == 0) || @@ -6087,8 +6078,9 @@ jsval_t call_js_code_with_args(struct js *js, const char *fn, jsoff_t fnlen, jsv jsval_t saved_scope = js->scope; if (global_scope_stack == NULL) utarray_new(global_scope_stack, &jsoff_icd); - utarray_push_back(global_scope_stack, &parent_scope_offset); jsval_t function_scope = mkobj(js, parent_scope_offset); + jsoff_t function_scope_offset = (jsoff_t) vdata(function_scope); + utarray_push_back(global_scope_stack, &function_scope_offset); js->scope = function_scope; jsval_t slot_name = get_slot(js, func_val, SLOT_NAME); @@ -6659,7 +6651,8 @@ static jsval_t do_op(struct js *js, uint8_t op, jsval_t lhs, jsval_t rhs) { uint8_t lhs_type = vtype(lhs); if (lhs_type != T_PROP && lhs_type != T_PROPREF) return js_mkerr_typed(js, JS_ERR_SYNTAX, "Invalid left-hand side expression in postfix operation"); - do_assign_op(js, op == TOK_POSTINC ? TOK_PLUS_ASSIGN : TOK_MINUS_ASSIGN, lhs, tov(1)); + jsval_t assign_res = do_assign_op(js, op == TOK_POSTINC ? TOK_PLUS_ASSIGN : TOK_MINUS_ASSIGN, lhs, tov(1)); + if (is_err(assign_res)) return assign_res; return l; } @@ -8757,10 +8750,9 @@ static jsval_t js_unary(struct js *js) { if (js->flags & F_NOEXEC) return operand; jsval_t resolved = resolveprop(js, operand); if (vtype(operand) == T_PROP || vtype(operand) == T_PROPREF) { - do_assign_op(js, op == TOK_POSTINC ? TOK_PLUS_ASSIGN : TOK_MINUS_ASSIGN, operand, tov(1)); - } else { - return js_mkerr_typed(js, JS_ERR_SYNTAX, "Invalid left-hand side in assignment"); - } + jsval_t assign_res = do_assign_op(js, op == TOK_POSTINC ? TOK_PLUS_ASSIGN : TOK_MINUS_ASSIGN, operand, tov(1)); + if (is_err(assign_res)) return assign_res; + } else return js_mkerr_typed(js, JS_ERR_SYNTAX, "Invalid left-hand side in assignment"); return do_op(js, op == TOK_POSTINC ? TOK_PLUS : TOK_MINUS, resolved, tov(1)); } @@ -10151,8 +10143,16 @@ static jsval_t js_for(struct js *js) { } else if (next(js) == TOK_OF) { is_for_of = true; js->consumed = 1; + } else if (next(js) == TOK_ASSIGN) { + js->pos = destructure_off; + js->consumed = 1; + if (is_const_var) v = js_const(js); + else if (is_var_decl) v = js_var_decl(js); + else v = js_let(js); + if (is_err2(&v, &res)) goto done; + has_destructure = false; } else { - res = js_mkerr_typed(js, JS_ERR_SYNTAX, "expected 'in' or 'of' after destructuring pattern"); + res = js_mkerr_typed(js, JS_ERR_SYNTAX, "expected 'in', 'of', or '=' after destructuring pattern"); goto done; } } else if (next(js) == TOK_IDENTIFIER) { @@ -10172,13 +10172,9 @@ static jsval_t js_for(struct js *js) { } else { js->pos = var_name_off; js->consumed = 1; - if (is_const_var) { - v = js_const(js); - } else if (is_var_decl) { - v = js_var_decl(js); - } else { - v = js_let(js); - } + if (is_const_var) v = js_const(js); + else if (is_var_decl) v = js_var_decl(js); + else v = js_let(js); if (is_err2(&v, &res)) goto done; } } @@ -11653,42 +11649,73 @@ static jsval_t js_var_decl(struct js *js) { js->consumed = 1; for (;;) { - EXPECT_IDENT(); - js->consumed = 0; - jsoff_t noff = js->toff, nlen = js->tlen; - char *name = (char *) &js->code[noff]; - - if (exe && (js->flags & F_STRICT) && is_strict_restricted(name, nlen)) { - return js_mkerr_typed(js, JS_ERR_SYNTAX, "cannot use '%.*s' as variable name in strict mode", (int) nlen, name); - } - - if (exe && (js->flags & F_STRICT) && is_strict_reserved(name, nlen)) { - return js_mkerr_typed(js, JS_ERR_SYNTAX, "'%.*s' is reserved in strict mode", (int) nlen, name); - } + uint8_t tok = next(js); - jsval_t v = js_mkundef(); - bool has_initializer = false; - js->consumed = 1; - if (next(js) == TOK_ASSIGN) { + if (tok == TOK_LBRACKET || tok == TOK_LBRACE) { + jsoff_t pattern_start = js->toff; + uint8_t close_tok = (tok == TOK_LBRACKET) ? TOK_RBRACKET : TOK_RBRACE; + js->consumed = 1; - v = js_expr(js); + int depth = 1; + while (depth > 0 && next(js) != TOK_EOF) { + if (js->tok == tok) depth++; + else if (js->tok == close_tok) depth--; + if (depth > 0) js->consumed = 1; + } + js->consumed = 1; + jsoff_t pattern_end = js->pos; + jsoff_t pattern_len = pattern_end - pattern_start; + + if (next(js) != TOK_ASSIGN) { + return js_mkerr_typed(js, JS_ERR_SYNTAX, "destructuring requires assignment"); + } js->consumed = 1; + + jsval_t v = js_expr(js); if (is_err(v)) return v; - has_initializer = true; - } - - if (exe) { - char decoded_name[256]; - size_t decoded_len = decode_ident_escapes(name, nlen, decoded_name, sizeof(decoded_name)); - jsoff_t existing_off = lkp(js, var_scope, decoded_name, decoded_len); - if (existing_off > 0) { - if (has_initializer && !is_err(v)) { - jsval_t key_val = js_mkstr(js, decoded_name, decoded_len); - setprop(js, var_scope, key_val, resolveprop(js, v)); + if (exe) { + jsval_t val = resolveprop(js, v); + jsval_t r = bind_destruct_pattern(js, &js->code[pattern_start], pattern_len, val, var_scope); + if (is_err(r)) return r; + } + } else { + EXPECT_IDENT(); + js->consumed = 0; + jsoff_t noff = js->toff, nlen = js->tlen; + char *name = (char *) &js->code[noff]; + + if (exe && (js->flags & F_STRICT) && is_strict_restricted(name, nlen)) { + return js_mkerr_typed(js, JS_ERR_SYNTAX, "cannot use '%.*s' as variable name in strict mode", (int) nlen, name); + } + + if (exe && (js->flags & F_STRICT) && is_strict_reserved(name, nlen)) { + return js_mkerr_typed(js, JS_ERR_SYNTAX, "'%.*s' is reserved in strict mode", (int) nlen, name); + } + + jsval_t v = js_mkundef(); + bool has_initializer = false; + js->consumed = 1; + if (next(js) == TOK_ASSIGN) { + js->consumed = 1; + v = js_expr(js); + if (is_err(v)) return v; + has_initializer = true; + } + + if (exe) { + char decoded_name[256]; + size_t decoded_len = decode_ident_escapes(name, nlen, decoded_name, sizeof(decoded_name)); + + jsoff_t existing_off = lkp(js, var_scope, decoded_name, decoded_len); + if (existing_off > 0) { + if (has_initializer && !is_err(v)) { + jsval_t key_val = js_mkstr(js, decoded_name, decoded_len); + setprop(js, var_scope, key_val, resolveprop(js, v)); + } + } else { + jsval_t x = mkprop(js, var_scope, js_mkstr(js, decoded_name, decoded_len), resolveprop(js, v), 0); + if (is_err(x)) return x; } - } else { - jsval_t x = mkprop(js, var_scope, js_mkstr(js, decoded_name, decoded_len), resolveprop(js, v), 0); - if (is_err(x)) return x; } } @@ -21189,7 +21216,7 @@ void js_set(struct js *js, jsval_t obj, const char *key, jsval_t val) { jsoff_t existing = lkp(js, obj, key, key_len); if (existing > 0) { if (is_const_prop(js, existing)) { - if (js->flags & F_STRICT) js_mkerr(js, "assignment to constant"); + js_mkerr(js, "assignment to constant"); return; } saveval(js, existing + sizeof(jsoff_t) * 2, val); @@ -21202,7 +21229,7 @@ void js_set(struct js *js, jsval_t obj, const char *key, jsval_t val) { jsoff_t existing = lkp(js, func_obj, key, key_len); if (existing > 0) { if (is_const_prop(js, existing)) { - if (js->flags & F_STRICT) js_mkerr(js, "assignment to constant"); + js_mkerr(js, "assignment to constant"); return; } saveval(js, existing + sizeof(jsoff_t) * 2, val); @@ -21730,10 +21757,16 @@ static jsval_t js_call_internal(struct js *js, jsval_t func, jsval_t bound_this, mkscope(js); js->flags = saved_flags; + if (global_scope_stack == NULL) utarray_new(global_scope_stack, &jsoff_icd); + jsoff_t function_scope_offset = (jsoff_t) vdata(js->scope); + + utarray_push_back(global_scope_stack, &function_scope_offset); + hoist_var_declarations_from_slot(js, js->scope, func_obj); + parsed_func_t *pf = get_or_parse_func(fn, fnlen); if (!pf) { - delscope(js); - js->scope = saved_scope; + if (global_scope_stack && utarray_len(global_scope_stack) > 0) utarray_pop_back(global_scope_stack); + delscope(js); js->scope = saved_scope; if (combined_args) free(combined_args); js->gc_suppress = saved_gc_suppress; return js_mkerr(js, "failed to parse function"); @@ -21807,8 +21840,8 @@ static jsval_t js_call_internal(struct js *js, jsval_t func, jsval_t bound_this, js->flags = caller_flags; js->this_val = saved_this; - delscope(js); - js->scope = saved_scope; + if (global_scope_stack && utarray_len(global_scope_stack) > 0) utarray_pop_back(global_scope_stack); + delscope(js); js->scope = saved_scope; if (combined_args) free(combined_args); js->gc_suppress = saved_gc_suppress; diff --git a/src/repl.c b/src/repl.c index ea3667c..e2e5c4e 100644 --- a/src/repl.c +++ b/src/repl.c @@ -331,7 +331,7 @@ static key_event_t read_key(void) { if (c == '\r' || c == '\n') return (key_event_t){ KEY_ENTER, 0 }; if (c == 3) { ctrl_c_pressed++; return (key_event_t){ KEY_EOF, 0 }; } if (c == 4 || c == 26) return (key_event_t){ KEY_EOF, 0 }; - if (isprint(c)) return (key_event_t){ KEY_CHAR, c }; + if (isprint(c) || (unsigned char)c >= 0x80) return (key_event_t){ KEY_CHAR, c }; return (key_event_t){ KEY_NONE, 0 }; } #define TERM_INIT() @@ -356,7 +356,7 @@ static key_event_t read_key(void) { } if (c == 127 || c == 8) return (key_event_t){ KEY_BACKSPACE, 0 }; if (c == '\n' || c == '\r') return (key_event_t){ KEY_ENTER, 0 }; - if (isprint(c)) return (key_event_t){ KEY_CHAR, c }; + if (isprint(c) || (unsigned char)c >= 0x80) return (key_event_t){ KEY_CHAR, c }; return (key_event_t){ KEY_NONE, 0 }; } #define TERM_INIT() do { \ diff --git a/tests/var.js b/tests/var.js new file mode 100644 index 0000000..a83abea --- /dev/null +++ b/tests/var.js @@ -0,0 +1,388 @@ +// ============================================ +// COMPREHENSIVE VAR DECLARATION BUG TEST +// ============================================ + +console.log('=== TEST 1: Regular IIFE (baseline) ==='); +(function () { + var a = 1; + var b = 2, + c = 3; + if (true) { + var d = 4; + } + { + var e = 5; + } + for (var f = 6; f < 7; f++) { + var g = 7; + } + while (false) { + var h = 8; + } + do { + var i = 9; + } while (false); + try { + var j = 10; + } catch (err) { + var k = 11; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' e=' + e + ' f=' + f + ' g=' + g + ' h=' + h + ' i=' + i + ' j=' + j + ' k=' + k); +})(); + +console.log('\n=== TEST 2: Arrow IIFE ==='); +(() => { + var a = 1; + var b = 2, + c = 3; + if (true) { + var d = 4; + } + { + var e = 5; + } + for (var f = 6; f < 7; f++) { + var g = 7; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' e=' + e + ' f=' + f + ' g=' + g); +})(); + +console.log('\n=== TEST 3: User-defined callback ==='); +function callFn(cb) { + cb(100); +} +callFn(function (x) { + var a = 1; + var b = 2, + c = 3; + if (true) { + var d = 4; + } + for (var f = 6; f < 7; f++) { + var g = 7; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' f=' + f + ' g=' + g + ' x=' + x); +}); + +console.log('\n=== TEST 4: User-defined callback (arrow) ==='); +callFn(x => { + var a = 1; + var b = 2, + c = 3; + if (true) { + var d = 4; + } + for (var f = 6; f < 7; f++) { + var g = 7; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' f=' + f + ' g=' + g + ' x=' + x); +}); + +console.log('\n=== TEST 5: forEach with regular function ==='); +[1].forEach(function (x, idx, arr) { + var a = 1; + var b = 2, + c = 3; + if (true) { + var d = 4; + } + { + var e = 5; + } + for (var f = 6; f < 7; f++) { + var g = 7; + } + while (false) { + var h = 8; + } + do { + var i = 9; + } while (false); + try { + var j = 10; + } catch (err) { + var k = 11; + } + switch (1) { + case 1: + var l = 12; + break; + } + console.log( + ' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' e=' + e + ' f=' + f + ' g=' + g + ' h=' + h + ' i=' + i + ' j=' + j + ' k=' + k + ' l=' + l + ); +}); + +console.log('\n=== TEST 6: forEach with arrow function ==='); +[1].forEach((x, idx, arr) => { + var a = 1; + var b = 2, + c = 3; + if (true) { + var d = 4; + } + { + var e = 5; + } + for (var f = 6; f < 7; f++) { + var g = 7; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' e=' + e + ' f=' + f + ' g=' + g); +}); + +console.log('\n=== TEST 7: map with regular function ==='); +[1].map(function (x) { + var a = 1; + if (true) { + var b = 2; + } + for (var c = 3; c < 4; c++) { + var d = 4; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d); + return x; +}); + +console.log('\n=== TEST 8: filter with regular function ==='); +[1].filter(function (x) { + var a = 1; + if (true) { + var b = 2; + } + for (var c = 3; c < 4; c++) { + var d = 4; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d); + return true; +}); + +console.log('\n=== TEST 9: reduce with regular function ==='); +[1, 2].reduce(function (acc, x) { + var a = 1; + if (true) { + var b = 2; + } + for (var c = 3; c < 4; c++) { + var d = 4; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' acc=' + acc + ' x=' + x); + return acc + x; +}, 0); + +console.log('\n=== TEST 10: find with regular function ==='); +[1].find(function (x) { + var a = 1; + for (var b = 2; b < 3; b++) { + var c = 3; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c); + return true; +}); + +console.log('\n=== TEST 11: some with regular function ==='); +[1].some(function (x) { + var a = 1; + for (var b = 2; b < 3; b++) { + var c = 3; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c); + return true; +}); + +console.log('\n=== TEST 12: every with regular function ==='); +[1].every(function (x) { + var a = 1; + for (var b = 2; b < 3; b++) { + var c = 3; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c); + return true; +}); + +console.log('\n=== TEST 13: sort with regular function ==='); +[2, 1].sort(function (a, b) { + var v = 1; + for (var i = 0; i < 1; i++) { + var w = 2; + } + console.log(' v=' + v + ' i=' + i + ' w=' + w); + return a - b; +}); + +console.log('\n=== TEST 14: flatMap with regular function ==='); +[1].flatMap(function (x) { + var a = 1; + for (var b = 2; b < 3; b++) { + var c = 3; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c); + return [x]; +}); + +console.log('\n=== TEST 15: findIndex with regular function ==='); +[1].findIndex(function (x) { + var a = 1; + for (var b = 2; b < 3; b++) { + var c = 3; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c); + return true; +}); + +console.log('\n=== TEST 16: let/const in forEach (should work) ==='); +[1].forEach(function (x) { + let a = 1; + const b = 2; + let c = 3, + d = 4; + if (true) { + let e = 5; + var f = 6; + } + for (let g = 7; g < 8; g++) { + let h = 8; + var i = 9; + } + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' f=' + f + ' i=' + i); +}); + +console.log('\n=== TEST 17: Nested forEach ==='); +[1].forEach(function (x) { + var outer = 'outer'; + [2].forEach(function (y) { + var inner = 'inner'; + for (var z = 0; z < 1; z++) { + var inFor = 'inFor'; + } + console.log(' outer=' + outer + ' inner=' + inner + ' z=' + z + ' inFor=' + inFor); + }); +}); + +console.log('\n=== TEST 18: setTimeout callback ==='); +setTimeout(function () { + var a = 1; + if (true) { + var b = 2; + } + for (var c = 3; c < 4; c++) { + var d = 4; + } + console.log(' setTimeout: a=' + a + ' b=' + b + ' c=' + c + ' d=' + d); +}, 0); + +console.log('\n=== TEST 19: Promise.then callback ==='); +Promise.resolve(1).then(function (x) { + var a = 1; + if (true) { + var b = 2; + } + for (var c = 3; c < 4; c++) { + var d = 4; + } + console.log(' Promise.then: a=' + a + ' b=' + b + ' c=' + c + ' d=' + d); +}); + +console.log('\n=== TEST 20: Object methods ==='); +Object.keys({ a: 1 }).forEach(function (k) { + var v = 'test'; + for (var i = 0; i < 1; i++) { + var w = 'for'; + } + console.log(' Object.keys.forEach: v=' + v + ' i=' + i + ' w=' + w); +}); + +console.log('\n=== TEST 21: String methods ==='); +'abc'.split('').forEach(function (c) { + var v = 'test'; + for (var i = 0; i < 1; i++) { + var w = 'for'; + } + console.log(' String.split.forEach: v=' + v + ' i=' + i + ' w=' + w + ' c=' + c); +}); + +console.log('\n=== TEST 22: Map.forEach ==='); +new Map([['a', 1]]).forEach(function (v, k) { + var a = 1; + for (var i = 0; i < 1; i++) { + var b = 2; + } + console.log(' Map.forEach: a=' + a + ' i=' + i + ' b=' + b); +}); + +console.log('\n=== TEST 23: Set.forEach ==='); +new Set([1]).forEach(function (v) { + var a = 1; + for (var i = 0; i < 1; i++) { + var b = 2; + } + console.log(' Set.forEach: a=' + a + ' i=' + i + ' b=' + b); +}); + +console.log('\n=== TEST 24: for...of loop ==='); +for (var item of [1, 2]) { + var a = 'test'; + console.log(' for...of: item=' + item + ' a=' + a); +} + +console.log('\n=== TEST 25: for...in loop ==='); +for (var key in { a: 1, b: 2 }) { + var v = 'test'; + console.log(' for...in: key=' + key + ' v=' + v); +} + +console.log('\n=== TEST 26: Nested functions in forEach ==='); +[1].forEach(function (x) { + var outer = 'outer'; + function inner() { + var innerVar = 'inner'; + console.log(' nested fn: outer=' + outer + ' innerVar=' + innerVar); + } + inner(); + console.log(' forEach body: outer=' + outer); +}); + +console.log('\n=== TEST 27: var in different for variants ==='); +[1].forEach(function (x) { + for (var a = 0; a < 1; a++) {} + for (var b in { x: 1 }) { + } + for (var c of [1]) { + } + var d = 'plain'; + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d); +}); + +console.log('\n=== TEST 28: Hoisting test ==='); +[1].forEach(function (x) { + console.log(' before: a=' + a + ' b=' + b); + var a = 1; + for (var b = 2; b < 3; b++) {} + console.log(' after: a=' + a + ' b=' + b); +}); + +console.log('\n=== TEST 29: Function expression assignment ==='); +[1].forEach(function (x) { + var fn = function () { + return 'test'; + }; + for (var i = 0; i < 1; i++) { + var fn2 = function () { + return 'for'; + }; + } + console.log(' fn=' + fn + ' fn()=' + (fn ? fn() : 'N/A') + ' fn2()=' + (fn2 ? fn2() : 'N/A')); +}); + +console.log('\n=== TEST 30: Destructuring var ==='); +try { + [1].forEach(function (x) { + var [a, b] = [1, 2]; + var { c, d } = { c: 3, d: 4 }; + for (var [e] = [5]; e < 6; e++) {} + console.log(' a=' + a + ' b=' + b + ' c=' + c + ' d=' + d + ' e=' + e); + }); +} catch (e) { + console.log(' destructuring error: ' + e.message); +} + +console.log('\n=== ASYNC TESTS (will print after) ===');